Aug 27 - Updated web dashboard card, inspection ownership

This commit is contained in:
2026-08-27 11:53:14 -04:00
parent a36cc93594
commit ba10ec42e0
5 changed files with 42 additions and 14 deletions
+4 -5
View File
@@ -895,15 +895,14 @@ A follow-up used to belong implicitly to whoever performed the original inspecti
**The assignee takes over.** Only the owner is notified, and only the owner sees it — the original inspector's list no longer shows a follow-up that was handed to someone else. In the API that means the two arms must be mutually exclusive:
```python
db.or_(
Inspection.follow_up_assigned_to == user.id,
db.and_(Inspection.follow_up_assigned_to.is_(None),
Inspection.inspector_id == user.id),
)
Inspection.follow_up_owned_by(user.id) # models/inspection.py
# → follow_up_assigned_to == uid OR (follow_up_assigned_to IS NULL AND inspector_id == uid)
```
Without the `is_(None)` on the second arm the original inspector keeps seeing it and two people turn up to do the same re-inspection.
**Ownership is stated twice and both live in the model**`follow_up_owner` for a loaded row, `follow_up_owned_by(user_id)` for a query. Three surfaces scope follow-ups: the mobile list filter, the web dashboard card, and the iPad stats KPI. They each wrote their own version at first and the two dashboards tested AUTHORSHIP, so an assignee saw the work in their list while both cards read 0 — the stats KPI disagreeing with the Follow-up Requests list directly beneath it. All three now call the predicate.
**The generic "inspectors see only their own inspections" filter has to be deferred** when `follow_up_required=true` is requested — an assigned follow-up lives on an inspection somebody *else* performed, so applying authorship first hides exactly the rows the assignee needs.
**Read access follows the LIST, not authorship (Aug 2026).** `index()` scopes an inspector by FACILITY (rule 58 — their scope covers all data in their contracted facilities), but `view()` and `export_pdf()` scoped by authorship, so the list offered rows that answered "Access denied" on click. It also blocked the 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. Both reads now use `_inspector_may_read()` (facility scope). **Writes stay owner-only**: `execute`, `save_draft_ajax`, `upload_photo_ajax` and `flag_issue` keep the authorship check.
+1 -7
View File
@@ -318,13 +318,7 @@ def list_inspections():
# 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(db.or_(
Inspection.follow_up_assigned_to == user.id,
db.and_(
Inspection.follow_up_assigned_to.is_(None),
Inspection.inspector_id == user.id,
),
))
query = query.filter(Inspection.follow_up_owned_by(user.id))
from_date_str = request.args.get('from_date')
if from_date_str:
+6 -1
View File
@@ -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()
+26
View File
@@ -240,6 +240,32 @@ class Inspection(db.Model):
follow-up.
"""
return self.follow_up_assignee or self.inspector
@staticmethod
def follow_up_owned_by(user_id):
"""SQL predicate: *user_id* owns this inspection's follow-up.
The query-side mirror of `follow_up_owner` above. Ownership has to be
expressed twice once for a loaded row, once in SQL so both live
here, together, and every caller uses one of them.
The two arms are mutually exclusive on purpose. Drop the `is_(None)`
from the second and an inspector keeps matching a follow-up that was
handed to someone else: two people turn up for the same re-inspection.
Callers: the mobile list filter, the web dashboard card, and the iPad
stats KPI. They previously each wrote their own version, and three of
them tested AUTHORSHIP so an assignee saw the work in their list but
a 0 on both dashboards.
"""
return db.or_(
Inspection.follow_up_assigned_to == user_id,
db.and_(
Inspection.follow_up_assigned_to.is_(None),
Inspection.inspector_id == user_id,
),
)
follow_ups = db.relationship('Inspection', backref=db.backref('parent', remote_side='Inspection.id'),
lazy='dynamic', foreign_keys='Inspection.parent_inspection_id')
+5 -1
View File
@@ -173,9 +173,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: