Two issues from the whole-branch review:
1. (Important) Observation detail's can_respond_to_department context var
did not include is_department_manager, so even though Task 2 let dept
managers pass the view's permission check, the 'Submit Response'
button stayed hidden from them. Now the context var matches the view
predicate. Regression test added.
2. (Minor) appreciation_list had no guard against now-invalid status
query params (?status=acknowledged from an old bookmark would show
an unexplained empty list). Unknown values are now silently reset
to 'all statuses'.
These fields already existed on the Complaint model and are read by
ComplaintService.change_status, but PX had to fill them via separate
endpoints after resolving. Now they're captured in the same modal as
the resolution text. Also fixed the view (complaint_change_status) to
extract resolution_category from POST and pass it to the service — the
service accepted the kwarg but the view never forwarded it.
Per the workflow realignment, the point of appreciation is recognition —
managers should see the appreciations their department received alongside
other feedback. Adds an 'Appreciations Received' card (amber theme) after
the Pending Actions section, showing the count and the 10 most recent
SENT appreciations. The card only renders when the department has at
least one appreciation.
Drops the dead AI_ANALYZED status (a prior data migration already converted
existing rows; the choice was never removed from the model) and the
unreachable ACKNOWLEDGED status (is_recipient was hardcoded False so the
acknowledge button never rendered).
Changes:
- AppreciationStatus: DRAFT, ACTIVATED, SENT only. SENT is terminal.
- VALID_APPRECIATION_TRANSITIONS: DRAFT→ACTIVATED→SENT, SENT terminal.
- Removed the model.acknowledge() method.
- DRF viewset acknowledge action: returns 410 Gone (deprecated).
- UI appreciation_acknowledge view: redirects with deprecation message.
- Removed URL routes: appreciation_acknowledge, appreciation_send_dept_reminder.
- appreciation_send_dept_reminder status check no longer references ACKNOWLEDGED.
- appreciation_list stats + status filter + badge: dropped ai_analyzed/acknowledged.
- appreciation_detail status badge + sent banner: dropped acknowledged references.
- Migration 0010_drop_acknowledged_status: AlterField on status choices.
AI analysis still runs on activation and is stored in ai_analysis JSON; it
just no longer has its own status. Badges and leaderboard fire at SENT.
6 tests lock in the new lifecycle and removed URLs.
When PX closes a suggestion that was actually implemented, ticking the
checkbox sends a high-value 'Good news — your suggestion was implemented'
SMS instead of the generic closed message. The flag is ignored for any
status other than CLOSED. Acknowledge-on-submit SMS already existed; this
completes the closed-loop value path.
The checkbox appears in the status-change form in feedback_detail.html.
Three tests cover: implemented flag on close, flag off on close, flag
ignored when transitioning to a non-closed status.
Per the workflow realignment, the department owns inquiry resolution
end-to-end. The PX-side respond modal and its 'Response to Patient'
tile button are removed; the 'Resolve' button in the 'Department has
responded' banner now POSTs directly to inquiry_change_status (with a
confirm dialog), which already enforces the contact_status gate.
The inquiry_respond view and URL are kept for backward compatibility
but no UI surfaces them. Two regression tests lock in the new behavior:
- showRespondModal JS function and respondModal div are gone
- Resolve form posts directly to inquiry_change_status with status=resolved
The inquiry_department_response view excluded dept managers even though
they can resolve the inquiry via inquiry_change_status. Now both actions
use the same handling_department_id predicate so forwarding locks the
old department out consistently.
- department_record_complaint: broaden permission to include any involved
department (mirrors the OR-filter used to surface complaints in the list)
- send_to_modal: simplify — sends directly to champion+manager, drop the
contact-person picker (loadDepartmentContacts is now a no-op stub)
- department_inquiry_detail: confirm dialogs on Resolve/Close; fold No-Response
into the contact_status dropdown
- department_detail: JS string-escaping fixes
Bug #42 (journeys): PatientJourneyStageInstanceViewSet.get_queryset()
called select_related('physician', 'survey_instance') but neither field
exists on the model. Changed 'physician' -> 'staff' (the actual FK)
and removed 'survey_instance' (no such relation). Endpoint now returns 200.
Bug #43 (standards): StandardAttachmentViewSet.get_queryset() filtered on
'departments__hospital' but StandardAttachment has no 'departments' field.
Fixed to traverse compliance__department__hospital. Also fixed invalid
ordering field 'uploaded_at' -> 'created_at'. Endpoint now returns 200.
API endpoint sweep: 73 DRF endpoints across all 21 apps tested, 0 errors.
Full URL sweep: 427 URL patterns tested, 0 server errors.
1. /organizations/api/organizations/ 500 — OrganizationSerializer referenced
non-existent fields (website, license_number, logo). Removed, added
preferred_language. Now returns 200 with correct data.
2. /journeys/api/stage-templates/ 500 — DRF auto-filter (DjangoFilterBackend)
tried to create filters for serializer computed fields (survey_template_name).
Added filter_backends=[] to disable auto-generation. Now returns 200.
3. /journeys/api/stage-instances/ 500 — same DRF filter issue + stale
select_related referencing non-existent FKs (physician, survey_instance).
Added filter_backends=[] + explicit select_related on valid FKs.
NOTE: still crashes the dev server (segfault during DRF request chain).
The queryset evaluates correctly in Python but the HTTP server dies.
This is a minor read-only API endpoint; the journey UI pages all work.
Needs further investigation of the DRF request middleware chain.
The ComplaintSerializer inherited 'required' for due_at from the model field
(not nullable at DB level), but the model's save() method auto-calculates
it via calculate_sla_due_date(). Added extra_kwargs = {'due_at': {'required': False}}
so API callers can omit it. Verified: POST without due_at returns 201 + reference.
Isolation test: HH-N admin vs E2E-HOSP data across 5 scenarios.
Found + fixed 3 real isolation gaps:
1. observation_list LEAKED unassigned observations across hospitals —
the filter Q(assigned_department__hospital=X) | Q(assigned_department__isnull=True)
showed ALL unassigned observations globally. Fixed: add hospital= filter
to the isnull branch.
2. complaint_add_note allowed cross-hospital note creation —
ComplaintService.add_note had no hospital check. Any authenticated user
from any hospital could add notes to any complaint. Fixed: added hospital
isolation check (same hospital or px_admin).
3. observation_detail accessible cross-hospital when assigned_department is null —
the RBAC check only ran if observation.assigned_department was set.
Fixed: added fallback hospital check for observations with no department.
Result: 16 PASS, 0 FAIL, 0 isolation violations.
Tested: list isolation (8 modules), detail isolation (3), write isolation (2),
px_admin hospital switching, observation no-dept edge case.
UnifiedAnalyticsService._filter_by_role tried queryset.filter(department=...) on
SurveyInstance which has no department FK → FieldError → 500 for dept_manager
and director users accessing the command center.
Fixed: check if the model actually has a department field before filtering;
fall back to hospital-level filtering for models without department.
Also: RBAC matrix test updated with more accurate state-based detection.
1. /complaints/templates/ — FieldError: order_by('name_en') on ComplaintTemplate
model which has 'name' (not 'name_en'). Fixed.
2. /executive/* (4 URLs) — NoReverseMatch: redirect('core:home') referenced a
non-existent URL name. Fixed to redirect('/').
3. /my/ and /my/performance/ — ValueError: QIProjectTask.filter(assigned_to=user)
passed a User to a Staff FK. Fixed to use user.staff_profile.
4. /organizations/hospitals/ — FieldError: Hospital.objects.filter(hospital=...)
on a self-referential field that doesn't exist. Fixed to filter by pk.
1. /complaints/templates/new/ — TemplateDoesNotExist: template_form.html was
missing. Created a functional template form (name, category, title pattern,
description).
2. /complaints/oncall/schedules/new/ — NoReverseMatch: redirect('dashboard')
should be redirect('/') since the URL name 'dashboard' doesn't exist.
3. /complaints/settings/escalation-rules/new/ — RelatedObjectDoesNotExist:
EscalationRuleForm accessed self.instance.hospital on an unsaved instance.
Changed to self.instance.hospital_id (safe FK ID check).
1. /config/test/ — json.loads(request.body) on GET crashed with JSONDecodeError.
Added @require_http_methods(['POST']) so GET returns 405 instead of 500.
2. /organizations/dropdowns/subsections/ — LegacySubSection.objects.order_by('name')
used wrong field name (model has name_en/name_ar, not 'name'). Fixed to 'name_en'.
3. /rca/create/ — RCACreateView.dispatch() called _check_rca_create(request) before
LoginRequiredMixin ran, so AnonymousUser hit is_px_admin() → AttributeError.
Added auth check before the RBAC check in dispatch().
Post-fix: authenticated sweep of all 242 UI URLs → 233 OK, 0 errors (500s).
Root cause: onboarding_complete existed as dead code (a module-level function
with 'self' param, never registered as a DRF @action). The template JS posted
to /accounts/users/onboarding/complete/ which 404'd.
Fix: added a POST handler to the existing ui_views.onboarding_complete view
that accepts JSON {username, password, password_confirm, signature}, calls
OnboardingService.complete_wizard (sets password, clears is_provisional,
sets acknowledgement_completed), and notifies admins. Updated the template
JS fetch URL to /accounts/onboarding/complete/ (the working endpoint).
Re-verified (headed): token activation → welcome → wizard steps → checklist →
activation form → POST completion → is_provisional=False, ack=True. ✅
Remaining: the browser JS submitActivation() has a timing/CSRF issue that
prevents the fetch from completing on button-click (the direct POST works).
Needs separate investigation.
Tests the full onboarding flow: token activation (auto-login) → welcome → wizard
content steps → checklist → activation (password) → completion → invalid token
→ admin provisional list.
Key finding: the wizard UI renders but the onboarding flow doesn't actually
COMPLETE — after walking all steps + submitting the password form, the user is
still is_provisional=True with no password set. The activation step lacks a
POST handler to finalize onboarding. Details in the test observations.
Harness: seed_e2e_provisional + get_e2e_onboarding_state CLI + spec.
NameError in export_utils.py (line 168 used PDCAPhaseChoices.choices without
importing it). Added the import. Re-verified: export now returns 200 with
correct content-type (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet).
Fixed 3 gaps in the QI Projects module:
Gap 1 (critical): team members couldn't manage their own tasks — the toggle
checkbox/edit/delete were gated behind admin-only can_edit. Now:
- _can_manage_task() helper: admins OR the task assignee OR project team members
- task_toggle_status + htmx_task_toggle_status use the new helper
- task_row.html shows the toggle for assignees (task.assigned_to.user_id check)
Gap 2: no "My QI Tasks" view — added /projects/my-tasks/ showing tasks assigned
to the current user across all projects, with toggle links + stats. Sidebar link.
Gap 3: no notification on task assignment — added apps/projects/signals.py
(post_save on QIProjectTask → create_in_app_notification). apps.py ready() wired.
Tested (headed, 11 PASS / 1 FAIL):
- Cross-department team members (Contact Center + different dept) can VIEW the
project AND toggle their assigned tasks (both PASS)
- My Tasks view loads for team members
- Excel export returns 500 (real bug, reported)
- Project close via edit form needs correct hospital UUID (test harness issue)
Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
Harness: seed_e2e_project + get_e2e_project_state CLI + qi-projects-workflow.spec.ts
One continuous E2E test from public-form creation through resolution:
1. CREATE (public form POST) -> captures CMP- reference
2. ACTIVATE (open -> in_progress)
3. SEND TO DEPARTMENT (creates ComplaintInvolvedDepartment)
4. CHAMPION RESPONDS (department response)
5. MANAGER APPROVES
6. PX ACCEPTS
7. RESOLVE
Fix: NameError in public_complaint_submit (reference_number was undefined after
removing the legacy CMP- generator). Also fixed the same in the inquiry public
submit path (ui_views.py). Both now read complaint.reference_number /
inquiry.reference_number after save().
Harness: get_e2e_complaint_id CLI + seed_e2e_complaint other_dept_id output.
Covers the recipient_type=person branch of the unified send-to endpoint for all
3 modules: assign to px_employee, verify the assignee can open the item detail,
then reassign to a different user. 3/3 pass, 0 FAIL.
Harness: get_e2e_user_id + get_e2e_assignment_state CLI helpers.
Behavioral consistency across complaint/inquiry/observation (re-run: 0 FAIL):
- activation gate on complaints: complaint_send_to + send_to_department_form now
reject status=open ("Activate this complaint before sending it to a department")
- observation resolve dead-end fixed: observation_change_status now allows
hospital_admin (was triage_perm/px_admin only) + added a Resolve action on the
observation detail page -> accepted dept responses can be resolved from the flow
- status validation: Observation.clean() rejects invalid statuses + a DB
CheckConstraint (migration 0016 normalizes legacy "new"->"open" first)
- inquiry sent_to_department consistency: inquiry_transfer_to_department now also
sets sent_to_department=True/At (matches observation/complaint for cross-module queries)
Lucide icon-race mitigation: added a defensive `window.lucide || {createIcons:noop}`
shim to the three base layouts + standalone CDN pages (login, select_hospital,
password reset) so the "lucide is not defined" ReferenceError can't throw during
navigation races. Audit listener classifies any residual occurrence as WARN (cosmetic).
Docs: docs/workflows.md records the intentional complaint vs inquiry/observation
differences (multi-dept join + manager review vs single-dept flat) + the field-name map.
Specs: champion spec now activates before send (matches the new gate).
An item could be sent to a department while still in its initial (open) state,
bypassing activation. Added a status guard to the 4 send entry points:
- observation_send_to_department / observation_send_to (AJAX)
- inquiry_transfer_to_department / inquiry_send_to (AJAX)
Rejects with "Activate this {observation/inquiry} before sending it to a
department." if status is open. Re-sends after a rejection still work (item
stays in_progress).
Also:
- seed_e2e_dept_response: observation status "new" -> "open" (valid initial;
"new" isn't a valid ObservationStatus, which is why activate never moved it)
- spec: Flow A/B/C now activate before send
Bugs 4-8 from the QA audit, all re-verified (re-run: 0 FAIL):
- token-response form pages (inquiry + observation, 8 templates) extended the
static dashboard base (no {% block content %}) -> form was dropped. Switched
to layouts/public_base.html; form + csrf now render, anonymous token POST works
- inquiry dept-response GET 500 (missing template) -> created
complaints/inquiry_department_response.html (mirrors the observation one)
- NameError get_email_header_html in notifications/settings_service.py
(send_inquiry_department_assigned/_resolved/_reopened) -> added to the imports
- "lucide is not defined" -> guarded the unguarded lucide.createIcons() in
layouts/base.html and added a guarded init to layouts/public_base.html
- dept analytics XHR ERR_ABORTED -> verified endpoint returns 200 (test artifact)
Report updated: §13 issues marked fixed.
Adds headed Playwright coverage for the full send-to-department lifecycle
(PX send -> champion investigates -> manager approves/rejects -> PX accepts ->
resolve), including the token investigation sub-flow and both reject loops.
Bugs fixed (found by the new test):
- champion_start_investigation: NameError - InvestigationAnswer not imported
(complaints/views.py) -> the "create questions" POST was 500ing
- champion_start_investigation: staff_member.phone_number -> Staff.phone
(would have AttributeError'd once the import was fixed)
Harness:
- create_e2e_isolated_env: bind dept-manager as department.manager + create
e2e-staff Staff profile in the champion's department
- seed_e2e_complaint, get_e2e_workflow_state CLI helpers for setup/assertions
- champion-manager-workflow.spec.ts (flows A/B/C1/C2)
Report: appended "Champion/Manager Workflow Audit" section.
Pre-production security fixes to prevent cross-hospital data leaks:
- Standards API: add get_queryset() filtering by department__hospital
- Reports service: add user param with hospital filtering to all querysets
- RCA views: replace is_superuser with tenant_hospital pattern, add access
checks to all 11 mutation views
- Notifications views: replace is_superuser patterns with _get_notification_hospital
helper across all 5 settings functions
- Appreciation API: add tenant_hospital fallback to AppreciationViewSet,
AppreciationStatsViewSet, and LeaderboardView
- AI Analytics: add tenant_hospital fallback in ExecutiveSummaryGenerator and
ActionRecommendationEngine
- SourceUserRestrictionMiddleware: remove None from ALLOWED_URL_NAMES
- Complaint export: fix nullable patient/due_at/description crashes in CSV
and Excel export, fix invalid get_category_display/get_source_display calls
E2E test updates:
- Update isolation gap tests to actively assert hospital filtering
- Fix CSV export test to use API context for download handling
- Switch clinical-staff tests to serial mode to prevent race conditions