Aug 27 - Update code to follow-up with ST functions
This commit is contained in:
@@ -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 `<select>` in the browser via `GET /inspections/facilities_for_project/<id>`. The routes receive and filter on `facility_id`; `contract_id` plays no role server-side. Do not add server-side `contract_id` filtering to these routes — it would duplicate what `facility_id` already provides. |
|
||||
|
||||
---
|
||||
@@ -1968,6 +1986,67 @@ copies the restrictions.
|
||||
|
||||
---
|
||||
|
||||
## 27c. Follow-up assignment + inspector read access (Aug 2026 ST parity)
|
||||
|
||||
Ported from ST (its phase53 + the two fixes shipped beside it).
|
||||
|
||||
### Assigning a follow-up (phase56)
|
||||
|
||||
A follow-up used to belong implicitly to whoever performed the original
|
||||
inspection. `inspections.follow_up_assigned_to` lets a director — or a
|
||||
**Customer Director**, for their own facilities — hand the re-inspection to
|
||||
someone else. `Inspection.follow_up_owner` (assignee *or* inspector) is the
|
||||
single definition of ownership, so the web display, the notification and the
|
||||
mobile API filter cannot disagree.
|
||||
|
||||
**The assignee takes over**: only the owner is notified, and only the owner
|
||||
sees it. In `GET /api/v1/inspections?follow_up_required=true` the two arms are
|
||||
mutually exclusive — without `is_(None)` on the second arm the original
|
||||
inspector keeps seeing a follow-up handed to someone else and two people turn
|
||||
up to do it. The generic "inspectors see only their own inspections" filter is
|
||||
**deferred** when follow-ups are requested, because an assigned follow-up lives
|
||||
on an inspection somebody else performed.
|
||||
|
||||
The picker (`_followup_assignees_for()`) is contract-scoped for the same reason
|
||||
the flag-issue list is (rule 105), offers only the two INSPECTOR roles, and the
|
||||
POST re-validates against it. A facility with no contract offers nobody —
|
||||
fail-closed, the follow-up stays with the original inspector.
|
||||
|
||||
**MT-only gap closed on the way:** MT's `GET /api/v1/inspections` had no
|
||||
`follow_up_required` filter at all, so the iPad's Follow-up Requests screen
|
||||
received the inspector's entire history. The filter now matches the web's
|
||||
definition of "follow-up" — flagged, completed, and not yet answered by a
|
||||
linked re-inspection (`~follow_ups.any()`).
|
||||
|
||||
### Inspector READ access follows the list, not authorship
|
||||
|
||||
`index()` scopes an inspector by FACILITY (rule 58), but `view()` and
|
||||
`export_pdf()` scoped by authorship — so the list offered rows that answered
|
||||
"Access denied" on click, and the follow-up assignee could not open the parent
|
||||
inspection they had just been asked to re-inspect. 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. `reinspect()` belongs to the follow-up's owner; the buttons render only
|
||||
for `is_own_inspection or owns_follow_up`, so the page never shows a control
|
||||
that fails on click.
|
||||
|
||||
### One rule, two expressions, three callers
|
||||
|
||||
Ownership has to be stated twice — once for a loaded row, once in SQL — so both
|
||||
live together in `models/inspection.py`:
|
||||
|
||||
* `follow_up_owner` — the property (assignee, else inspector)
|
||||
* `follow_up_owned_by(user_id)` — the query predicate
|
||||
|
||||
Every query that scopes follow-ups calls the predicate: the mobile list filter,
|
||||
the web dashboard card, and the iPad stats KPI. They each used to write their
|
||||
own version and three tested AUTHORSHIP, so an assignee saw the work in their
|
||||
list while both dashboards read 0 — the stats KPI sitting directly above the
|
||||
Follow-up Requests list it disagreed with. Fixed in ST at the same time.
|
||||
Pinned by `tests/test_followup_ownership.py`.
|
||||
|
||||
---
|
||||
|
||||
## 28. Coding Rules for AI Assistants
|
||||
|
||||
These rules apply to every change made to this codebase, without exception.
|
||||
|
||||
Reference in New Issue
Block a user