Phase 1
This commit is contained in:
@@ -205,3 +205,6 @@ cython_debug/
|
|||||||
marimo/_static/
|
marimo/_static/
|
||||||
marimo/_lsp/
|
marimo/_lsp/
|
||||||
__marimo__/
|
__marimo__/
|
||||||
|
|
||||||
|
wsgi.py
|
||||||
|
gunicorn_config.py
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_mail import Mail
|
||||||
|
from config import config
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Initialize extensions
|
||||||
|
db = SQLAlchemy()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
migrate = Migrate()
|
||||||
|
mail = Mail()
|
||||||
|
|
||||||
|
def create_app(config_name='default'):
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
app.config.from_object(config[config_name])
|
||||||
|
|
||||||
|
# Initialize extensions with app
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
mail.init_app(app)
|
||||||
|
|
||||||
|
# Configure login manager
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
|
login_manager.login_message = 'Please log in to access this page.'
|
||||||
|
login_manager.login_message_category = 'info'
|
||||||
|
|
||||||
|
# Create upload directory if it doesn't exist
|
||||||
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||||
|
|
||||||
|
# Register blueprints
|
||||||
|
from app.routes import auth, dashboard, inspections, templates, reports
|
||||||
|
|
||||||
|
app.register_blueprint(auth.bp)
|
||||||
|
app.register_blueprint(dashboard.bp)
|
||||||
|
app.register_blueprint(inspections.bp)
|
||||||
|
app.register_blueprint(templates.bp)
|
||||||
|
app.register_blueprint(reports.bp)
|
||||||
|
|
||||||
|
# Create database tables
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
return app
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from app.models.user import User
|
||||||
|
from app.models.facility import Facility, Area
|
||||||
|
from app.models.inspection import InspectionTemplate, ChecklistItem, Inspection, InspectionResult
|
||||||
|
from app.models.issue import Issue
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class Facility(db.Model):
|
||||||
|
__tablename__ = 'facilities'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(255), nullable=False)
|
||||||
|
address = db.Column(db.Text)
|
||||||
|
contact_person = db.Column(db.String(100))
|
||||||
|
contact_phone = db.Column(db.String(20))
|
||||||
|
active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
areas = db.relationship('Area', backref='facility', lazy='dynamic')
|
||||||
|
inspections = db.relationship('Inspection', backref='facility', lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Facility {self.name}>'
|
||||||
|
|
||||||
|
class Area(db.Model):
|
||||||
|
__tablename__ = 'areas'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id'), nullable=False)
|
||||||
|
name = db.Column(db.String(255), nullable=False)
|
||||||
|
area_type = db.Column(db.String(50))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
inspections = db.relationship('Inspection', backref='area', lazy='dynamic')
|
||||||
|
issues = db.relationship('Issue', backref='area', lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Area {self.name}>'
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class InspectionTemplate(db.Model):
|
||||||
|
__tablename__ = 'inspection_templates'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(255), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
frequency = db.Column(db.Enum('daily', 'weekly', 'monthly', 'quarterly'))
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
checklist_items = db.relationship('ChecklistItem', backref='template', lazy='dynamic', cascade='all, delete-orphan')
|
||||||
|
inspections = db.relationship('Inspection', backref='template', lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<InspectionTemplate {self.name}>'
|
||||||
|
|
||||||
|
class ChecklistItem(db.Model):
|
||||||
|
__tablename__ = 'checklist_items'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
template_id = db.Column(db.Integer, db.ForeignKey('inspection_templates.id'), nullable=False)
|
||||||
|
category = db.Column(db.String(100))
|
||||||
|
item_description = db.Column(db.Text, nullable=False)
|
||||||
|
scoring_type = db.Column(db.Enum('pass_fail', 'rating_5', 'rating_10'))
|
||||||
|
weight = db.Column(db.Numeric(3, 2), default=1.00)
|
||||||
|
requires_photo = db.Column(db.Boolean, default=False)
|
||||||
|
display_order = db.Column(db.Integer)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
results = db.relationship('InspectionResult', backref='checklist_item', lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ChecklistItem {self.item_description[:30]}>'
|
||||||
|
|
||||||
|
class Inspection(db.Model):
|
||||||
|
__tablename__ = 'inspections'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
template_id = db.Column(db.Integer, db.ForeignKey('inspection_templates.id'), nullable=False)
|
||||||
|
facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id'), nullable=False)
|
||||||
|
area_id = db.Column(db.Integer, db.ForeignKey('areas.id'))
|
||||||
|
inspector_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
inspection_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
overall_score = db.Column(db.Numeric(5, 2))
|
||||||
|
status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress')
|
||||||
|
notes = db.Column(db.Text)
|
||||||
|
completed_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
results = db.relationship('InspectionResult', backref='inspection', lazy='dynamic', cascade='all, delete-orphan')
|
||||||
|
issues = db.relationship('Issue', backref='inspection', lazy='dynamic')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Inspection {self.id} - {self.inspection_date}>'
|
||||||
|
|
||||||
|
class InspectionResult(db.Model):
|
||||||
|
__tablename__ = 'inspection_results'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
inspection_id = db.Column(db.Integer, db.ForeignKey('inspections.id'), nullable=False)
|
||||||
|
checklist_item_id = db.Column(db.Integer, db.ForeignKey('checklist_items.id'), nullable=False)
|
||||||
|
score = db.Column(db.Numeric(5, 2))
|
||||||
|
passed = db.Column(db.Boolean)
|
||||||
|
comments = db.Column(db.Text)
|
||||||
|
photo_path = db.Column(db.String(255))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<InspectionResult {self.id}>'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class Issue(db.Model):
|
||||||
|
__tablename__ = 'issues'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
inspection_id = db.Column(db.Integer, db.ForeignKey('inspections.id'))
|
||||||
|
area_id = db.Column(db.Integer, db.ForeignKey('areas.id'), nullable=False)
|
||||||
|
severity = db.Column(db.Enum('low', 'medium', 'high', 'critical'), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=False)
|
||||||
|
photo_path = db.Column(db.String(255))
|
||||||
|
status = db.Column(db.Enum('open', 'in_progress', 'resolved'), default='open')
|
||||||
|
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
reported_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
resolved_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Issue {self.id} - {self.severity}>'
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from app import db, login_manager
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||||
|
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
inspections = db.relationship('Inspection', backref='inspector', lazy='dynamic')
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_user, logout_user, login_required
|
||||||
|
from app import db
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
|
|
||||||
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
if user and user.check_password(password):
|
||||||
|
login_user(user)
|
||||||
|
next_page = request.args.get('next')
|
||||||
|
return redirect(next_page or url_for('dashboard.index'))
|
||||||
|
else:
|
||||||
|
flash('Invalid username or password', 'danger')
|
||||||
|
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@bp.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash('You have been logged out successfully', 'success')
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
bp = Blueprint('dashboard', __name__)
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
@bp.route('/dashboard')
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
return render_template('dashboard.html', user=current_user)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('inspections', __name__, url_prefix='/inspections')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def index():
|
||||||
|
return "Inspections module - Coming in Phase 2"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('reports', __name__, url_prefix='/reports')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def index():
|
||||||
|
return "Reports module - Coming in Phase 2"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('templates', __name__, url_prefix='/templates')
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def index():
|
||||||
|
return "Templates module - Coming in Phase 2"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Janitorial QC System{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">
|
||||||
|
<i class="bi bi-clipboard-check"></i> Janitorial QC
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('inspections.index') }}">Inspections</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('templates.index') }}">Templates</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('reports.index') }}">Reports</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle"></i> {{ current_user.username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="#">Profile</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard - Janitorial QC{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>Welcome, {{ current_user.username }}!</h2>
|
||||||
|
<p class="text-muted">Role: {{ current_user.role|title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-primary mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bi bi-clipboard-data"></i> Today's Inspections</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-success mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bi bi-check-circle"></i> Completed</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-warning mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bi bi-exclamation-triangle"></i> Issues Open</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-info mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bi bi-graph-up"></i> Avg Score</h5>
|
||||||
|
<p class="card-text display-6">--</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login - Janitorial QC{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-primary text-white text-center">
|
||||||
|
<h4><i class="bi bi-clipboard-check"></i> Janitorial QC System</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# Secret key for session management
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||||
|
'mysql+pymysql://jqc_admin:Jqc4dm1n141!@localhost/janitorial_qc'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_ECHO = False
|
||||||
|
|
||||||
|
# Upload configuration
|
||||||
|
UPLOAD_FOLDER = os.path.join(basedir, 'app/static/uploads')
|
||||||
|
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
|
# Session configuration
|
||||||
|
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
||||||
|
SESSION_COOKIE_SECURE = False # Change to True on production
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# Mail configuration (configure later)
|
||||||
|
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
|
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
|
||||||
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
||||||
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||||
|
|
||||||
|
class DevelopmentConfig(Config):
|
||||||
|
DEBUG = True
|
||||||
|
SQLALCHEMY_ECHO = True
|
||||||
|
|
||||||
|
class ProductionConfig(Config):
|
||||||
|
DEBUG = False
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'development': DevelopmentConfig,
|
||||||
|
'production': ProductionConfig,
|
||||||
|
'default': DevelopmentConfig
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
Flask-SQLAlchemy==3.1.1
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
Flask-WTF==1.2.1
|
||||||
|
Flask-Mail==0.9.1
|
||||||
|
Flask-Migrate==4.0.5
|
||||||
|
PyMySQL==1.1.0
|
||||||
|
cryptography==41.0.7
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
Pillow==10.1.0
|
||||||
|
WTForms==3.1.1
|
||||||
|
email-validator==2.1.0
|
||||||
|
gunicorn==21.2.0
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from app import create_app, db
|
||||||
|
from app.models import User, Facility, Area, InspectionTemplate, ChecklistItem, Inspection, InspectionResult, Issue
|
||||||
|
import os
|
||||||
|
|
||||||
|
app = create_app(os.getenv('FLASK_ENV') or 'default')
|
||||||
|
|
||||||
|
@app.shell_context_processor
|
||||||
|
def make_shell_context():
|
||||||
|
return {
|
||||||
|
'db': db,
|
||||||
|
'User': User,
|
||||||
|
'Facility': Facility,
|
||||||
|
'Area': Area,
|
||||||
|
'InspectionTemplate': InspectionTemplate,
|
||||||
|
'ChecklistItem': ChecklistItem,
|
||||||
|
'Inspection': Inspection,
|
||||||
|
'InspectionResult': InspectionResult,
|
||||||
|
'Issue': Issue
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
Reference in New Issue
Block a user