Aug 27 - Update code to follow-up with ST functions
This commit is contained in:
@@ -175,9 +175,13 @@ def index():
|
||||
if not inspector_facility_ids:
|
||||
followup_q = followup_q.filter(False)
|
||||
else:
|
||||
# OWNERSHIP, not authorship — see Inspection.follow_up_owned_by().
|
||||
# An assigned follow-up lives on an inspection somebody else
|
||||
# performed, so testing inspector_id made the card read 0 for the
|
||||
# very person who had been asked to do the work.
|
||||
followup_q = followup_q.filter(
|
||||
Inspection.facility_id.in_(inspector_facility_ids),
|
||||
Inspection.inspector_id == current_user.id,
|
||||
Inspection.follow_up_owned_by(current_user.id),
|
||||
)
|
||||
elif is_customer:
|
||||
if customer_facility_ids:
|
||||
|
||||
+139
-5
@@ -786,7 +786,11 @@ def view(inspection_id):
|
||||
if inspection is None:
|
||||
abort(404)
|
||||
|
||||
if current_user.is_inspector and inspection.inspector_id != current_user.id:
|
||||
# Read access matches the LIST (rule 58) — an inspector may open anything
|
||||
# at their contracted facilities, not only what they performed. Editing
|
||||
# someone else's inspection is still refused (execute / save-draft /
|
||||
# upload-photo / flag-issue keep the authorship check).
|
||||
if current_user.is_inspector and not _inspector_may_read(inspection, current_user):
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('inspections.index'))
|
||||
if current_user.role == 'customer':
|
||||
@@ -983,7 +987,23 @@ def view(inspection_id):
|
||||
'unchanged': sum(1 for r in rows if r['delta'] == 0),
|
||||
}
|
||||
|
||||
followup_assignees = _followup_assignees_for(inspection, current_user)
|
||||
# An inspector viewing SOMEBODY ELSE's inspection gets a read-only page.
|
||||
# Without this the buttons would all render and then fail on click — the
|
||||
# same list-says-yes / page-says-no mismatch this change removes.
|
||||
is_own_inspection = (not current_user.is_inspector
|
||||
or inspection.inspector_id == current_user.id)
|
||||
# Whoever is expected to carry out the follow-up (phase56) may start the
|
||||
# re-inspection even though the original inspection is not theirs.
|
||||
owns_follow_up = bool(
|
||||
inspection.follow_up_required
|
||||
and inspection.follow_up_owner
|
||||
and inspection.follow_up_owner.id == current_user.id
|
||||
)
|
||||
return render_template('inspections/view.html',
|
||||
followup_assignees=followup_assignees,
|
||||
is_own_inspection=is_own_inspection,
|
||||
owns_follow_up=owns_follow_up,
|
||||
inspection=inspection,
|
||||
form_fields=form_fields,
|
||||
form_data=form_data,
|
||||
@@ -1325,7 +1345,11 @@ def export_pdf(inspection_id):
|
||||
if inspection is None:
|
||||
abort(404)
|
||||
|
||||
if current_user.is_inspector and inspection.inspector_id != current_user.id:
|
||||
# Read access matches the LIST (rule 58) — an inspector may open anything
|
||||
# at their contracted facilities, not only what they performed. Editing
|
||||
# someone else's inspection is still refused (execute / save-draft /
|
||||
# upload-photo / flag-issue keep the authorship check).
|
||||
if current_user.is_inspector and not _inspector_may_read(inspection, current_user):
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('inspections.index'))
|
||||
if current_user.role == 'customer':
|
||||
@@ -1410,6 +1434,68 @@ def _view_url(inspection_id):
|
||||
return url_for('inspections.view', inspection_id=inspection_id)
|
||||
|
||||
|
||||
def _inspector_may_read(inspection, user):
|
||||
"""May this inspector OPEN someone else's inspection?
|
||||
|
||||
Yes, when it happened at a facility on one of their contracts — the same
|
||||
scope `index()` uses (rule 58: an inspector's scope covers all data in their
|
||||
contracted facilities, not just their own work).
|
||||
|
||||
This used to test authorship instead, and the two disagreed: the list
|
||||
showed every inspection at the inspector's facilities, then clicking one
|
||||
said "Access denied". It also blocked the phase56 follow-up assignee from
|
||||
opening the parent inspection they had just been asked to re-inspect —
|
||||
the button they needed was on a page they could not reach.
|
||||
|
||||
READ only. Editing someone else's inspection is still refused: execute,
|
||||
save-draft, upload-photo and flag-issue all keep the authorship check.
|
||||
"""
|
||||
fids = get_inspector_scope(user)
|
||||
if fids is None: # not an inspector — no scoping applies here
|
||||
return True
|
||||
return bool(fids) and inspection.facility_id in fids
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -1613,6 +1699,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()
|
||||
@@ -1678,22 +1765,48 @@ def flag_followup(inspection_id):
|
||||
|
||||
note = request.form.get('follow_up_note', '').strip() or None
|
||||
|
||||
# ── Assignee (phase56) ────────────────────────────────────────────────
|
||||
# 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,
|
||||
@@ -1752,6 +1865,7 @@ def clear_followup(inspection_id):
|
||||
# whoever raised the previous one.
|
||||
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}',
|
||||
@@ -1776,6 +1890,26 @@ def reinspect(inspection_id):
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('inspections.index'))
|
||||
|
||||
# An inspector may re-inspect their OWN work, or work they have been
|
||||
# handed the follow-up for (phase56). Anything else at a contracted
|
||||
# facility is readable but not theirs to redo — starting a re-inspection
|
||||
# of a colleague's inspection uninvited only creates confusion about who
|
||||
# is doing it.
|
||||
if current_user.is_inspector:
|
||||
if parent.follow_up_required and parent.follow_up_owner:
|
||||
# A live follow-up has exactly ONE owner (phase56). Even the
|
||||
# original inspector does not start it once it has been handed to
|
||||
# someone else — that is the whole point of assigning it, and two
|
||||
# people turning up is the failure being designed out.
|
||||
may = parent.follow_up_owner.id == current_user.id
|
||||
else:
|
||||
# No follow-up outstanding: re-inspecting your own work is fine,
|
||||
# someone else's is not yours to redo uninvited.
|
||||
may = parent.inspector_id == current_user.id
|
||||
if not may:
|
||||
flash('That re-inspection has been assigned to someone else.', 'warning')
|
||||
return redirect(_view_url(inspection_id))
|
||||
|
||||
session['reinspect_parent_id'] = parent.id
|
||||
session['reinspect_template_id'] = parent.template_id
|
||||
session['reinspect_facility_id'] = parent.facility_id
|
||||
|
||||
Reference in New Issue
Block a user