Skip to content

Commit b9c7038

Browse files
committed
feat: third/fourth layout fix, notify_submitter migration (v0.37.15)
forms.py: - Fix third/fourth width fields rendering on separate lines by grouping consecutive same-width fields into a shared Row (up to 3 for third, 4 for fourth) -- same pattern already used for half-width fields models.py (WorkflowNotification): - Add notify_submitter BooleanField(default=False) so the submitter can be targeted as a recipient from the new notification system - Update clean() to validate at least one recipient source is set - Update __str__ to include submitter in recipient description migrations: - 0066: schema migration adding notify_submitter column - 0067: data migration converting legacy notify_on_* flags to WorkflowNotification rows with notify_submitter=True and static_emails from additional_notify_emails tasks.py: - _collect_notification_recipients: add submission param; prepend submitter email when notif.notify_submitter is True - send_workflow_definition_notifications: pass submission= to recipient helper - 4 legacy tasks: early-return guard when notify_submitter row exists admin.py: - WorkflowNotificationInline + Admin: add notify_submitter field - Legacy notifications fieldset: labelled Deprecated with migration guidance Bump 0.37.14 to 0.37.15
1 parent c09b987 commit b9c7038

File tree

8 files changed

+245
-48
lines changed

8 files changed

+245
-48
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [0.37.15] - 2026-03-27
11+
12+
### Fixed
13+
- **`third`/`fourth` width fields rendering on separate lines** — The `_setup_layout` method now collects consecutive same-width fields into a shared `Row` (up to 3 for `third`/`col-md-4`, up to 4 for `fourth`/`col-md-3`), exactly like the existing `half` grouping logic. Previously each field got its own `Row`, forcing them onto individual lines regardless of width.
14+
15+
### Added
16+
- **`WorkflowNotification.notify_submitter`** — New boolean field (default `False`). When checked, the submission's submitter email is always included as a recipient alongside any `email_field` or `static_emails`. This is the replacement for the legacy `notify_on_*` flags.
17+
- **Schema migration `0066`** — Adds `notify_submitter` to `WorkflowNotification`.
18+
- **Data migration `0067`** — Converts existing `WorkflowDefinition.notify_on_*` flags into `WorkflowNotification` rows with `notify_submitter=True` and `static_emails` copied from `additional_notify_emails`. Idempotent: skips if a matching row already exists.
19+
- **`_collect_notification_recipients` handles `notify_submitter`** — Passes through a `submission` parameter; prepends `submission.submitter.email` when `notify_submitter=True`.
20+
- **Legacy tasks defer to `WorkflowNotification`**`send_rejection_notification`, `send_approval_notification`, `send_submission_notification`, and `send_withdrawal_notification` now check for an existing `WorkflowNotification` row with `notify_submitter=True` for the matching event type. If found, they return early to avoid sending duplicate emails alongside the new system.
21+
- **Admin updates**`WorkflowNotificationInline` and `WorkflowNotificationAdmin` both expose `notify_submitter` in fieldsets and list view. The legacy notifications fieldset is now labelled `⚠️ Deprecated` with an explanatory banner directing admins to the new `WorkflowNotification` inline.
22+
- **`WorkflowNotification.clean()` validation** — Raises `ValidationError` if none of `notify_submitter`, `email_field`, or `static_emails` is set.
23+
1024
## [0.37.14] - 2026-03-27
1125

1226
### Fixed

django_forms_workflows/admin.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,8 @@ class WorkflowNotificationInline(nested_admin.NestedStackedInline):
447447
None,
448448
{
449449
"fields": (
450-
("notification_type", "email_field"),
451-
"static_emails",
450+
("notification_type", "notify_submitter"),
451+
("email_field", "static_emails"),
452452
"subject_template",
453453
)
454454
},
@@ -1457,18 +1457,18 @@ class WorkflowDefinitionAdmin(nested_admin.NestedModelAdmin):
14571457
},
14581458
),
14591459
(
1460-
"Legacy Notifications (submitter + additional emails)",
1460+
"⚠️ Deprecated — Legacy Notifications (submitter + additional emails)",
14611461
{
14621462
"classes": ("collapse",),
14631463
"description": (
1464-
"<strong>These toggles control the built-in submitter notification emails</strong> "
1465-
"(submission received, approved, rejected, withdrawn). "
1466-
"They send one email to the submitter plus any addresses in "
1467-
"<em>Additional notify emails</em>.<br><br>"
1468-
"For <strong>granular, per-event, per-recipient rules</strong> — "
1469-
"e.g. a separate email to an advisor only on approval, or a "
1470-
"conditional alert to a department head — use the "
1471-
"<strong>Workflow Notifications</strong> inline below instead."
1464+
"<strong style='color:#b45309;'>DEPRECATED.</strong> "
1465+
"These flags are superseded by the <strong>Workflow Notifications</strong> inline below. "
1466+
"A data migration has already converted enabled flags into WorkflowNotification rows "
1467+
"with <em>Notify submitter</em> checked — those rows now control submitter emails for this workflow. "
1468+
"The legacy flags are skipped automatically whenever a matching WorkflowNotification row exists.<br><br>"
1469+
"You can safely leave these enabled for now; they will be removed in a future release. "
1470+
"To fully migrate, disable each flag here and confirm that a corresponding "
1471+
"Workflow Notification row (with <em>Notify submitter</em> = ✓) is configured below."
14721472
),
14731473
"fields": (
14741474
(
@@ -1530,12 +1530,13 @@ class WorkflowNotificationAdmin(admin.ModelAdmin):
15301530
list_display = (
15311531
"workflow_form",
15321532
"notification_type",
1533+
"notify_submitter",
15331534
"email_field",
15341535
"static_emails_truncated",
15351536
"has_conditions",
15361537
"subject_template_truncated",
15371538
)
1538-
list_filter = ("notification_type", "workflow__form_definition")
1539+
list_filter = ("notification_type", "notify_submitter", "workflow__form_definition")
15391540
list_select_related = ("workflow__form_definition",)
15401541
search_fields = (
15411542
"workflow__form_definition__name",
@@ -1549,8 +1550,8 @@ class WorkflowNotificationAdmin(admin.ModelAdmin):
15491550
{
15501551
"fields": (
15511552
"workflow",
1552-
("notification_type", "email_field"),
1553-
"static_emails",
1553+
("notification_type", "notify_submitter"),
1554+
("email_field", "static_emails"),
15541555
"subject_template",
15551556
)
15561557
},

django_forms_workflows/forms.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -211,26 +211,31 @@ def __init__(self, form_definition, user=None, initial_data=None, *args, **kwarg
211211
)
212212
)
213213
i += 1
214-
elif field.width == "third":
215-
layout_fields.append(
216-
Div(
217-
Row(
218-
Column(Field(field.field_name), css_class="col-md-4"),
219-
),
220-
css_class=f"field-wrapper field-{field.field_name}",
221-
)
222-
)
223-
i += 1
224-
elif field.width == "fourth":
214+
elif field.width in ("third", "fourth"):
215+
# Collect consecutive same-width fields (up to 3 for third, 4 for fourth)
216+
# into a single Row so they sit side-by-side.
217+
col_class = "col-md-4" if field.width == "third" else "col-md-3"
218+
max_per_row = 3 if field.width == "third" else 4
219+
group = []
220+
while (
221+
i < len(fields)
222+
and len(group) < max_per_row
223+
and fields[i].width == field.width
224+
and fields[i].field_type != "section"
225+
):
226+
group.append(fields[i])
227+
i += 1
225228
layout_fields.append(
226-
Div(
227-
Row(
228-
Column(Field(field.field_name), css_class="col-md-3"),
229-
),
230-
css_class=f"field-wrapper field-{field.field_name}",
229+
Row(
230+
*[
231+
Div(
232+
Field(f.field_name),
233+
css_class=f"{col_class} field-wrapper field-{f.field_name}",
234+
)
235+
for f in group
236+
]
231237
)
232238
)
233-
i += 1
234239
else:
235240
layout_fields.append(
236241
Div(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 5.2.7 on 2026-03-27 19:22
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("django_forms_workflows", "0065_workflownotification"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="workflownotification",
15+
name="notify_submitter",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="If checked, the person who submitted the form is always included as a recipient. Use this to replace the legacy 'notify on …' flags with a fully-configurable rule.",
19+
),
20+
),
21+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Data migration: convert legacy notify_on_* flags on WorkflowDefinition into
3+
granular WorkflowNotification rows with notify_submitter=True.
4+
5+
For each WorkflowDefinition we create up to four WorkflowNotification rows —
6+
one per enabled legacy flag. Any additional_notify_emails are copied to
7+
static_emails on the same row so existing CC behaviour is preserved.
8+
9+
Rows are only created if no WorkflowNotification of that type already exists
10+
for the workflow, keeping the migration idempotent.
11+
"""
12+
13+
from django.db import migrations
14+
15+
FLAG_MAP = [
16+
("notify_on_submission", "submission_received"),
17+
("notify_on_approval", "approval_notification"),
18+
("notify_on_rejection", "rejection_notification"),
19+
("notify_on_withdrawal", "withdrawal_notification"),
20+
]
21+
22+
23+
def convert_legacy_flags(apps, schema_editor):
24+
WorkflowDefinition = apps.get_model("django_forms_workflows", "WorkflowDefinition")
25+
WorkflowNotification = apps.get_model("django_forms_workflows", "WorkflowNotification")
26+
27+
for workflow in WorkflowDefinition.objects.all():
28+
static_emails = (workflow.additional_notify_emails or "").strip()
29+
existing_types = set(
30+
WorkflowNotification.objects.filter(workflow=workflow).values_list(
31+
"notification_type", flat=True
32+
)
33+
)
34+
35+
for flag_field, notification_type in FLAG_MAP:
36+
if not getattr(workflow, flag_field, False):
37+
continue
38+
if notification_type in existing_types:
39+
# Already configured via the new system — skip to avoid duplicates.
40+
continue
41+
WorkflowNotification.objects.create(
42+
workflow=workflow,
43+
notification_type=notification_type,
44+
notify_submitter=True,
45+
static_emails=static_emails,
46+
)
47+
48+
49+
def reverse_migration(apps, schema_editor):
50+
"""
51+
Reverse: remove WorkflowNotification rows that were created by this migration
52+
(identified by notify_submitter=True and no email_field set).
53+
We cannot distinguish them from manually-created rows with the same pattern,
54+
so we only remove rows that have notify_submitter=True and no email_field.
55+
"""
56+
WorkflowNotification = apps.get_model("django_forms_workflows", "WorkflowNotification")
57+
WorkflowNotification.objects.filter(notify_submitter=True, email_field="").delete()
58+
59+
60+
class Migration(migrations.Migration):
61+
dependencies = [
62+
("django_forms_workflows", "0066_add_notify_submitter_to_workflownotification"),
63+
]
64+
65+
operations = [
66+
migrations.RunPython(convert_legacy_flags, reverse_migration),
67+
]
68+

django_forms_workflows/models.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,11 +1039,12 @@ class WorkflowNotification(models.Model):
10391039
10401040
Recipients are the union of:
10411041
1042+
* ``notify_submitter`` — if True, the person who submitted the form is always included.
10421043
* ``email_field`` — slug of the form field whose value is the recipient email
10431044
(resolved from ``form_data`` at send time, varies per submission).
10441045
* ``static_emails`` — comma-separated fixed addresses always included.
10451046
1046-
At least one of the two must be provided.
1047+
At least one of the three must be set.
10471048
10481049
``conditions`` (same JSON format as ``WorkflowStage.trigger_conditions``)
10491050
are evaluated against ``form_data`` before sending; leave blank to always send.
@@ -1070,6 +1071,13 @@ class WorkflowNotification(models.Model):
10701071
default="approval_notification",
10711072
help_text="Which workflow event triggers this notification.",
10721073
)
1074+
notify_submitter = models.BooleanField(
1075+
default=False,
1076+
help_text=(
1077+
"If checked, the person who submitted the form is always included as a recipient. "
1078+
"Use this to replace the legacy 'notify on …' flags with a fully-configurable rule."
1079+
),
1080+
)
10731081
email_field = models.CharField(
10741082
max_length=200,
10751083
blank=True,
@@ -1112,16 +1120,33 @@ class Meta:
11121120
verbose_name = "Workflow Notification"
11131121
verbose_name_plural = "Workflow Notifications"
11141122

1123+
def clean(self):
1124+
from django.core.exceptions import ValidationError
1125+
1126+
if (
1127+
not self.notify_submitter
1128+
and not self.email_field
1129+
and not self.static_emails
1130+
):
1131+
raise ValidationError(
1132+
"At least one recipient source must be set: "
1133+
"'Notify submitter', 'Email field', or 'Static emails'."
1134+
)
1135+
11151136
def __str__(self) -> str:
1116-
recipient_desc = (
1117-
self.email_field
1118-
or (
1137+
parts = []
1138+
if self.notify_submitter:
1139+
parts.append("submitter")
1140+
if self.email_field:
1141+
parts.append(f"field:{self.email_field}")
1142+
if self.static_emails:
1143+
trimmed = (
11191144
self.static_emails[:40] + "…"
11201145
if len(self.static_emails) > 40
11211146
else self.static_emails
11221147
)
1123-
or "(no recipients)"
1124-
)
1148+
parts.append(trimmed)
1149+
recipient_desc = ", ".join(parts) if parts else "(no recipients)"
11251150
return f"{self.get_notification_type_display()}{recipient_desc}"
11261151

11271152

0 commit comments

Comments
 (0)