Skip to content

Commit e2bf59e

Browse files
committed
add docs on configuring notifications
1 parent d6ff42b commit e2bf59e

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

docs/NOTIFICATIONS.md

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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

Comments
 (0)