Jul 26 - Update scheduled inspection settings for iPad app
This commit is contained in:
+85
-3
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user