Multi-Step Forms with Django Form Wizard: A Practical Guide

Suraj Singh Bisht
3 min readNov 11, 2024

--

Photo by Faisal on Unsplash

Multi-step forms are essential for complex data collection processes, but implementing them efficiently can be challenging.

Django’s form wizard provides a robust solution for managing these workflows. This guide demonstrates practical implementations based on real-world applications.

Why Use Django Form Wizard?

Multi-step forms can quickly become a mess of validation logic and state management. Django’s form wizard handles these complexities for you, providing:

  • Session management between steps
  • Data persistence across form submissions
  • Flexible template rendering
  • Clean separation of concerns

Understanding Django Form Wizard

Django Form Wizard efficiently handles:

  • State management across form steps
  • Data validation and persistence
  • Template rendering
  • File uploads

Let’s explore a production-ready implementation for user registration.

A practical example of a user registration system with multiple steps. We’ll focus on making it maintainable and user-friendly.

1. Form Structure

Start with well-structured form classes that handle specific data collection needs:

from django import forms
from django.contrib.auth.models import User

# User Registration Form
class RegistrationBasicForm(forms.ModelForm):
password = forms.CharField(
label="Password",
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Enter password'
}),
required=True
)
password_confirm = forms.CharField(
label="Confirm Password",
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Confirm password'
}),
required=True
)

class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
widgets = {
'username': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Username'
}),
'email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Email address'
})
}

def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
password_confirm = cleaned_data.get('password_confirm')

if password and password_confirm:
if len(password) < 8:
raise forms.ValidationError(
"Password must be at least 8 characters long"
)
if password != password_confirm:
raise forms.ValidationError(
"Passwords do not match"
)
return cleaned_data

2. Wizard View Implementation

The wizard view coordinates the form sequence and handles data processing

from django.contrib.auth.mixins import LoginRequiredMixin
from formtools.wizard.views import SessionWizardView
from django.core.files.storage import FileSystemStorage
import os

class UserRegistrationWizard(SessionWizardView):
FORMS = [
("basic_info", RegistrationBasicForm),
("profile", ProfileDetailsForm),
]

TEMPLATES = {
"basic_info": "registration/basic_info.html",
"profile": "registration/profile.html",
}

file_storage = FileSystemStorage(
location=os.path.join(settings.MEDIA_ROOT, 'temp_uploads')
)

def get_template_names(self):
return [self.TEMPLATES[self.steps.current]]

def process_step(self, form):
data = self.get_all_cleaned_data()
return super().process_step(form)

def done(self, form_list, **kwargs):
data = self.get_all_cleaned_data()

user = User.objects.create_user(
username=data['username'],
email=data['email'],
password=data['password'],
first_name=data.get('first_name', ''),
last_name=data.get('last_name', '')
)

if 'profile_picture' in data and data['profile_picture']:
profile = user.profile
profile.picture = data['profile_picture']
profile.bio = data.get('bio', '')
profile.save()

self.file_storage.delete_temporary_files()

return HttpResponseRedirect(reverse('registration_complete'))

def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
context.update({
'current_step_name': self.steps.current,
'total_steps': len(self.FORMS),
'current_step_number': list(self.FORMS).index(self.steps.current) + 1,
'progress_percentage': (
(list(self.FORMS).index(self.steps.current) + 1)
* 100
/ len(self.FORMS)
)
})
return context

3. Template Implementation

Create a clean, user-friendly interface:

{% extends "base.html" %}

{% block content %}
<div class="container mt-5">

<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ wizard.management_form }}

{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}

{% for field in form %}
<div class="mb-3">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="text-danger">{{ field.errors }}</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}

<div class="d-flex justify-content-between mt-4">
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit"
value="{{ wizard.steps.prev }}"
class="btn btn-secondary">Previous</button>
{% endif %}

<button type="submit" class="btn btn-primary">
{% if is_last_step %}Complete{% else %}Next{% endif %}
</button>
</div>
</form>
</div>
{% endblock %}

Pro Tips for Production Use

  1. Handle File Uploads Properly
from django.core.files.storage import FileSystemStorage
fs = FileSystemStorage(location='/tmp/wizard-uploads/')

2. Add Form Validation

def clean_password_confirm(self):
password = self.cleaned_data.get('password')
password_confirm = self.cleaned_data.get('password_confirm')
if password and password_confirm and password != password_confirm:
raise forms.ValidationError("Passwords don't match")
return password_confirm

3. Implement Progress Tracking

def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
context['progress'] = (int(self.steps.current) + 1) * 100 / len(self.FORMS)
return context

Conclusion

Django Form Wizard provides a robust foundation for implementing multi-step forms. By following these implementation patterns and considering production requirements, you can create efficient and user-friendly data collection processes. Regular testing with users and monitoring completion rates helps ensure ongoing effectiveness.

For additional insights into complex implementations or specific use cases, consider reviewing Django’s documentation and the form wizard source code.

--

--

Responses (1)