FormWizard: multiple-step forms in Django
March 24, 2008
Have you ever had to implement a multiple-step form? It can be a headache trying to keep track of all the data, especially while validating each form in the process. Django’s new FormWizard class makes it wickedly easy. Note: this is for Django r7236 and later.
To show off FormWizard, I’ve created project called “gandalf” with an app called “thegrey”. Inside this application I have a file called forms.py that houses the two steps of my form, as well as the wizard itself:
from django import newforms as forms
from django.http import HttpResponseRedirect
from django.contrib.formtools.wizard import FormWizard
class ContactOne(forms.Form):
name = forms.CharField()
email = forms.EmailField()
class ContactTwo(forms.Form):
CHOICES = [(x, x) for x in ("I like your site", "I hate your site", "I don't care")]
feeling = forms.ChoiceField(choices=CHOICES)
message = forms.CharField(widget=forms.widgets.Textarea)
class ContactWizard(FormWizard):
def done(self, request, form_list):
# Send an email or save to the database, or whatever you want with
# form parameters in form_list
return HttpResponseRedirect('/contact/thanks/')
The first class, ContactOne, simply accepts a name and an email field. The second class provides a drop-down for users’ to choose the feeling of the site, and a textarea to write their comments. The third class is our wizard; it’s done method is called when all steps of the form process are complete.
To get this wizard going, we’ll attach it to a url, “/contact/” like so (we’ll also add a thanks page that is simply a direct_to_template call) in our urls.py:
from django.conf.urls.defaults import *
from gandalf.thegrey.forms import ContactOne, ContactTwo, ContactWizard
urlpatterns = patterns('',
(r'^contact/$', ContactWizard([ContactOne, ContactTwo])),
(r'^contact/thanks/$', 'django.views.generic.simple.direct_to_template', {'template': 'thanks.html'})
)
By default, a FormWizard class requests the forms/wizard.html template, though we can over-ride this by adding a get_template to the class:
def get_template(self, step):
return 'path/to/another/template/%s.html' % step
For our example, however, let’s just use the default forms/wizard.html. Setting up the form couldn’t be easier:
{% extends 'base.html' %}
{% block content %}
<form action='.' method='POST'>
<table>
{% ifequal step 2 %}
<p>You're almost done!</p>
{% endifequal %}
<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
{{ previous_fields|safe }}
{{ form }}
</table>
<p><input type="submit" value="Submit" /></p>
</form>
{% endblock %}
Skipping down to the line after we start the table, you’ll see that if we’re on the second step, the user will see a note indicating their almost done. The next line is simply letting the form know from which step this form is coming. The next line puts the previous fields into the form so we don’t lose any data. Lastly we dump the form.
Here’s what it ends up looking like:

Woops, I forgot my email address!




And we’re done. The FormWizard class has made it simple to create multiple-step forms in Django, without the traditional hassle of handling successive steps of data properly. This method is also a good way of splitting up forms that are just too long, making the process considerably simpler for the end-user, who is, after all, the most important person.
Add your comment
No HTML; Only URLs and line breaks are converted.
Comments
Oooh, cool! Thanks for catching this one.
Posted by Pete
just a note that you can even use the Wizard class to create dynamic wizards - you simply start with a form and then in process_step you can override self.forms to add any additional forms.
Posted by Honza Kral
I like the idea and what's behind it. But I tried to implement this on a file upload form - and it doesn't work, because the forms are instanciated without the request.FILES attribute, and the standard wizard templates don't use multipart/form-data forms...
Posted by Jan Oberst
This sounds great. By any chance is there built-in support for going "back" a step in the wizard -- either via browser back-button or an alternate form button? If not built-in, are there any obvious reasons why going "back" wouldn't work?
Thanks.
Posted by Bill
Jan Oberst:
also its not recommended since the data from previous steps are stored in hidden fields... that's not possible for FileField.
Bill:
if you go back (which you can by sending the data and sending lower STEP value), you will loose data from all forms later than the step you are returning to - motivation behind that is that the forms could be dynamically generated and wouldnt make any sense if you changed one of their predecessors.
Posted by Honza Kral
@Honza -
It looks like you were the lead on this feature -- thanks for working on it.
I'll read the docs, but since I have you on the line, let me see if I understand this...
If I'm on step 3 and want to go back to step 2 via a form "Back" button (i.e. not the Browser 'back' button), I just manipulate the STEP value (presumably via javascript) and post the step 3 form? The step 2 code can ignore the extra data that came from step 3?
I guess that using the browser back button would work without any effort on the developer's part, but the user would get a re-POST warning from the browser.
And just to close the loop, you're saying that if I then submit step 2 (for the second time), I can't expect my original step 3 data to be there (totally reasonable).
Thanks again.
Posted by Bill
Is there a way to associate data between the pages? (Is this what is meant by "dynamic" wizards in the comments?)
I want to be able to give a limited subset of options on the second page based on the entries on the first page.
Posted by mrben
I'm always fearful of hidden fields. I guess I'm afraid of hidden fields because it allows the user to meddle with data that the view had previously validated.
After step 2, I suppose I would no longer validate the fields from step 1 in the view, and count on my model to report inconsistencies.
So, if I were to change the value of the hidden email field in step 2 to an invalid value, I'd get an error from the model, when saving the data in the ContactWizard's class done function.
I suppose I could just send the user to a 404 page (or even better; a page warning him not to meddle in the affairs of FormWizards).
Would the above way of working be enough to prevent people meddling with the hidden fields in step 2? Is there not a server-based way of keeping track of the data entered (and validated) in a previous form, so that users can no longer alter previously entered and validated data?
Posted by LaundroMat
From the docs page: "The server validates the data. If it’s invalid, the form is displayed again, with error messages. If it’s valid, the server calculates a secure hash of the data and presents the user with the next form, saving the validated data and hash in <input type="hidden"> fields."
Posted by SuperJared
Jared...
Thanks for the more complex form example. I'm having a problem with something simpler that maybe you could help me with.
I have a boolean in my model and want to select it's value with a radio button... (which works). However, I can't seem to get the display of it to be successful... e.g. if it's True then "True" should be selected.
I think it's just a problem w/ my template syntax;
<input type="radio" name="sync_updates" value="True"
{% if enabled %} checked {% endif %}
>Yes
<input type="radio" name="sync_updates" value="False"
{% if enabled %} checked {% endif %}
>No
Any thoughts?
Posted by Jay
Call me silly but I think you should be using a checkbox over a true/false radio selection.
Posted by SuperJared
@SuperJared
Doesn't matter much to me what's in use. The radio seemed easiest. I wanted something more "substantial" then a checkbox but it certainly would be more succinct.
I'll give it a try but if you've got any thoughts as to how to do more complex logic in the templating engine I'm all eyes.
Posted by Jay
Just a question, but is this also a good way to use as anti-spam measure, instead of a captcha? Or do 'robots' nowadays understand multi-part forms?
Or do you think it is too user unfriendly?
Posted by Roderik
@Jay, if you're using XHTML you might need to explicitly value checked="checked"
@Roderik, While I'm sure this will help prevent some spammers, I doubt these types of forms would completely inhibit them.
Posted by SuperJared
Very nice! I tried it on Django version 0.97-pre-SVN-7433 and it works great. Thanks.
Posted by Rico