Jul 26 - Update scheduled inspection settings for iPad app

This commit is contained in:
2026-07-26 14:51:50 -04:00
parent 16858108b9
commit 3ab84ac016
2 changed files with 94 additions and 4 deletions
+85 -3
View File
@@ -95,6 +95,58 @@ def _parse_datetime(value):
return None
def _resolve_schedule(schedule_id, user):
"""Resolve a client-supplied scheduled_inspection_id, or None.
The iPad sends this when the inspector taps Start on a scheduled row;
without it the inspection lands unlinked and the schedule is never fulfilled
(no "Scheduled" badge, and the dashboard banner never clears).
NON-BLOCKING BY DESIGN. A bad link drops the link and logs a warning — it
never fails the submission. The app is offline-first, so a schedule can
legitimately be deleted or reassigned while a completed inspection sits in
the outbox for days; erroring here would retry-fail that inspection and
strand the inspector's work (and its photos) permanently. A missed fulfil is
recoverable from the web UI; a stranded submission is not.
The ownership check still matters: accepting a foreign link would let one
inspector fulfil another's schedule. So the link is refused — but the
inspection itself is still accepted.
"""
from app.models.scheduled_inspection import ScheduledInspection
sched = db.session.get(ScheduledInspection, schedule_id)
if sched is None:
logger.warning('API INSPECTIONS | unknown scheduled_inspection_id=%s from user=%s '
'— submitting unlinked', schedule_id, user.username)
return None
if user.role == 'inspector' and sched.inspector_id != user.id:
logger.warning('API INSPECTIONS | scheduled_inspection_id=%s not assigned to user=%s '
'— submitting unlinked', schedule_id, user.username)
return None
return sched
def _fulfill_schedule(inspection):
"""Roll the originating schedule forward / deactivate it. Caller commits.
Mirrors the web execute route: one-time schedules deactivate (so the
dashboard banner, which filters on active, disappears), recurring ones
advance to their next occurrence and reset the reminder flags.
"""
if not inspection.scheduled_inspection_id:
return
from app.models.scheduled_inspection import ScheduledInspection
sched = db.session.get(ScheduledInspection, inspection.scheduled_inspection_id)
if sched is None:
return
sched.fulfill()
logger.info('API INSPECTIONS | schedule fulfilled | schedule=%s | inspection=%s | next=%s',
sched.id, inspection.id,
sched.next_due_date if sched.active else 'deactivated')
def _media(key):
"""Absolute display URL for a storage key (presigned on R2, absolute-static
on local). '' for falsy keys. Used for iPad image rendering."""
@@ -157,6 +209,10 @@ def _inspection_payload(inspection):
'follow_up_required': inspection.follow_up_required,
'follow_up_note': inspection.follow_up_note,
'parent_inspection_id': inspection.parent_inspection_id,
# Set when this inspection was started from a ScheduledInspection —
# drives the "Scheduled" badge on the web list and lets the iPad show
# the same marker in history.
'scheduled_inspection_id': inspection.scheduled_inspection_id,
}
@@ -333,6 +389,12 @@ def create_inspection():
if parent is None:
return api_error('Parent inspection not found', 404)
# ── Optional schedule link (started from a ScheduledInspection) ───────
scheduled_inspection_id = None
if data.get('scheduled_inspection_id'):
sched = _resolve_schedule(data['scheduled_inspection_id'], user)
scheduled_inspection_id = sched.id if sched else None
# ── Score calculation ─────────────────────────────────────────────────
overall_score = data.get('overall_score')
if overall_score is None and status == 'completed':
@@ -383,11 +445,19 @@ def create_inspection():
parent_inspection_id = parent_inspection_id,
submit_latitude = submit_latitude,
submit_longitude = submit_longitude,
scheduled_inspection_id = scheduled_inspection_id,
)
db.session.add(inspection)
db.session.flush()
# ── Fulfil the originating schedule ───────────────────────────────────
# Staged into the same atomic commit as the inspection, mirroring the web
# execute route. Without this the schedule stays active: the dashboard
# banner and the iPad "Scheduled" section never clear.
if status == 'completed':
_fulfill_schedule(inspection)
# ── Auto-clear follow-up flag on parent ───────────────────────────────
# When a completed re-inspection arrives that links to a parent, clear
# follow_up_required on the parent automatically. This mirrors the web
@@ -512,6 +582,13 @@ def update_inspection(inspection_id):
prev_status = inspection.status
# Allow the link to be set/corrected on PATCH too — the iPad may create the
# inspection as a draft first and only attach the schedule on submit.
if data.get('scheduled_inspection_id'):
sched = _resolve_schedule(data['scheduled_inspection_id'], user)
if sched is not None:
inspection.scheduled_inspection_id = sched.id
if 'status' in data:
inspection.status = data['status']
@@ -527,12 +604,17 @@ def update_inspection(inspection_id):
elif data.get('status') == 'completed' and not inspection.completed_at:
inspection.completed_at = now_eastern()
db.session.commit()
# Notify when a draft transitions to completed — mirrors the POST handler.
transitioning_to_complete = (
data.get('status') == 'completed' and prev_status != 'completed'
)
# Fulfil the schedule on the draft → completed transition only, so a later
# PATCH on an already-completed inspection can't roll it forward twice.
if transitioning_to_complete:
_fulfill_schedule(inspection)
db.session.commit()
# Notify when a draft transitions to completed — mirrors the POST handler.
if transitioning_to_complete:
score_val = inspection.overall_score
score_display = f'{score_val:.1f}%' if score_val is not None else 'N/A'