31 KiB
Observations — Workflow & Lifecycle
Source of truth:
apps/observations/. An "observation" is a staff safety/quality observation (not a patient complaint). This document describes the current implementation only. Every claim is cited asfile:line.
⚠ Read first — several stale-doc/latent-bug findings contradict
docs/workflows.mdandREADME.md. Most important: the PX "accept/reject" department-response review step documented indocs/workflows.md:44-47no longer exists in code — the fields were removed (migration0020). Full list at the end.
1. Purpose
The Observations app is the staff observation / safety-quality reporting module (apps/observations/README.md:3). Any staff member can report issues they notice; submission may be anonymous (no login) with optional Staff ID + Name (README.md:7,13; model docstring models.py:237-246). PX360 staff then triage and route to a responsible department and/or create a PX Action.
It is explicitly not a complaint: no patient complainant, no multi-department join model, no manager-review tier (docs/workflows.md:41-47). The reporter is a staff observer; "patient-facing response" in code (models.py:565) actually means the response shown back to the staff reporter via the public track page.
Anonymous detection: is_anonymous returns True when neither reporter_staff_id nor reporter_name is set (models.py:666).
2. How a Case Starts
2.1 Creation channels (two entry points, both calling ObservationService.create_observation services.py:43)
| Channel | View | URL name | Auth | Form |
|---|---|---|---|---|
| Public (anonymous-allowed) | observation_create_public (views.py:162) |
observations:observation_create_public (urls.py:30) → /observations/new/ |
None | ObservationPublicForm (forms.py:29) |
| Internal (staff) | observation_create (views.py:301) |
observations:observation_create (urls.py:41) → /observations/create/ |
@login_required |
ObservationInternalForm (forms.py:181) |
Note:
PublicObservationForm(forms.py:717) also exists but is not wired to any URL; onlyObservationPublicFormis used by the view (views.py:42).
2.2 Public form — required fields (ObservationPublicForm, forms.py:29)
Meta.fields (forms.py:68): hospital, location_type, area, title, description, location_text, incident_datetime, reporter_staff_id, reporter_name, reporter_phone, reporter_email.
- Required:
description(min length 10,forms.py:173);location_type(forced requiredforms.py:147). - Optional:
area,title,location_text,incident_datetime(defaulted to nowforms.py:163),reporter_*(optional → enables anonymity). - No severity/category field on the public form → public submissions always come in as MEDIUM severity, no category until triage/AI assigns them (
views.py:188). - Spam protection: honeypot
websitefield (forms.py:41, validatedforms.py:166). Attachments:ALLOWED_EXTENSIONS/MAX_FILE_SIZE10 MB (forms.py:25).
2.3 Internal form — required fields (ObservationInternalForm, forms.py:181)
Meta.fields (forms.py:196): hospital, location_type, area, description, section, incident_datetime, patient_file_number, assigned_department, assigned_to, px_source.
- Required:
hospital,location_type,description(min 10). - Auto-fills reporter from logged-in user (
views.py:322) — internal observations are never anonymous. Setssource_legacy="staff_portal"(vs public"public_form",views.py:204). - If
assigned_tochosen at creation, the view immediately moves to IN_PROGRESS + writes a status log "Auto-assigned during creation" (views.py:339).
2.4 What happens immediately after submission (ObservationService.create_observation, services.py:43)
Inside one @transaction.atomic (services.py:44):
Observation.objects.create(...)with defaultstatus=OPEN(models.py:375).- Tracking code generated by the model's
save()override (models.py:650): if adding andhospital_idset →generate_reference("OBS", hospital)→OBS-YYYYMM-NNNN(apps/core/reference.py:32); else randomOBS-XXXXXX(models.py:26). Uniqueness loop retries. - Initial status log:
ObservationStatusLog(from_status="", to_status=OPEN, comment="Observation submitted")(services.py:113). - Attachments created (
services.py:118); metadata auto-extracted inObservationAttachment.save()(models.py:865). - New-observation notification queued to
notify_staff_new_item.delay("observation", id)(services.py:126) →send_new_observation_notification(tasks.py:266) →ObservationService.notify_new_observation(services.py:430) emails every user in the "PX Admin" group (services.py:440). - AI analysis queued:
analyze_observation_with_ai.delay(id)(services.py:134; tasktasks.py:283) — overwritesseverity, matchescategory, setstitle, stores result inmetadata["ai_analysis"](tasks.py:347); also drops a bilingualObservationNote(tasks.py:410). - Public path redirects to success page showing tracking code (
views.py:207,225).
Signal side: signals.py only logger.infos creation and status-log events (signals.py:14) — no signal-driven status change.
Token fields (response_token/response_token_used/response_token_sent_at, added 0015) are empty at creation — generated only when sent to a department (views.py:1269).
3. Complete Lifecycle
3.1 The four statuses (models.py:43-49)
OPEN, IN_PROGRESS, RESOLVED, CLOSED.
3.2 VALID_OBSERVATION_TRANSITIONS (models.py:52-57) — verified
OPEN → {IN_PROGRESS}
IN_PROGRESS → {RESOLVED}
RESOLVED → {CLOSED, IN_PROGRESS} # IN_PROGRESS = reopen
CLOSED → {IN_PROGRESS} # reopen
Note the two reopen edges: RESOLVED→IN_PROGRESS and CLOSED→IN_PROGRESS.
3.3 Enforcement layers
- Service:
ObservationService.change_status(services.py:144) checksVALID_OBSERVATION_TRANSITIONSand raisesValueErroron illegal moves (services.py:166). - Model
clean():Observation.clean()(models.py:640) raisesValidationErrorifstatusnot in the four valid choices. (Validates the value, not the transition.) - DB
CheckConstraint:observation_status_validrestrictingstatus ∈ {open, in_progress, resolved, closed}(models.py:633), added in0016:30. - ⚠ Bypass:
observation_respond(views.py:1001) setsstatus="resolved"directly, skipping both the service transition check AND the status-log creation (Finding #7).
3.4 Original statuses & the simplification mapping
Original choices (0001_initial.py:151): new, triaged, assigned, in_progress, resolved, closed, rejected, duplicate, contacted (+ contacted_no_response on log fields).
0014_map_statuses.py (data migration):
| Old | → New status |
contact_status |
|---|---|---|
new |
open |
not_contacted |
triaged/assigned/contacted |
in_progress |
not_contacted / contacted |
rejected/duplicate |
closed |
not_contacted |
0013_simplify_statuses.py moved "contacted" out of status into a separate contact_status enum (not_contacted / contacted / contacted_no_response, models.py:379-389).
⚠ Stale:
tests.pyreferencesObservationStatus.NEW/.TRIAGED/.ASSIGNEDwhich no longer exist — would fail to import.README.md:78lists the old statuses;README.md:70the oldOBS-ABC123format.
4. Status Definitions
| Status | Purpose | When entered | Who moves forward | Next |
|---|---|---|---|---|
| OPEN | Newly submitted, not picked up. Default on create. | At creation (services.py:114) |
Anyone who activates/triages/assigns it (observation_activate views.py:854, observation_triage views.py:729, observation_assign views.py:799) — all flip OPEN→IN_PROGRESS. Send endpoints reject OPEN (activation gate). |
IN_PROGRESS only |
| IN_PROGRESS | Being worked. activated_at stamped on first entry (services.py:177); due_at computed from SLA. |
On activate/triage/assign; on reopen from RESOLVED/CLOSED; on assign from resolved/closed | PX/Hosp-Admin/PX-Mgmt/PX-Employee/Dept-Manager (varies by view) | RESOLVED only |
| RESOLVED | Work complete; awaiting closure. resolved_at/resolved_by stamped (services.py:186). Triggers notify_resolution (services.py:205). |
Via change_status to RESOLVED; or via observation_respond which auto-resolves when a PX response is sent (views.py:1000) |
triage_observation perm OR px_admin OR hospital_admin (views.py:773); also anyone allowed to observation_respond (views.py:978) |
CLOSED, or reopen to IN_PROGRESS |
| CLOSED | Terminal. closed_at/closed_by stamped (services.py:189). |
Via change_status to CLOSED (same gate) |
Same | Reopen to IN_PROGRESS |
Helper: is_active_status True for open/in_progress (models.py:771). check_overdue treats resolved/closed/rejected/duplicate as inactive (models.py:757) — rejected/duplicate literals are harmless leftovers.
5. Workflow Actions
Public (no login)
| Action | View (views.py) |
URL name | Effect |
|---|---|---|---|
| Submit (public) | observation_create_public :162 |
observation_create_public |
Creates OPEN; redirects to success page |
| Success page | observation_submitted :225 |
observation_submitted |
Shows tracking code |
| Track by code | observation_track :241 |
observation_track |
Public status + dept response view (no internal notes) |
| Token response (champion, no login) | observation_respond_with_token :1987 |
observation_respond_with_token |
Validates token; champion submits dept response |
Internal (login required)
| Action | View (views.py) |
URL name | Permission gate | Effect |
|---|---|---|---|---|
| Create (internal) | observation_create :301 |
observation_create |
@login_required |
Creates OPEN; if assigned_to → IN_PROGRESS |
| Triage | observation_triage :729 |
observation_triage |
triage_observation perm OR px_admin |
ObservationService.triage_observation → sets dept/assignee, → IN_PROGRESS |
| Change status | observation_change_status :765 |
observation_change_status |
triage_observation perm OR px_admin OR hospital_admin (:773) — the gate the brief quotes |
Uses ObservationService.change_status → enforces transitions |
| Assign/Reassign | observation_assign :799 |
observation_assign |
px_admin/hospital_admin/px_management/px_employee | Sets assigned_to; if resolved/closed → reopen to IN_PROGRESS + clear stamps; if open → IN_PROGRESS |
| Activate (take) | observation_activate :854 |
observation_activate |
px_admin/hospital_admin/px_management/px_employee | Assigns to self; OPEN→IN_PROGRESS |
| Reopen | observation_reopen :908 |
observation_reopen |
px_admin/hospital_admin/px_management/px_employee | Only from resolved/closed; → IN_PROGRESS; clears stamps |
| Add note | observation_add_note :945 |
observation_add_note |
@login_required |
ObservationService.add_note |
| PX outward response | observation_respond :973 |
observation_respond |
px_admin/hospital_admin/px_management/px_employee OR assigned_to (:978) |
Sets response/responded_at/responded_by/response_sent_at; auto-resolves if not already resolved/closed (:994). ⚠ Bypasses transition validator & status log. |
| Generate AI response | observation_generate_ai_response :1020 |
observation_generate_ai_response |
same as respond | Returns bilingual JSON draft; does not persist |
| Send to department (legacy) | observation_send_to_department :1179 |
observation_send_to_department |
px_admin/hospital_admin/department_manager/px_management/px_employee | Activation gate: rejects OPEN. Sets assigned_department, sent_to_department=True, dept-response SLA, response_token, emails champion token link (§14) |
| Escalate (manual) | observation_escalate :1335 |
observation_escalate |
px_admin/hospital_admin/px_management/px_employee | Refuses closed/cancelled/rejected; sets escalated_at, emails target |
| Send-to (AJAX) | observation_send_to :1433 |
observation_send_to |
px_admin/hospital_admin/department_manager/px_management/px_employee | Activation gate. Branch: person (assigns User) or department (emails+SMS champion+manager). ⚠ Reads nonexistent observation.reference_number (:1564) → AttributeError. |
| Dept response (logged-in) | observation_department_response :1612 |
observation_department_response |
px_admin OR hospital_admin OR (is_champion AND assigned_department==user.department) |
Stores department_response_en/ar, stamps department_responded_at/_by, AI summary, notifies reporter |
| Send dept-response reminder | observation_send_dept_response_reminder :1752 |
observation_send_dept_response_reminder |
px_admin/hospital_admin/px_management/px_employee | Refuses if already responded; emails champion; stamps reminder |
| Convert to PX Action | observation_convert_to_action :1113 |
observation_convert_to_action |
px_admin/hospital_admin/px_management/px_employee | One-shot guard on action_id; creates PXAction |
| Soft delete/Restore | observation_soft_delete/observation_restore :1961/1975 |
— | px_admin/hospital_admin/px_management/px_employee | |
observation_pdf :2066 |
observation_pdf |
@login_required |
||
| Category CRUD | — | urls.py:77-83 |
observations.manage_categories via @permission_required |
Delete blocked if in use |
6. Decision Points
- Activation gate (shared rule): OPEN observations cannot be sent to a department — both send endpoints reject
status == OPEN(views.py:1196, 1457). Must first reach IN_PROGRESS via activate/triage/assign. - Triage needed? New observations arrive OPEN with no dept/category (public) or optional dept (internal). Triage sets dept/owner/category (
services.py:215). - AI classification (
tasks.py:283): proposes severity/category/title post-create. - Send to person vs department (
observation_send_to):recipient_typebranch (views.py:1463). - Department has a contact target?
has_contact_target(dept)gates the dept list; legacy send requires champion or manager (views.py:1212). - Contact person valid?
department.is_valid_contact_person(contact_person_id)(views.py:1221). - Already responded? Reminder short-circuits if
department_responded_atset (views.py:1763). - Already converted? Convert short-circuits on
action_id(views.py:1129). - Convert-to-action vs send-to-dept — mutually exclusive paths chosen by PX staff.
- ⚠ No formal PX accept/reject decision point exists anymore (Finding #1). The implicit "review" is
observation_respond.
7. Assignment Flow
assigned_department(models.py:445) — the single responsible department (SET_NULL). This is the "outgoing department" field reused by the send flow; there is no separate outgoing-department field, unlike Inquiry.assigned_to(models.py:453) — individual user assignee.assigned_at(models.py:461).- Section (
models.py:353) — finer-grained. - Reassignment:
observation_assignlogs old→new in the comment (views.py:841);observation_activatereassigns to self (views.py:873). - Transfer to department: send endpoints overwrite
assigned_department(views.py:1240, 1538) — effectively reassignment + send. - Owner cascade
get_owner()(models.py:685): section(champion→supervisor→deputy_supervisor) → department(champion→deputy_manager→supervisor→deputy_supervisor→manager_2nd→manager_3rd). - Escalation targets computed in detail view from
dept.get_role_holders()+ Hospital Admin/Department Manager groups (views.py:665). - Final owner = whoever resolves/closes (
resolved_by/closed_by). - SLA config per (hospital, severity):
ObservationSLAConfig(models.py:138);Observation.get_sla_config()(models.py:733).dept_response_hours(default 48,models.py:183) drives dept-response SLA.
8. Investigation Process
No separate investigation sub-flow for observations (contrast with Complaints, docs/workflows.md:25-28). No Investigation/Question/Explanation models.
- Notes =
ObservationNote(models.py:879):note,created_by,is_internal(default True). Plus genericnotes = GenericRelation("core.Note")(models.py:619). - Audit trail =
ObservationStatusLog(models.py:904): every status change recorded bychange_status(services.py:196). The champion-response path writes a pseudo-log withfrom_status == to_status(views.py:1668); the token path does not create a status log — small inconsistency. - Stage timeline built by
_build_observation_stage_timeline(views.py:83) from status logs + timestamp fields.
9. Communication Flow
All notifications via apps.notifications.services.NotificationService.
| Event | Trigger | Recipient | Channel | Code |
|---|---|---|---|---|
| New observation | create | "PX Admin" group | services.py:430 |
|
| Assignment | triage/assign | assigned_to |
services.py:496 (⚠ notify_assignment defined but never called) |
|
| Sent to department | send endpoints | champion (+manager) | email (+SMS in send_to) | views.py:1268, 1552 |
| Token link emailed | send-to-department | contact person | email w/ one-time link | views.py:1282 |
| Department responded | dept response / token | reporter (reporter_phone/email) |
SMS + email w/ track URL | views.py:1684, 2039 |
| SLA reminder (resolution) | send_observation_sla_reminders |
assigned_to |
tasks.py:53 |
|
| Dept-response reminders (auto) | send_observation_dept_response_reminders |
dept champion | tasks.py:511 |
|
| Dept-response reminder (manual) | observation_send_dept_response_reminder |
dept champion | views.py:1752 |
|
| Resolution/Closure | change_status → RESOLVED/CLOSED |
assigned_to + dept manager |
services.py:559 |
|
| Escalation | observation_escalate |
chosen escalate-to user | views.py:1393 |
|
| Monthly follow-up | (disabled) | assigned_to |
tasks.py:211 (beat commented out, config/celery.py:193,198) |
|
| Public track page | observer enters code | observer (self-serve) | web | views.py:241 |
Acknowledgement/"need more info"/"no observer response" tracked by contact_status (not_contacted/contacted/contacted_no_response, models.py:379), set to contacted on send-to-department (views.py:1255, 1584). No automated "no response" flip — no view currently writes contacted_no_response.
Token-response path: /observations/<uuid:pk>/respond/<str:token>/ (urls.py:36) → observation_respond_with_token (views.py:1987). Validates token, refuses if used, one-shot.
10. Escalation Flow
Manual
observation_escalate (views.py:1335): PX/admin/management/employee picks a Staff target + reason + email subject/body. Refuses closed/cancelled/rejected (views.py:1350). Stamps escalated_at, writes status log + note, emails target.
"Automatic" — present in config but DISABLED in code
- Config fields on
ObservationSLAConfig:dept_response_auto_escalate_enabled(default True,models.py:195),dept_response_escalation_hours_overdue(default 0 = immediately,models.py:198). - Seed defaults (
seed_observation_dept_response_sla.py:39):sla_hours=72,dept_response_hours=48, first reminder 12h, second 4h, auto-escalate enabled, escalation 0h. - Task
check_overdue_observation_dept_responses(tasks.py:447) flags overdue but the escalation branch is dead code: always logs "Auto-escalation skipped (disabled)" andcontinues (tasks.py:499). So escalation is manual only. - SLA breach detection:
Observation.check_overdue(models.py:757) setsis_overdue/breached_at. Driven bycheck_overdue_observations(tasks.py:19) every 15 min. - Manager involvement: only via manual escalation or the escalation-targets list. No manager-approval tier in the response review.
⚠ Celery beat contradicts task docstrings: all four observation tasks run at
crontab(minute="*/15")(config/celery.py:86-114), buttasks.py:56and:517claim "every hour". Monthly follow-up beats are commented out (config/celery.py:193,198) — never fire.
11. Resolution Process
- Who can resolve: the
observation_change_statusgate (views.py:773) =triage_observationperm ORis_px_admin()ORis_hospital_admin()— asdocs/workflows.md:45-46states. Additionallyobservation_respond(views.py:978) lets px_admin/hospital_admin/px_management/px_employee or the current assignee resolve by sending a response. - How resolved:
ObservationService.change_status(..., RESOLVED)stampsresolved_at/resolved_by(services.py:186), writesObservationStatusLog, firesnotify_resolution. The alternateobservation_respondsets fields inline (views.py:997) and additionally recordsresponded_at/responded_by/response_sent_at(models.py:570, added0017). - Approval required: ⚠ No separate approval step exists anymore. The historical
dept_response_acceptance_status(pending/acceptable/not_acceptable) was removed in0020. Today the champion's department response is not formally accepted/rejected — PX simply decides whether to write a patient-facingresponse(which resolves the case) or to reopen/re-ask via notes. - Observer confirmation: no observer satisfaction step.
0018addedsatisfaction/satisfaction_set_at, but0019removed both (0019:13) — so satisfaction is no longer captured for observations.
12. Closure Process
- When closeable: from RESOLVED only (
models.py:55). - Who closes: same
triage_observationOR px_admin OR hospital_admin gate (views.py:773), viaobservation_change_status. - Side effects:
closed_at/closed_by(services.py:189);notify_resolutionemailed. - Reopen: CLOSED → IN_PROGRESS is legal (
models.py:56). Two entry points:observation_reopen(views.py:908, clears resolved/closed stamps) andobservation_assign(views.py:825, implicit reopen-on-reassign). - Permanently completed: no "archived"/"locked" state — CLOSED is terminal but always reopenable. Soft-delete (
views.py:1961) is the only "removal" path, reversible via restore.
13. Exception Flows
- Duplicate/Invalid/Rejected: former statuses, now collapsed to
closedby0014_map_statuses.py:19-20. Today no dedicated "mark duplicate/invalid" action; PX closes + notes. (UI color maps forrejected/duplicatelinger inadmin.py:171,models.py:728but are unreachable.) - Wrong department: re-send via send endpoints (overwrites
assigned_department) or reassign. - Missing info: triage note /
ObservationNote;contact_statussettable tocontacted(no view setscontacted_no_response). - No observer response: tracked conceptually via
contact_status="contacted_no_response"but no automated transition writes it. - Converted to PX Action:
observation_convert_to_action(views.py:1113); one-shot guard onaction_id; the observation is not auto-closed on conversion. - Withdrawn: no dedicated status/action. Anonymous/identified reporter cannot retract; only PX can soft-delete.
- Merged: no merge feature.
- Reopened:
observation_reopen(views.py:908) from resolved/closed → IN_PROGRESS.
14. Department-Response Sub-Flow
⚠ The documented "champion responds → PX accepts/rejects → resolve" loop (
docs/workflows.md:44) is only partially implemented today. The accept/reject tier was removed (0020). What remains is "champion responds → PX writes outward response (resolve)". Both the intended design and the actual code are documented below.
14.1 Migration history of the sub-flow
0004_add_sent_to_department_fields.py:13— addedsent_to_department(bool) +sent_to_department_at.0005_data_sent_to_department_backfill.py:5— backfilledsent_to_department=Truefor any observation with anassigned_department.0001_initial.py:165-175— original dept-response fields.0001_initial.py:176-178+0020:13-28—dept_response_acceptance_status(+_at/_by/_notes) added then removed. Net effect: no acceptance fields today.0015_add_response_token.py:13—response_token/response_token_used/response_token_sent_at.0017_observation_responded_at...py:15—responded_at/responded_by/response/response_en/ar/response_sent_at(the PX outward response).
14.2 The activation gate (shared rule)
Both send endpoints refuse status == OPEN:
observation_send_to_department:views.py:1195→ "Activate this observation before sending it to a department."observation_send_to:views.py:1456→ HTTP 400 JSON. So an observation must first reach IN_PROGRESS (via triage/activate/assign) before forwarding.
14.3 Send → receive
-
Send (
observation_send_to_departmentviews.py:1179orobservation_send_toviews.py:1433):- Sets
assigned_department = department(views.py:1240, 1538). - Sets
sent_to_department = True,sent_to_department_at = now,forwarded_to_dept_at = now(views.py:1241-1243, 1539-1541) — the uniform cross-module "sent" signal. - Computes
dept_response_sla_due_at = now + dept_response_hours; resets overdue/reminder/escalation stamps (views.py:1245-1252, 1543-1550). - Sets
contact_status="contacted"(views.py:1255, 1584). - Legacy path generates
response_token(views.py:1273) and emails the chosen contact person a one-time linkhttps://{domain}/observations/{pk}/respond/{token}/(views.py:1282). - Unified path emails+SMSes champion and manager via
get_champion_and_manager— but ⚠ crashes onobservation.reference_number(views.py:1564-1574, Finding #2 —reference_numberdoesn't exist on Observation, onlytracking_code). - Writes an internal
ObservationNote(views.py:1261, 1590).
- Sets
-
Who receives it: the department champion primarily (reminder recipient is
dept.champion.user,tasks.py:542); the dept manager is a secondary recipient in the unified send path.
14.4 Champion's response submission (two surfaces, same fields)
- Logged-in champion:
observation_department_response(views.py:1612), gated byis_champion()ANDobservation.assigned_department == user.department(views.py:1619). Form:ObservationDepartmentResponseForm(forms.py:654) — requiresresponse_enorresponse_ar(forms.py:682). - Token (no login):
observation_respond_with_token(views.py:1987), validatesresponse_token(:1998) andresponse_token_used(:2001).
Both store department_response_en/ar, stamp department_responded_at + department_responded_by (logged-in path only), clear dept_response_is_overdue, mark response_token_used=True (token path), generate AI summary into department_response_summary_en/ar (views.py:1644, 2022), write a pseudo ObservationStatusLog (logged-in path only, views.py:1668), and notify the reporter via SMS/email with the public track URL (views.py:1684, 2039).
14.5 ONE-level review — what actually exists
- Per
docs/workflows.md:41-47(intended): "champion responds → PX accepts/rejects → resolve" usingdept_response_acceptance_status. - In current code:
dept_response_acceptance_statusand friends were removed (0020). There is nopx_accept/px_rejectURL or view. The de-facto "review" is the PX user runningobservation_respond(views.py:973) to writeresponse/response_en/ar,responded_at/_by,response_sent_at(models.py:565), which also flips status toresolved(views.py:1000). - ⚠ Reject loop / "response cleared, returns to champion": described in
docs/workflows.md:16-17as a shared rule, but there is no implementation for observations — no view clearsdepartment_response_en/aror resetsdepartment_responded_at. If PX is unhappy, the only options today are to reopen (observation_reopen) and re-send, or to add a note. Document as a gap.
14.6 Token-response path (summary)
- URL:
/observations/<uuid:pk>/respond/<str:token>/(urls.py:36). - Token generated only at send-time (
views.py:1273), stored inresponse_token(models.py:551), uniqueness-indexed. - Validation: token equality + non-empty (
views.py:1998); not already used (views.py:2001, fieldresponse_token_usedmodels.py:555). - One-shot: sets
response_token_used=Trueon submit (views.py:2019); subsequent visits renderresponse_already_submitted.html. - No-auth requirement explicit (
views.py:1987).
15. End-to-End Example
Staff member submits observation via public form (anonymous allowed)
↓ (views.py:162) → status=OPEN, tracking OBS-202607-0001,
initial status log written, AI analysis dispatched,
PX admins notified
PX triages (sets department, assignee, category, severity)
↓ (views.py:729) → status=IN_PROGRESS, activated_at, due_at computed
[DECISION: send to department or convert to action]
PX sends to Department X
↓ (views.py:1179) → assigned_department=X, sent_to_department=True,
dept_response_sla_due_at set, response_token generated,
token link emailed to champion, contact_status=contacted
[DECISION: champion responds via token or logged-in]
Champion submits dept response via token link
↓ (views.py:1987) → department_response_en set, department_responded_at,
response_token_used=True, AI summary generated,
reporter notified SMS+email
[⚠ NO PX accept/reject — field removed]
[DECISION POINT: PX must write outward response]
PX writes outward response
↓ (views.py:973) → response set, responded_at/by, response_sent_at,
status=resolved (auto), resolved_at/by
[⚠ bypasses transition validator & status log]
[DECISION POINT: close or reopen]
PX closes
↓ (views.py:765) → status=closed, closed_at/by, notify_resolution
[PERMANENT unless reopened → IN_PROGRESS]
Appendix — Flagged gaps vs docs/workflows.md / README.md
dept_response_acceptance_statusremoved (0020) — PX accept/reject no longer implemented; nopx_accept/px_rejectview.observation_send_toAttributeError (views.py:1564-1574) — reads nonexistentobservation.reference_number; should betracking_code.- Stale test suite —
tests.pyreferencesObservationStatus.NEW/.TRIAGED/.ASSIGNED. - Celery beat contradicts task docstrings — all run
*/15, not "every hour"; monthly follow-up beats commented out. - Auto dept-response escalation disabled (
tasks.py:499) despite config flag defaulting True. README.mdoutdated — old statuses, old tracking format.observation_respondbypasses transition validator & status log (views.py:1001) — inconsistency in audit trail.- No reject loop for observations (no view clears dept response) — gap vs
docs/workflows.md:16-17. satisfactionremoved (0019) — no observer satisfaction step.- Stale literals —
check_overdue/escalate referencecancelled/rejected/duplicatewhich can never occur.