24 KiB
Suggestions — Workflow & Lifecycle
Source of truth:
apps/feedback/. Suggestions are implemented asFeedbackType.SUGGESTIONon the sharedFeedbackmodel (apps/feedback/models.py:24). There is no dedicated model, status set, or workflow of its own — every lifecycle claim here applies to allFeedbackTypevalues (COMPLIMENT, SUGGESTION, GENERAL, INQUIRY, SATISFACTION_CHECK). Suggestion-specific behavior is called out where it exists. This document describes the current implementation only. Every claim is cited asfile:line.
1. Purpose
The Suggestions module captures ideas/recommendations from patients, staff, and external sources and routes them through a lightweight triage-and-acknowledge workflow owned centrally by the Patient Experience (PX) team.
FeedbackType.SUGGESTION = "suggestion"(models.py:24).- The
Feedbackmodel docstring (models.py:4-9): "Tracks patient feedback (compliments, suggestions, general feedback)" and "Manages feedback workflow (submitted → reviewed → acknowledged → closed)". - The word "Suggestion" is a naming convention only throughout the UI layer — the underlying record is a generic
Feedback. E.g.templates/feedback/feedback_detail.html:4("Suggestion Detail"); every audit event is namedsuggestion_*(suggestion_createdviews.py:377,suggestion_updatedviews.py:437,suggestion_deletedviews.py:494,public_suggestion_submittedviews.py:1020).
2. How a Case Starts
Four distinct creation channels, all producing a Feedback row with feedback_type=SUGGESTION, status=SUBMITTED, no assigned_to/department.
2.1 Channel A — Public Suggestion Form (unauthenticated)
- View:
public_suggestion_submit(views.py:937-1029);@csrf_exempt+ POST-only — deliberately NOT@login_required(views.py:937). - URL:
path("public/suggestion/", ..., name="public_suggestion_submit")(urls.py:36). - Form:
PublicSuggestionForm(forms.py:251) — a plainforms.Form. - Rate limiting: per-IP cap of 5 submissions / 300s → HTTP 429 (
views.py:944). - Accepts JSON and form-encoded (
views.py:951). - Required:
contact_name,contact_phone,message,hospital(views.py:966). Optional:contact_email,category/Area (defaults toOTHER,views.py:977),title(defaults tomessage[:100]).
Post-save (views.py:992-1024): patient=None at creation; linked asynchronously by link_feedback_patient.delay(id, phone) (views.py:1015); analyze_suggestion_with_ai.delay(id) (views.py:1013); notify_staff_new_item.delay("suggestion", id) (views.py:1014); audit public_suggestion_submitted (views.py:1019). Returns short pseudo-reference "SG-{uuid[:8]}" (views.py:1026) — not the internal SGT-... reference; the public cannot track by reference.
2.2 Channel B — Internal Staff Form (authenticated)
- View:
feedback_create(views.py:293-405);@login_required. URLfeedback_create(urls.py:16). - Does not use
FeedbackForm— builds theFeedbackdirectly from POST (views.py:311), alwaysfeedback_type=SUGGESTION(views.py:343) — i.e. this view is hard-coded to create suggestions only. - Required:
contact_name,contact_phone,message,hospital(views.py:318-326). Optional:title. - Post-save: synchronous
find_or_link_patient(views.py:336);analyze_suggestion_with_ai.delay(views.py:365);notify_staff_new_item.delay(views.py:366); internalFeedbackResponse"Suggestion submitted by {user}" (views.py:368); auditsuggestion_created(views.py:376).
2.3 Channel C — External API (machine-to-machine)
- View:
ExternalSuggestionCreateView(apps/integrations/api_views.py:700),POST /api/v1/external/suggestions/. - Auth via API key; hospital must match scope. Returns the canonical
reference_numbergenerated bysave()(SGT-YYYYMM-NNNN); the same value is used to retrieve the record viaGET /api/v1/external/suggestions/<reference_number>/. Same post-save tasks (api_views.py:765).
2.4 Channel D — PX Source-User Portal
- View:
source_user_create_suggestion(apps/px_sources/ui_views.py:1387);@login_required; guarded bysource_user.can_create_suggestions. - Uses
PublicSuggestionForm(ui_views.py:1400). Setssource=source,metadata={"created_via":"source_user_portal"}. Fires onlyanalyze_suggestion_with_ai(notably does NOT callnotify_staff_new_item).
2.5 What happens immediately after submission (all channels)
Feedback.save()override (models.py:261): ifreference_numberblank →generate_reference("SGT", hospital)→SGT-YYYYMM-NNNN(global monthly sequence,apps/core/reference.py:32). Comment on field (models.py:168): "internal-only, not publicly trackable".- Default
status = SUBMITTED(models.py:196). - No Django signals — there is no
apps/feedback/signals.pyandapps.pyregisters noready()handler. All side-effects are triggered explicitly by views/tasks.
⚠ Gap:
FeedbackForm(forms.py:13) exists but is used only byfeedback_update(views.py:421), not creation.feedback_createbuilds the object manually, bypassing the form'sclean()validation.
2.6 Are CommentImport / PatientComment bulk-import channels? No.
CommentImport (models.py:479) and PatientComment (models.py:534) describe a separate, parallel workflow (monthly IT exports of raw comments; "Steps 0-5"). PatientComment.suggestions (models.py:600) is just free-text extracted from a comment — it does not create a Feedback row. No code path creates CommentImport records (admin-only). See §16.
3. Complete Lifecycle
3.1 FeedbackStatus enum (models.py:30-37)
SUBMITTED, REVIEWED, ACKNOWLEDGED, CLOSED, REOPENED.
3.2 VALID_FEEDBACK_TRANSITIONS (models.py:40-46) — verified exact
SUBMITTED → [REVIEWED, CLOSED]
REVIEWED → [ACKNOWLEDGED, CLOSED]
ACKNOWLEDGED → [CLOSED]
CLOSED → [REOPENED]
REOPENED → [REVIEWED, ACKNOWLEDGED, CLOSED]
3.3 Enforcement layers
- Model: NOT enforced — no
clean()onFeedback. - DB: NOT enforced — no
CheckConstraintinMetaor migrations. - View: enforced only in
feedback_change_status(views.py:776-859): rejects no-op same-status (views.py:807); computesallowed = VALID_FEEDBACK_TRANSITIONS.get(old_status, [])and rejectsnew_status not in allowed(views.py:811-817). - Admin: NOT enforced.
Implication: transitions are soft. PX Admin editing via Django admin, or any direct ORM write, can move a suggestion to any status. Only the staff UI enforces the rules.
4. Status Definitions
| Status | Purpose | When entered | Who moves forward | Next |
|---|---|---|---|---|
SUBMITTED (models.py:33) |
Initial entry for every suggestion from every channel. UI badge: blue. | At save() on creation (models.py:197) |
Any PX/hospital user with change-status perm (§5) | REVIEWED, CLOSED |
REVIEWED (models.py:34) |
PX staffer has triaged/read. Sets reviewed_at/reviewed_by (views.py:821). UI badge: orange. |
feedback_change_status choosing reviewed |
Same perm group | ACKNOWLEDGED, CLOSED |
ACKNOWLEDGED (models.py:35) |
PX formally acknowledged/accepted. Sets acknowledged_at/acknowledged_by (views.py:824). UI badge: green. |
feedback_change_status choosing acknowledged |
Same | CLOSED |
CLOSED (models.py:36) |
Terminal. Sets closed_at/closed_by (views.py:827). Reachable from SUBMITTED, REVIEWED, ACKNOWLEDGED, REOPENED. UI badge: slate. When closed, Quick Actions + Edit hidden (feedback_detail.html:51,299). |
feedback_change_status |
Same | REOPENED |
REOPENED (models.py:37) |
Closed suggestion re-opened. Clears closed_at/closed_by (views.py:830); does NOT clear acknowledged_*/reviewed_*. |
feedback_change_status |
Same | REVIEWED, ACKNOWLEDGED, CLOSED |
Note on ACKNOWLEDGED: a
FeedbackResponsewithresponse_type="acknowledgment"is valid (models.py:372) and exposed byFeedbackResponseForm(forms.py:121), butfeedback_change_statusdoes not require a response to exist before allowing ACKNOWLEDGED. No programmatic coupling — acknowledgement is a status change; the optional acknowledgment response is an independent timeline entry.
5. Workflow Actions
5.1 List & Detail (read)
| Action | View | URL name | Permission |
|---|---|---|---|
| List | feedback_list (views.py:37) |
feedback_list (urls.py:13) |
@login_required; RBAC filtering (views.py:59): PX Admin all/hospital-filtered; Hospital Admin → own; Dept Manager → own dept; else own hospital |
| Detail | feedback_detail (views.py:194) |
feedback_detail (urls.py:14) |
@login_required; access checks (views.py:213) |
Detail computes a workflow stepper: Submitted → Assigned → Reviewed → Acknowledged → Closed (views.py:278) — advisory only, not authoritative.
5.2 Status change — feedback_change_status (views.py:776)
URL feedback_change_status (urls.py:21); POST-only. Permission (views.py:783): is_px_admin OR is_hospital_admin OR is_px_management OR is_px_employee. Logic: validate status value; reject no-op; enforce VALID_FEEDBACK_TRANSITIONS; apply side-effects (REVIEWED→reviewed_at/by, ACKNOWLEDGED→acknowledged_*, CLOSED→closed_*, REOPENED→clear closed_*); create FeedbackResponse(response_type="status_change"); audit. Form: FeedbackStatusChangeForm (forms.py:219) — lists all statuses without filtering (filtering in view).
5.3 Assign — feedback_assign (views.py:721)
URL feedback_assign (urls.py:20). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. Sets assigned_to/assigned_at; creates FeedbackResponse("assignment"); audit. Form: FeedbackAssignForm (forms.py:238). Candidate set: get_assignable_users(hospital) = active users in group "PX Employee" at the suggestion's hospital. No status restriction in the view (UI panel hidden when closed).
5.4 Add Response / Timeline — feedback_add_response (views.py:862)
URL feedback_add_response (urls.py:24). Permission: PX/Hosp-Admin/PX-Mgmt/Department-Manager. Creates FeedbackResponse with caller-supplied response_type (status_change/assignment/note/response/acknowledgment). No status restriction.
5.5 Send to Department — feedback_send_to_department (awareness-only)
URL feedback_send_to_department (urls.py:22). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. Precondition: feedback.department already set (views.py:1104). Emails department.champion/manager/deputy_manager with "This is for your awareness. No response is required." (views.py:1126). Creates is_internal=False note. Does NOT change status, does NOT create a department-response workflow. See §14.
5.6 Send-to (AJAX) — feedback_send_to
URL feedback_send_to (urls.py:23). Permission: PX/Hosp-Admin/PX-Mgmt/PX-Employee. Two modes: recipient_type="person" (assigns to a User + emails) or "department" (links feedback.department + emails/SMSs champion+manager via get_champion_and_manager; SMS says "No response is required"). Rejects dept with no champion/manager.
5.7 Toggle flags
| Action | View | Permission | Effect |
|---|---|---|---|
| Toggle featured | feedback_toggle_featured (views.py:895) |
PX/Hosp-Admin | flips is_featured (models.py:225) |
| Toggle follow-up | feedback_toggle_follow_up (views.py:916) |
PX/Hosp-Admin | flips requires_follow_up (models.py:227) |
5.8 Create PX Action — feedback_create_action
URL feedback_create_action (urls.py:28). Permission: PX/Hosp-Admin. Creates a PXAction (generic FK to Feedback), source_type="suggestion", status="open". One-way spawn — does not change Feedback status; "promote this suggestion into a tracked improvement action".
5.9 Attachments
FeedbackAttachment (models.py:332): no dedicated view — listed on detail (views.py:230) but creatable only via Django admin or programmatically.
5.10 CRUD
Create (feedback_create views.py:293); Update (feedback_update views.py:408 — PX/Hosp-Admin only); Delete-soft (feedback_delete views.py:476 — PX/Hosp-Admin only; feedback.soft_delete sets is_deleted/deleted_at/deleted_by).
6. Decision Points
The only enforced decision point is the transition gate inside feedback_change_status (views.py:811): is new_status in VALID_FEEDBACK_TRANSITIONS[old_status]?
Beyond that, every other "decision" is operator judgement — the code does not branch:
- "Needs review" — no predicate; SUBMITTED→REVIEWED or SUBMITTED→CLOSED directly.
- "Needs response before acknowledge" — not enforced.
- "Can be acknowledged directly" — only from REVIEWED or REOPENED; SUBMITTED→ACKNOWLEDGED forbidden.
- "Close vs reopen" — operator choice.
⚠ Gap — reopen unreachable via UI after closure: the Quick Actions panel (containing the status-change form) is hidden when
status == 'closed'(feedback_detail.html:299). The map allows CLOSED→REOPENED but no staff UI button offers it. Reopen is only possible via Django admin or direct ORM.
7. Assignment Flow
- Field:
Feedback.assigned_to(models.py:201);assigned_at(models.py:204). - Initial owner:
Nonefor every channel. - Reassignment:
feedback_assignoverwrites each call — no history field; history preserved only viaFeedbackResponse("assignment")entries. - Candidate set:
get_assignable_users(hospital)= active users in "PX Employee" group. Suggestions are assigned to PX staff only — never to a department manager/champion via this form. - Department routing:
Feedback.department(models.py:148) is not auto-assigned by any creation channel and is not used for ownership/routing. Populated only when a PX user manually sends the suggestion to a department — and even then it's for notification only, not ownership transfer (§14).
8. Investigation Process
No investigation phase. Compared to complaints/inquiries/observations there is no RCA-required state, no evidence-gathering, no explanation request, no SLA-driven investigation timer.
Closest analogues:
- AI pre-analysis (
analyze_suggestion_with_ai,tasks.py:184): async after creation. CallsAIService.analyze_suggestion(message, title)(apps/core/ai_service.py:2124) and overwritescategory/priority, optionallytitle, stores analysis undermetadata["ai_analysis"](tasks.py:219). - Optional RCA linkage:
feedback_detail(views.py:236) fetchesRootCauseAnalysisobjects; template has a (commented-out) "Initiate RCA" link. An RCA can be attached but is not part of the suggestion workflow.
CommentActionPlan — a SEPARATE action-tracking sub-flow (NOT on Feedback)
CommentActionPlan (models.py:660) attaches to PatientComment (IT-export pipeline), not Feedback. It has its own status enum CommentActionPlanStatus (PENDING/ON_PROCESS/COMPLETED, models.py:654) and timeframe/evidences/responsible_department fields. Unrelated to the live suggestion lifecycle (§16).
9. Communication Flow
| Event | Channel | Where |
|---|---|---|
| Acknowledgement | none automatic — operator may add response_type="acknowledgment" FeedbackResponse |
feedback_add_response |
| Response to patient | FeedbackResponse("response", is_internal=False) |
feedback_add_response |
| Closure | no outbound message — only internal timeline note | feedback_change_status |
Crucially, no email/SMS is ever sent to the suggester (the patient/contact) by the feedback app. All notifications go inbound to PX/department staff:
- On creation:
notify_staff_new_item("suggestion", id)emails/SMSs PX Admins (working-hours: all; after-hours: on-call) —complaints/tasks.py:2489; config blockcomplaints/tasks.py:2548. Templateemails/new_suggestion_notification.html. - On send-to-department: emails champion/manager/deputy (
views.py:1113) or champion+manager (views.py:1255) with explicit "No response is required".
So the suggester receives only the immediate {"reference":"SG-..."} JSON from the public endpoint (views.py:1026) and nothing further unless a PX operator manually contacts them outside the system. Suggestions are inbound-only communication.
10. Escalation Flow
None. Confirmed:
Feedbackhas no SLA fields (nodue_date,sla_deadline,escalated_at).- Celery beat (
config/celery.py) has SLA/escalation tasks for complaints, inquiries, observations — but nothing for feedback/suggestions. - The only feedback-related beat entry is
analyze-feedback-sentiment→process_pending_sentiment_analysisevery 30 min (config/celery.py:201) — analytics, not escalation. - Grep for
feedback.*sla|feedback.*escalatreturned no matches.
AuditEvent.EVENT_TYPES includes "escalation"/"sla_breach" (apps/core/models.py:89) but these are never emitted by the feedback app.
11. Resolution Process
Suggestions have no formal "resolution" — the resolution-equivalent statuses are ACKNOWLEDGED and CLOSED.
- Who can move to ACKNOWLEDGED/CLOSED: PX/Hosp-Admin/PX-Mgmt/PX-Employee (
views.py:783). - Approval required: none.
- Patient confirmation: not present — the suggester has no authenticated presence and is never asked to confirm.
- Evidence: none on
Feedback(exists only onCommentActionPlan.evidences,models.py:728, in the separate IT-export pipeline).
"Resolution" for a suggestion is simply an operator deciding to flip status and optionally recording an internal/external FeedbackResponse note.
12. Closure Process
- When closeable: from SUBMITTED, REVIEWED, ACKNOWLEDGED, or REOPENED (
models.py:41-45) — closable from almost every state. - Who closes: PX/Hosp-Admin/PX-Mgmt/PX-Employee (
views.py:783). - Side-effects:
closed_at/closed_by(views.py:827);FeedbackResponse("status_change")created; audit. - Reopen conditions: CLOSED → REOPENED is the only exit; reopening clears
closed_at/closed_by, leavesacknowledged_*/reviewed_*intact. - Permanently completed: no "permanent close" flag. A suggestion can be reopened indefinitely (REOPENED → CLOSED → …).
- Soft-delete: independent of status —
feedback_deletecallssoft_delete; restorable viaSoftDeleteModel.restore()(no UI button — admin only).
13. Exception Flows
The codebase implements none of the classic exception statuses as first-class concepts:
| Exception | Implemented? | Evidence |
|---|---|---|
| Duplicate | No | No duplicate_of FK, no detection task. Operators close+note. |
| Withdrawn | No | No WITHDRAWN status; suggester has no withdraw channel. |
| Invalid | No | No INVALID/REJECTED. SUBMITTED→CLOSED is closest (models.py:41). |
| Spam | No dedicated status | Only the public endpoint's IP rate-limit (5/300s → 429, views.py:944). Spam closed+noted manually. |
| Converted-to-complaint | No | No converter. Closest is feedback_create_action spawning a PXAction, not a Complaint. |
| Merged | No | No merge view, no merged_into FK. |
| Reopened | Yes | models.py:37,44; views.py:830. |
| Missing info | No | No "awaiting info" status. requires_follow_up flag (models.py:227, toggled views.py:916) is the only soft signal — a boolean, not a state. |
14. Department-Response Sub-Flow — NOT present for Suggestions
This is the single most important architectural difference between Suggestions and Complaints/Inquiries/Observations.
Explicit confirmation — suggestions are NOT routed to departments for response:
feedback_send_to_departmentemail body: "This is for your awareness. No response is required." (views.py:1126).feedback_send_toSMS body: "PX360: Feedback '...' logged for {dept}. ... No response is required." (views.py:1273,1284).feedback_send_todocstring: "Awareness-only: no response required from the department." (views.py:1151).
What "send to department" actually does:
- Optionally sets
feedback.department(views.py:1252;feedback_send_to_departmentrequires it set,views.py:1104). - Looks up champion + manager via
get_champion_and_manager. - Sends email (+ SMS in send_to) to those people.
- Writes an
is_internal=FalseFeedbackResponsenote recording who was notified. - Does not change status. Does not create any "department response" record. Has no SLA, no reminder, no overdue check.
Contrast with complaints/inquiries/observations which have dedicated check-overdue-*-dept-responses and send-*-dept-response-reminders beat tasks — feedback/suggestions have none.
Conclusion: Suggestions are handled centrally by PX. Departments may be informed for awareness but are never obligated to act within the suggestion lifecycle. If PX wants a department to actually do something, they must escalate via feedback_create_action to spawn a tracked PXAction (separate module).
15. End-to-End Example
Patient submits suggestion via public form
↓ (views.py:937) → status=SUBMITTED, ref SGT-202607-0001,
AI analysis dispatched, PX admins notified
[⚠ suggester NOT auto-notified beyond immediate JSON ack]
PX staff assigns to a PX Employee
↓ (views.py:721) → assigned_to set; FeedbackResponse("assignment")
PX reviews
↓ (views.py:776) → status=REVIEWED, reviewed_at/by
[DECISION: acknowledge or close]
PX acknowledges
↓ (views.py:776) → status=ACKNOWLEDGED, acknowledged_at/by
[optional: add FeedbackResponse("acknowledgment") to timeline]
PX closes
↓ (views.py:776) → status=CLOSED, closed_at/by; Quick Actions hidden
[REOPEN only via admin/ORM — not via staff UI]
[NO department involvement; NO outbound patient comms]
16. Parallel Pipelines (NOT part of the Suggestion lifecycle)
These exist in the same app but are distinct reporting/analytics workflows, not part of the live suggestion lifecycle. Documented here for completeness.
16.1 CommentActionPlan (models.py:660)
Attaches to PatientComment, not Feedback. Status enum CommentActionPlanStatus (COMPLETED/ON_PROCESS/PENDING — default models.py:715). Fields: problem_number, comment_text/_en, frequency, recommendation (required), responsible_department, timeframe (e.g. "Q3"), evidences (free-text completion evidence), month/year. No transition map — free-editable via admin. Listed read-only in action_plan_list ("Step 5", views.py:607).
16.2 PatientComment (models.py:534)
Raw-unit model of the IT-export pipeline. Imported in monthly batches via CommentImport (models.py:479), which tracks month/year/source_file/status (pending/processing/completed/failed) and counts. Each PatientComment carries: source_category (Appointment/Inpatient/Outpatient), comment_text, classification/sub_category, sentiment keyword fields, suggestions extracted text (just text, not a link), sentiment, is_classified, mentioned_doctor_name, frequency, month/year. Views: read-only list (comment_list views.py:538) + Excel exports Step 1/Step 2 (export_utils.py). Creation is admin-only (no code path creates CommentImport).
Bottom line: treat Comments & Action Plans as a separate "IT-export reporting" module that happens to live in the same Django app, not part of the Suggestions lifecycle.
Appendix — Flagged gaps / ambiguities
- Reopen unreachable via UI after closure (Quick Actions hidden when
closed,feedback_detail.html:299). CLOSED→REOPENED allowed by map but no staff UI button. reference_numberdivergence (external API resolved) —save()generates canonicalSGT-YYYYMM-NNNN; the external API now returns and retrieves by this canonical reference. Public portal still diverges: the public submit endpoint returns a differentSG-{uuid[:8]}(views.py:1026), so the public cannot track by the real reference via the web portal.FeedbackFormunused for creation —feedback_createbuilds manually, bypassing formclean().- Transitions unenforced at DB/model/admin — only
feedback_change_statusenforces. - No outbound communication to suggester by design.
feedback_send_to_departmentrequiresdepartmentset,feedback_send_tosets it — overlapping confusingly.- AI task overwrites operator-entered category/priority (
tasks.py:219) — may clobber a manual categorization made immediately after creation.