From 093d3a00432e582e10910c113123c6ebe56efdb9 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Wed, 1 Apr 2026 13:15:05 -0400 Subject: [PATCH] Apr 1 2026: clean up code --- app/api/auth.py | 2 +- app/api/decorators.py | 3 ++- app/routes/audit.py | 11 ++++------- app/routes/customers.py | 4 ++-- app/routes/dashboard.py | 2 +- app/routes/inspections.py | 4 ++-- app/routes/issues.py | 17 +++++++++-------- app/routes/notifications.py | 6 ++++-- app/routes/projects.py | 4 ++-- app/routes/scheduled_reports.py | 16 ++++++++++++++-- app/routes/templates.py | 12 +++++++++++- 11 files changed, 52 insertions(+), 29 deletions(-) diff --git a/app/api/auth.py b/app/api/auth.py index c20012f..a976138 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -170,7 +170,7 @@ def refresh(): request.remote_addr) return api_error('Refresh token is invalid or expired', 401) - user = User.query.get(rt_row.user_id) + user = db.session.get(User, rt_row.user_id) if user is None or not user.active: rt_row.revoke() db.session.commit() diff --git a/app/api/decorators.py b/app/api/decorators.py index 916a8cc..16014d6 100644 --- a/app/api/decorators.py +++ b/app/api/decorators.py @@ -34,6 +34,7 @@ from flask import request, g, abort from app.api.jwt_utils import decode_access_token from app.api.errors import api_error +from app import db from app.models.user import User logger = logging.getLogger(__name__) @@ -62,7 +63,7 @@ def jwt_required(f): return api_error('Access token is invalid or expired', 401) user_id = int(payload.get('sub', 0)) - user = User.query.get(user_id) + user = db.session.get(User, user_id) if user is None: return api_error('User not found', 401) diff --git a/app/routes/audit.py b/app/routes/audit.py index 5cd7068..45113b9 100644 --- a/app/routes/audit.py +++ b/app/routes/audit.py @@ -2,6 +2,7 @@ import logging from datetime import datetime, timedelta from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_required +from app import db from app.models.audit import AuditLog from app.models.user import User from app.utils.decorators import admin_required @@ -55,13 +56,13 @@ def index(): # Distinct action and entity_type values for the filter dropdowns distinct_actions = ( - db.session.query(AuditLog.action) + AuditLog.query.with_entities(AuditLog.action) .distinct() .order_by(AuditLog.action) .all() ) distinct_entity_types = ( - db.session.query(AuditLog.entity_type) + AuditLog.query.with_entities(AuditLog.entity_type) .distinct() .order_by(AuditLog.entity_type) .all() @@ -144,8 +145,4 @@ def purge(): f'older than {label} have been permanently deleted.', 'success' if deleted else 'info', ) - return redirect(url_for('audit.index')) - - -# Avoid circular import — imported after function definitions -from app import db # noqa: E402 \ No newline at end of file + return redirect(url_for('audit.index')) \ No newline at end of file diff --git a/app/routes/customers.py b/app/routes/customers.py index 1b3dc67..4f05bb0 100644 --- a/app/routes/customers.py +++ b/app/routes/customers.py @@ -249,8 +249,8 @@ def add_assignment(customer_id): def remove_assignment(assignment_id): assignment = CustomerAssignment.query.get_or_404(assignment_id) customer_id = assignment.user_id - customer = User.query.get(customer_id) - project = Project.query.get(assignment.project_id) + customer = db.session.get(User, customer_id) + project = db.session.get(Project, assignment.project_id) username = customer.username if customer else f'user_id={customer_id}' project_name = project.name if project else f'project_id={assignment.project_id}' diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 8e8cfe4..772d54d 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -241,7 +241,7 @@ def facility_trend(): if facility_id not in cids: return jsonify({'labels': [], 'data': [], 'facility': ''}), 403 - facility = Facility.query.get(facility_id) + facility = db.session.get(Facility, facility_id) if not facility: return jsonify({'labels': [], 'data': [], 'facility': ''}) diff --git a/app/routes/inspections.py b/app/routes/inspections.py index 8a7c43a..f9bee91 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -318,7 +318,7 @@ def execute(inspection_id): # fields (rating, pass_fail) so the inspector doesn't re-enter static # data but must re-evaluate every scoreable item fresh. if not saved_responses and inspection.parent_inspection_id: - parent = Inspection.query.get(inspection.parent_inspection_id) + parent = db.session.get(Inspection, inspection.parent_inspection_id) if parent and parent.notes: try: parent_parsed = json.loads(parent.notes) @@ -717,7 +717,7 @@ def flag_issue(inspection_id): ) if issue.assigned_to: - assignee = User.query.get(issue.assigned_to) + assignee = db.session.get(User, issue.assigned_to) if assignee and assignee.id != current_user.id: notify( recipient = assignee, diff --git a/app/routes/issues.py b/app/routes/issues.py index 83a6b0d..319644e 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -107,7 +107,7 @@ def view(issue_id): if current_user.role == 'customer': from app.models.facility import Area cids = get_customer_scope(current_user) or [] - area = Area.query.get(issue.area_id) + area = db.session.get(Area, issue.area_id) if not area or area.facility_id not in cids: flash('Access denied.', 'danger') return redirect(url_for('issues.index')) @@ -167,7 +167,7 @@ def view(issue_id): # 1. Status changed — notify assignee if old_status != issue.status and new_assigned_to: - assignee = User.query.get(new_assigned_to) + assignee = db.session.get(User, new_assigned_to) if assignee and assignee.id != actor_id: notify( recipient = assignee, @@ -186,7 +186,7 @@ def view(issue_id): # 2. Reassigned — notify new assignee if (old_assigned_to != new_assigned_to) and new_assigned_to: - new_assignee = User.query.get(new_assigned_to) + new_assignee = db.session.get(User, new_assigned_to) if new_assignee and new_assignee.id != actor_id: notify( recipient = new_assignee, @@ -204,7 +204,7 @@ def view(issue_id): # 3. Unassigned — notify previous assignee if old_assigned_to and old_assigned_to != new_assigned_to: - old_assignee = User.query.get(old_assigned_to) + old_assignee = db.session.get(User, old_assigned_to) if old_assignee and old_assignee.id != actor_id: notify( recipient = old_assignee, @@ -221,7 +221,7 @@ def view(issue_id): # 4. Comment added — notify assignee if comment_body and new_assigned_to: - commentee = User.query.get(new_assigned_to) + commentee = db.session.get(User, new_assigned_to) if commentee and commentee.id != actor_id: notify( recipient = commentee, @@ -250,7 +250,8 @@ def view(issue_id): f'to "{issue.status.replace("_"," ").title()}"' ) if old_assigned_to != new_assigned_to: - new_name = User.query.get(new_assigned_to).username if new_assigned_to else 'Unassigned' + _new_assignee_obj = db.session.get(User, new_assigned_to) if new_assigned_to else None + new_name = _new_assignee_obj.username if _new_assignee_obj else 'Unassigned' changes.append(f'reassigned to {new_name}') if comment_body: changes.append(f'new comment added by {current_user.username}') @@ -378,7 +379,7 @@ def create(): f'severity={issue.severity}; assigned_to={issue.assigned_to}') if issue.assigned_to: - assignee = User.query.get(issue.assigned_to) + assignee = db.session.get(User, issue.assigned_to) if assignee and assignee.id != current_user.id: notify( recipient = assignee, @@ -397,7 +398,7 @@ def create(): db.session.commit() # ── Notify customer portal users for this facility ────────── - area = Area.query.get(issue.area_id) + area = db.session.get(Area, issue.area_id) if area: notify_customers_for_facility( facility_id = area.facility_id, diff --git a/app/routes/notifications.py b/app/routes/notifications.py index aba239a..2fe8bd1 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -3,7 +3,7 @@ import logging from flask import (Blueprint, jsonify, request, abort, render_template, redirect, url_for, flash, current_app) from flask_login import login_required, current_user -from app import db +from app import db, csrf from app.models.notification import ( Notification, NotificationPreference, ALL_EVENT_TYPES ) @@ -166,6 +166,7 @@ def preferences(): # ── Digest trigger (called by cron) ─────────────────────────────────────────── @bp.route('/send-digest', methods=['POST']) +@csrf.exempt def send_digest(): """Trigger digest email delivery. Protected by a shared secret token. @@ -198,6 +199,7 @@ def send_digest(): # ── SLA alert trigger (called by cron) ──────────────────────────────────────── @bp.route('/check-sla', methods=['POST']) +@csrf.exempt def check_sla(): """Scan all open issues for SLA breaches and dispatch alerts. @@ -219,4 +221,4 @@ def check_sla(): sent = send_sla_alerts() logger.info('SLA CHECK TRIGGERED | notifications_sent=%s', sent) - return jsonify({'ok': True, 'notifications_sent': sent}) + return jsonify({'ok': True, 'notifications_sent': sent}) \ No newline at end of file diff --git a/app/routes/projects.py b/app/routes/projects.py index 6b34bd2..f66ec61 100644 --- a/app/routes/projects.py +++ b/app/routes/projects.py @@ -181,7 +181,7 @@ def add_assignment(project_id): db.session.add(assignment) db.session.commit() - user = User.query.get(form.user_id.data) + user = db.session.get(User, form.user_id.data) scope_label = f'facility_id={facility_id}' if facility_id else 'all facilities' logger.info('PROJECTS | assignment_add | admin=%s customer=%s project_id=%s scope=%s', current_user.username, user.username, project_id, scope_label) @@ -208,7 +208,7 @@ def remove_assignment(assignment_id): assignment = CustomerAssignment.query.get_or_404(assignment_id) project_id = assignment.project_id project = Project.query.get_or_404(project_id) - user = User.query.get(assignment.user_id) + user = db.session.get(User, assignment.user_id) username = user.username if user else f'user_id={assignment.user_id}' assignment_id_snap = assignment.id diff --git a/app/routes/scheduled_reports.py b/app/routes/scheduled_reports.py index 9ef538a..2d4ce3f 100644 --- a/app/routes/scheduled_reports.py +++ b/app/routes/scheduled_reports.py @@ -32,7 +32,7 @@ from flask_login import login_required, current_user from flask_mail import Message from sqlalchemy import func -from app import db, mail +from app import db, mail, csrf from app.models.scheduled_report import ScheduledReport from app.models.inspection import Inspection, InspectionTemplate from app.models.facility import Facility, Area @@ -419,6 +419,9 @@ def send_now(report_id): if ok: report.last_sent_at = now_eastern() db.session.commit() + log_action(ACTION_UPDATE, 'ScheduledReport', report.id, report.name, + f'manual send_now by {current_user.username}; ' + f'frequency={report.frequency}; recipients={len(report.recipient_list())}') flash(f'Report "{report.name}" sent successfully.', 'success') else: flash(f'Failed to send report "{report.name}". Check application logs.', 'danger') @@ -428,6 +431,7 @@ def send_now(report_id): # ── Cron endpoint ───────────────────────────────────────────────────────────── @bp.route('/send', methods=['POST']) +@csrf.exempt def send(): """Token-protected endpoint called by cron to dispatch due reports. @@ -452,12 +456,20 @@ def send(): sent, failed = 0, 0 for report in due: ok = _send_report(report) + # Always advance next_send_at so a failed report does not get + # retried on every subsequent cron run. last_sent_at is only + # updated on a successful delivery so the UI accurately reflects + # when the last good email was dispatched. + report.next_send_at = _compute_next_send(frequency, now) if ok: report.last_sent_at = now - report.next_send_at = _compute_next_send(frequency, now) sent += 1 else: failed += 1 + logger.error( + 'SCHEDULED REPORT FAILED | id=%s | name=%r | frequency=%s', + report.id, report.name, frequency, + ) db.session.commit() logger.info('SCHEDULED REPORTS CRON | frequency=%s | due=%s | sent=%s | failed=%s', diff --git a/app/routes/templates.py b/app/routes/templates.py index aabcd23..d4c3e7d 100644 --- a/app/routes/templates.py +++ b/app/routes/templates.py @@ -305,6 +305,9 @@ def create_checklist_item(template_id): ) db.session.add(item) db.session.commit() + 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}') flash('Checklist item added successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=template.id)) @@ -331,6 +334,9 @@ def edit_checklist_item(item_id): item.weight = form.weight.data item.requires_photo = form.requires_photo.data db.session.commit() + 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}') flash('Checklist item updated successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=item.template_id)) @@ -349,8 +355,12 @@ def edit_checklist_item(item_id): def delete_checklist_item(item_id): item = ChecklistItem.query.get_or_404(item_id) template_id = item.template_id + item_desc = item.item_description[:80] + item_id_snap = item.id db.session.delete(item) db.session.commit() + log_action(ACTION_DELETE, 'ChecklistItem', item_id_snap, item_desc, + f'template_id={template_id}') flash('Checklist item deleted successfully.', 'success') return redirect(url_for('templates.edit_template', template_id=template_id)) @@ -363,7 +373,7 @@ def reorder_items(template_id): item_order = request.json.get('item_order', []) for index, item_id in enumerate(item_order): - item = ChecklistItem.query.get(item_id) + item = db.session.get(ChecklistItem, item_id) if item and item.template_id == template.id: item.display_order = index