Skip to content

Commit 6e36933

Browse files
committed
feat: add notify_assignee_on_final_decision flag to WorkflowStage
Add a per-stage boolean that controls whether dynamically-assigned approvers (resolved via assignee_form_field) are included as recipients on final approval/rejection notifications. Defaults to False, making this opt-in per stage rather than the previous unconditional inclusion. - New field on WorkflowStage model with migration 0073 - Gate dynamic assignee email collection in _collect_notification_recipients - Add field to both inline and standalone WorkflowStage admin - Update existing test to set the flag explicitly
1 parent 0104ded commit 6e36933

7 files changed

Lines changed: 47 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.42.0] - 2026-03-31
11+
12+
### Added
13+
- **Stage-level `notify_assignee_on_final_decision` flag** — New boolean field on `WorkflowStage` that controls whether a stage's dynamically-assigned approver (resolved via `assignee_form_field`) is included as a recipient on final approval/rejection notifications. Defaults to `False`, making dynamic assignee notification opt-in per stage rather than the previous unconditional inclusion. This gives administrators granular control over which stage assignees receive final decision emails.
14+
1015
## [0.38.6] - 2026-03-30
1116

1217
### Fixed

django_forms_workflows/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ class WorkflowStageInline(nested_admin.NestedStackedInline):
471471
"approve_label",
472472
"requires_manager_approval",
473473
("assignee_form_field", "assignee_lookup_type"),
474-
"validate_assignee_group",
474+
("validate_assignee_group", "notify_assignee_on_final_decision"),
475475
("allow_reassign", "allow_send_back"),
476476
"allow_edit_form_data",
477477
"trigger_conditions",
@@ -1430,6 +1430,7 @@ class WorkflowStageAdmin(nested_admin.NestedModelAdmin):
14301430
"assignee_form_field",
14311431
"assignee_lookup_type",
14321432
"validate_assignee_group",
1433+
"notify_assignee_on_final_decision",
14331434
"allow_reassign",
14341435
"allow_send_back",
14351436
),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 6.0.3 on 2026-03-31 13:30
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("django_forms_workflows", "0072_add_change_history"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="workflowstage",
15+
name="notify_assignee_on_final_decision",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="Include this stage's dynamically-assigned approver as a recipient on final approval/rejection notifications. Only effective when assignee_form_field is set and a user was successfully resolved.",
19+
),
20+
),
21+
]

django_forms_workflows/models.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,14 @@ class WorkflowStage(models.Model):
880880
"for sites where SSO does not populate first/last name."
881881
),
882882
)
883+
notify_assignee_on_final_decision = models.BooleanField(
884+
default=False,
885+
help_text=(
886+
"Include this stage's dynamically-assigned approver as a recipient "
887+
"on final approval/rejection notifications. Only effective when "
888+
"assignee_form_field is set and a user was successfully resolved."
889+
),
890+
)
883891
allow_send_back = models.BooleanField(
884892
default=False,
885893
help_text=(
@@ -1070,11 +1078,14 @@ class WorkflowNotification(models.Model):
10701078
Recipients are the union of:
10711079
10721080
* ``notify_submitter`` — if True, the person who submitted the form is always included.
1081+
* ``notify_assignees`` — if True, all dynamically-assigned workflow approvers
1082+
(resolved via ``assignee_form_field`` on any stage) are included.
1083+
Only effective for approval and rejection notifications.
10731084
* ``email_field`` — slug of the form field whose value is the recipient email
10741085
(resolved from ``form_data`` at send time, varies per submission).
10751086
* ``static_emails`` — comma-separated fixed addresses always included.
10761087
1077-
At least one of the three must be set.
1088+
At least one of the four must be set.
10781089
10791090
``conditions`` (same JSON format as ``WorkflowStage.trigger_conditions``)
10801091
are evaluated against ``form_data`` before sending; leave blank to always send.
@@ -1155,18 +1166,21 @@ def clean(self):
11551166

11561167
if (
11571168
not self.notify_submitter
1169+
and not self.notify_assignees
11581170
and not self.email_field
11591171
and not self.static_emails
11601172
):
11611173
raise ValidationError(
11621174
"At least one recipient source must be set: "
1163-
"'Notify submitter', 'Email field', or 'Static emails'."
1175+
"'Notify submitter', 'Notify assignees', 'Email field', or 'Static emails'."
11641176
)
11651177

11661178
def __str__(self) -> str:
11671179
parts = []
11681180
if self.notify_submitter:
11691181
parts.append("submitter")
1182+
if self.notify_assignees:
1183+
parts.append("assignees")
11701184
if self.email_field:
11711185
parts.append(f"field:{self.email_field}")
11721186
if self.static_emails:

django_forms_workflows/tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ def _collect_notification_recipients(
788788
.filter(assigned_to__isnull=False)
789789
.exclude(workflow_stage__assignee_form_field__isnull=True)
790790
.exclude(workflow_stage__assignee_form_field="")
791+
.filter(workflow_stage__notify_assignee_on_final_decision=True)
791792
.values_list("assigned_to__email", flat=True)
792793
)
793794
for email in dynamic_assignee_emails:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-forms-workflows"
3-
version = "0.41.1"
3+
version = "0.42.0"
44
description = "Enterprise-grade, database-driven form builder with approval workflows and external data integration"
55
license = "LGPL-3.0-only"
66
readme = "README.md"

tests/test_notifications.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def test_final_decision_notifications_include_dynamic_assignee(
3737
approval_logic="all",
3838
assignee_form_field="advisor_email",
3939
assignee_lookup_type="email",
40+
notify_assignee_on_final_decision=True,
4041
)
4142
WorkflowNotification.objects.create(
4243
workflow=workflow,

0 commit comments

Comments
 (0)