From 2d68bad966ae8dda21d836e44b0be0dbd3734677 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 27 Aug 2026 11:54:07 -0400 Subject: [PATCH] Aug 27 - Update code to follow-up with ST functions --- CLAUDE.md | 85 ++++++++++- app/api/inspections.py | 51 ++++++- app/api/photos.py | 22 ++- app/api/stats.py | 7 +- app/models/inspection.py | 58 +++++++ app/routes/dashboard.py | 6 +- app/routes/inspections.py | 144 +++++++++++++++++- app/templates/inspections/view.html | 58 ++++++- .../versions/phase56_followup_assignee.py | 71 +++++++++ tests/test_followup_ownership.py | 131 ++++++++++++++++ 10 files changed, 617 insertions(+), 16 deletions(-) create mode 100644 migrations/versions/phase56_followup_assignee.py create mode 100644 tests/test_followup_ownership.py diff --git a/CLAUDE.md b/CLAUDE.md index 05273ec..acac20e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -843,7 +843,7 @@ limiter = Limiter( ## 17. Alembic Migration Chain -**Current HEAD:** `phase55_template_contracts`. +**Current HEAD:** `phase56_followup_assignee`. **Chain root:** `0003_add_user_active` — a guarded squashed baseline (MT-2) that recreates the full 25-table schema with INFORMATION_SCHEMA guards. The original baseline migrations (0001/0002/0003) were lost; this file restores the chain root so Alembic can build the revision map. `down_revision = None`. @@ -884,7 +884,8 @@ limiter = Limiter( → phase48_schedule_parent_inspection → phase49_followup_requested_by → phase50_sched_acknowledged → phase51_external_inspector → phase52_user_ui_theme → phase53_knowledge_sort_order - → phase54_user_notif_matrix → phase55_template_contracts ← HEAD + → phase54_user_notif_matrix → phase55_template_contracts + → phase56_followup_assignee ← HEAD ``` `phase54` / `phase55` port the ST August-2026 work (ST calls them phase51 / @@ -907,7 +908,22 @@ means SHARED**, so every pre-existing form stays available everywhere and the migration cannot change behaviour on deploy. Table-existence check — safe to re-run. -**Deploy order for both (tenant DBs):** +#### phase56 — assign a follow-up to another inspector + +Adds `inspections.follow_up_assigned_to` (FK → `users.id`, ON DELETE SET NULL) +— see §27c. **No backfill:** NULL means the follow-up belongs to the +inspection's own inspector, which is what every existing row already means, so +deploying cannot change who owns anything. + +**This is the THIRD FK from `inspections` to `users`** (`inspector_id`, +`follow_up_requested_by`, and now this). Every relationship spanning the two +tables must pin `foreign_keys` explicitly or the mapper is ambiguous — and it +raises on first ORM *use*, not at import, so the app starts cleanly and then +every request 500s. Column + constraint checks — safe to re-run. + +ST calls this phase53; MT's chain was already past that number. Match by NAME. + +**Deploy order (tenant DBs):** ```bash python -m control.tenant_migrate upgrade --tenant all sudo systemctl restart gunicorn @@ -1507,6 +1523,8 @@ set -a; . /etc/jqc/control.env; set +a | 105 | **The flag-issue assignee list is contract-scoped, and BOTH call sites must use `_assignable_staff_for()`; a failed flag-issue POST must return non-2xx** | `execute()` renders the dropdown, `flag_issue()` builds the choices that validate the POST — the choices are the security boundary. An org-wide list let anyone assign another client's Customer Inspector, who was then emailed the facility name and issue description. And the offcanvas JS branches on `res.ok`, so a 200 re-render of an invalid form reads as success: the panel closes, the page reloads, and no issue exists. | | 106 | **Name the Groq model in the chat error log, and keep `_DEFAULT_GROQ_MODEL` current** | Groq retires models without notice; when the configured one disappears the API 404s and EVERY question returns the generic "problem reaching the AI assistant" reply, with nothing else broken — invisible until a customer complains. The fix needs no deploy, only `GROQ_MODEL`, which is exactly what the log line must say. | | 107 | **`viewer_is_our_staff` in `issues/view.html` is an explicit role ALLOWLIST, and `external_inspector` is absent on purpose** | `not current_user.is_customer_account` fails OPEN — a missing attribute yields Jinja `Undefined`, `not Undefined` is true, and the internal-process chrome renders for exactly the accounts it must be hidden from. This is not a rule-87 violation: rule 87 governs capability/scoping, where a Customer Inspector must behave like our inspector; this asks "does this person work for us?", the one place the two genuinely differ. | +| 108 | **A follow-up has exactly ONE owner: use `follow_up_owner` (row) / `follow_up_owned_by()` (query) — never re-derive it** | Assignee when set, original inspector otherwise. The API's two arms must be mutually exclusive (`follow_up_assigned_to == me` OR `assigned_to IS NULL AND inspector_id == me`); drop the `IS NULL` and two people turn up for the same re-inspection. The authorship filter must be DEFERRED when `follow_up_required=true` is requested, or the rows the assignee needs are hidden before the ownership test runs. | +| 109 | **Inspector READ access is facility scope; WRITE access is authorship** | `index()` lists by facility (rule 58), so `view()`/`export_pdf()` must too — scoping reads by authorship made the list offer rows that said "Access denied" on click, and locked the follow-up assignee out of the parent inspection. `execute`, `save_draft_ajax`, `upload_photo_ajax` and `flag_issue` keep the authorship check: readable is not editable. | | 98 | **Reports R1 + R2 contract cascade is client-side only — facility_id is the sole DB filter** | The Contract dropdown in `reports/issues_aging.html` and `reports/sla_compliance.html` has no `name` attribute and is never submitted. It exists only to narrow the Facility ` + + {# ── Assign it (phase56) ──────────────────────────────────────── + Optional. Left blank, the follow-up stays with whoever performed + the original inspection — the behaviour before this existed. The + list is contract-scoped in _followup_assignees_for(), so a + Customer Director only ever sees inspectors on their own + contracts. #} + {% if followup_assignees %} +
+ + +
+ Choose someone else to carry out the re-inspection. They are + notified and it appears in their list on the web and the iPad; + the original inspector is not asked to do it. +
+
+ {% endif %}