2
FormWizard class -- implements a multi-page form, validating between each
3
step and storing the form's state as HTML hidden fields so that no state is
4
stored on the server side.
7
import cPickle as pickle
9
from django import forms
10
from django.conf import settings
11
from django.http import Http404
12
from django.shortcuts import render_to_response
13
from django.template.context import RequestContext
14
from django.utils.hashcompat import md5_constructor
15
from django.utils.translation import ugettext_lazy as _
16
from django.contrib.formtools.utils import security_hash
18
class FormWizard(object):
19
# Dictionary of extra template context variables.
22
# The HTML (and POST data) field name for the "step" variable.
23
step_field_name="wizard_step"
25
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
27
def __init__(self, form_list, initial=None):
28
"form_list should be a list of Form classes (not instances)."
29
self.form_list = form_list[:]
30
self.initial = initial or {}
31
self.step = 0 # A zero-based counter keeping track of which step we're in.
34
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
36
def get_form(self, step, data=None):
37
"Helper method that returns the Form instance for the given step."
38
return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
41
"Helper method that returns the number of steps."
42
# You might think we should just set "self.form_list = len(form_list)"
43
# in __init__(), but this calculation needs to be dynamic, because some
44
# hook methods might alter self.form_list.
45
return len(self.form_list)
47
def __call__(self, request, *args, **kwargs):
49
Main method that does all the hard work, conforming to the Django view
52
if 'extra_context' in kwargs:
53
self.extra_context.update(kwargs['extra_context'])
54
current_step = self.determine_step(request, *args, **kwargs)
55
self.parse_params(request, *args, **kwargs)
58
if current_step >= self.num_steps():
59
raise Http404('Step %s does not exist' % current_step)
61
# For each previous step, verify the hash and process.
62
# TODO: Move "hash_%d" to a method to make it configurable.
63
for i in range(current_step):
64
form = self.get_form(i, request.POST)
65
if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
66
return self.render_hash_failure(request, i)
67
self.process_step(request, form, i)
69
# Process the current step. If it's valid, go to the next step or call
70
# done(), depending on whether any steps remain.
71
if request.method == 'POST':
72
form = self.get_form(current_step, request.POST)
74
form = self.get_form(current_step)
76
self.process_step(request, form, current_step)
77
next_step = current_step + 1
79
# If this was the last step, validate all of the forms one more
80
# time, as a sanity check, and call done().
81
num = self.num_steps()
83
final_form_list = [self.get_form(i, request.POST) for i in range(num)]
85
# Validate all the forms. If any of them fail validation, that
86
# must mean the validator relied on some other input, such as
87
# an external Web site.
88
for i, f in enumerate(final_form_list):
90
return self.render_revalidation_failure(request, i, f)
91
return self.done(request, final_form_list)
93
# Otherwise, move along to the next step.
95
form = self.get_form(next_step)
96
self.step = current_step = next_step
98
return self.render(form, request, current_step)
100
def render(self, form, request, step, context=None):
101
"Renders the given Form object, returning an HttpResponse."
102
old_data = request.POST
105
hidden = forms.HiddenInput()
106
# Collect all data from previous steps and render it as HTML hidden fields.
107
for i in range(step):
108
old_form = self.get_form(i, old_data)
109
hash_name = 'hash_%s' % i
110
prev_fields.extend([bf.as_hidden() for bf in old_form])
111
prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
112
return self.render_template(request, form, ''.join(prev_fields), step, context)
114
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
116
def prefix_for_step(self, step):
117
"Given the step, returns a Form prefix to use."
120
def render_hash_failure(self, request, step):
122
Hook for rendering a template if a hash check failed.
124
step is the step that failed. Any previous step is guaranteed to be
127
This default implementation simply renders the form for the given step,
128
but subclasses may want to display an error message, etc.
130
return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')})
132
def render_revalidation_failure(self, request, step, form):
134
Hook for rendering a template if final revalidation failed.
136
It is highly unlikely that this point would ever be reached, but See
137
the comment in __call__() for an explanation.
139
return self.render(form, request, step)
141
def security_hash(self, request, form):
143
Calculates the security hash for the given HttpRequest and Form instances.
145
Subclasses may want to take into account request-specific information,
146
such as the IP address.
148
return security_hash(request, form)
150
def determine_step(self, request, *args, **kwargs):
152
Given the request object and whatever *args and **kwargs were passed to
153
__call__(), returns the current step (which is zero-based).
155
Note that the result should not be trusted. It may even be a completely
156
invalid number. It's not the job of this method to validate it.
161
step = int(request.POST.get(self.step_field_name, 0))
166
def parse_params(self, request, *args, **kwargs):
168
Hook for setting some state, given the request object and whatever
169
*args and **kwargs were passed to __call__(), sets some state.
171
This is called at the beginning of __call__().
175
def get_template(self, step):
177
Hook for specifying the name of the template to use for a given step.
179
Note that this can return a tuple of template names if you'd like to
180
use the template system's select_template() hook.
182
return 'forms/wizard.html'
184
def render_template(self, request, form, previous_fields, step, context=None):
186
Renders the template for the given step, returning an HttpResponse object.
188
Override this method if you want to add a custom context, return a
189
different MIME type, etc. If you only need to override the template
190
name, use get_template() instead.
192
The template will be rendered with the following context:
193
step_field -- The name of the hidden field containing the step.
194
step0 -- The current step (zero-based).
195
step -- The current step (one-based).
196
step_count -- The total number of steps.
197
form -- The Form instance for the current step (either empty
199
previous_fields -- A string representing every previous data field,
200
plus hashes for completed forms, all in the form of
201
hidden fields. Note that you'll need to run this
202
through the "safe" template filter, to prevent
203
auto-escaping, because it's raw HTML.
205
context = context or {}
206
context.update(self.extra_context)
207
return render_to_response(self.get_template(step), dict(context,
208
step_field=self.step_field_name,
211
step_count=self.num_steps(),
213
previous_fields=previous_fields
214
), context_instance=RequestContext(request))
216
def process_step(self, request, form, step):
218
Hook for modifying the FormWizard's internal state, given a fully
219
validated Form object. The Form is guaranteed to have clean, valid
222
This method should *not* modify any of that data. Rather, it might want
223
to set self.extra_context or dynamically alter self.form_list, based on
224
previously submitted forms.
226
Note that this method is called every time a page is rendered for *all*
231
# METHODS SUBCLASSES MUST OVERRIDE ########################################
233
def done(self, request, form_list):
235
Hook for doing something with the validated data. This is responsible
236
for the final processing.
238
form_list is a list of Form instances, each containing clean, valid
241
raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)