diff --git a/app/models/user.py b/app/models/user.py index 725c29b..f5ba008 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -5,7 +5,8 @@ from app.utils.time_utils import now_eastern @login_manager.user_loader def load_user(user_id): - return User.query.get(int(user_id)) + from app import db + return db.session.get(User, int(user_id)) class User(UserMixin, db.Model): __tablename__ = 'users' @@ -16,10 +17,9 @@ class User(UserMixin, db.Model): email = db.Column(db.String(255), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) role = db.Column( - # 'supervisor' retained temporarily so the Enum is valid before the - # migration UPDATE runs. The migration removes it after all rows are - # updated to 'director'. - db.Enum('admin', 'supervisor', 'director', 'inspector', 'project_manager', 'customer'), + # Phase 11 migration complete — 'supervisor' removed from both the DB + # ENUM and this Python-side declaration. Director is the canonical role. + db.Enum('admin', 'director', 'inspector', 'project_manager', 'customer'), nullable=False ) created_at = db.Column(db.DateTime, default=now_eastern) diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 6d058b8..fc1a332 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,3 +1,4 @@ +import logging from flask import Blueprint, render_template from flask_login import login_required, current_user from app import db @@ -13,6 +14,8 @@ from app.utils.time_utils import now_eastern bp = Blueprint('dashboard', __name__) +logger = logging.getLogger(__name__) + @bp.route('/') @bp.route('/dashboard') diff --git a/app/routes/facilities.py b/app/routes/facilities.py index 7a2c983..cf1cd07 100644 --- a/app/routes/facilities.py +++ b/app/routes/facilities.py @@ -1,3 +1,4 @@ +import logging from flask import Blueprint, render_template, redirect, url_for, flash, request, abort from flask_login import login_required, current_user from app import db @@ -10,6 +11,8 @@ from app.utils.scope import get_customer_scope bp = Blueprint('facilities', __name__, url_prefix='/facilities') +logger = logging.getLogger(__name__) + @bp.route('/') @login_required def list_facilities(): @@ -43,6 +46,8 @@ def create_facility(): db.session.add(facility) db.session.commit() + logger.info('FACILITIES | create | user=%s | facility_id=%s name=%r', + current_user.username, facility.id, facility.name) log_action(ACTION_CREATE, 'Facility', facility.id, facility.name, f'contact={facility.contact_person or ""}; project_id={facility.project_id}; active={facility.active}') flash(f'Facility "{facility.name}" created successfully.', 'success') @@ -85,6 +90,8 @@ def edit_facility(facility_id): facility.active = form.active.data db.session.commit() + logger.info('FACILITIES | edit | user=%s | facility_id=%s name=%r', + current_user.username, facility.id, facility.name) log_action(ACTION_UPDATE, 'Facility', facility.id, facility.name, f'project_id={facility.project_id}; active={facility.active}') flash(f'Facility "{facility.name}" updated successfully.', 'success') @@ -108,6 +115,8 @@ def delete_facility(facility_id): facility_id_snap = facility.id db.session.delete(facility) db.session.commit() + logger.info('FACILITIES | delete | user=%s | facility_id=%s name=%r', + current_user.username, facility_id_snap, facility_name) log_action(ACTION_DELETE, 'Facility', facility_id_snap, facility_name) flash(f'Facility "{facility_name}" has been permanently deleted.', 'success') return redirect(url_for('facilities.list_facilities')) @@ -134,6 +143,8 @@ def create_area(facility_id): db.session.add(area) db.session.commit() + logger.info('FACILITIES | create_area | user=%s | area_id=%s name=%r facility=%r', + current_user.username, area.id, area.name, facility.name) log_action(ACTION_CREATE, 'Area', area.id, area.name, f'facility={facility.name}; type={area.area_type or ""}') flash(f'Area "{area.name}" created successfully.', 'success') @@ -159,6 +170,8 @@ def edit_area(area_id): area.facility_id = form.facility_id.data db.session.commit() + logger.info('FACILITIES | edit_area | user=%s | area_id=%s name=%r', + current_user.username, area.id, area.name) log_action(ACTION_UPDATE, 'Area', area.id, area.name, f'facility_id={area.facility_id}; type={area.area_type or ""}') flash(f'Area "{area.name}" updated successfully.', 'success') @@ -192,6 +205,8 @@ def delete_area(area_id): area_id_snap = area.id db.session.delete(area) db.session.commit() + logger.info('FACILITIES | delete_area | user=%s | area_id=%s name=%r', + current_user.username, area_id_snap, area_name) log_action(ACTION_DELETE, 'Area', area_id_snap, area_name) flash(f'Area "{area_name}" deleted successfully.', 'success') return redirect(url_for('facilities.view_facility', facility_id=facility_id)) \ No newline at end of file diff --git a/app/routes/issues.py b/app/routes/issues.py index 1e554f7..d3b5a22 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -142,7 +142,7 @@ def view(issue_id): return redirect(url_for('issues.index')) form = IssueUpdateForm(obj=issue) - staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all() + staff = User.query.filter(User.role.in_(['admin', 'director', 'inspector'])).order_by(User.username).all() form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff] form.status.data = form.status.data or issue.status @@ -385,7 +385,7 @@ def unfollow(issue_id): def create(): form = IssueForm() areas = Area.query.join(Facility).filter(Facility.active == True).order_by(Facility.name, Area.name).all() - staff = User.query.filter(User.role.in_(['director','inspector'])).order_by(User.username).all() + staff = User.query.filter(User.role.in_(['admin', 'director', 'inspector'])).order_by(User.username).all() form.area_id.choices = [(a.id, f"{a.facility.name} — {a.name}") for a in areas] form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff] diff --git a/app/routes/reports.py b/app/routes/reports.py index 3b97be0..107b7c8 100644 --- a/app/routes/reports.py +++ b/app/routes/reports.py @@ -1,5 +1,6 @@ import csv import io +import logging from datetime import datetime, timedelta from app.utils.time_utils import now_eastern from flask import (Blueprint, render_template, request, @@ -13,9 +14,12 @@ from app.models.issue import Issue from app.models.user import User from app.utils.decorators import supervisor_required from app.utils.scope import get_customer_scope +from app.utils.audit import log_action, ACTION_EXPORT bp = Blueprint('reports', __name__, url_prefix='/reports') +logger = logging.getLogger(__name__) + def _date_range(): """Parse ?start= and ?end= query params; default to last 30 days.""" @@ -360,6 +364,17 @@ def facility_scorecard(facility_id): def export_inspections(): start, end = _date_range() + logger.info( + 'REPORTS | export_inspections | user=%s | range=%s to %s', + current_user.username, + start.strftime('%Y-%m-%d'), + end.strftime('%Y-%m-%d'), + ) + log_action( + ACTION_EXPORT, 'Inspection', None, 'CSV Export', + f'range={start.strftime("%Y-%m-%d")} to {end.strftime("%Y-%m-%d")}', + ) + rows = db.session.query( Inspection.id, Inspection.inspection_date, @@ -414,6 +429,17 @@ def export_inspections(): def export_issues(): start, end = _date_range() + logger.info( + 'REPORTS | export_issues | user=%s | range=%s to %s', + current_user.username, + start.strftime('%Y-%m-%d'), + end.strftime('%Y-%m-%d'), + ) + log_action( + ACTION_EXPORT, 'Issue', None, 'CSV Export', + f'range={start.strftime("%Y-%m-%d")} to {end.strftime("%Y-%m-%d")}', + ) + rows = db.session.query( Issue.id, Issue.reported_at, diff --git a/app/routes/templates.py b/app/routes/templates.py index b8fda17..26eabd6 100644 --- a/app/routes/templates.py +++ b/app/routes/templates.py @@ -1,3 +1,4 @@ +import logging from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, abort from flask_login import login_required, current_user from app import db @@ -9,6 +10,8 @@ from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DEL bp = Blueprint('templates', __name__, url_prefix='/templates') +logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------- # Template CRUD @@ -36,6 +39,8 @@ def create_template(): ) db.session.add(template) db.session.commit() + logger.info('TEMPLATES | create | user=%s | template_id=%s name=%r', + current_user.username, template.id, template.name) log_action(ACTION_CREATE, 'Template', template.id, template.name, f'frequency={template.frequency}') @@ -73,6 +78,8 @@ def edit_template(template_id): template.description = form.description.data template.frequency = form.frequency.data db.session.commit() + logger.info('TEMPLATES | edit | user=%s | template_id=%s name=%r', + current_user.username, template.id, template.name) log_action(ACTION_UPDATE, 'Template', template.id, template.name, f'frequency={template.frequency}') flash(f'Template "{template.name}" updated successfully.', 'success') @@ -114,6 +121,8 @@ def rename_template(template_id): template.frequency = new_frequency db.session.commit() + logger.info('TEMPLATES | rename | user=%s | template_id=%s name=%r', + current_user.username, template.id, template.name) log_action(ACTION_UPDATE, 'Template', template.id, template.name, f'frequency={template.frequency}; via=rename') flash(f'Template "{template.name}" updated successfully.', 'success') @@ -136,6 +145,8 @@ def delete_template(template_id): template_id_snap = template.id db.session.delete(template) db.session.commit() + logger.info('TEMPLATES | delete | user=%s | template_id=%s name=%r', + current_user.username, template_id_snap, template_name) log_action(ACTION_DELETE, 'Template', template_id_snap, template_name) flash(f'Template "{template_name}" deleted successfully.', 'success') @@ -178,6 +189,8 @@ def duplicate_template(template_id): new_tpl.form_schema = src.form_schema db.session.commit() + logger.info('TEMPLATES | duplicate | user=%s | new_template_id=%s source_id=%s', + current_user.username, new_tpl.id, src.id) log_action(ACTION_CREATE, 'Template', new_tpl.id, new_tpl.name, f'duplicated_from={src.id}; frequency={new_tpl.frequency}') @@ -274,6 +287,8 @@ def save_form_schema(template_id): template.form_schema = sanitised db.session.commit() + logger.info('TEMPLATES | save_form_schema | user=%s | template_id=%s fields=%s', + current_user.username, template.id, len(sanitised)) log_action(ACTION_UPDATE, 'Template', template.id, template.name, f'form_schema saved; field_count={len(sanitised)}') @@ -323,6 +338,8 @@ def create_checklist_item(template_id): ) db.session.add(item) db.session.commit() + logger.info('TEMPLATES | create_checklist_item | user=%s | item_id=%s template_id=%s', + current_user.username, item.id, template.id) log_action(ACTION_CREATE, 'ChecklistItem', item.id, item.item_description[:80], f'template_id={template.id}; category={item.category or ""}; ' f'scoring_type={item.scoring_type}') @@ -354,6 +371,8 @@ def edit_checklist_item(item_id): item.weight = form.weight.data item.requires_photo = form.requires_photo.data db.session.commit() + logger.info('TEMPLATES | edit_checklist_item | user=%s | item_id=%s template_id=%s', + current_user.username, item.id, item.template_id) log_action(ACTION_UPDATE, 'ChecklistItem', item.id, item.item_description[:80], f'template_id={item.template_id}; category={item.category or ""}; ' f'scoring_type={item.scoring_type}') @@ -381,6 +400,8 @@ def delete_checklist_item(item_id): item_id_snap = item.id db.session.delete(item) db.session.commit() + logger.info('TEMPLATES | delete_checklist_item | user=%s | item_id=%s template_id=%s', + current_user.username, item_id_snap, template_id) log_action(ACTION_DELETE, 'ChecklistItem', item_id_snap, item_desc, f'template_id={template_id}') flash('Checklist item deleted successfully.', 'success') diff --git a/app/utils/notifications.py b/app/utils/notifications.py index e3f50e8..83d4cfe 100644 --- a/app/utils/notifications.py +++ b/app/utils/notifications.py @@ -310,7 +310,7 @@ def notify_customers_for_facility( from app.models.facility import Facility from app.models.user import User - facility = Facility.query.get(facility_id) + facility = db.session.get(Facility, facility_id) if not facility: logger.warning( 'notify_customers_for_facility | facility_id=%s not found', facility_id @@ -342,7 +342,7 @@ def notify_customers_for_facility( return for user_id in notified_user_ids: - user = User.query.get(user_id) + user = db.session.get(User, user_id) if not user or not user.active or user.role != 'customer': continue try: @@ -397,7 +397,7 @@ def send_pending_digests(frequency: str = 'daily'): sent_count = 0 for user_id in pending_user_ids: - user = User.query.get(user_id) + user = db.session.get(User, user_id) if not user or not user.email: continue