From 8ff38578adb0968d148fa2bea952955cce73d160 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 16 Jul 2026 16:37:49 -0400 Subject: [PATCH] Jul 16 - Fill the gaps between Single-tenant mode and Multi-tenant mode --- app/api/comments.py | 2 +- app/api/inspections.py | 2 +- app/api/issues.py | 2 +- app/api/photos.py | 2 +- app/api/stats.py | 2 +- app/api/templates.py | 2 +- app/models/notification_matrix.py | 1 + app/models/user.py | 2 +- app/routes/dashboard.py | 5 ++- app/routes/inspections.py | 2 +- app/routes/issues.py | 32 ++++++++------ app/templates/auth/users.html | 2 +- app/templates/base.html | 6 +-- app/templates/dashboard.html | 2 +- app/templates/facilities/list.html | 4 +- app/templates/facilities/view.html | 4 +- app/templates/issues/list.html | 8 ++-- app/templates/issues/view.html | 10 ++--- app/templates/reports/_subnav.html | 4 +- app/utils/decorators.py | 26 +++++++++++- app/utils/forms.py | 1 + app/utils/notifications.py | 1 + migrations/versions/phase41_auditor_role.py | 47 +++++++++++++++++++++ 23 files changed, 125 insertions(+), 44 deletions(-) create mode 100644 migrations/versions/phase41_auditor_role.py diff --git a/app/api/comments.py b/app/api/comments.py index dd6b631..cd5ddcc 100644 --- a/app/api/comments.py +++ b/app/api/comments.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_comments', __name__) -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} def _comment_payload(comment: IssueComment) -> dict: diff --git a/app/api/inspections.py b/app/api/inspections.py index 9363d7c..7fd44c0 100644 --- a/app/api/inspections.py +++ b/app/api/inspections.py @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_inspections', __name__) -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} def _merge_form_data(existing: dict, incoming: dict) -> dict: diff --git a/app/api/issues.py b/app/api/issues.py index 2c77462..054086e 100644 --- a/app/api/issues.py +++ b/app/api/issues.py @@ -42,7 +42,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_issues', __name__) -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} _VALID_SEVERITY = {'low', 'medium', 'high', 'critical'} _VALID_STATUSES = {'open', 'in_progress', 'resolved', 'pending_verification'} _UUID_RE = re.compile( diff --git a/app/api/photos.py b/app/api/photos.py index 852874d..3005f82 100644 --- a/app/api/photos.py +++ b/app/api/photos.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_photos', __name__) _ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'} -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} def _allowed_file(filename: str) -> bool: diff --git a/app/api/stats.py b/app/api/stats.py index 2762f5d..41abe55 100644 --- a/app/api/stats.py +++ b/app/api/stats.py @@ -40,7 +40,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_stats', __name__) -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} @bp.route('/stats/dashboard', methods=['GET']) diff --git a/app/api/templates.py b/app/api/templates.py index 6a347d1..b1f3f1c 100644 --- a/app/api/templates.py +++ b/app/api/templates.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) bp = Blueprint('api_templates', __name__) # Customer role cannot access template data — inspectors and above only -_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'} +_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'} def _template_summary_payload(template: InspectionTemplate) -> dict: diff --git a/app/models/notification_matrix.py b/app/models/notification_matrix.py index 7d6780d..80e7a7b 100644 --- a/app/models/notification_matrix.py +++ b/app/models/notification_matrix.py @@ -40,6 +40,7 @@ MATRIX_ROLES = [ ('director', 'Director'), ('inspector', 'Inspector'), ('project_manager', 'Project Manager'), + ('auditor', 'Auditor'), ('customer', 'Customer'), ('custom', 'Custom Recipients'), ] diff --git a/app/models/user.py b/app/models/user.py index f427d1e..b930e82 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -19,7 +19,7 @@ class User(UserMixin, db.Model): role = db.Column( # 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'), + db.Enum('admin', 'director', 'inspector', 'project_manager', 'customer', 'auditor'), nullable=False ) created_at = db.Column(db.DateTime, default=now_eastern) diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index c72bb78..64211ae 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -29,6 +29,7 @@ def index(): is_privileged = current_user.role in ['admin', 'director'] is_customer = current_user.role == 'customer' is_project_manager = current_user.role == 'project_manager' + is_auditor = current_user.role == 'auditor' # Resolve facility scope customer_facility_ids = get_customer_scope(current_user) # None for non-customers @@ -299,9 +300,9 @@ def index(): unassigned_q = unassigned_q.filter(False) # not relevant for customers unassigned_open = unassigned_q.count() - # ── Inspector activity today (admin / director / PM only) ───────────────── + # ── Inspector activity today (admin / director / PM / auditor only) ─────── inspector_activity = [] - if is_privileged or is_project_manager: + if is_privileged or is_project_manager or is_auditor: active_inspectors = ( User.query .filter_by(role='inspector', active=True) diff --git a/app/routes/inspections.py b/app/routes/inspections.py index f87a8f1..64fb2cb 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -596,7 +596,7 @@ def execute(inspection_id): return redirect(url_for('inspections.execute', inspection_id=inspection_id)) staff_for_flag_issue = User.query.filter( - User.role.in_(['admin', 'director', 'inspector', 'project_manager']), + User.role.in_(['director', 'inspector', 'project_manager', 'auditor']), User.active == True, ).order_by(User.full_name, User.username).all() diff --git a/app/routes/issues.py b/app/routes/issues.py index fd03dcb..eaed549 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -15,7 +15,8 @@ from app.models.notification import ( EVENT_CUSTOMER_ISSUE_UPDATED, ) from app.utils.forms import IssueForm, IssueUpdateForm -from app.utils.decorators import supervisor_required, project_manager_required +from app.utils.decorators import (supervisor_required, project_manager_required, + issue_manager_required) from app.utils.notifications import notify, notify_customers_for_facility, notify_by_matrix from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_EXPORT from app.tenancy.gates import quota_soft_check @@ -346,7 +347,7 @@ def index(): # Staff for quick-assign dropdown — same roles as the full issue form staff = User.query.filter( - User.role.in_(['admin', 'director', 'inspector']), User.active == True + User.role.in_(['director', 'inspector', 'auditor']), User.active == True ).order_by(User.username).all() # Reporters dropdown — users who have actually filed at least one issue @@ -421,7 +422,14 @@ def view(issue_id): return redirect(url_for('issues.view', issue_id=issue_id)) form = IssueUpdateForm(obj=issue) - staff = User.query.filter(User.role.in_(['admin', 'director', 'inspector'])).order_by(User.username).all() + staff = User.query.filter(User.role.in_(['director', 'inspector', 'auditor'])).order_by(User.username).all() + # Preserve any pre-existing assignee who is no longer in the assignable set + # (e.g. an admin assigned before admins were removed from the dropdown) so + # saving the form doesn't silently unassign them. + if issue.assigned_to and issue.assigned_to not in [u.id for u in staff]: + current_assignee = db.session.get(User, issue.assigned_to) + if current_assignee: + staff.append(current_assignee) form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.display_name) for u in staff] form.status.data = form.status.data or issue.status @@ -431,7 +439,7 @@ def view(issue_id): issue.status = form.status.data - if current_user.role in ['admin', 'director']: + if current_user.role in ['admin', 'director', 'auditor']: issue.assigned_to = form.assigned_to.data or None if form.status.data == 'resolved' and not issue.resolved_at: @@ -449,7 +457,7 @@ def view(issue_id): issue.result_notes = form.result_notes.data or None # Vendor / contractor assignment — admin, director, project_manager only - if current_user.role in ('admin', 'director', 'project_manager'): + if current_user.role in ('admin', 'director', 'project_manager', 'auditor'): issue.vendor_name = form.vendor_name.data.strip() or None issue.vendor_contact = form.vendor_contact.data.strip() or None issue.vendor_notes = form.vendor_notes.data.strip() or None @@ -695,7 +703,7 @@ def unfollow(issue_id): @login_required @quota_soft_check('issues') def create(): - if current_user.role not in ('admin', 'director', 'customer'): + if current_user.role not in ('admin', 'director', 'customer', 'auditor'): abort(403) from app.models.project import Project, CustomerAssignment @@ -717,7 +725,7 @@ def create(): else: facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all() projects = Project.query.filter_by(active=True).order_by(Project.name).all() - staff = User.query.filter(User.role.in_(['admin', 'director', 'inspector'])).order_by(User.username).all() + staff = User.query.filter(User.role.in_(['director', 'inspector', 'auditor'])).order_by(User.username).all() form.facility_id.choices = [(f.id, f.name) for f in facilities] form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.display_name) for u in staff] @@ -803,7 +811,7 @@ def create(): @bp.route('//verify', methods=['POST']) @login_required -@supervisor_required +@issue_manager_required def verify(issue_id): """Supervisor sign-off: confirms resolution is satisfactory and closes the issue.""" issue = db.session.get(Issue, issue_id) @@ -837,7 +845,7 @@ def verify(issue_id): @bp.route('/bulk-verify', methods=['POST']) @login_required -@supervisor_required +@issue_manager_required def bulk_verify(): """Verify multiple pending-verification issues in a single action.""" issue_ids = request.form.getlist('issue_ids', type=int) @@ -884,7 +892,7 @@ def request_verification(issue_id): # Only the assignee, director, or admin can request verification can_act = ( - current_user.role in ['admin', 'director'] + current_user.role in ['admin', 'director', 'auditor'] or issue.assigned_to == current_user.id ) if not can_act: @@ -927,7 +935,7 @@ def request_verification(issue_id): @bp.route('/verification-queue') @login_required -@supervisor_required +@issue_manager_required def verification_queue(): """Supervisor queue of all issues awaiting verification, grouped by facility.""" from app.models.facility import Facility, Area @@ -1028,7 +1036,7 @@ def delete(issue_id): @login_required def quick_assign(issue_id): """Inline assignee update from the issues list — returns JSON.""" - if current_user.role not in ('admin', 'director'): + if current_user.role not in ('admin', 'director', 'auditor'): return jsonify({'ok': False, 'error': 'Permission denied'}), 403 issue = db.session.get(Issue, issue_id) diff --git a/app/templates/auth/users.html b/app/templates/auth/users.html index 78b24b8..21e8f97 100644 --- a/app/templates/auth/users.html +++ b/app/templates/auth/users.html @@ -37,7 +37,7 @@ {{ user.full_name or '—' }} {{ user.email }} - + {{ user.role.replace('_',' ')|title }} diff --git a/app/templates/base.html b/app/templates/base.html index 85a69fe..927acf2 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -139,7 +139,7 @@ - {% if current_user.role in ['admin', 'director', 'project_manager'] %} + {% if current_user.role in ['admin', 'director', 'project_manager', 'auditor'] %} @@ -155,7 +155,7 @@ - {% if current_user.role in ['admin', 'director', 'project_manager'] %} + {% if current_user.role in ['admin', 'director', 'project_manager', 'auditor'] %} @@ -163,7 +163,7 @@ - {% if current_user.role in ['admin', 'director'] %} + {% if current_user.role in ['admin', 'director', 'auditor'] %}