Tuesday, 21 July 2009

Improved Django FormWizard

A few months back I had a project that I thought needed a wizard-style interface for one of its forms. For a while now Django has included the FormWizard class in django.contrib.formtools.wizard so I decided to use that. However, I immediately hit a couple of issues with it.

FormWizard requires you to output the previous_fields context variable in each of the form's step templates. Django's FormWizard previous_fields is just a string of raw HTML. If you are using some other kind of markup to do your forms e.g. XForms, or even javascript widgets from dojo or extjs, then this is no good.

I quickly came up with a fix for this and opened #10557 which has now been slated to be included in Django 1.2.....

Wait a minute! Django 1.1 isn't even out yet!

The second issue was that FormWizard didn't handle FormSets.

Who knows when 1.2 will be out so I've bundled up my code and released it as wadofstuff.django.forms 1.0.0.

From the README:

Functions

security_hash(request, form, exclude=None, *args)

Calculates a security hash for the given Form/FormSet instance.

This creates a list of the form field names/values in a deterministic
order, pickles the result with the SECRET_KEY setting, then takes an md5
hash of that.

Allows a list of form fields to be excluded from the hash calculation. This
is useful form fields that may have their values set programmatically.

Classes

BoundFormWizard

A subclass of Django's FormWizard that adds the following functionality:
  • Renders previous_fields as a list of bound form fields in the template context rather than as raw html.
  • Can handle FormSets.
The usage of this class is identical to that documented here with the exception that when rendering previous_fields you should change your wizard step templates from:

{{ previous_fields|safe }}

to:

{% for f in previous_fields %}{{ f.as_hidden }}{% endfor %}

The latest stable release for the forms module can be obtained by:


Note: The BoundFormWizard class can only handle regular FormSets and not ModelFormSets. I have ModelFormWizard class that I'll be adding to a future release that can handle simple Models.

8 comments:

Richard said...

Good work. Is there a way to skip a form step? That's what we really need...

qor72 said...

You can skip a form step by removing it from the form_list of the FormWizard. I have a dynamic wizard that adds and removes steps as the user works through it... kills showing a raw % complete, but allows the work to be done.

dingue said...

Hi,

How do you use the formset with the wizard. I can see how to use a formset with views and templates but how do you define it in a way that you can pass it to the wizard? Is it possible to have the formset inserted in an existing form like an inline?

matthew said...

Hi,

You can use formsets with BoundFormWizard:

from django import forms
from wadofstuff.django.forms import BoundFormWizard

class Form1(forms.Form):
    field1 = forms.CharField()

class Form2(forms.Form):
    field2 = forms.CharField()

FormSet1 = forms.formsets.formset_factory(Form1)

urlpatterns = patterns('',
    url(r'^bound-formset/$', BoundFormWizard([FormSet1, Form2])),
)

The formset will be contained in its own step and cannot be inline with another form.

E. Myller said...
This comment has been removed by the author.
Bryan said...

This works beautifully!
Thanks for ending my hours frustration :D

Justin said...

Hello,

After a install of the 1.0 package in windows xp, the import is actually from wadofstuff.django.forms.wizard import BoundFormWizard.

Thanks for this.

matthew said...

@Justin: a newer version, 1.1.0, of wadofstuff.django.forms is available where you only have to import from wadofstuff.django.forms.