Skip to content

Commit 5dbff23

Browse files
matteiusclaude
andcommitted
feat: standardised stage naming + fix approval page data rendering
Adds ApprovalTask.display_label and format_stage_label() as the single source of truth for stage labels. Format: "{swi.label}: Step {order}: {name}" for sub-workflow tasks, "Step {order}: {name}" otherwise. Replaces scattered one-off formatters and the hardcoded "Payment {index}" prefix in the approval inbox serializer. Fixes the "Prior Approval Step Responses" block on approve.html, which was iterating section.fields as a flat list even though the builder returns width-aware row groups. Rendering now delegates to the shared _form_data_rows.html partial used by submission detail, restoring width grouping, section headers, currency formatting ($X,XXX.XX), and display_text rendering. Also removes the workflow_name_label override in submission-detail stage headers that was replacing per-stage names with a single workflow label. Migration note: hosts whose SubWorkflowDefinition.label_template includes stage-level detail (e.g. "Stage 2: Payment {index}") should update it to just the instance portion ("Payment {index}"). The helper appends stage info automatically. Bumps version to 0.71.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64518b7 commit 5dbff23

8 files changed

Lines changed: 94 additions & 66 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [0.71.0] - 2026-04-16
11+
12+
### Added
13+
- **`ApprovalTask.display_label` + `format_stage_label` helper** — single
14+
source of truth for the standardised stage label rendered across every
15+
surface. Format: `"{swi.label}: Step {stage.order}: {stage.name}"` for
16+
sub-workflow tasks, `"Step {stage.order}: {stage.name}"` otherwise. Replaces
17+
the hardcoded `"Payment {index}"` string in the approval inbox serializer
18+
and scattered one-off formatters on the approval, submission detail, and
19+
PDF surfaces.
20+
21+
### Fixed
22+
- **Approval page "Prior Approval Step Responses" now honours width
23+
grouping, section headers, and currency formatting** — the section was
24+
iterating `section.fields` as a flat list despite the helper returning
25+
width-aware row groups (pair/triple/full/section/display_text). Rendering
26+
now delegates to the shared `_form_data_rows.html` partial already used
27+
by submission detail, so the layout, currency symbols, and section
28+
dividers match the rest of the app.
29+
- **Standardised stage naming across approval inbox, approval page,
30+
submission detail, submission PDF, and bulk PDF.** Removed the
31+
`workflow_name_label` override in submission-detail stage headers that
32+
was hiding per-stage names, and dropped the hardcoded `"Payment"`
33+
prefix in the inbox serializer (consumers should set
34+
``SubWorkflowDefinition.label_template`` appropriately, e.g.
35+
`"Payment {index}"`).
36+
37+
### Migration notes
38+
- If your deployment has a `SubWorkflowDefinition` whose `label_template`
39+
includes stage-level detail (e.g. `"Stage 2: Payment {index}"`), update it
40+
to just the instance portion (e.g. `"Payment {index}"`). The stage/step
41+
information is now appended automatically by the label helper, so the
42+
template should only describe the *instance*.
43+
1044
## [0.70.1] - 2026-04-16
1145

1246
### Fixed

django_forms_workflows/models.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,6 +2399,45 @@ class Meta:
23992399
def __str__(self):
24002400
return f"{self.step_name} for {self.submission}"
24012401

2402+
@property
2403+
def display_label(self) -> str:
2404+
"""Standardised human-readable label for this approval task.
2405+
2406+
Single source of truth shared by the approval inbox, approval page,
2407+
submission detail, and PDF exports so the same task reads identically
2408+
across every surface.
2409+
2410+
Format:
2411+
* Sub-workflow task → ``"{swi.label}: Step {stage.order}: {stage.name}"``
2412+
* Otherwise → ``"Step {stage.order}: {stage.name}"``
2413+
2414+
Falls back gracefully when the stage or sub-workflow instance is missing.
2415+
"""
2416+
stage = self.workflow_stage
2417+
swi = self.sub_workflow_instance
2418+
return format_stage_label(stage, swi=swi, fallback_name=self.step_name)
2419+
2420+
2421+
def format_stage_label(stage, swi=None, fallback_name: str = "") -> str:
2422+
"""Return the standardised display label for a workflow stage.
2423+
2424+
Shared helper used by views and templates that need to render a stage's
2425+
label the same way the approval inbox does, without needing an
2426+
ApprovalTask in hand (e.g. historical stages on submission detail or
2427+
PDF exports).
2428+
"""
2429+
name = (getattr(stage, "name", None) or fallback_name or "").strip()
2430+
order = getattr(stage, "order", None)
2431+
step_part = f"Step {order}: {name}" if order and name else name
2432+
if swi is not None:
2433+
try:
2434+
swi_label = swi.label
2435+
except Exception:
2436+
swi_label = ""
2437+
if swi_label:
2438+
return f"{swi_label}: {step_part}" if step_part else swi_label
2439+
return step_part
2440+
24022441

24032442
class AuditLog(models.Model):
24042443
"""

django_forms_workflows/templates/django_forms_workflows/approve.html

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ <h5 class="mb-0"><i class="bi bi-paperclip"></i> Attachments</h5>
209209
<div class="card-header {% if section.status == 'approved' %}bg-success{% elif section.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %} text-white">
210210
<h5 class="mb-0">
211211
<i class="bi {% if section.status == 'approved' %}bi-check-circle{% elif section.status == 'rejected' %}bi-x-circle{% else %}bi-hourglass{% endif %}"></i>
212-
{{ section.step_name }}
212+
{{ section.display_label|default:section.step_name }}
213213
{% if section.status == 'approved' %}
214214
<span class="badge bg-light text-success float-end">Approved</span>
215215
{% elif section.status == 'rejected' %}
@@ -248,7 +248,7 @@ <h5 class="mb-0">
248248
<!-- Current Step Fields Section with Decision inline -->
249249
<div class="card mb-4 border-primary">
250250
<div class="card-header bg-primary text-white">
251-
<h5 class="mb-0"><i class="bi bi-pencil-square"></i> {% if current_step_number %}Step {{ current_step_number }}: {% endif %}{{ task.step_name }}
251+
<h5 class="mb-0"><i class="bi bi-pencil-square"></i> {{ task.display_label }}
252252
<span class="badge bg-light text-primary float-end">Your Action Required</span>
253253
</h5>
254254
</div>
@@ -398,7 +398,7 @@ <h5 class="mb-0"><i class="bi bi-pencil-square"></i> Prior Approval Step Respons
398398
<div class="px-3 pt-3 pb-1 d-flex justify-content-between align-items-start flex-wrap gap-2">
399399
<div>
400400
<div class="d-flex align-items-center gap-2 mb-1">
401-
<h6 class="mb-0 fw-bold">{{ section.step_name }}</h6>
401+
<h6 class="mb-0 fw-bold">{{ section.display_label|default:section.step_name }}</h6>
402402
{% if section.status == "approved" %}
403403
<span class="badge bg-success">&#10003; Approved</span>
404404
{% elif section.status == "rejected" %}
@@ -421,53 +421,7 @@ <h6 class="mb-0 fw-bold">{{ section.step_name }}</h6>
421421
{% if section.fields %}
422422
<div class="px-3 pb-3">
423423
<table class="table table-bordered table-sm mb-0">
424-
{% for field in section.fields %}
425-
<tr>
426-
<th style="width: 30%;">{{ field.label }}</th>
427-
<td>
428-
{% if field.value.url %}
429-
{% if field.value.content_type and 'image' in field.value.content_type %}
430-
<div class="mb-2">
431-
<img src="{{ field.value.url }}" alt="{{ field.value.filename }}" class="img-fluid" style="max-width: 400px; max-height: 400px;">
432-
</div>
433-
{% endif %}
434-
<a href="{{ field.value.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
435-
<i class="bi bi-download"></i> {{ field.value.filename }}
436-
</a>
437-
<small class="text-muted ms-2">
438-
{% if field.value.size %}({{ field.value.size|filesizeformat }}){% endif %}
439-
</small>
440-
{% elif field.value.path %}
441-
<span class="text-muted">{{ field.value.filename }}</span>
442-
<small class="text-muted ms-2">(File stored but URL unavailable)</small>
443-
{% elif field.value|is_signature %}
444-
<img src="{{ field.value }}" alt="Signature" class="img-fluid" style="max-width: 400px; max-height: 160px; border: 1px solid #dee2e6; border-radius: .375rem;">
445-
{% elif field.value.0.filename %}
446-
<ul class="list-unstyled mb-0">
447-
{% for f in field.value %}
448-
<li class="mb-1">
449-
{% if f.url %}
450-
{% if f.content_type and 'image' in f.content_type %}
451-
<div class="mb-1">
452-
<img src="{{ f.url }}" alt="{{ f.filename }}" class="img-fluid" style="max-width: 200px; max-height: 200px;">
453-
</div>
454-
{% endif %}
455-
<a href="{{ f.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
456-
<i class="bi bi-download"></i> {{ f.filename }}
457-
</a>
458-
<small class="text-muted ms-2">{% if f.size %}({{ f.size|filesizeformat }}){% endif %}</small>
459-
{% else %}
460-
<span class="text-muted">{{ f.filename }}</span>
461-
{% endif %}
462-
</li>
463-
{% endfor %}
464-
</ul>
465-
{% else %}
466-
{{ field.value }}
467-
{% endif %}
468-
</td>
469-
</tr>
470-
{% endfor %}
424+
{% include "django_forms_workflows/_form_data_rows.html" with form_data_ordered=section.fields %}
471425
</table>
472426
</div>
473427
{% endif %}

django_forms_workflows/templates/django_forms_workflows/submission_bulk_pdf.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135

136136
{% if not item.hide_approval_history %}
137137
{% for section in item.approval_step_sections %}
138-
<div class="step-section-header">{{ section.step_name }}</div>
138+
<div class="step-section-header">{{ section.display_label|default:section.step_name }}</div>
139139
<p class="step-meta">
140140
{% if section.completed_by %}Completed by {{ section.completed_by }}{% if section.completed_at %} &middot; {{ section.completed_at|date:"N j, Y \a\t g:i a" }}{% endif %}{% endif %}
141141
{% if section.comments %}&nbsp;&nbsp;Comment: {{ section.comments }}{% endif %}

django_forms_workflows/templates/django_forms_workflows/submission_detail.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ <h5 class="mb-0"><i class="bi bi-pencil-square"></i> Approval Step Responses</h5
125125
<div class="px-3 pt-3 pb-1 d-flex justify-content-between align-items-start flex-wrap gap-2">
126126
<div>
127127
<div class="d-flex align-items-center gap-2 mb-1">
128-
<h6 class="mb-0 fw-bold">{{ section.step_name }}</h6>
128+
<h6 class="mb-0 fw-bold">{{ section.display_label|default:section.step_name }}</h6>
129129
{% if section.status == "approved" %}
130130
<span class="badge bg-success">&#10003; Approved</span>
131131
{% elif section.status == "rejected" %}
@@ -208,7 +208,7 @@ <h5 class="mb-3"><i class="bi bi-diagram-3"></i> Approval History</h5>
208208
{% elif stage.approved_count > 0 %}bg-primary text-white
209209
{% else %}bg-light text-muted{% endif %}">
210210
<span>
211-
<strong>{% if workflow_name_label %}{{ workflow_name_label }}{% else %}Stage {{ stage.number }}{% if not stage.has_multiple_stages %}: {{ stage.name }}{% endif %}{% endif %}</strong>
211+
<strong>{% if stage.has_multiple_stages %}Stage {{ stage.number }}{% else %}{{ stage.display_label }}{% endif %}</strong>
212212
</span>
213213
<span class="badge bg-white
214214
{% if stage.rejected_count > 0 %}text-danger
@@ -234,7 +234,7 @@ <h5 class="mb-3"><i class="bi bi-diagram-3"></i> Approval History</h5>
234234
<tbody>
235235
{% for task in stage.tasks %}
236236
<tr>
237-
<td><strong>{{ task.step_display_name }}</strong></td>
237+
<td><strong>{{ task.display_label }}</strong></td>
238238
<td>
239239
{% if task.assigned_to %}
240240
{{ task.assigned_to.get_full_name|default:task.assigned_to.username }}
@@ -303,7 +303,7 @@ <h5 class="mb-3"><i class="bi bi-diagram-3"></i> Approval History</h5>
303303
{% for stage in section.stage_groups %}
304304
{% for task in stage.tasks %}
305305
<tr>
306-
<td><strong>{{ stage.name }}</strong></td>
306+
<td><strong>{{ task.display_label }}</strong></td>
307307
<td>
308308
{% if task.assigned_to %}
309309
{{ task.assigned_to.get_full_name|default:task.assigned_to.username }}
@@ -355,7 +355,7 @@ <h5 class="mb-0">Approval History</h5>
355355
<tbody>
356356
{% for task in approval_tasks %}
357357
<tr>
358-
<td>{{ task.step_name }}</td>
358+
<td>{{ task.display_label }}</td>
359359
<td>
360360
{% if task.assigned_to %}
361361
{{ task.assigned_to.get_full_name|default:task.assigned_to.username }}

django_forms_workflows/templates/django_forms_workflows/submission_pdf.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ <h3 style="margin-top: 20px;">Attachments</h3>
268268
{% if not hide_approval_history %}
269269
{% for section in approval_step_sections %}
270270
<div class="step-section-header">
271-
<span class="step-name-cell">{{ section.step_name }}</span>
271+
<span class="step-name-cell">{{ section.display_label|default:section.step_name }}</span>
272272
<span class="step-badge-cell">
273273
{% if section.status == "approved" %}
274274
<span class="step-badge step-badge-approved">&#10003; Approved</span>

django_forms_workflows/views.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,7 @@ def submission_detail(request, submission_id):
11061106
target[stage_key] = {
11071107
"number": task.stage_number or 0,
11081108
"name": stage_name,
1109+
"display_label": task.display_label,
11091110
"approval_logic": (
11101111
task.workflow_stage.approval_logic
11111112
if task.workflow_stage
@@ -1196,6 +1197,7 @@ def submission_detail(request, submission_id):
11961197
stage_map[stage_key] = {
11971198
"number": task.stage_number or 0,
11981199
"name": stage_name,
1200+
"display_label": task.display_label,
11991201
"approval_logic": approval_logic,
12001202
"tasks": [],
12011203
"approved_count": 0,
@@ -3456,15 +3458,16 @@ def _build_approval_step_sections(submission):
34563458
task.completed_by.get_full_name() or task.completed_by.username
34573459
)
34583460

3459-
# Section header: use the stored step_name directly; it now bakes in
3460-
# the stage's approve_label when tasks are created (workflow_engine.py).
3461+
# Section header: use the task's centralised display_label so the
3462+
# section header matches the inbox / approve / PDF rendering exactly.
34613463
stage_order = task.stage_number or task.step_number or 1
34623464
step_name = task.step_name or f"Step {stage_order}"
34633465

34643466
sections.append(
34653467
{
34663468
"step_number": stage_order,
34673469
"step_name": step_name,
3470+
"display_label": task.display_label,
34683471
"status": task.status,
34693472
"group_name": (
34703473
task.assigned_group.name if task.assigned_group else None
@@ -4186,12 +4189,10 @@ def approval_inbox_ajax(request):
41864189
approve_url = reverse("forms_workflows:approve_submission", args=[task.id])
41874190
det_url = reverse("forms_workflows:submission_detail", args=[sub.id])
41884191

4189-
# For sub-workflow tasks, prefix the stage name with the instance index
4190-
# so "Payment Request" becomes "Payment 1: Payment Request".
4191-
stage_name = task.workflow_stage.name if task.workflow_stage else ""
4192-
swi = task.sub_workflow_instance
4193-
if swi and stage_name:
4194-
stage_name = f"Payment {swi.index}: {stage_name}"
4192+
# Standardised label used across inbox, approve page, submission
4193+
# detail, and PDFs — driven by ApprovalTask.display_label so any
4194+
# format change happens in one place.
4195+
stage_name = task.display_label
41954196

41964197
row = {
41974198
"DT_RowId": f"row_{task.id}",

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.70.1"
3+
version = "0.71.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)