Skip to content

Commit 9d35953

Browse files
committed
fix: resolve sub-workflow self-referencing bug in sync import
When parent and sub workflows belong to the same FormDefinition, the import logic used .workflow (which returns .workflows.first()) to resolve the sub-workflow reference, always picking the parent workflow and producing a self-referencing SubWorkflowDefinition. - Serialize sub_workflow_stage_names for disambiguation - Add _resolve_sub_workflow() that excludes the parent and matches by stage names when parent/sub share the same form - All 580 existing tests pass
1 parent f6248a4 commit 9d35953

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

django_forms_workflows/sync_api.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,14 @@ def _serialize_workflow_stage(stage):
276276
def _serialize_sub_workflow_config(swc):
277277
if swc is None:
278278
return None
279+
# Include sub-workflow stage names so the importer can disambiguate when
280+
# the parent and sub workflows belong to the same FormDefinition.
281+
sub_wf_stage_names = list(
282+
swc.sub_workflow.stages.order_by("order").values_list("name", flat=True)
283+
)
279284
return {
280285
"sub_workflow_form_slug": swc.sub_workflow.form_definition.slug,
286+
"sub_workflow_stage_names": sub_wf_stage_names,
281287
"section_label": swc.section_label,
282288
"count_field": swc.count_field,
283289
"label_template": swc.label_template,
@@ -593,6 +599,46 @@ def _get_or_create_category(data, category_cache=None):
593599
return cat
594600

595601

602+
def _resolve_sub_workflow(sub_wf_form, sub_wf_data, parent_workflow):
603+
"""Resolve the correct WorkflowDefinition for a sub-workflow reference.
604+
605+
When the parent and sub workflows belong to the **same** FormDefinition
606+
(e.g. Course Development Request has both a "Contract Approval" parent
607+
workflow and a "Payment" sub-workflow on the same form), the naïve
608+
``sub_wf_form.workflow`` (which returns ``.workflows.first()``) will
609+
return the parent workflow — producing a self-referencing
610+
SubWorkflowDefinition.
611+
612+
To disambiguate, we:
613+
1. Exclude the parent workflow from the candidate set.
614+
2. If ``sub_workflow_stage_names`` was serialised, match on the workflow
615+
whose stages (by name + order) match the exported data.
616+
3. Fall back to the first remaining workflow on the form.
617+
"""
618+
if sub_wf_form is None:
619+
return None
620+
621+
candidates = sub_wf_form.workflows.all()
622+
623+
# If the parent belongs to the same form, exclude it so we don't
624+
# accidentally self-reference.
625+
if parent_workflow.form_definition_id == sub_wf_form.id:
626+
candidates = candidates.exclude(pk=parent_workflow.pk)
627+
628+
# Try matching by stage names if available in the serialised payload.
629+
expected_stages = sub_wf_data.get("sub_workflow_stage_names")
630+
if expected_stages:
631+
for candidate in candidates:
632+
actual_stages = list(
633+
candidate.stages.order_by("order").values_list("name", flat=True)
634+
)
635+
if actual_stages == expected_stages:
636+
return candidate
637+
638+
# Fall back to the first remaining candidate.
639+
return candidates.first()
640+
641+
596642
@transaction.atomic
597643
def import_form(form_data, conflict="update", category_cache=None):
598644
"""
@@ -818,7 +864,7 @@ def import_form(form_data, conflict="update", category_cache=None):
818864
if sub_wf_data:
819865
sub_form_slug = sub_wf_data.get("sub_workflow_form_slug")
820866
sub_wf_form = FormDefinition.objects.filter(slug=sub_form_slug).first()
821-
sub_wf = sub_wf_form.workflow if sub_wf_form else None
867+
sub_wf = _resolve_sub_workflow(sub_wf_form, sub_wf_data, parent_workflow=wf)
822868
if sub_wf:
823869
SubWorkflowDefinition.objects.update_or_create(
824870
parent_workflow=wf,

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.63.6"
3+
version = "0.63.7"
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)