Form wizards break long forms into steps, guiding users through smaller forms while preserving data between steps.
Key Concepts
- SessionWizardView: Stores wizard data server-side in session.
- form_list: List of (name, FormClass) tuples defining steps.
- done(): Called when all steps complete, saves combined data.
- management_form: Hidden fields tracking current step.
- condition_dict: Callables controlling step visibility.
Real World Context
A registration wizard: personal info step 1, address step 2, preferences step 3. Each validated independently. done() combines all data to create the account.
Deep Dive
Multi-Step Forms with django-formtools
Form wizards break long forms into manageable steps, improving user experience for complex data entry.
Installation
bashpip install django-formtools
python# settings.py INSTALLED_APPS = [ # ... 'formtools', ]
Basic Wizard
python# forms.py from django import forms class PersonalInfoForm(forms.Form): first_name = forms.CharField(max_length=100) last_name = forms.CharField(max_length=100) email = forms.EmailField() class AddressForm(forms.Form): street = forms.CharField(max_length=200) city = forms.CharField(max_length=100) state = forms.CharField(max_length=50) zip_code = forms.CharField(max_length=10) class PreferencesForm(forms.Form): newsletter = forms.BooleanField(required=False) notifications = forms.ChoiceField( choices=[('all', 'All'), ('important', 'Important Only'), ('none', 'None')] )
python# views.py from formtools.wizard.views import SessionWizardView WIZARD_FORMS = [ ('personal', PersonalInfoForm), ('address', AddressForm), ('preferences', PreferencesForm), ] class RegistrationWizard(SessionWizardView): template_name = 'registration_wizard.html' form_list = WIZARD_FORMS def done(self, form_list, form_dict, **kwargs): """Called when all steps are complete.""" # Combine all form data data = {} for form in form_list: data.update(form.cleaned_data) # Create user and profile user = User.objects.create_user( username=data['email'], email=data['email'], first_name=data['first_name'], last_name=data['last_name'], ) Profile.objects.create( user=user, street=data['street'], city=data['city'], newsletter=data['newsletter'], ) return redirect('registration_complete')
python# urls.py from .views import RegistrationWizard, WIZARD_FORMS urlpatterns = [ path('register/', RegistrationWizard.as_view(WIZARD_FORMS), name='register'), ]
Wizard Template
html<!-- templates/registration_wizard.html --> {% extends 'base.html' %} {% block content %} <h1>Registration - Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</h1> <!-- Progress indicator --> <div class="progress"> {% for step in wizard.steps.all %} <span class="{% if step == wizard.steps.current %}active{% endif %}"> {{ step }} </span> {% endfor %} </div> <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ wizard.management_form }} <h2>{{ wizard.steps.current }}</h2> {{ wizard.form.as_p }} <div class="navigation"> {% if wizard.steps.prev %} <button name="wizard_goto_step" value="{{ wizard.steps.prev }}"> Previous </button> {% endif %} <button type="submit">{% if wizard.steps.next %}Next{% else %}Submit{% endif %}</button> </div> </form> {% endblock %}
Conditional Steps
pythonclass ConditionalWizard(SessionWizardView): form_list = [ ('basic', BasicInfoForm), ('business', BusinessInfoForm), # Only for business accounts ('payment', PaymentForm), ] condition_dict = { 'business': lambda wizard: wizard.get_cleaned_data_for_step('basic').get('account_type') == 'business' } def get_form_initial(self, step): """Set initial data for a step.""" initial = super().get_form_initial(step) if step == 'payment': basic_data = self.get_cleaned_data_for_step('basic') if basic_data: initial['email'] = basic_data.get('email') return initial
Accessing Previous Step Data
pythonclass MyWizard(SessionWizardView): def get_context_data(self, form, **kwargs): context = super().get_context_data(form, **kwargs) # Access all previous data all_data = {} for step in self.get_form_list(): step_data = self.get_cleaned_data_for_step(step) if step_data: all_data.update(step_data) context['previous_data'] = all_data return context
File Uploads in Wizard
pythonfrom formtools.wizard.views import SessionWizardView from django.core.files.storage import default_storage class FileWizard(SessionWizardView): # Use file storage for wizard files file_storage = default_storage def done(self, form_list, **kwargs): # Access uploaded files for form in form_list: if hasattr(form, 'files'): for field, file in form.files.items(): # Process file pass
Common Pitfalls
- Missing wizard.management_form -- POST fails with validation error.
- get_cleaned_data_for_step() returns None -- Unvisited steps. Handle gracefully.
- File uploads without file_storage -- Files lost between steps.
Best Practices
- Use named steps -- More readable condition_dict references.
- Show progress indicator -- Reduces user anxiety about form length.
- Pre-populate later steps -- Override get_form_initial().
Summary
- SessionWizardView breaks forms into validated steps.
- Define named tuples in form_list.
- done() saves combined data after all steps.
- Include wizard.management_form in template.
- Set file_storage for file uploads.
Code Examples
python
from formtools.wizard.views import SessionWizardView
class RegistrationWizard(SessionWizardView):
form_list = [
('personal', PersonalInfoForm),
('address', AddressForm),
('preferences', PreferencesForm),
]
template_name = 'registration_wizard.html'
def done(self, form_list, form_dict, **kwargs):
# Called when all steps complete
data = {}
for form in form_list:
data.update(form.cleaned_data)
User.objects.create(**data)
return redirect('welcome')