Skip to content

Commit 521ac2d

Browse files
committed
Check pt multi step form builder
1 parent 8402663 commit 521ac2d

File tree

16 files changed

+5008
-57
lines changed

16 files changed

+5008
-57
lines changed

django_forms_workflows/form_builder_views.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,13 @@ def form_builder_load(request, form_id):
237237
'requires_login': form_definition.requires_login,
238238
'allow_save_draft': form_definition.allow_save_draft,
239239
'allow_withdrawal': form_definition.allow_withdrawal,
240+
'enable_multi_step': form_definition.enable_multi_step,
241+
'form_steps': form_definition.form_steps or [],
242+
'enable_auto_save': form_definition.enable_auto_save,
243+
'auto_save_interval': form_definition.auto_save_interval,
240244
'fields': fields_data,
241245
}
242-
246+
243247
return JsonResponse(form_data)
244248

245249

@@ -265,6 +269,10 @@ def form_builder_save(request):
265269
requires_login = data.get('requires_login', True)
266270
allow_save_draft = data.get('allow_save_draft', True)
267271
allow_withdrawal = data.get('allow_withdrawal', True)
272+
enable_multi_step = data.get('enable_multi_step', False)
273+
form_steps = data.get('form_steps', [])
274+
enable_auto_save = data.get('enable_auto_save', True)
275+
auto_save_interval = data.get('auto_save_interval', 30)
268276
fields_data = data.get('fields', [])
269277

270278
# Validate required fields
@@ -286,6 +294,10 @@ def form_builder_save(request):
286294
form_definition.requires_login = requires_login
287295
form_definition.allow_save_draft = allow_save_draft
288296
form_definition.allow_withdrawal = allow_withdrawal
297+
form_definition.enable_multi_step = enable_multi_step
298+
form_definition.form_steps = form_steps
299+
form_definition.enable_auto_save = enable_auto_save
300+
form_definition.auto_save_interval = auto_save_interval
289301
form_definition.version += 1 # Increment version on edit
290302
form_definition.save()
291303
else:
@@ -298,6 +310,10 @@ def form_builder_save(request):
298310
requires_login=requires_login,
299311
allow_save_draft=allow_save_draft,
300312
allow_withdrawal=allow_withdrawal,
313+
enable_multi_step=enable_multi_step,
314+
form_steps=form_steps,
315+
enable_auto_save=enable_auto_save,
316+
auto_save_interval=auto_save_interval,
301317
created_by=request.user,
302318
)
303319

django_forms_workflows/forms.py

Lines changed: 165 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,34 @@ def __init__(self, form_definition, user=None, initial_data=None, *args, **kwarg
5353
HTML(f'<h3 class="mt-4 mb-3">{field.field_label}</h3>')
5454
)
5555
else:
56+
# Wrap field in a div with field-wrapper class for multi-step support
57+
field_wrapper_class = f"field-wrapper field-{field.field_name}"
58+
5659
if field.width == "half":
5760
layout_fields.append(
58-
Row(
59-
Column(Field(field.field_name), css_class="col-md-6"),
61+
Div(
62+
Row(
63+
Column(Field(field.field_name), css_class="col-md-6"),
64+
),
65+
css_class=field_wrapper_class
6066
)
6167
)
6268
elif field.width == "third":
6369
layout_fields.append(
64-
Row(
65-
Column(Field(field.field_name), css_class="col-md-4"),
70+
Div(
71+
Row(
72+
Column(Field(field.field_name), css_class="col-md-4"),
73+
),
74+
css_class=field_wrapper_class
6675
)
6776
)
6877
else:
69-
layout_fields.append(Field(field.field_name))
78+
layout_fields.append(
79+
Div(
80+
Field(field.field_name),
81+
css_class=field_wrapper_class
82+
)
83+
)
7084

7185
# Add submit buttons
7286
buttons = [Submit("submit", "Submit", css_class="btn btn-primary")]
@@ -79,6 +93,13 @@ def __init__(self, form_definition, user=None, initial_data=None, *args, **kwarg
7993

8094
self.helper.layout = Layout(*layout_fields)
8195

96+
# Add form ID for JavaScript targeting
97+
self.helper.form_id = f"form_{form_definition.slug}"
98+
self.helper.attrs = {
99+
'data-form-enhancements': 'true',
100+
'data-form-slug': form_definition.slug,
101+
}
102+
82103
def _parse_choices(self, choices):
83104
"""
84105
Parse choices from either JSON format or comma-separated string.
@@ -352,3 +373,142 @@ def _get_prefill_value(self, prefill_source, prefill_config=None):
352373
logger.error(f"Error getting prefill value for {prefill_source}: {e}")
353374

354375
return ""
376+
377+
def get_enhancements_config(self):
378+
"""
379+
Generate JavaScript configuration for form enhancements.
380+
Returns a dictionary that can be serialized to JSON.
381+
"""
382+
import json
383+
384+
config = {
385+
'autoSaveEnabled': getattr(self.form_definition, 'enable_auto_save', True),
386+
'autoSaveInterval': getattr(self.form_definition, 'auto_save_interval', 30) * 1000, # Convert to ms
387+
'autoSaveEndpoint': f'/forms/{self.form_definition.slug}/auto-save/',
388+
'multiStepEnabled': getattr(self.form_definition, 'enable_multi_step', False),
389+
'steps': getattr(self.form_definition, 'form_steps', None) or [],
390+
'conditionalRules': [],
391+
'fieldDependencies': [],
392+
'validationRules': [],
393+
}
394+
395+
# Collect conditional rules from all fields
396+
for field in self.form_definition.fields.all():
397+
# Legacy simple conditional logic
398+
if field.show_if_field and field.show_if_value:
399+
config['conditionalRules'].append({
400+
'targetField': field.field_name,
401+
'action': 'show',
402+
'operator': 'AND',
403+
'conditions': [{
404+
'field': field.show_if_field,
405+
'operator': 'equals',
406+
'value': field.show_if_value,
407+
}]
408+
})
409+
410+
# Advanced conditional rules
411+
if hasattr(field, 'conditional_rules') and field.conditional_rules:
412+
if isinstance(field.conditional_rules, str):
413+
try:
414+
rules = json.loads(field.conditional_rules)
415+
except json.JSONDecodeError:
416+
rules = None
417+
else:
418+
rules = field.conditional_rules
419+
420+
if rules:
421+
config['conditionalRules'].append({
422+
'targetField': field.field_name,
423+
**rules
424+
})
425+
426+
# Field dependencies
427+
if hasattr(field, 'field_dependencies') and field.field_dependencies:
428+
if isinstance(field.field_dependencies, str):
429+
try:
430+
deps = json.loads(field.field_dependencies)
431+
except json.JSONDecodeError:
432+
deps = []
433+
else:
434+
deps = field.field_dependencies
435+
436+
if deps:
437+
config['fieldDependencies'].extend(deps)
438+
439+
# Validation rules
440+
validation_rules = []
441+
442+
if field.required:
443+
validation_rules.append({
444+
'type': 'required',
445+
'message': f'{field.field_label} is required'
446+
})
447+
448+
if field.field_type == 'email':
449+
validation_rules.append({
450+
'type': 'email',
451+
'message': 'Please enter a valid email address'
452+
})
453+
454+
if field.field_type == 'url':
455+
validation_rules.append({
456+
'type': 'url',
457+
'message': 'Please enter a valid URL'
458+
})
459+
460+
if field.min_length:
461+
validation_rules.append({
462+
'type': 'min',
463+
'value': field.min_length,
464+
'message': f'Minimum {field.min_length} characters required'
465+
})
466+
467+
if field.max_length:
468+
validation_rules.append({
469+
'type': 'max',
470+
'value': field.max_length,
471+
'message': f'Maximum {field.max_length} characters allowed'
472+
})
473+
474+
if field.min_value is not None:
475+
validation_rules.append({
476+
'type': 'min_value',
477+
'value': float(field.min_value),
478+
'message': f'Minimum value is {field.min_value}'
479+
})
480+
481+
if field.max_value is not None:
482+
validation_rules.append({
483+
'type': 'max_value',
484+
'value': float(field.max_value),
485+
'message': f'Maximum value is {field.max_value}'
486+
})
487+
488+
if field.regex_validation:
489+
validation_rules.append({
490+
'type': 'pattern',
491+
'value': field.regex_validation,
492+
'message': field.regex_error_message or 'Invalid format'
493+
})
494+
495+
# Custom validation rules from field config
496+
if hasattr(field, 'validation_rules') and field.validation_rules:
497+
if isinstance(field.validation_rules, str):
498+
try:
499+
custom_rules = json.loads(field.validation_rules)
500+
except json.JSONDecodeError:
501+
custom_rules = []
502+
else:
503+
custom_rules = field.validation_rules
504+
505+
if custom_rules:
506+
validation_rules.extend(custom_rules)
507+
508+
if validation_rules:
509+
config['validationRules'].append({
510+
'field': field.field_name,
511+
'rules': validation_rules
512+
})
513+
514+
return config
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Generated migration for advanced conditional logic
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('django_forms_workflows', '0004_formtemplate'),
10+
]
11+
12+
operations = [
13+
# Add advanced conditional logic fields to FormField
14+
migrations.AddField(
15+
model_name='formfield',
16+
name='conditional_rules',
17+
field=models.JSONField(
18+
blank=True,
19+
null=True,
20+
help_text='Advanced conditional rules with AND/OR logic. Format: {"operator": "AND|OR", "conditions": [{"field": "field_name", "operator": "equals", "value": "value"}], "action": "show|hide|require|enable"}',
21+
),
22+
),
23+
migrations.AddField(
24+
model_name='formfield',
25+
name='validation_rules',
26+
field=models.JSONField(
27+
blank=True,
28+
null=True,
29+
help_text='Client-side validation rules. Format: [{"type": "required|email|min|max|pattern|custom", "value": "...", "message": "Error message"}]',
30+
),
31+
),
32+
migrations.AddField(
33+
model_name='formfield',
34+
name='field_dependencies',
35+
field=models.JSONField(
36+
blank=True,
37+
null=True,
38+
help_text='Field dependencies for cascade updates. Format: [{"sourceField": "field_name", "targetField": "dependent_field", "apiEndpoint": "/api/endpoint/"}]',
39+
),
40+
),
41+
42+
# Add multi-step form support to FormDefinition
43+
migrations.AddField(
44+
model_name='formdefinition',
45+
name='enable_multi_step',
46+
field=models.BooleanField(
47+
default=False,
48+
help_text='Enable multi-step form with progress indicators',
49+
),
50+
),
51+
migrations.AddField(
52+
model_name='formdefinition',
53+
name='form_steps',
54+
field=models.JSONField(
55+
blank=True,
56+
null=True,
57+
help_text='Multi-step configuration. Format: [{"title": "Step 1", "fields": ["field1", "field2"]}]',
58+
),
59+
),
60+
61+
# Add auto-save configuration to FormDefinition
62+
migrations.AddField(
63+
model_name='formdefinition',
64+
name='enable_auto_save',
65+
field=models.BooleanField(
66+
default=True,
67+
help_text='Enable automatic draft saving',
68+
),
69+
),
70+
migrations.AddField(
71+
model_name='formdefinition',
72+
name='auto_save_interval',
73+
field=models.IntegerField(
74+
default=30,
75+
help_text='Auto-save interval in seconds',
76+
),
77+
),
78+
79+
# Add step information to FormField for multi-step forms
80+
migrations.AddField(
81+
model_name='formfield',
82+
name='step_number',
83+
field=models.IntegerField(
84+
null=True,
85+
blank=True,
86+
help_text='Step number for multi-step forms (1, 2, 3, etc.)',
87+
),
88+
),
89+
]
90+

django_forms_workflows/models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ class FormDefinition(models.Model):
6565
default=True, help_text="Form requires authentication"
6666
)
6767

68+
# Client-Side Enhancements
69+
enable_multi_step = models.BooleanField(
70+
default=False,
71+
help_text='Enable multi-step form with progress indicators',
72+
)
73+
form_steps = models.JSONField(
74+
blank=True,
75+
null=True,
76+
help_text='Multi-step configuration. Format: [{"title": "Step 1", "fields": ["field1", "field2"]}]',
77+
)
78+
enable_auto_save = models.BooleanField(
79+
default=True,
80+
help_text='Enable automatic draft saving',
81+
)
82+
auto_save_interval = models.IntegerField(
83+
default=30,
84+
help_text='Auto-save interval in seconds',
85+
)
86+
6887
# Metadata
6988
created_at = models.DateTimeField(auto_now_add=True)
7089
updated_at = models.DateTimeField(auto_now=True)
@@ -349,6 +368,34 @@ def get_prefill_source_key(self):
349368
max_length=200, blank=True, help_text="Value that triggers showing this field"
350369
)
351370

371+
# Advanced Conditional Logic (Client-Side Enhancements)
372+
conditional_rules = models.JSONField(
373+
blank=True,
374+
null=True,
375+
help_text='Advanced conditional rules with AND/OR logic. Format: {"operator": "AND|OR", "conditions": [{"field": "field_name", "operator": "equals", "value": "value"}], "action": "show|hide|require|enable"}',
376+
)
377+
378+
# Dynamic Field Validation (Client-Side)
379+
validation_rules = models.JSONField(
380+
blank=True,
381+
null=True,
382+
help_text='Client-side validation rules. Format: [{"type": "required|email|min|max|pattern|custom", "value": "...", "message": "Error message"}]',
383+
)
384+
385+
# Field Dependencies (Cascade Updates)
386+
field_dependencies = models.JSONField(
387+
blank=True,
388+
null=True,
389+
help_text='Field dependencies for cascade updates. Format: [{"sourceField": "field_name", "targetField": "dependent_field", "apiEndpoint": "/api/endpoint/"}]',
390+
)
391+
392+
# Multi-Step Forms
393+
step_number = models.IntegerField(
394+
null=True,
395+
blank=True,
396+
help_text='Step number for multi-step forms (1, 2, 3, etc.)',
397+
)
398+
352399
# File Upload Settings
353400
allowed_extensions = models.CharField(
354401
max_length=200, blank=True, help_text="Comma-separated: pdf,doc,docx,xls,xlsx"

0 commit comments

Comments
 (0)