|
0
|
1 |
""" |
|
|
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. |
|
|
5 |
""" |
|
|
6 |
|
|
|
7 |
import cPickle as pickle |
|
|
8 |
|
|
|
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 |
|
|
17 |
|
|
|
18 |
class FormWizard(object): |
|
|
19 |
# Dictionary of extra template context variables. |
|
|
20 |
extra_context = {} |
|
|
21 |
|
|
|
22 |
# The HTML (and POST data) field name for the "step" variable. |
|
|
23 |
step_field_name="wizard_step" |
|
|
24 |
|
|
|
25 |
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### |
|
|
26 |
|
|
|
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. |
|
|
32 |
|
|
|
33 |
def __repr__(self): |
|
|
34 |
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) |
|
|
35 |
|
|
|
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)) |
|
|
39 |
|
|
|
40 |
def num_steps(self): |
|
|
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) |
|
|
46 |
|
|
|
47 |
def __call__(self, request, *args, **kwargs): |
|
|
48 |
""" |
|
|
49 |
Main method that does all the hard work, conforming to the Django view |
|
|
50 |
interface. |
|
|
51 |
""" |
|
|
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) |
|
|
56 |
|
|
|
57 |
# Sanity check. |
|
|
58 |
if current_step >= self.num_steps(): |
|
|
59 |
raise Http404('Step %s does not exist' % current_step) |
|
|
60 |
|
|
|
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) |
|
|
68 |
|
|
|
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) |
|
|
73 |
else: |
|
|
74 |
form = self.get_form(current_step) |
|
|
75 |
if form.is_valid(): |
|
|
76 |
self.process_step(request, form, current_step) |
|
|
77 |
next_step = current_step + 1 |
|
|
78 |
|
|
|
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() |
|
|
82 |
if next_step == num: |
|
|
83 |
final_form_list = [self.get_form(i, request.POST) for i in range(num)] |
|
|
84 |
|
|
|
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): |
|
|
89 |
if not f.is_valid(): |
|
|
90 |
return self.render_revalidation_failure(request, i, f) |
|
|
91 |
return self.done(request, final_form_list) |
|
|
92 |
|
|
|
93 |
# Otherwise, move along to the next step. |
|
|
94 |
else: |
|
|
95 |
form = self.get_form(next_step) |
|
|
96 |
self.step = current_step = next_step |
|
|
97 |
|
|
|
98 |
return self.render(form, request, current_step) |
|
|
99 |
|
|
|
100 |
def render(self, form, request, step, context=None): |
|
|
101 |
"Renders the given Form object, returning an HttpResponse." |
|
|
102 |
old_data = request.POST |
|
|
103 |
prev_fields = [] |
|
|
104 |
if old_data: |
|
|
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) |
|
|
113 |
|
|
|
114 |
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## |
|
|
115 |
|
|
|
116 |
def prefix_for_step(self, step): |
|
|
117 |
"Given the step, returns a Form prefix to use." |
|
|
118 |
return str(step) |
|
|
119 |
|
|
|
120 |
def render_hash_failure(self, request, step): |
|
|
121 |
""" |
|
|
122 |
Hook for rendering a template if a hash check failed. |
|
|
123 |
|
|
|
124 |
step is the step that failed. Any previous step is guaranteed to be |
|
|
125 |
valid. |
|
|
126 |
|
|
|
127 |
This default implementation simply renders the form for the given step, |
|
|
128 |
but subclasses may want to display an error message, etc. |
|
|
129 |
""" |
|
|
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.')}) |
|
|
131 |
|
|
|
132 |
def render_revalidation_failure(self, request, step, form): |
|
|
133 |
""" |
|
|
134 |
Hook for rendering a template if final revalidation failed. |
|
|
135 |
|
|
|
136 |
It is highly unlikely that this point would ever be reached, but See |
|
|
137 |
the comment in __call__() for an explanation. |
|
|
138 |
""" |
|
|
139 |
return self.render(form, request, step) |
|
|
140 |
|
|
|
141 |
def security_hash(self, request, form): |
|
|
142 |
""" |
|
|
143 |
Calculates the security hash for the given HttpRequest and Form instances. |
|
|
144 |
|
|
|
145 |
Subclasses may want to take into account request-specific information, |
|
|
146 |
such as the IP address. |
|
|
147 |
""" |
|
|
148 |
return security_hash(request, form) |
|
|
149 |
|
|
|
150 |
def determine_step(self, request, *args, **kwargs): |
|
|
151 |
""" |
|
|
152 |
Given the request object and whatever *args and **kwargs were passed to |
|
|
153 |
__call__(), returns the current step (which is zero-based). |
|
|
154 |
|
|
|
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. |
|
|
157 |
""" |
|
|
158 |
if not request.POST: |
|
|
159 |
return 0 |
|
|
160 |
try: |
|
|
161 |
step = int(request.POST.get(self.step_field_name, 0)) |
|
|
162 |
except ValueError: |
|
|
163 |
return 0 |
|
|
164 |
return step |
|
|
165 |
|
|
|
166 |
def parse_params(self, request, *args, **kwargs): |
|
|
167 |
""" |
|
|
168 |
Hook for setting some state, given the request object and whatever |
|
|
169 |
*args and **kwargs were passed to __call__(), sets some state. |
|
|
170 |
|
|
|
171 |
This is called at the beginning of __call__(). |
|
|
172 |
""" |
|
|
173 |
pass |
|
|
174 |
|
|
|
175 |
def get_template(self, step): |
|
|
176 |
""" |
|
|
177 |
Hook for specifying the name of the template to use for a given step. |
|
|
178 |
|
|
|
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. |
|
|
181 |
""" |
|
|
182 |
return 'forms/wizard.html' |
|
|
183 |
|
|
|
184 |
def render_template(self, request, form, previous_fields, step, context=None): |
|
|
185 |
""" |
|
|
186 |
Renders the template for the given step, returning an HttpResponse object. |
|
|
187 |
|
|
|
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. |
|
|
191 |
|
|
|
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 |
|
|
198 |
or with errors). |
|
|
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. |
|
|
204 |
""" |
|
|
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, |
|
|
209 |
step0=step, |
|
|
210 |
step=step + 1, |
|
|
211 |
step_count=self.num_steps(), |
|
|
212 |
form=form, |
|
|
213 |
previous_fields=previous_fields |
|
|
214 |
), context_instance=RequestContext(request)) |
|
|
215 |
|
|
|
216 |
def process_step(self, request, form, step): |
|
|
217 |
""" |
|
|
218 |
Hook for modifying the FormWizard's internal state, given a fully |
|
|
219 |
validated Form object. The Form is guaranteed to have clean, valid |
|
|
220 |
data. |
|
|
221 |
|
|
|
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. |
|
|
225 |
|
|
|
226 |
Note that this method is called every time a page is rendered for *all* |
|
|
227 |
submitted steps. |
|
|
228 |
""" |
|
|
229 |
pass |
|
|
230 |
|
|
|
231 |
# METHODS SUBCLASSES MUST OVERRIDE ######################################## |
|
|
232 |
|
|
|
233 |
def done(self, request, form_list): |
|
|
234 |
""" |
|
|
235 |
Hook for doing something with the validated data. This is responsible |
|
|
236 |
for the final processing. |
|
|
237 |
|
|
|
238 |
form_list is a list of Form instances, each containing clean, valid |
|
|
239 |
data. |
|
|
240 |
""" |
|
|
241 |
raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) |