|
| 1 | +# Notification System |
| 2 | + |
| 3 | +The notification system in `django-forms-workflows` provides flexible, event-driven email notifications throughout the lifecycle of a form submission. Notifications can be configured at multiple levels of granularity — from workflow-wide rules to per-stage controls — allowing administrators to precisely target who receives which emails and when. |
| 4 | + |
| 5 | +## Architecture Overview |
| 6 | + |
| 7 | +``` |
| 8 | +Form Submission Lifecycle |
| 9 | +───────────────────────── |
| 10 | + Submitted ──► Stage 1 activates ──► Stage 2 activates ──► Final Decision |
| 11 | + │ │ │ │ |
| 12 | + ▼ ▼ ▼ ▼ |
| 13 | + submission approval_request approval_request approval_notification |
| 14 | + _received (stage-level) (stage-level) rejection_notification |
| 15 | + withdrawal_notification |
| 16 | +``` |
| 17 | + |
| 18 | +Notifications are dispatched as **Celery tasks** (asynchronous when a broker is available, synchronous fallback otherwise). The system supports three independent notification layers that fire in parallel: |
| 19 | + |
| 20 | +| Layer | Model | Scope | Typical Use | |
| 21 | +|-------|-------|-------|-------------| |
| 22 | +| **Workflow Notifications** | `WorkflowNotification` | Workflow-wide events | Notify submitter, static CC lists, or form-field-based recipients on final decisions | |
| 23 | +| **Stage Form-Field Notifications** | `StageFormFieldNotification` | Per-stage events | Notify a dynamic recipient (e.g., advisor email from form data) when a stage activates or on final decision | |
| 24 | +| **Stage Assignee Notifications** | `WorkflowStage.notify_assignee_on_final_decision` | Per-stage flag | Automatically include the stage's dynamically-assigned approver in final decision emails | |
| 25 | + |
| 26 | +## Notification Events |
| 27 | + |
| 28 | +| Event Key | When It Fires | Available On | |
| 29 | +|-----------|--------------|--------------| |
| 30 | +| `submission_received` | Form is first submitted | WorkflowNotification, StageFormFieldNotification | |
| 31 | +| `approval_request` | A workflow stage activates (task created) | StageFormFieldNotification | |
| 32 | +| `approval_notification` | Submission receives final approval | WorkflowNotification, StageFormFieldNotification | |
| 33 | +| `rejection_notification` | Submission is rejected | WorkflowNotification, StageFormFieldNotification | |
| 34 | +| `withdrawal_notification` | Submitter withdraws the submission | WorkflowNotification | |
| 35 | + |
| 36 | +## Email Templates |
| 37 | + |
| 38 | +Each event type maps to a dedicated HTML template: |
| 39 | + |
| 40 | +| Event | Template | |
| 41 | +|-------|----------| |
| 42 | +| `submission_received` | `emails/submission_notification.html` | |
| 43 | +| `approval_request` | `emails/approval_request.html` | |
| 44 | +| `approval_notification` | `emails/approval_notification.html` | |
| 45 | +| `rejection_notification` | `emails/rejection_notification.html` | |
| 46 | +| `withdrawal_notification` | `emails/withdrawal_notification.html` | |
| 47 | +| Batched digest | `emails/notification_digest.html` | |
| 48 | + |
| 49 | +All templates extend a shared `emails/email_styles.html` for consistent branding. Templates receive the `submission`, `submission_url`, and (where applicable) `approval_url` and `task` in their context. |
| 50 | + |
| 51 | +## Recipient Resolution |
| 52 | + |
| 53 | +Every notification rule resolves recipients through a common function (`_collect_notification_recipients`) that combines multiple sources in order: |
| 54 | + |
| 55 | +1. **Submitter** — If `notify_submitter = True`, the `submission.submitter.email` is included. |
| 56 | +2. **Dynamic assignees** — For `approval_notification` and `rejection_notification` events, all stages with `notify_assignee_on_final_decision = True` contribute their resolved approver's email (from `ApprovalTask.assigned_to.email`). |
| 57 | +3. **Email field** — A form field slug (e.g., `advisor_email`) whose submitted value is an email address. Resolved from `form_data` at send time, so it varies per submission. |
| 58 | +4. **Static emails** — A comma-separated list of fixed addresses (e.g., `registrar@example.edu, dean@example.edu`). |
| 59 | + |
| 60 | +Recipients are deduplicated; no address receives the same notification twice. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## Layer 1: Workflow Notifications (`WorkflowNotification`) |
| 65 | + |
| 66 | +Workflow Notifications are attached to a `WorkflowDefinition` and fire on workflow-level events. They are the primary mechanism for notifying the submitter, static CC lists, and form-field-based recipients. |
| 67 | + |
| 68 | +### Fields |
| 69 | + |
| 70 | +| Field | Type | Description | |
| 71 | +|-------|------|-------------| |
| 72 | +| `workflow` | ForeignKey | The parent WorkflowDefinition | |
| 73 | +| `notification_type` | Choice | Which event triggers this rule | |
| 74 | +| `notify_submitter` | Boolean | Include the form submitter as a recipient | |
| 75 | +| `email_field` | CharField | Form field slug containing a recipient email | |
| 76 | +| `static_emails` | CharField | Comma-separated fixed email addresses | |
| 77 | +| `subject_template` | CharField | Custom subject line (supports `{form_name}`, `{submission_id}` placeholders) | |
| 78 | +| `conditions` | JSONField | Optional conditions evaluated against `form_data` | |
| 79 | + |
| 80 | +### Validation |
| 81 | + |
| 82 | +At least one recipient source (`notify_submitter`, `email_field`, or `static_emails`) must be set. The `clean()` method enforces this. |
| 83 | + |
| 84 | +### Admin Configuration |
| 85 | + |
| 86 | +Workflow Notifications appear as an inline on the WorkflowDefinition admin page. Multiple rules can be created per workflow, each targeting different events and recipient groups. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## Layer 2: Stage Form-Field Notifications (`StageFormFieldNotification`) |
| 91 | + |
| 92 | +Stage Form-Field Notifications are attached to a `WorkflowStage` and support all four event types including `approval_request` (stage activation). They are ideal for notifying external parties whose email is captured in the form. |
| 93 | + |
| 94 | +### Fields |
| 95 | + |
| 96 | +| Field | Type | Description | |
| 97 | +|-------|------|-------------| |
| 98 | +| `stage` | ForeignKey | The parent WorkflowStage | |
| 99 | +| `notification_type` | Choice | Which event triggers this rule | |
| 100 | +| `email_field` | CharField | Form field slug containing a recipient email | |
| 101 | +| `static_emails` | CharField | Comma-separated fixed email addresses | |
| 102 | +| `subject_template` | CharField | Custom subject line (supports `{form_name}`, `{submission_id}`) | |
| 103 | +| `conditions` | JSONField | Optional conditions evaluated against `form_data` | |
| 104 | + |
| 105 | +### Key Difference from Workflow Notifications |
| 106 | + |
| 107 | +- Supports `approval_request` event type (fires when the stage activates). |
| 108 | +- Does **not** have `notify_submitter` — use a WorkflowNotification rule for that. |
| 109 | +- Always fires immediately regardless of the workflow's `notification_cadence` setting. |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## Layer 3: Stage Assignee Notifications (`notify_assignee_on_final_decision`) |
| 114 | + |
| 115 | +When a workflow stage uses **dynamic assignment** (`assignee_form_field` + `assignee_lookup_type`), the system resolves a form field value (e.g., a full name like "Jane Smith") to a Django `User` and assigns the approval task to that user. |
| 116 | + |
| 117 | +The `notify_assignee_on_final_decision` boolean on `WorkflowStage` controls whether that resolved user is automatically included as a recipient on final approval/rejection notifications. |
| 118 | + |
| 119 | +### How It Works |
| 120 | + |
| 121 | +1. At **stage creation time**, the workflow engine reads the form field value and resolves it to a `User` via the configured lookup type (`email`, `username`, `full_name`, or `ldap`). |
| 122 | +2. The resolved `User` is stored as `ApprovalTask.assigned_to`. |
| 123 | +3. At **notification time**, if `notify_assignee_on_final_decision = True`, the system queries all approval tasks for the submission where the stage has this flag set, and adds each `assigned_to.email` to the recipient list. |
| 124 | + |
| 125 | +### Prerequisites |
| 126 | + |
| 127 | +- The stage must have `assignee_form_field` configured. |
| 128 | +- The resolved `User` must have a populated `email` field. |
| 129 | +- For `full_name` lookup: the user must exist in Django with matching `first_name`/`last_name`, or be JIT-provisioned via LDAP fallback. |
| 130 | + |
| 131 | +### Admin Configuration |
| 132 | + |
| 133 | +The flag appears in the WorkflowStage inline (alongside `validate_assignee_group`) and in the standalone WorkflowStage admin under "Dynamic Assignment". |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Conditional Notifications |
| 138 | + |
| 139 | +All notification rules support optional **conditions** — a JSON structure evaluated against `form_data` before sending. This uses the same format as `WorkflowStage.trigger_conditions`: |
| 140 | + |
| 141 | +```json |
| 142 | +{ |
| 143 | + "operator": "AND", |
| 144 | + "conditions": [ |
| 145 | + {"field": "department", "operator": "equals", "value": "Graduate Studies"}, |
| 146 | + {"field": "amount", "operator": "gt", "value": "5000"} |
| 147 | + ] |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | + |
| 152 | +Batching applies to **Workflow Notifications** and **approval request** emails. Stage Form-Field Notifications (`StageFormFieldNotification`) always fire immediately. |
| 153 | + |
| 154 | +Queued notifications are stored in the `PendingNotification` model and dispatched by the `send_batched_notifications` periodic Celery task. |
| 155 | + |
| 156 | +Additional batching controls on `WorkflowDefinition`: |
| 157 | + |
| 158 | +| Field | Description | |
| 159 | +|-------|-------------| |
| 160 | +| `notification_cadence_day` | Day of week (0=Mon–6=Sun) for weekly, or day of month (1–31) for monthly | |
| 161 | +| `notification_cadence_time` | Time of day to send the digest (defaults to 08:00) | |
| 162 | +| `notification_cadence_form_field` | For `form_field_date`: the slug of the date field to read | |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +## Privacy: Hiding Approval History |
| 167 | + |
| 168 | +The `hide_approval_history` flag on `WorkflowDefinition` controls whether rejection/approval notification emails include the full approval history (individual approver names and comments). When enabled: |
| 169 | + |
| 170 | +- The submitter only sees the final decision (approved/rejected). |
| 171 | +- Approvers and admins can still see the full history in the admin. |
| 172 | +- The `hide_approval_history` context variable is passed to all notification templates. |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Configuration Scenarios |
| 177 | + |
| 178 | +### Scenario 1: Simple — Notify the Submitter on Approval/Rejection |
| 179 | + |
| 180 | +> "When a form is approved or rejected, the person who submitted it should get an email." |
| 181 | +
|
| 182 | +**Configuration:** |
| 183 | +1. Create a `WorkflowNotification` with: |
| 184 | + - `notification_type` = `approval_notification` |
| 185 | + - `notify_submitter` = ✅ |
| 186 | +2. Create another `WorkflowNotification` with: |
| 187 | + - `notification_type` = `rejection_notification` |
| 188 | + - `notify_submitter` = ✅ |
| 189 | + |
| 190 | +No `email_field` or `static_emails` needed. |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +### Scenario 2: Notify an Advisor from Form Data |
| 195 | + |
| 196 | +> "The form has an 'Advisor Email' field. When submitted, the advisor should get a notification. When approved, the advisor should also be told." |
| 197 | +
|
| 198 | +**Configuration:** |
| 199 | +1. On Stage 1, create a `StageFormFieldNotification`: |
| 200 | + - `notification_type` = `submission_received` |
| 201 | + - `email_field` = `advisor_email` |
| 202 | +2. On Stage 1, create another `StageFormFieldNotification`: |
| 203 | + - `notification_type` = `approval_notification` |
| 204 | + - `email_field` = `advisor_email` |
| 205 | + |
| 206 | +The email address is read from the form's submitted data each time. |
| 207 | + |
| 208 | +--- |
| 209 | + |
| 210 | +### Scenario 3: Notify the Dynamically-Assigned Approver on Final Decision |
| 211 | + |
| 212 | +> "Stage 1 assigns a reviewer by full name (e.g., 'Jane Smith' selected from a dropdown). That reviewer should be notified when the workflow reaches its final decision." |
| 213 | +
|
| 214 | +**Configuration:** |
| 215 | +1. On `WorkflowStage` (Stage 1): |
| 216 | + - `assignee_form_field` = `reviewer_name` |
| 217 | + - `assignee_lookup_type` = `full_name` |
| 218 | + - `notify_assignee_on_final_decision` = ✅ |
| 219 | +2. Create a `WorkflowNotification` with: |
| 220 | + - `notification_type` = `approval_notification` |
| 221 | + - `notify_submitter` = ✅ (optional — if the submitter should also be notified) |
| 222 | + |
| 223 | +The reviewer's email is resolved from the Django `User` record that matched the full name lookup. No separate email field is needed on the form. |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +### Scenario 4: CC a Department on All Submissions |
| 228 | + |
| 229 | +> "Every time this form is submitted, the registrar's office should get a copy." |
| 230 | +
|
| 231 | +**Configuration:** |
| 232 | +1. Create a `WorkflowNotification` with: |
| 233 | + - `notification_type` = `submission_received` |
| 234 | + - `static_emails` = `registrar@example.edu` |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +### Scenario 5: Conditional Notification to Dean |
| 239 | + |
| 240 | +> "Only notify the dean if the department is 'Graduate Studies' AND the request amount exceeds $5,000." |
| 241 | +
|
| 242 | +**Configuration:** |
| 243 | +1. Create a `WorkflowNotification` with: |
| 244 | + - `notification_type` = `approval_notification` |
| 245 | + - `static_emails` = `dean@example.edu` |
| 246 | + - `conditions`: |
| 247 | + ```json |
| 248 | + { |
| 249 | + "operator": "AND", |
| 250 | + "conditions": [ |
| 251 | + {"field": "department", "operator": "equals", "value": "Graduate Studies"}, |
| 252 | + {"field": "amount", "operator": "gt", "value": "5000"} |
| 253 | + ] |
| 254 | + } |
| 255 | + ``` |
| 256 | + |
| 257 | +The dean only receives the email if both conditions are met. |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +### Scenario 6: Multi-Stage with Selective Assignee Notifications |
| 262 | + |
| 263 | +> "A three-stage workflow: Stage 1 (Department Chair), Stage 2 (HR Review), Stage 3 (VP Approval). Only the Department Chair and VP should be notified on final decision — not HR." |
| 264 | + |
| 265 | +**Configuration:** |
| 266 | +1. Stage 1 (`Department Chair`): |
| 267 | + - `assignee_form_field` = `department_chair` |
| 268 | + - `assignee_lookup_type` = `full_name` |
| 269 | + - `notify_assignee_on_final_decision` = ✅ |
| 270 | +2. Stage 2 (`HR Review`): |
| 271 | + - Assign via approval groups (no dynamic assignee) |
| 272 | + - `notify_assignee_on_final_decision` = ❌ (or leave default `False`) |
| 273 | +3. Stage 3 (`VP Approval`): |
| 274 | + - `assignee_form_field` = `vp_name` |
| 275 | + - `assignee_lookup_type` = `full_name` |
| 276 | + - `notify_assignee_on_final_decision` = ✅ |
| 277 | +4. Create a `WorkflowNotification`: |
| 278 | + - `notification_type` = `approval_notification` |
| 279 | + - `notify_submitter` = ✅ |
| 280 | + |
| 281 | +Result: On final approval, the submitter, Department Chair, and VP all receive emails. HR does not. |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +### Scenario 7: Batched Daily Digest |
| 286 | + |
| 287 | +> "Instead of sending an email for every submission, send the registrar a daily summary." |
| 288 | + |
| 289 | +**Configuration:** |
| 290 | +1. On the `WorkflowDefinition`: |
| 291 | + - `notification_cadence` = `daily` |
| 292 | + - `notification_cadence_time` = `08:00` |
| 293 | +2. Create a `WorkflowNotification`: |
| 294 | + - `notification_type` = `submission_received` |
| 295 | + - `static_emails` = `registrar@example.edu` |
| 296 | + |
| 297 | +All submissions throughout the day are batched into a single digest email sent at 8:00 AM. |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +### Scenario 8: Custom Subject Line |
| 302 | + |
| 303 | +> "Approval emails should say 'Your Leave Request Has Been Approved' instead of the default." |
| 304 | + |
| 305 | +**Configuration:** |
| 306 | +1. On the `WorkflowNotification`: |
| 307 | + - `subject_template` = `Your Leave Request Has Been Approved (ID {submission_id})` |
| 308 | + |
| 309 | +Placeholders `{form_name}` and `{submission_id}` are available. |
| 310 | + |
| 311 | +--- |
| 312 | + |
| 313 | +## Email Backend Configuration |
| 314 | + |
| 315 | +The system supports multiple email backends: |
| 316 | + |
| 317 | +| Backend | Setting | Use Case | |
| 318 | +|---------|---------|----------| |
| 319 | +| Console | `django.core.mail.backends.console.EmailBackend` | Development/testing — prints to stdout | |
| 320 | +| SMTP | `django.core.mail.backends.smtp.EmailBackend` | Traditional SMTP relay | |
| 321 | +| Gmail API | `django_forms_workflows.email_backends.GmailAPIBackend` | Google Workspace with service account | |
| 322 | + |
| 323 | +Configure via the `EMAIL_BACKEND` environment variable. For Gmail API, also set: |
| 324 | +- `DEFAULT_FROM_EMAIL` — sender address (e.g., `donotreply@example.edu`) |
| 325 | +- `GMAIL_DELEGATED_USER` — the user to impersonate |
| 326 | +- `GMAIL_SERVICE_ACCOUNT_KEY_BASE64` — base64-encoded service account JSON key |
| 327 | + |
| 328 | +--- |
| 329 | + |
| 330 | +## Troubleshooting |
| 331 | + |
| 332 | +### No emails are being sent |
| 333 | + |
| 334 | +1. **Check `EMAIL_BACKEND`** — ensure it's not set to `console` in production. |
| 335 | +2. **Check Celery** — notifications are dispatched as Celery tasks. If Celery is not running, the system falls back to synchronous dispatch, but verify `CELERY_TASK_ALWAYS_EAGER` is set appropriately. |
| 336 | +3. **Check recipient resolution** — if no recipients resolve (empty `email_field` value, no `static_emails`, `notify_submitter` is `False`), the notification is silently skipped. Check logs for "no recipients resolved" messages. |
| 337 | + |
| 338 | +### Dynamic assignee not receiving final decision emails |
| 339 | + |
| 340 | +1. **Check `notify_assignee_on_final_decision`** — must be `True` on the relevant stage. |
| 341 | +2. **Check `assignee_form_field`** — the stage must use dynamic assignment. |
| 342 | +3. **Check User.email** — the resolved Django User must have a populated email field. |
| 343 | +4. **Check lookup resolution** — if using `full_name`, the user must exist with matching `first_name`/`last_name`, or LDAP fallback must be configured. |
| 344 | + |
| 345 | +### Conditional notification not firing |
| 346 | + |
| 347 | +1. **Check the conditions JSON** — ensure field names match form field slugs exactly. |
| 348 | +2. **Check operator syntax** — valid operators: `equals`, `not_equals`, `gt`, `lt`, `gte`, `lte`, `contains`, `in`. |
| 349 | +3. **Check logs** — condition evaluation errors are logged as warnings. |
| 350 | + |
| 351 | +### Batched notifications not sending |
| 352 | + |
| 353 | +1. **Check `send_batched_notifications` periodic task** — must be in the Celery beat schedule. |
| 354 | +2. **Check `PendingNotification` records** — query the table to see if notifications are being queued. |
| 355 | +3. **Check cadence settings** — `notification_cadence_day` and `notification_cadence_time` must be set correctly for weekly/monthly. |
| 356 | + |
| 357 | +## Notification Batching |
| 358 | + |
| 359 | +Workflows can opt into **digest-style batching** instead of immediate delivery via the `notification_cadence` field on `WorkflowDefinition`: |
| 360 | + |
| 361 | +| Cadence | Behavior | |
| 362 | +|---------|----------| |
| 363 | +| `immediate` | Each notification fires as a separate email right away (default) | |
| 364 | +| `daily` | Notifications are queued and sent as a single digest email once per day | |
| 365 | +| `weekly` | Queued and sent once per week on the configured day | |
| 366 | +| `monthly` | Queued and sent once per month on the configured day | |
| 367 | +| `form_field_date` | Queued and sent on the date specified in a form field | |
| 368 | + |
0 commit comments