Aug 27 - Update code to follow-up with ST functions
This commit is contained in:
+49
-2
@@ -228,6 +228,11 @@ def _inspection_payload(inspection):
|
||||
'inspector_notes': inspector_notes,
|
||||
# ── Follow-up / re-inspection fields ──────────────────────────────
|
||||
'follow_up_required': inspection.follow_up_required,
|
||||
# phase56 — who is to perform the follow-up. NULL means the
|
||||
# inspection's own inspector, which is what it always meant.
|
||||
'follow_up_assigned_to': inspection.follow_up_assigned_to,
|
||||
'follow_up_assigned_to_name': (inspection.follow_up_assignee.display_name
|
||||
if inspection.follow_up_assignee else None),
|
||||
'follow_up_note': inspection.follow_up_note,
|
||||
'parent_inspection_id': inspection.parent_inspection_id,
|
||||
# ── Originating schedule (MT-14) ──────────────────────────────────
|
||||
@@ -257,6 +262,9 @@ def list_inspections():
|
||||
offset int default 0
|
||||
facility_id int filter by facility
|
||||
status str filter by status (completed, in_progress, flagged)
|
||||
follow_up_required
|
||||
bool 'true'/'1' — only inspections awaiting a re-inspection,
|
||||
scoped to the caller's own follow-ups (see below)
|
||||
from_date str ISO date (YYYY-MM-DD) — include inspections on/after this date
|
||||
to_date str ISO date (YYYY-MM-DD) — include inspections on/before this date
|
||||
|
||||
@@ -282,8 +290,15 @@ def list_inspections():
|
||||
|
||||
query = Inspection.query
|
||||
|
||||
# Inspectors only see their own inspections
|
||||
if user.is_inspector:
|
||||
# Inspectors only see their own inspections.
|
||||
#
|
||||
# EXCEPT when asking for follow-up requests: a follow-up can now be handed
|
||||
# to a different inspector (phase56), and that request lives on an
|
||||
# inspection somebody ELSE performed. Applying this filter first would hide
|
||||
# exactly the rows the assignee needs, so it is deferred to the follow-up
|
||||
# block below, which applies ownership instead of authorship.
|
||||
wants_follow_ups = request.args.get('follow_up_required', '').lower() in ('true', '1')
|
||||
if user.is_inspector and not wants_follow_ups:
|
||||
query = query.filter(Inspection.inspector_id == user.id)
|
||||
|
||||
# Optional filters
|
||||
@@ -295,6 +310,38 @@ def list_inspections():
|
||||
if status:
|
||||
query = query.filter(Inspection.status == status)
|
||||
|
||||
if wants_follow_ups:
|
||||
# MT had no follow_up_required filter at all, so the iPad's Follow-up
|
||||
# Requests screen — which calls ?follow_up_required=true — received the
|
||||
# inspector's ENTIRE history and presented it as outstanding requests.
|
||||
#
|
||||
# "Follow-up" must mean exactly what it means everywhere on the web
|
||||
# (inspections.index / reports status_filter == 'follow_up'): flagged,
|
||||
# completed, and not yet answered by a linked re-inspection.
|
||||
#
|
||||
# The ~follow_ups.any() clause is the one that matters. The web execute
|
||||
# route never clears follow_up_required on the parent — it only stops
|
||||
# listing it once a child exists — so filtering on the flag alone would
|
||||
# return follow-ups that were already satisfied on the web, forever.
|
||||
# On the iPad those rows are undismissable: pull_follow_up_requests()
|
||||
# keeps receiving them and update(from:) resets fulfilledLocally, so the
|
||||
# FOLLOW-UP REQUESTED card would never clear. (The mobile POST path does
|
||||
# clear the parent flag, so only web-completed re-inspections stick.)
|
||||
query = query.filter(
|
||||
Inspection.follow_up_required.is_(True),
|
||||
Inspection.status == 'completed',
|
||||
).filter(~Inspection.follow_ups.any())
|
||||
|
||||
# Ownership, not authorship (phase56). Mirrors
|
||||
# Inspection.follow_up_owner: an assigned follow-up belongs to the
|
||||
# assignee ALONE, an unassigned one to the inspection's own inspector.
|
||||
#
|
||||
# The two arms are mutually exclusive on purpose. Without the second
|
||||
# arm's `is_(None)` an inspector would keep seeing a follow-up that had
|
||||
# been handed to someone else, and two people would turn up to do it.
|
||||
if user.is_inspector:
|
||||
query = query.filter(Inspection.follow_up_owned_by(user.id))
|
||||
|
||||
from_date_str = request.args.get('from_date')
|
||||
if from_date_str:
|
||||
try:
|
||||
|
||||
+21
-1
@@ -79,16 +79,27 @@ def upload_photo():
|
||||
if user.role not in _ALLOWED_ROLES:
|
||||
return api_error('Access denied', 403)
|
||||
|
||||
# Every rejection below is logged at WARNING with the user. Only SUCCESSES
|
||||
# were logged before, so when an inspector's photos failed repeatedly there
|
||||
# was nothing server-side to explain why — and a photo that exhausts its
|
||||
# upload attempts costs the inspection its evidence (see the iPad's
|
||||
# PendingPhoto.lastUploadError for the device half of this).
|
||||
if 'file' not in request.files:
|
||||
logger.warning('API PHOTOS | rejected | reason=no_file_part | user=%s',
|
||||
user.username)
|
||||
return api_error('No file provided', 400)
|
||||
|
||||
file_obj = request.files['file']
|
||||
entity_type = request.form.get('entity_type', 'inspection')
|
||||
|
||||
if not file_obj or not file_obj.filename:
|
||||
logger.warning('API PHOTOS | rejected | reason=empty_file | user=%s',
|
||||
user.username)
|
||||
return api_error('Empty file', 400)
|
||||
|
||||
if not _allowed_file(file_obj.filename):
|
||||
logger.warning('API PHOTOS | rejected | reason=bad_extension | file=%r | user=%s',
|
||||
file_obj.filename, user.username)
|
||||
return api_error(
|
||||
f'File type not allowed. Accepted: {", ".join(sorted(_ALLOWED_EXTENSIONS))}',
|
||||
400
|
||||
@@ -120,7 +131,16 @@ def upload_photo():
|
||||
# stamped FileStorage keeps the original filename, so the derived key — and
|
||||
# the tenant prefix applied inside S3Backend — are unaffected.
|
||||
from app.utils import storage
|
||||
server_path = storage.save(file_obj, subfolder)
|
||||
try:
|
||||
server_path = storage.save(file_obj, subfolder)
|
||||
except Exception as exc:
|
||||
# A storage failure is the most likely cause of a REPEATED upload
|
||||
# failure (disk full, R2 credentials/quota). Name it explicitly —
|
||||
# otherwise it surfaces only as a generic 500 with no link to the
|
||||
# inspector who is losing evidence photos.
|
||||
logger.error('API PHOTOS | STORAGE WRITE FAILED | user=%s | entity_type=%s | '
|
||||
'subfolder=%s | error=%s', user.username, entity_type, subfolder, exc)
|
||||
return api_error('Could not store the photo. Please retry.', 500)
|
||||
|
||||
logger.info(
|
||||
'API PHOTOS | uploaded | entity_type=%s | path=%s | user=%s | '
|
||||
|
||||
+6
-1
@@ -152,9 +152,14 @@ def dashboard_stats():
|
||||
if not fids:
|
||||
followup_q = followup_q.filter(False)
|
||||
else:
|
||||
# OWNERSHIP, not authorship: a follow-up handed to this inspector
|
||||
# belongs to them even though somebody else performed the original.
|
||||
# This tile sits directly above the Follow-up Requests list, which
|
||||
# filters the same way — counting authorship here made the two
|
||||
# disagree on the same screen.
|
||||
followup_q = followup_q.filter(
|
||||
Inspection.facility_id.in_(fids),
|
||||
Inspection.inspector_id == user.id,
|
||||
Inspection.follow_up_owned_by(user.id),
|
||||
)
|
||||
pending_followups = followup_q.count()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user