Aug 25 - Implement new function allow Director (internal & customer) to assign and inspection to another inspector

This commit is contained in:
2026-08-25 12:24:34 -04:00
parent b31f5f03da
commit aced3d0602
6 changed files with 286 additions and 8 deletions
+73 -3
View File
@@ -969,7 +969,9 @@ def view(inspection_id):
'unchanged': sum(1 for r in rows if r['delta'] == 0),
}
followup_assignees = _followup_assignees_for(inspection, current_user)
return render_template('inspections/view.html',
followup_assignees=followup_assignees,
inspection=inspection,
form_fields=form_fields,
form_data=form_data,
@@ -1394,6 +1396,46 @@ def _view_url(inspection_id):
return url_for('inspections.view', inspection_id=inspection_id)
def _followup_assignees_for(inspection, actor):
"""Inspectors who may be handed this inspection's follow-up.
Contract-scoped, for the same reason the flag-issue list is (rule 93): a
Customer Director must never see — let alone assign work to — another
client's inspector, and one of our own directors picking the wrong name
would leak this facility to an outsider.
Only the two INSPECTOR roles are offered: a follow-up is an inspection, and
directors/PMs/auditors hold no InspectorAssignment, so they cannot be
scoped to a contract and could not open the re-inspection anyway.
A facility with no contract yields nobody — fail-closed, leaving the
follow-up with the original inspector.
"""
from app.models.inspector_assignment import InspectorAssignment
project_id = inspection.facility.project_id if inspection.facility else None
if not project_id:
return []
users = (
User.query
.join(InspectorAssignment, InspectorAssignment.user_id == User.id)
.filter(
InspectorAssignment.project_id == project_id,
User.role.in_(User.INSPECTOR_ROLES),
User.active == True,
)
.order_by(User.full_name, User.username)
.all()
)
seen, out = set(), []
for u in users: # the join repeats across assignments
if u.id not in seen:
seen.add(u.id)
out.append(u)
return out
def _collect_inspection_photos(inspection):
"""Relative storage keys owned by an inspection, for cleanup after delete.
@@ -1597,6 +1639,7 @@ def bulk_action():
insp.follow_up_note = None
insp.follow_up_requested_by = None
insp.follow_up_requested_at = None
insp.follow_up_assigned_to = None
cleared.append(insp)
changed += 1
db.session.commit()
@@ -1661,22 +1704,48 @@ def flag_followup(inspection_id):
note = request.form.get('follow_up_note', '').strip() or None
# ── Assignee (phase53) ────────────────────────────────────────────────
# Optional. Blank keeps the original behaviour: the follow-up belongs to
# the inspection's own inspector. Validated against the contract-scoped
# list rather than trusted, so a crafted id cannot hand work to another
# customer's inspector (and tell them this facility's name in the email).
assignee_id = request.form.get('follow_up_assigned_to', type=int) or None
if assignee_id:
allowed = {u.id for u in _followup_assignees_for(inspection, current_user)}
if assignee_id not in allowed:
current_app.logger.warning(
'FOLLOW-UP | out-of-contract assignee blocked | inspection=%s | '
'assignee=%s | by=%s',
inspection_id, assignee_id, current_user.username)
flash('That inspector is not assigned to this facility\'s contract.',
'danger')
return redirect(_view_url(inspection_id))
inspection.follow_up_required = True
inspection.follow_up_note = note
inspection.follow_up_requested_by = current_user.id
inspection.follow_up_requested_at = now_eastern()
inspection.follow_up_assigned_to = assignee_id
db.session.commit()
note_suffix = f' Note: {note}' if note else ''
who = (f'The customer ({current_user.display_name})' if is_customer
else current_user.display_name)
assigned_suffix = ''
if inspection.follow_up_assignee:
assigned_suffix = (f' It has been assigned to '
f'{inspection.follow_up_assignee.display_name}.')
body = (
f'{who} has requested a follow-up re-inspection '
f'of "{inspection.template.name}" at {inspection.facility.name}.{note_suffix}'
f'of "{inspection.template.name}" at {inspection.facility.name}.'
f'{assigned_suffix}{note_suffix}'
)
# Notify the original inspector so they see it on the iPad.
inspector = db.session.get(User, inspection.inspector_id)
# Notify whoever now OWNS the follow-up — the assignee when one was named,
# otherwise the original inspector (Inspection.follow_up_owner). Notifying
# the original inspector for work that has been handed to someone else is
# noise, and worse, it implies they are expected to do it.
inspector = inspection.follow_up_owner
if inspector and inspector.id != current_user.id:
notify(
recipient = inspector,
@@ -1732,6 +1801,7 @@ def clear_followup(inspection_id):
inspection.follow_up_note = None
inspection.follow_up_requested_by = None
inspection.follow_up_requested_at = None
inspection.follow_up_assigned_to = None
db.session.commit()
log_action(ACTION_UPDATE, 'Inspection', inspection_id,
f'{inspection.template.name} @ {inspection.facility.name}',