Skip to content

Commit 96fbac3

Browse files
committed
Add approval history privacy, fix template heading, clone workflows
- Template: 'Approval History' is always the section heading; workflow name_label is used in stage card headers instead of 'Stage N' - Privacy: add hide_approval_history field to WorkflowDefinition. When enabled, submitters see only the final decision (approved/rejected/ under review) — approvers and admins still see full history. - Clone: both admin action and form builder API now clone workflows, stages, and stage approval groups when duplicating a form. - Migration 0047_add_hide_approval_history - Bump version to 0.24.0
1 parent 08fedff commit 96fbac3

7 files changed

Lines changed: 180 additions & 5 deletions

File tree

django_forms_workflows/admin.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ class WorkflowDefinitionInline(admin.StackedInline):
278278
fields = [
279279
"name_label",
280280
"requires_approval",
281+
"hide_approval_history",
281282
("notify_on_submission", "notify_on_approval", "notify_on_rejection"),
282283
"additional_notify_emails",
283284
("allow_bulk_export", "allow_bulk_pdf_export"),
@@ -504,6 +505,52 @@ def clone_forms(self, request, queryset):
504505
cloned_form.view_groups.set(form.view_groups.all())
505506
cloned_form.admin_groups.set(form.admin_groups.all())
506507

508+
# Clone workflows, stages, and stage approval groups
509+
for wf in form.workflows.all():
510+
original_stages = list(
511+
wf.stages.prefetch_related("approval_groups").order_by(
512+
"order"
513+
)
514+
)
515+
cloned_wf = WorkflowDefinition.objects.create(
516+
form_definition=cloned_form,
517+
name_label=wf.name_label,
518+
requires_approval=wf.requires_approval,
519+
approval_deadline_days=wf.approval_deadline_days,
520+
send_reminder_after_days=wf.send_reminder_after_days,
521+
auto_approve_after_days=wf.auto_approve_after_days,
522+
notify_on_submission=wf.notify_on_submission,
523+
notify_on_approval=wf.notify_on_approval,
524+
notify_on_rejection=wf.notify_on_rejection,
525+
notify_on_withdrawal=wf.notify_on_withdrawal,
526+
additional_notify_emails=wf.additional_notify_emails,
527+
notification_cadence=wf.notification_cadence,
528+
notification_cadence_day=wf.notification_cadence_day,
529+
notification_cadence_time=wf.notification_cadence_time,
530+
notification_cadence_form_field=wf.notification_cadence_form_field,
531+
visual_workflow_data=wf.visual_workflow_data,
532+
hide_approval_history=wf.hide_approval_history,
533+
allow_bulk_export=wf.allow_bulk_export,
534+
allow_bulk_pdf_export=wf.allow_bulk_pdf_export,
535+
)
536+
for stage in original_stages:
537+
cloned_stage = WorkflowStage.objects.create(
538+
workflow=cloned_wf,
539+
name=stage.name,
540+
order=stage.order,
541+
approval_logic=stage.approval_logic,
542+
requires_manager_approval=stage.requires_manager_approval,
543+
approve_label=stage.approve_label,
544+
)
545+
for sag in StageApprovalGroup.objects.filter(
546+
stage=stage
547+
).order_by("position"):
548+
StageApprovalGroup.objects.create(
549+
stage=cloned_stage,
550+
group=sag.group,
551+
position=sag.position,
552+
)
553+
507554
cloned_count += 1
508555
except Exception as e:
509556
self.message_user(

django_forms_workflows/form_builder_views.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
from django.shortcuts import get_object_or_404, render
1616
from django.views.decorators.http import require_GET, require_POST
1717

18-
from .models import FormDefinition, FormField, FormTemplate, PrefillSource
18+
from .models import (
19+
FormDefinition,
20+
FormField,
21+
FormTemplate,
22+
PrefillSource,
23+
StageApprovalGroup,
24+
WorkflowDefinition,
25+
WorkflowStage,
26+
)
1927

2028
logger = logging.getLogger(__name__)
2129

@@ -143,6 +151,50 @@ def form_builder_clone(request, form_id):
143151
cloned_form.view_groups.set(original_form.view_groups.all())
144152
cloned_form.admin_groups.set(original_form.admin_groups.all())
145153

154+
# Clone workflows, stages, and stage approval groups
155+
for wf in original_form.workflows.all():
156+
original_stages = list(
157+
wf.stages.prefetch_related("approval_groups").order_by("order")
158+
)
159+
cloned_wf = WorkflowDefinition.objects.create(
160+
form_definition=cloned_form,
161+
name_label=wf.name_label,
162+
requires_approval=wf.requires_approval,
163+
approval_deadline_days=wf.approval_deadline_days,
164+
send_reminder_after_days=wf.send_reminder_after_days,
165+
auto_approve_after_days=wf.auto_approve_after_days,
166+
notify_on_submission=wf.notify_on_submission,
167+
notify_on_approval=wf.notify_on_approval,
168+
notify_on_rejection=wf.notify_on_rejection,
169+
notify_on_withdrawal=wf.notify_on_withdrawal,
170+
additional_notify_emails=wf.additional_notify_emails,
171+
notification_cadence=wf.notification_cadence,
172+
notification_cadence_day=wf.notification_cadence_day,
173+
notification_cadence_time=wf.notification_cadence_time,
174+
notification_cadence_form_field=wf.notification_cadence_form_field,
175+
visual_workflow_data=wf.visual_workflow_data,
176+
hide_approval_history=wf.hide_approval_history,
177+
allow_bulk_export=wf.allow_bulk_export,
178+
allow_bulk_pdf_export=wf.allow_bulk_pdf_export,
179+
)
180+
for stage in original_stages:
181+
cloned_stage = WorkflowStage.objects.create(
182+
workflow=cloned_wf,
183+
name=stage.name,
184+
order=stage.order,
185+
approval_logic=stage.approval_logic,
186+
requires_manager_approval=stage.requires_manager_approval,
187+
approve_label=stage.approve_label,
188+
)
189+
for sag in StageApprovalGroup.objects.filter(stage=stage).order_by(
190+
"position"
191+
):
192+
StageApprovalGroup.objects.create(
193+
stage=cloned_stage,
194+
group=sag.group,
195+
position=sag.position,
196+
)
197+
146198
return JsonResponse(
147199
{
148200
"success": True,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 6.0.2 on 2026-03-13 19:12
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("django_forms_workflows", "0046_workflow_definition_one_to_many"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="workflowdefinition",
15+
name="hide_approval_history",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="When enabled, the submitter will not see approval history or individual approval steps — only the final decision (approved / rejected) is shown. Approvers and admins can still see the full history.",
19+
),
20+
),
21+
]

django_forms_workflows/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,16 @@ class WorkflowDefinition(models.Model):
651651
help_text="Visual workflow builder layout (nodes and connections)",
652652
)
653653

654+
# Privacy
655+
hide_approval_history = models.BooleanField(
656+
default=False,
657+
help_text=(
658+
"When enabled, the submitter will not see approval history or "
659+
"individual approval steps — only the final decision (approved / rejected) "
660+
"is shown. Approvers and admins can still see the full history."
661+
),
662+
)
663+
654664
# Bulk Export
655665
allow_bulk_export = models.BooleanField(
656666
default=False,

django_forms_workflows/templates/django_forms_workflows/submission_detail.html

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,41 @@ <h6 class="mb-0 fw-bold">{{ section.step_name }}</h6>
167167
<!-- Approval History -->
168168
{% if approval_tasks %}
169169

170-
{% if stage_groups %}
170+
{% if hide_approval_history %}
171+
{# ---- Privacy mode: submitter sees only the final decision ---- #}
172+
<h5 class="mb-3"><i class="bi bi-shield-lock"></i> Approval Status</h5>
173+
<div class="card mb-4
174+
{% if submission.status == 'approved' %}border-success
175+
{% elif submission.status == 'rejected' %}border-danger
176+
{% else %}border-warning{% endif %}">
177+
<div class="card-body text-center py-4">
178+
{% if submission.status == 'approved' %}
179+
<span class="badge bg-success fs-5 px-4 py-2">Approved</span>
180+
{% if submission.completed_at %}
181+
<p class="text-muted mt-2 mb-0">Decision made on {{ submission.completed_at|date:"F d, Y g:i A" }}</p>
182+
{% endif %}
183+
{% elif submission.status == 'rejected' %}
184+
<span class="badge bg-danger fs-5 px-4 py-2">Rejected</span>
185+
{% if submission.completed_at %}
186+
<p class="text-muted mt-2 mb-0">Decision made on {{ submission.completed_at|date:"F d, Y g:i A" }}</p>
187+
{% endif %}
188+
{% for task in approval_tasks %}
189+
{% if task.status == 'rejected' and task.comments %}
190+
<div class="alert alert-danger mt-3 mb-0 text-start">
191+
<strong>Reason:</strong> {{ task.comments }}
192+
</div>
193+
{% endif %}
194+
{% endfor %}
195+
{% else %}
196+
<span class="badge bg-warning text-dark fs-5 px-4 py-2">Under Review</span>
197+
<p class="text-muted mt-2 mb-0">Your submission is being reviewed. You will be notified when a decision is made.</p>
198+
{% endif %}
199+
</div>
200+
</div>
201+
202+
{% elif stage_groups %}
171203
{# ---- Staged workflow: one card per stage ---- #}
172-
<h5 class="mb-3"><i class="bi bi-diagram-3"></i> {% if workflow_name_label %}{{ workflow_name_label }}{% else %}Approval History{% endif %}</h5>
204+
<h5 class="mb-3"><i class="bi bi-diagram-3"></i> Approval History</h5>
173205
{% for stage in stage_groups %}
174206
<div class="card mb-3
175207
{% if stage.rejected_count > 0 %}border-danger
@@ -182,7 +214,7 @@ <h5 class="mb-3"><i class="bi bi-diagram-3"></i> {% if workflow_name_label %}{{
182214
{% elif stage.approved_count > 0 %}bg-primary text-white
183215
{% else %}bg-light text-muted{% endif %}">
184216
<span>
185-
<strong>Stage {{ stage.number }}{% if not stage.has_multiple_stages %}: {{ stage.name }}{% endif %}</strong>
217+
<strong>{% if workflow_name_label %}{{ workflow_name_label }}{% else %}Stage {{ stage.number }}{% if not stage.has_multiple_stages %}: {{ stage.name }}{% endif %}{% endif %}</strong>
186218
</span>
187219
<span class="badge bg-white
188220
{% if stage.rejected_count > 0 %}text-danger

django_forms_workflows/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,18 @@ def submission_detail(request, submission_id):
713713
or request.user.groups.filter(id__in=form_def.admin_groups.all()).exists()
714714
)
715715

716+
# Privacy: hide approval history from the submitter when configured.
717+
# Approvers and admins always see the full history.
718+
is_submitter_only = (
719+
submission.submitter == request.user
720+
and not request.user.is_superuser
721+
and not user_can_approve(request.user, submission)
722+
and not request.user.groups.filter(id__in=form_def.admin_groups.all()).exists()
723+
)
724+
hide_approval_history = bool(
725+
workflow and workflow.hide_approval_history and is_submitter_only
726+
)
727+
716728
return render(
717729
request,
718730
"django_forms_workflows/submission_detail.html",
@@ -730,6 +742,7 @@ def submission_detail(request, submission_id):
730742
"approval_field_names": approval_field_names,
731743
"approval_step_sections": approval_step_sections,
732744
"can_approve_pdf": can_approve_pdf,
745+
"hide_approval_history": hide_approval_history,
733746
},
734747
)
735748

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.23.2"
3+
version = "0.24.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)