Skip to content

Commit 89b7dd0

Browse files
committed
feat: allow workflow stages to enable editing of submission data
Add allow_edit_form_data boolean field to WorkflowStage (default False). When enabled, approvers at that stage see the original submission fields as editable inputs instead of a read-only table. Edited data is merged back into the submission on approve; rejected submissions keep original data untouched. - Add WorkflowStage.allow_edit_form_data BooleanField (default=False) - Add migration 0070 for the new field - Include missing migration 0071 for PendingNotification notification_type - Update approve_submission view to build DynamicForm pre-populated with existing form_data when allow_edit_form_data is True - Validate and merge edited data on approve, skip on reject - Update approve.html to render editable form or read-only table based on stage configuration - Expose allow_edit_form_data in admin (inline and standalone) - Add 6 tests covering default value, context presence, editable label rendering, disabled state, approve-with-edits, and reject-ignores-edits - Bump version to 0.40.0
1 parent f692c24 commit 89b7dd0

File tree

8 files changed

+357
-15
lines changed

8 files changed

+357
-15
lines changed

django_forms_workflows/admin.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ class WorkflowStageInline(nested_admin.NestedStackedInline):
426426
("assignee_form_field", "assignee_lookup_type"),
427427
"validate_assignee_group",
428428
("allow_reassign", "allow_send_back"),
429+
"allow_edit_form_data",
429430
"trigger_conditions",
430431
)
431432

@@ -1383,6 +1384,17 @@ class WorkflowStageAdmin(nested_admin.NestedModelAdmin):
13831384
),
13841385
},
13851386
),
1387+
(
1388+
"Editable Form Data",
1389+
{
1390+
"classes": ("collapse",),
1391+
"description": (
1392+
"When enabled, approvers at this stage can edit the original "
1393+
"submission data instead of viewing it read-only."
1394+
),
1395+
"fields": ("allow_edit_form_data",),
1396+
},
1397+
),
13861398
(
13871399
"Conditional Trigger",
13881400
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
(
8+
"django_forms_workflows",
9+
"0069_add_signature_field_type",
10+
),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="workflowstage",
16+
name="allow_edit_form_data",
17+
field=models.BooleanField(
18+
default=False,
19+
help_text=(
20+
"Allow approvers at this stage to edit the original form submission "
21+
"data. When enabled, the submission fields are shown as editable "
22+
"inputs instead of a read-only table. Changes are saved when the "
23+
"approver approves the submission."
24+
),
25+
),
26+
),
27+
]
28+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 6.0.3 on 2026-03-30 20:39
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("django_forms_workflows", "0070_workflowstage_allow_edit_form_data"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="pendingnotification",
15+
name="notification_type",
16+
field=models.CharField(
17+
choices=[
18+
("submission_received", "Submission Received"),
19+
("approval_request", "Approval Request"),
20+
("approval_notification", "Approval Notification"),
21+
("rejection_notification", "Rejection Notification"),
22+
("withdrawal_notification", "Withdrawal Notification"),
23+
],
24+
max_length=30,
25+
),
26+
),
27+
]

django_forms_workflows/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,15 @@ class WorkflowStage(models.Model):
881881
"approval groups."
882882
),
883883
)
884+
allow_edit_form_data = models.BooleanField(
885+
default=False,
886+
help_text=(
887+
"Allow approvers at this stage to edit the original form submission "
888+
"data. When enabled, the submission fields are shown as editable "
889+
"inputs instead of a read-only table. Changes are saved when the "
890+
"approver approves the submission."
891+
),
892+
)
884893

885894
class Meta:
886895
ordering = ["order"]

django_forms_workflows/templates/django_forms_workflows/approve.html

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,15 @@ <h5 class="mb-0"><i class="bi bi-info-circle"></i> Submission Information</h5>
178178
</div>
179179

180180
{% if has_approval_step_fields and approval_step_form %}
181-
<!-- Submission Data Section (Read-only) -->
181+
<!-- Submission Data Section -->
182182
<div class="card mb-4">
183183
<div class="card-header bg-secondary text-white">
184-
<h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data</h5>
184+
<h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data{% if allow_edit_form_data %} <small class="text-white-50">(Editable)</small>{% endif %}</h5>
185185
</div>
186186
<div class="card-body">
187+
{% if allow_edit_form_data and editable_form %}
188+
{{ editable_form|crispy }}
189+
{% else %}
187190
<table class="table table-bordered table-sm">
188191
{% for entry in form_data_ordered %}
189192
{% if entry.key not in approval_field_names %}
@@ -230,6 +233,7 @@ <h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data</h5>
230233
{% endif %}
231234
{% endfor %}
232235
</table>
236+
{% endif %}
233237
</div>
234238
</div>
235239

@@ -449,9 +453,12 @@ <h6 class="mb-3"><i class="bi bi-clipboard-check"></i> Your Decision</h6>
449453
<!-- Submission Data -->
450454
<div class="card mb-4">
451455
<div class="card-header bg-secondary text-white">
452-
<h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data</h5>
456+
<h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data{% if allow_edit_form_data %} <small class="text-white-50">(Editable)</small>{% endif %}</h5>
453457
</div>
454458
<div class="card-body">
459+
{% if allow_edit_form_data and editable_form %}
460+
{{ editable_form|crispy }}
461+
{% else %}
455462
<table class="table table-bordered">
456463
{% for entry in form_data_ordered %}
457464
<tr>
@@ -483,6 +490,7 @@ <h5 class="mb-0"><i class="bi bi-file-text"></i> Submission Data</h5>
483490
</tr>
484491
{% endfor %}
485492
</table>
493+
{% endif %}
486494
</div>
487495
</div>
488496
{% if resolved_attachments %}

django_forms_workflows/views.py

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,15 @@ def approve_submission(request, task_id):
10531053
.exists()
10541054
)
10551055

1056+
# Check if this stage allows editing the original submission data.
1057+
allow_edit_form_data = bool(
1058+
task.workflow_stage_id
1059+
and task.workflow_stage
1060+
and task.workflow_stage.allow_edit_form_data
1061+
)
1062+
10561063
approval_step_form = None
1064+
editable_form = None
10571065

10581066
if request.method == "POST":
10591067
decision = request.POST.get("decision")
@@ -1147,20 +1155,32 @@ def approve_submission(request, task_id):
11471155
request, "Please correct the errors in the approval fields."
11481156
)
11491157
_fd = _resolve_form_data_urls(submission.form_data)
1158+
_ctx = {
1159+
"task": task,
1160+
"submission": submission,
1161+
"approval_step_form": approval_step_form,
1162+
"has_approval_step_fields": has_approval_step_fields,
1163+
"allow_edit_form_data": allow_edit_form_data,
1164+
"form_data": _fd,
1165+
"form_data_ordered": _build_ordered_form_data(submission, _fd),
1166+
"resolved_attachments": _resolve_attachments(
1167+
submission.attachments
1168+
),
1169+
}
1170+
if allow_edit_form_data:
1171+
from .forms import DynamicForm
1172+
1173+
_ctx["editable_form"] = DynamicForm(
1174+
form_def,
1175+
user=request.user,
1176+
initial_data=submission.form_data,
1177+
data=request.POST,
1178+
files=request.FILES,
1179+
)
11501180
return render(
11511181
request,
11521182
"django_forms_workflows/approve.html",
1153-
{
1154-
"task": task,
1155-
"submission": submission,
1156-
"approval_step_form": approval_step_form,
1157-
"has_approval_step_fields": has_approval_step_fields,
1158-
"form_data": _fd,
1159-
"form_data_ordered": _build_ordered_form_data(submission, _fd),
1160-
"resolved_attachments": _resolve_attachments(
1161-
submission.attachments
1162-
),
1163-
},
1183+
_ctx,
11641184
)
11651185

11661186
# Update submission form_data with approval step fields.
@@ -1181,6 +1201,58 @@ def approve_submission(request, task_id):
11811201
submission.form_data = updated_data
11821202
submission.save()
11831203

1204+
# If this stage allows editing form data, validate and merge on approve
1205+
if allow_edit_form_data and decision == "approve":
1206+
from .forms import DynamicForm
1207+
1208+
editable_form = DynamicForm(
1209+
form_def,
1210+
user=request.user,
1211+
initial_data=submission.form_data,
1212+
data=request.POST,
1213+
files=request.FILES,
1214+
)
1215+
if not editable_form.is_valid():
1216+
messages.error(
1217+
request, "Please correct the errors in the submission data."
1218+
)
1219+
_fd = _resolve_form_data_urls(submission.form_data)
1220+
_ctx = {
1221+
"task": task,
1222+
"submission": submission,
1223+
"editable_form": editable_form,
1224+
"allow_edit_form_data": allow_edit_form_data,
1225+
"has_approval_step_fields": has_approval_step_fields,
1226+
"form_data": _fd,
1227+
"form_data_ordered": _build_ordered_form_data(submission, _fd),
1228+
"resolved_attachments": _resolve_attachments(
1229+
submission.attachments
1230+
),
1231+
}
1232+
if has_approval_step_fields:
1233+
from .forms import ApprovalStepForm
1234+
1235+
_ctx["approval_step_form"] = ApprovalStepForm(
1236+
form_definition=field_form_def,
1237+
submission=submission,
1238+
approval_task=task,
1239+
user=request.user,
1240+
data=request.POST,
1241+
files=request.FILES,
1242+
)
1243+
return render(
1244+
request,
1245+
"django_forms_workflows/approve.html",
1246+
_ctx,
1247+
)
1248+
# Merge edited fields into submission data, preserving any
1249+
# approval-step fields that are not part of the original form.
1250+
edited_data = serialize_form_data(
1251+
editable_form.cleaned_data, submission_id=submission.id
1252+
)
1253+
submission.form_data.update(edited_data)
1254+
submission.save()
1255+
11841256
# Update task
11851257
task.status = "approved" if decision == "approve" else "rejected"
11861258
task.completed_by = request.user
@@ -1238,6 +1310,16 @@ def approve_submission(request, task_id):
12381310
user=request.user,
12391311
)
12401312

1313+
# Build the editable form data form if the stage allows it
1314+
if allow_edit_form_data:
1315+
from .forms import DynamicForm
1316+
1317+
editable_form = DynamicForm(
1318+
form_def,
1319+
user=request.user,
1320+
initial_data=submission.form_data,
1321+
)
1322+
12411323
# Build approval progress context from workflow stages
12421324
all_approval_field_names: list = []
12431325
approval_stages: list = []
@@ -1355,6 +1437,8 @@ def approve_submission(request, task_id):
13551437
"submission": submission,
13561438
"approval_step_form": approval_step_form,
13571439
"has_approval_step_fields": has_approval_step_fields,
1440+
"allow_edit_form_data": allow_edit_form_data,
1441+
"editable_form": editable_form,
13581442
"approval_field_names": all_approval_field_names,
13591443
# staged
13601444
"approval_stages": approval_stages,

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.39.0"
3+
version = "0.40.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"

0 commit comments

Comments
 (0)