diff --git a/app/routes/audit.py b/app/routes/audit.py index 45113b9..ab07e62 100644 --- a/app/routes/audit.py +++ b/app/routes/audit.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timedelta -from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask import Blueprint, render_template, request, redirect, url_for, flash, abort from flask_login import login_required from app import db from app.models.audit import AuditLog @@ -88,7 +88,9 @@ def index(): @login_required @admin_required def view(log_id): - entry = AuditLog.query.get_or_404(log_id) + entry = db.session.get(AuditLog, log_id) + if entry is None: + abort(404) return render_template('audit/view.html', entry=entry) diff --git a/app/routes/auth.py b/app/routes/auth.py index dd9fb2b..b1b7f45 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -171,7 +171,9 @@ def create_user(): @login_required @supervisor_required def edit_user(user_id): - user = User.query.get_or_404(user_id) + user = db.session.get(User, user_id) + if user is None: + abort(404) form = UserForm(user=user, obj=user) if form.validate_on_submit(): @@ -198,7 +200,9 @@ def edit_user(user_id): @login_required @supervisor_required def delete_user(user_id): - user = User.query.get_or_404(user_id) + user = db.session.get(User, user_id) + if user is None: + abort(404) if user.id == current_user.id: flash('Cannot delete your own account.', 'danger') @@ -229,7 +233,9 @@ def delete_user(user_id): @login_required @supervisor_required def toggle_active(user_id): - user = User.query.get_or_404(user_id) + user = db.session.get(User, user_id) + if user is None: + abort(404) if user.id == current_user.id: flash('You cannot disable your own account.', 'danger') diff --git a/app/routes/customers.py b/app/routes/customers.py index 7081079..e425e2a 100644 --- a/app/routes/customers.py +++ b/app/routes/customers.py @@ -13,7 +13,7 @@ Provides a single screen to: """ import logging -from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask import Blueprint, render_template, redirect, url_for, flash, request, abort from flask_login import login_required, current_user from app import db from app.models.user import User @@ -238,7 +238,9 @@ def _send_invite_email(user, token): @supervisor_required def resend_invite(customer_id): """Generate a fresh token and resend the set-password invitation email.""" - customer = User.query.get_or_404(customer_id) + customer = db.session.get(User, customer_id) + if customer is None: + abort(404) if customer.role != 'customer': flash('This action is only for customer accounts.', 'warning') return redirect(url_for('customers.index')) @@ -297,7 +299,9 @@ def set_password(token): @login_required @supervisor_required def edit(customer_id): - customer = User.query.get_or_404(customer_id) + customer = db.session.get(User, customer_id) + if customer is None: + abort(404) if customer.role != 'customer': flash('This page is only for customer accounts.', 'warning') return redirect(url_for('customers.index')) @@ -329,7 +333,9 @@ def edit(customer_id): @supervisor_required def manage(customer_id): """Single-customer detail page: profile + all assignments.""" - customer = User.query.get_or_404(customer_id) + customer = db.session.get(User, customer_id) + if customer is None: + abort(404) if customer.role != 'customer': flash('This page is only for customer accounts.', 'warning') return redirect(url_for('customers.index')) @@ -365,7 +371,9 @@ def manage(customer_id): @login_required @supervisor_required def add_assignment(customer_id): - customer = User.query.get_or_404(customer_id) + customer = db.session.get(User, customer_id) + if customer is None: + abort(404) if customer.role != 'customer': flash('Assignments are only for customer accounts.', 'warning') return redirect(url_for('customers.index')) @@ -377,7 +385,9 @@ def add_assignment(customer_id): flash('Please select a contract.', 'warning') return redirect(url_for('customers.manage', customer_id=customer_id)) - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) # Guard: duplicate assignment existing = CustomerAssignment.query.filter_by( @@ -414,7 +424,9 @@ def add_assignment(customer_id): @login_required @supervisor_required def remove_assignment(assignment_id): - assignment = CustomerAssignment.query.get_or_404(assignment_id) + assignment = db.session.get(CustomerAssignment, assignment_id) + if assignment is None: + abort(404) customer_id = assignment.user_id customer = db.session.get(User, customer_id) project = db.session.get(Project, assignment.project_id) @@ -439,7 +451,9 @@ def remove_assignment(assignment_id): @login_required @supervisor_required def toggle_active(customer_id): - customer = User.query.get_or_404(customer_id) + customer = db.session.get(User, customer_id) + if customer is None: + abort(404) if customer.role != 'customer': flash('This action is only for customer accounts.', 'warning') return redirect(url_for('customers.index')) @@ -747,6 +761,8 @@ def bulk_import(): @supervisor_required def facilities_for_project(project_id): from flask import jsonify - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) facilities = project.facilities.filter_by(active=True).order_by(Facility.name).all() return jsonify([{'id': f.id, 'name': f.name} for f in facilities]) \ No newline at end of file diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index b4c539b..49c662f 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -68,6 +68,15 @@ def index(): ).filter(Area.facility_id.in_(customer_facility_ids)) open_issues = open_issues_q.count() + # Severity breakdown for the open issues card + open_issues_all = open_issues_q.all() + severity_breakdown = { + 'critical': sum(1 for i in open_issues_all if i.severity == 'critical'), + 'high': sum(1 for i in open_issues_all if i.severity == 'high'), + 'medium': sum(1 for i in open_issues_all if i.severity == 'medium'), + 'low': sum(1 for i in open_issues_all if i.severity == 'low'), + } + # ── Average score (last 30 days) ─────────────────────────────────────── score_q = db.session.query(func.avg(Inspection.overall_score)).filter( Inspection.status == 'completed', @@ -195,6 +204,7 @@ def index(): today_inspections = today_inspections, completed_today = completed_today, open_issues = open_issues, + severity_breakdown = severity_breakdown, avg_score = round(avg_score, 2) if avg_score else None, recent_inspections = recent_inspections, total_facilities = total_facilities, diff --git a/app/routes/facilities.py b/app/routes/facilities.py index 56049d6..7a2c983 100644 --- a/app/routes/facilities.py +++ b/app/routes/facilities.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask import Blueprint, render_template, redirect, url_for, flash, request, abort from flask_login import login_required, current_user from app import db from app.models.facility import Facility, Area @@ -53,7 +53,9 @@ def create_facility(): @bp.route('/') @login_required def view_facility(facility_id): - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) if current_user.role == 'customer': cids = get_customer_scope(current_user) or [] if facility_id not in cids: @@ -66,7 +68,9 @@ def view_facility(facility_id): @login_required @supervisor_required def edit_facility(facility_id): - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) form = FacilityForm(obj=facility) projects = Project.query.filter_by(active=True).order_by(Project.name).all() form.project_id.choices = [(0, '— None —')] + [(p.id, p.name) for p in projects] @@ -92,7 +96,9 @@ def edit_facility(facility_id): @login_required @admin_required def delete_facility(facility_id): - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) if facility.inspections.count() > 0: flash(f'Cannot delete "{facility.name}" — it has existing inspection records.', 'danger') @@ -112,7 +118,9 @@ def delete_facility(facility_id): @login_required @supervisor_required def create_area(facility_id): - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) form = AreaForm() form.facility_id.choices = [(facility.id, facility.name)] form.facility_id.data = facility.id @@ -137,7 +145,9 @@ def create_area(facility_id): @login_required @supervisor_required def edit_area(area_id): - area = Area.query.get_or_404(area_id) + area = db.session.get(Area, area_id) + if area is None: + abort(404) form = AreaForm(obj=area) facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all() @@ -160,7 +170,9 @@ def edit_area(area_id): @login_required @supervisor_required def delete_area(area_id): - area = Area.query.get_or_404(area_id) + area = db.session.get(Area, area_id) + if area is None: + abort(404) facility_id = area.facility_id if area.inspections.count() > 0: diff --git a/app/routes/inspections.py b/app/routes/inspections.py index 6e5a9c3..513a89d 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime from app.utils.time_utils import now_eastern from flask import (Blueprint, render_template, redirect, url_for, - flash, request, current_app, jsonify, Response) + flash, request, current_app, jsonify, Response, abort) from flask_login import login_required, current_user from app import db from app.models.inspection import (Inspection, InspectionTemplate, @@ -218,7 +218,8 @@ def index(): facilities=facilities, status_filter=status_filter, facility_filter=facility_filter, - follow_up_filter=follow_up_filter) + follow_up_filter=follow_up_filter, + now=now_eastern()) # ── Start ───────────────────────────────────────────────────────────────────── @@ -246,7 +247,9 @@ def start(): form.area_id.choices = [(0, '— No specific area —')] + [(a.id, a.name) for a in areas] if form.validate_on_submit(): - template = InspectionTemplate.query.get_or_404(form.template_id.data) + template = db.session.get(InspectionTemplate, form.template_id.data) + if template is None: + abort(404) if not template.get_form_schema(): flash('This template has no form fields yet. Please build the form in the template editor first.', 'warning') @@ -290,7 +293,9 @@ def areas_for_facility(facility_id): @bp.route('//execute', methods=['GET', 'POST']) @login_required def execute(inspection_id): - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) if current_user.role == 'inspector' and inspection.inspector_id != current_user.id: flash('Access denied.', 'danger') @@ -430,7 +435,9 @@ def _save_draft(inspection, responses): @bp.route('//save-draft', methods=['POST']) @login_required def save_draft_ajax(inspection_id): - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) if current_user.role == 'inspector' and inspection.inspector_id != current_user.id: return jsonify({'ok': False, 'error': 'Access denied'}), 403 @@ -466,7 +473,9 @@ def save_draft_ajax(inspection_id): @bp.route('/') @login_required def view(inspection_id): - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) if current_user.role == 'inspector' and inspection.inspector_id != current_user.id: flash('Access denied.', 'danger') @@ -665,7 +674,9 @@ def view(inspection_id): @bp.route('//flag-issue', methods=['GET', 'POST']) @login_required def flag_issue(inspection_id): - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) if current_user.role == 'inspector' and inspection.inspector_id != current_user.id: flash('Access denied.', 'danger') @@ -751,7 +762,9 @@ def flag_issue(inspection_id): @login_required def export_pdf(inspection_id): """Generate and stream a PDF report for the given inspection.""" - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) if current_user.role == 'inspector' and inspection.inspector_id != current_user.id: flash('Access denied.', 'danger') @@ -821,7 +834,9 @@ def export_pdf(inspection_id): @supervisor_required def flag_followup(inspection_id): """Mark an inspection as requiring a follow-up re-inspection.""" - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) note = request.form.get('follow_up_note', '').strip() or None inspection.follow_up_required = True @@ -844,7 +859,9 @@ def flag_followup(inspection_id): @supervisor_required def clear_followup(inspection_id): """Clear the follow-up required flag once actioned.""" - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) inspection.follow_up_required = False inspection.follow_up_note = None db.session.commit() @@ -863,7 +880,9 @@ def reinspect(inspection_id): """Pre-fill the Start Inspection form with the same template/facility, linking the new inspection to the parent via parent_inspection_id.""" from flask import session - parent = Inspection.query.get_or_404(inspection_id) + parent = db.session.get(Inspection, inspection_id) + if parent is None: + abort(404) if current_user.role == 'customer': flash('Access denied.', 'danger') @@ -886,7 +905,9 @@ def reinspect(inspection_id): @login_required @supervisor_required def delete(inspection_id): - inspection = Inspection.query.get_or_404(inspection_id) + inspection = db.session.get(Inspection, inspection_id) + if inspection is None: + abort(404) insp_id = inspection.id insp_date = inspection.inspection_date.strftime('%Y-%m-%d %H:%M') diff --git a/app/routes/issues.py b/app/routes/issues.py index b3222aa..1e554f7 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -1,7 +1,8 @@ import os +import logging from app.utils.time_utils import now_eastern from flask import (Blueprint, render_template, redirect, url_for, - flash, request, current_app, jsonify) + flash, request, current_app, jsonify, abort) from flask_login import login_required, current_user from app import db from app.models.issue import Issue, IssueComment, IssueFollower @@ -21,6 +22,8 @@ from app.utils.sla import sla_status bp = Blueprint('issues', __name__, url_prefix='/issues') +logger = logging.getLogger(__name__) + # ── Shared helper ───────────────────────────────────────────────────────────── @@ -65,10 +68,16 @@ def index(): severity_filter = request.args.get('severity', '') status_filter = request.args.get('status', '') sla_filter = request.args.get('sla', '') + facility_filter = request.args.get('facility_id', '') + if severity_filter: q = q.filter(Issue.severity == severity_filter) if status_filter: q = q.filter(Issue.status == status_filter) + if facility_filter: + q = q.join(Area, Issue.area_id == Area.id, isouter=True).filter( + Area.facility_id == int(facility_filter) + ) # SLA filter — applied in Python after DB query since SLA is computed issues_paged = q.paginate(page=page, per_page=25, error_out=False) @@ -86,12 +95,29 @@ def index(): for f in IssueFollower.query.filter_by(user_id=current_user.id).all() } + # Facilities for the filter dropdown — scoped for customers, full list otherwise + if current_user.role == 'customer': + facility_ids = get_customer_scope(current_user) or [] + facilities = Facility.query.filter( + Facility.id.in_(facility_ids), Facility.active == True + ).order_by(Facility.name).all() + else: + facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all() + + # 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 + ).order_by(User.username).all() + return render_template('issues/list.html', issues=issues_paged, issue_items=filtered_items, severity_filter=severity_filter, status_filter=status_filter, sla_filter=sla_filter, + facility_filter=facility_filter, + facilities=facilities, + staff=staff, followed_ids=followed_ids) @@ -100,7 +126,9 @@ def index(): @bp.route('/', methods=['GET', 'POST']) @login_required def view(issue_id): - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) if current_user.role == 'inspector' and issue.assigned_to != current_user.id: flash('Access denied. You can only view issues assigned to you.', 'danger') @@ -306,7 +334,9 @@ def view(issue_id): @bp.route('//follow', methods=['POST']) @login_required def follow(issue_id): - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) if not issue.is_followed_by(current_user): follower = IssueFollower(issue_id=issue.id, user_id=current_user.id) db.session.add(follower) @@ -326,7 +356,9 @@ def follow(issue_id): @bp.route('//unfollow', methods=['POST']) @login_required def unfollow(issue_id): - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) follower = issue.followers.filter_by(user_id=current_user.id).first() if follower: db.session.delete(follower) @@ -430,7 +462,9 @@ def create(): @supervisor_required def verify(issue_id): """Supervisor sign-off: confirms resolution is satisfactory and closes the issue.""" - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) if issue.status not in ('resolved', 'pending_verification'): flash('Only resolved or pending-verification issues can be verified.', 'warning') @@ -461,7 +495,9 @@ def verify(issue_id): @login_required def request_verification(issue_id): """Inspector/assignee marks the issue as pending director verification.""" - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) if current_user.role == 'customer': flash('Access denied.', 'danger') @@ -559,7 +595,9 @@ def delete(issue_id): Restricted to admin and director roles. The deletion is recorded in the audit log before the record is removed so there is always a trace. """ - issue = Issue.query.get_or_404(issue_id) + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) # Snapshot fields needed for logging before deletion issue_id_snap = issue.id @@ -598,3 +636,56 @@ def delete(issue_id): flash(f'Issue #{issue_id_snap} has been permanently deleted.', 'success') return redirect(url_for('issues.index')) + + +# ── Quick-assign (AJAX) ─────────────────────────────────────────────────────── + +@bp.route('//quick-assign', methods=['POST']) +@login_required +def quick_assign(issue_id): + """Inline assignee update from the issues list — returns JSON.""" + if current_user.role not in ('admin', 'director'): + return jsonify({'ok': False, 'error': 'Permission denied'}), 403 + + issue = db.session.get(Issue, issue_id) + if issue is None: + abort(404) + data = request.get_json(silent=True) or {} + new_user_id = data.get('user_id') # int or None (unassign) + + old_assigned_to = issue.assigned_to + + if new_user_id: + user = db.session.get(User, int(new_user_id)) + if not user: + return jsonify({'ok': False, 'error': 'User not found'}), 404 + issue.assigned_to = user.id + label = user.display_name + else: + issue.assigned_to = None + label = '— Unassigned —' + + db.session.commit() + + # Notify new assignee if changed + if new_user_id and old_assigned_to != issue.assigned_to: + from app.utils.notifications import notify + from app.models.notification import EVENT_ISSUE_ASSIGNED + notify( + recipient = user, + title = f'Issue #{issue.id} Assigned to You', + body = (f'You have been assigned Issue #{issue.id} ' + f'({issue.severity} severity) by {current_user.display_name}.'), + link = url_for('issues.view', issue_id=issue.id), + issue_id = issue.id, + event_type = EVENT_ISSUE_ASSIGNED, + send_email = True, + ) + + log_action(ACTION_UPDATE, 'Issue', issue.id, + f'#{issue.id}', + f'quick-assign: assigned_to={label} by {current_user.username}') + logger.info('ISSUE QUICK-ASSIGN | issue_id=%s | assigned_to=%s | by=%s', + issue.id, label, current_user.username) + + return jsonify({'ok': True, 'label': label}) diff --git a/app/routes/notifications.py b/app/routes/notifications.py index 2fe8bd1..0e8a8b4 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -80,7 +80,9 @@ def index(): @bp.route('//mark-read', methods=['POST']) @login_required def mark_read(notif_id): - notif = Notification.query.get_or_404(notif_id) + notif = db.session.get(Notification, notif_id) + if notif is None: + abort(404) if notif.user_id != current_user.id: abort(403) notif.is_read = True diff --git a/app/routes/projects.py b/app/routes/projects.py index 2255a30..17e2bd3 100644 --- a/app/routes/projects.py +++ b/app/routes/projects.py @@ -10,7 +10,7 @@ Access matrix: """ import logging -from flask import Blueprint, render_template, redirect, url_for, flash, request +from flask import Blueprint, render_template, redirect, url_for, flash, request, abort from flask_login import login_required, current_user from app import db from app.models.project import Project, CustomerAssignment @@ -72,7 +72,9 @@ def create(): @login_required @project_manager_required def view(project_id): - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) facilities = project.facilities.order_by(Facility.name).all() assignments = ( CustomerAssignment.query @@ -95,7 +97,9 @@ def view(project_id): @login_required @supervisor_required def edit(project_id): - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) form = ProjectForm(obj=project) pm_users = User.query.filter_by(role='project_manager', active=True).order_by(User.username).all() form.project_manager_id.choices = [(0, '— None —')] + [(u.id, u.username) for u in pm_users] @@ -123,7 +127,9 @@ def edit(project_id): @login_required @admin_required def delete(project_id): - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) if project.facilities.count() > 0: flash(f'Cannot delete "{project.name}" — it has linked facilities. ' @@ -147,7 +153,9 @@ def delete(project_id): @login_required @admin_required def add_assignment(project_id): - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) form = CustomerAssignmentForm() # Customer users only @@ -205,9 +213,13 @@ def add_assignment(project_id): @login_required @admin_required def remove_assignment(assignment_id): - assignment = CustomerAssignment.query.get_or_404(assignment_id) + assignment = db.session.get(CustomerAssignment, assignment_id) + if assignment is None: + abort(404) project_id = assignment.project_id - project = Project.query.get_or_404(project_id) + project = db.session.get(Project, project_id) + if project is None: + abort(404) user = db.session.get(User, assignment.user_id) username = user.username if user else f'user_id={assignment.user_id}' diff --git a/app/routes/reports.py b/app/routes/reports.py index 6ebc75f..3b97be0 100644 --- a/app/routes/reports.py +++ b/app/routes/reports.py @@ -3,7 +3,7 @@ import io from datetime import datetime, timedelta from app.utils.time_utils import now_eastern from flask import (Blueprint, render_template, request, - Response, stream_with_context) + Response, stream_with_context, abort) from flask_login import login_required, current_user from sqlalchemy import func from app import db @@ -183,7 +183,9 @@ def facility_report(facility_id): from flask import flash, redirect, url_for flash('Access denied.', 'danger') return redirect(url_for('dashboard.index')) - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) if current_user.role == 'customer': cids = get_customer_scope(current_user) or [] if facility_id not in cids: @@ -233,7 +235,9 @@ def facility_scorecard(facility_id): flash('Access denied.', 'danger') return redirect(url_for('dashboard.index')) - facility = Facility.query.get_or_404(facility_id) + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) if current_user.role == 'customer': cids = get_customer_scope(current_user) or [] diff --git a/app/routes/scheduled_reports.py b/app/routes/scheduled_reports.py index 2d4ce3f..b2ee39e 100644 --- a/app/routes/scheduled_reports.py +++ b/app/routes/scheduled_reports.py @@ -27,7 +27,7 @@ import logging from datetime import datetime, timedelta from flask import (Blueprint, render_template, redirect, url_for, flash, - request, jsonify, current_app) + request, jsonify, current_app, abort) from flask_login import login_required, current_user from flask_mail import Message from sqlalchemy import func @@ -317,7 +317,9 @@ def create(): @login_required @supervisor_required def edit(report_id): - report = ScheduledReport.query.get_or_404(report_id) + report = db.session.get(ScheduledReport, report_id) + if report is None: + abort(404) facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all() if request.method == 'POST': @@ -345,7 +347,9 @@ def edit(report_id): @login_required @supervisor_required def delete(report_id): - report = ScheduledReport.query.get_or_404(report_id) + report = db.session.get(ScheduledReport, report_id) + if report is None: + abort(404) name = report.name rid = report.id db.session.delete(report) @@ -360,7 +364,9 @@ def delete(report_id): @supervisor_required def preview(report_id): """Render the scheduled report email in-browser for review.""" - report = ScheduledReport.query.get_or_404(report_id) + report = db.session.get(ScheduledReport, report_id) + if report is None: + abort(404) start, end = _date_window(report.frequency) data = _build_report_data(report, start, end) base_url = current_app.config.get('APP_BASE_URL', '').rstrip('/') @@ -382,7 +388,9 @@ def preview_pdf(report_id): from flask import Response from app.utils.pdf_export import generate_scheduled_report_pdf - report = ScheduledReport.query.get_or_404(report_id) + report = db.session.get(ScheduledReport, report_id) + if report is None: + abort(404) start, end = _date_window(report.frequency) data = _build_report_data(report, start, end) @@ -414,7 +422,9 @@ def preview_pdf(report_id): @supervisor_required def send_now(report_id): """Manually trigger a single report — useful for testing.""" - report = ScheduledReport.query.get_or_404(report_id) + report = db.session.get(ScheduledReport, report_id) + if report is None: + abort(404) ok = _send_report(report) if ok: report.last_sent_at = now_eastern() diff --git a/app/routes/templates.py b/app/routes/templates.py index d4c3e7d..b8fda17 100644 --- a/app/routes/templates.py +++ b/app/routes/templates.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +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 from app.models.inspection import InspectionTemplate, ChecklistItem @@ -48,7 +48,9 @@ def create_template(): @bp.route('/') @login_required def view_template(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) form_fields = template.get_form_schema() return render_template( 'templates/view.html', @@ -61,7 +63,9 @@ def view_template(template_id): @login_required @supervisor_required def edit_template(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) form = InspectionTemplateForm(obj=template) if form.validate_on_submit(): @@ -87,7 +91,9 @@ def edit_template(template_id): @login_required @supervisor_required def rename_template(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) new_name = request.form.get('name', '').strip() if not new_name: @@ -118,7 +124,9 @@ def rename_template(template_id): @login_required @supervisor_required def delete_template(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) if template.inspections.count() > 0: flash('Cannot delete template with existing inspections.', 'danger') @@ -138,7 +146,9 @@ def delete_template(template_id): @login_required @supervisor_required def duplicate_template(template_id): - src = InspectionTemplate.query.get_or_404(template_id) + src = db.session.get(InspectionTemplate, template_id) + if src is None: + abort(404) # Duplicate the template header new_tpl = InspectionTemplate( @@ -183,7 +193,9 @@ def duplicate_template(template_id): @login_required @supervisor_required def form_editor(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) form_schema = template.get_form_schema() return render_template( 'templates/form_editor.html', @@ -197,7 +209,9 @@ def form_editor(template_id): @supervisor_required def save_form_schema(template_id): """AJAX endpoint — receives the full form schema as JSON and persists it.""" - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) data = request.get_json(silent=True) if data is None: @@ -270,7 +284,9 @@ def save_form_schema(template_id): @login_required def form_preview(template_id): """Renders a read-only preview of the dynamic form.""" - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) form_fields = template.get_form_schema() return render_template( 'templates/form_preview.html', @@ -287,7 +303,9 @@ def form_preview(template_id): @login_required @supervisor_required def create_checklist_item(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) form = ChecklistItemForm() if form.validate_on_submit(): @@ -324,7 +342,9 @@ def create_checklist_item(template_id): @login_required @supervisor_required def edit_checklist_item(item_id): - item = ChecklistItem.query.get_or_404(item_id) + item = db.session.get(ChecklistItem, item_id) + if item is None: + abort(404) form = ChecklistItemForm(obj=item) if form.validate_on_submit(): @@ -353,7 +373,9 @@ def edit_checklist_item(item_id): @login_required @supervisor_required def delete_checklist_item(item_id): - item = ChecklistItem.query.get_or_404(item_id) + item = db.session.get(ChecklistItem, item_id) + if item is None: + abort(404) template_id = item.template_id item_desc = item.item_description[:80] item_id_snap = item.id @@ -369,7 +391,9 @@ def delete_checklist_item(item_id): @login_required @supervisor_required def reorder_items(template_id): - template = InspectionTemplate.query.get_or_404(template_id) + template = db.session.get(InspectionTemplate, template_id) + if template is None: + abort(404) item_order = request.json.get('item_order', []) for index, item_id in enumerate(item_order): diff --git a/app/templates/base.html b/app/templates/base.html index 916dc52..81c8348 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -99,38 +99,38 @@