05/25 Improvement 1
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
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, abort)
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app import db, limiter
|
||||
from app.models.inspection import (Inspection, InspectionTemplate,
|
||||
ChecklistItem, InspectionResult)
|
||||
from app.models.facility import Facility, Area
|
||||
@@ -28,6 +29,15 @@ bp = Blueprint('inspections', __name__, url_prefix='/inspections')
|
||||
|
||||
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
|
||||
|
||||
# Magic-byte signatures for allowed image formats.
|
||||
# Checked against the first 8 bytes of the upload to prevent extension spoofing.
|
||||
_IMAGE_MAGIC = (
|
||||
b'\xff\xd8\xff', # JPEG
|
||||
b'\x89PNG\r\n\x1a\n', # PNG
|
||||
b'GIF87a', # GIF 87a
|
||||
b'GIF89a', # GIF 89a
|
||||
)
|
||||
|
||||
INPUT_FIELD_TYPES = {
|
||||
'text', 'textarea', 'number', 'date', 'email',
|
||||
'checkbox', 'checkbox_group', 'radio', 'select',
|
||||
@@ -42,6 +52,11 @@ def _save_photo(file_obj, subfolder='inspection_photos'):
|
||||
ext = file_obj.filename.rsplit('.', 1)[-1].lower()
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
return None
|
||||
# Validate magic bytes to prevent extension-spoofed uploads.
|
||||
header = file_obj.read(8)
|
||||
file_obj.seek(0)
|
||||
if not any(header.startswith(m) for m in _IMAGE_MAGIC):
|
||||
return None
|
||||
filename = f"{uuid.uuid4().hex}.{ext}"
|
||||
dest_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], subfolder)
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
@@ -403,7 +418,9 @@ def execute(inspection_id):
|
||||
inspection.status = 'completed'
|
||||
inspection.completed_at = now_eastern()
|
||||
|
||||
_save_responses(inspection, responses)
|
||||
# Snapshot the current template schema so view() renders correctly
|
||||
# even if the template is later edited or deleted.
|
||||
_save_responses(inspection, responses, snapshot_schema=form_fields)
|
||||
# NOTE: do NOT commit here — inspection fields and all notification
|
||||
# rows are staged together and committed atomically below.
|
||||
|
||||
@@ -437,14 +454,20 @@ def execute(inspection_id):
|
||||
flash('Draft saved. You can continue filling in the form later.', 'success')
|
||||
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.active == True,
|
||||
).order_by(User.full_name, User.username).all()
|
||||
|
||||
return render_template('inspections/execute.html',
|
||||
inspection=inspection,
|
||||
form_fields=form_fields,
|
||||
saved_responses=saved_responses)
|
||||
saved_responses=saved_responses,
|
||||
staff_for_flag_issue=staff_for_flag_issue)
|
||||
|
||||
|
||||
def _save_responses(inspection, responses):
|
||||
"""Persist final form responses into inspection.notes as JSON."""
|
||||
def _save_responses(inspection, responses, snapshot_schema=None):
|
||||
"""Persist form responses (and optionally the template schema) into inspection.notes."""
|
||||
existing = {}
|
||||
if inspection.notes:
|
||||
try:
|
||||
@@ -452,6 +475,8 @@ def _save_responses(inspection, responses):
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
existing = {'_inspector_notes': inspection.notes}
|
||||
existing['_form_data'] = responses
|
||||
if snapshot_schema is not None:
|
||||
existing['_template_schema'] = snapshot_schema
|
||||
inspection.notes = json.dumps(existing)
|
||||
|
||||
|
||||
@@ -503,6 +528,7 @@ def save_draft_ajax(inspection_id):
|
||||
|
||||
@bp.route('/<int:inspection_id>/upload-photo', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("30 per minute")
|
||||
def upload_photo_ajax(inspection_id):
|
||||
inspection = db.session.get(Inspection, inspection_id)
|
||||
if inspection is None:
|
||||
@@ -544,9 +570,22 @@ def view(inspection_id):
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('inspections.index'))
|
||||
|
||||
template = inspection.template
|
||||
form_fields = sorted(template.get_form_schema(),
|
||||
key=lambda f: (f.get('row', 0), f.get('col', 0)))
|
||||
template = inspection.template
|
||||
|
||||
# Prefer the schema snapshotted at submit time so that edits to the template
|
||||
# after this inspection was completed do not corrupt the historical view.
|
||||
form_fields = None
|
||||
if inspection.notes:
|
||||
try:
|
||||
_snap = json.loads(inspection.notes)
|
||||
if isinstance(_snap, dict) and '_template_schema' in _snap:
|
||||
form_fields = sorted(_snap['_template_schema'],
|
||||
key=lambda f: (f.get('row', 0), f.get('col', 0)))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
if form_fields is None:
|
||||
form_fields = sorted(template.get_form_schema(),
|
||||
key=lambda f: (f.get('row', 0), f.get('col', 0)))
|
||||
|
||||
form_data = {}
|
||||
if inspection.notes:
|
||||
|
||||
Reference in New Issue
Block a user