Django

Code

root/django/branches/gis/django/contrib/formtools/wizard.py

Revision 8215, 10.2 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7981-8001,8003-8011,8013-8033,8035-8036,8038-8039,8041-8063,8065-8076,8078-8139,8141-8154,8156-8214 via svnmerge from trunk.

  • Property svn:eol-style set to native
Line 
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
16 class FormWizard(object):
17     # Dictionary of extra template context variables.
18     extra_context = {}
19
20     # The HTML (and POST data) field name for the "step" variable.
21     step_field_name="wizard_step"
22
23     # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
24
25     def __init__(self, form_list, initial=None):
26         "form_list should be a list of Form classes (not instances)."
27         self.form_list = form_list[:]
28         self.initial = initial or {}
29         self.step = 0 # A zero-based counter keeping track of which step we're in.
30
31     def __repr__(self):
32         return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
33
34     def get_form(self, step, data=None):
35         "Helper method that returns the Form instance for the given step."
36         return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
37
38     def num_steps(self):
39         "Helper method that returns the number of steps."
40         # You might think we should just set "self.form_list = len(form_list)"
41         # in __init__(), but this calculation needs to be dynamic, because some
42         # hook methods might alter self.form_list.
43         return len(self.form_list)
44
45     def __call__(self, request, *args, **kwargs):
46         """
47         Main method that does all the hard work, conforming to the Django view
48         interface.
49         """
50         if 'extra_context' in kwargs:
51             self.extra_context.update(kwargs['extra_context'])
52         current_step = self.determine_step(request, *args, **kwargs)
53         self.parse_params(request, *args, **kwargs)
54
55         # Sanity check.
56         if current_step >= self.num_steps():
57             raise Http404('Step %s does not exist' % current_step)
58
59         # For each previous step, verify the hash and process.
60         # TODO: Move "hash_%d" to a method to make it configurable.
61         for i in range(current_step):
62             form = self.get_form(i, request.POST)
63             if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
64                 return self.render_hash_failure(request, i)
65             self.process_step(request, form, i)
66
67         # Process the current step. If it's valid, go to the next step or call
68         # done(), depending on whether any steps remain.
69         if request.method == 'POST':
70             form = self.get_form(current_step, request.POST)
71         else:
72             form = self.get_form(current_step)
73         if form.is_valid():
74             self.process_step(request, form, current_step)
75             next_step = current_step + 1
76
77             # If this was the last step, validate all of the forms one more
78             # time, as a sanity check, and call done().
79             num = self.num_steps()
80             if next_step == num:
81                 final_form_list = [self.get_form(i, request.POST) for i in range(num)]
82
83                 # Validate all the forms. If any of them fail validation, that
84                 # must mean the validator relied on some other input, such as
85                 # an external Web site.
86                 for i, f in enumerate(final_form_list):
87                     if not f.is_valid():
88                         return self.render_revalidation_failure(request, i, f)
89                 return self.done(request, final_form_list)
90
91             # Otherwise, move along to the next step.
92             else:
93                 form = self.get_form(next_step)
94                 current_step = next_step
95
96         return self.render(form, request, current_step)
97
98     def render(self, form, request, step, context=None):
99         "Renders the given Form object, returning an HttpResponse."
100         old_data = request.POST
101         prev_fields = []
102         if old_data:
103             hidden = forms.HiddenInput()
104             # Collect all data from previous steps and render it as HTML hidden fields.
105             for i in range(step):
106                 old_form = self.get_form(i, old_data)
107                 hash_name = 'hash_%s' % i
108                 prev_fields.extend([bf.as_hidden() for bf in old_form])
109                 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
110         return self.render_template(request, form, ''.join(prev_fields), step, context)
111
112     # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
113
114     def prefix_for_step(self, step):
115         "Given the step, returns a Form prefix to use."
116         return str(step)
117
118     def render_hash_failure(self, request, step):
119         """
120         Hook for rendering a template if a hash check failed.
121
122         step is the step that failed. Any previous step is guaranteed to be
123         valid.
124
125         This default implementation simply renders the form for the given step,
126         but subclasses may want to display an error message, etc.
127         """
128         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.'})
129
130     def render_revalidation_failure(self, request, step, form):
131         """
132         Hook for rendering a template if final revalidation failed.
133
134         It is highly unlikely that this point would ever be reached, but See
135         the comment in __call__() for an explanation.
136         """
137         return self.render(form, request, step)
138
139     def security_hash(self, request, form):
140         """
141         Calculates the security hash for the given HttpRequest and Form instances.
142
143         This creates a list of the form field names/values in a deterministic
144         order, pickles the result with the SECRET_KEY setting and takes an md5
145         hash of that.
146
147         Subclasses may want to take into account request-specific information,
148         such as the IP address.
149         """
150         data = [(bf.name, bf.data or '') for bf in form] + [settings.SECRET_KEY]
151         # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
152         # Python 2.3, but Django requires 2.3 anyway, so that's OK.
153         pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
154         return md5_constructor(pickled).hexdigest()
155
156     def determine_step(self, request, *args, **kwargs):
157         """
158         Given the request object and whatever *args and **kwargs were passed to
159         __call__(), returns the current step (which is zero-based).
160
161         Note that the result should not be trusted. It may even be a completely
162         invalid number. It's not the job of this method to validate it.
163         """
164         if not request.POST:
165             return 0
166         try:
167             step = int(request.POST.get(self.step_field_name, 0))
168         except ValueError:
169             return 0
170         return step
171
172     def parse_params(self, request, *args, **kwargs):
173         """
174         Hook for setting some state, given the request object and whatever
175         *args and **kwargs were passed to __call__(), sets some state.
176
177         This is called at the beginning of __call__().
178         """
179         pass
180
181     def get_template(self, step):
182         """
183         Hook for specifying the name of the template to use for a given step.
184
185         Note that this can return a tuple of template names if you'd like to
186         use the template system's select_template() hook.
187         """
188         return 'forms/wizard.html'
189
190     def render_template(self, request, form, previous_fields, step, context=None):
191         """
192         Renders the template for the given step, returning an HttpResponse object.
193
194         Override this method if you want to add a custom context, return a
195         different MIME type, etc. If you only need to override the template
196         name, use get_template() instead.
197
198         The template will be rendered with the following context:
199             step_field -- The name of the hidden field containing the step.
200             step0      -- The current step (zero-based).
201             step       -- The current step (one-based).
202             step_count -- The total number of steps.
203             form       -- The Form instance for the current step (either empty
204                           or with errors).
205             previous_fields -- A string representing every previous data field,
206                           plus hashes for completed forms, all in the form of
207                           hidden fields. Note that you'll need to run this
208                           through the "safe" template filter, to prevent
209                           auto-escaping, because it's raw HTML.
210         """
211         context = context or {}
212         context.update(self.extra_context)
213         return render_to_response(self.get_template(self.step), dict(context,
214             step_field=self.step_field_name,
215             step0=step,
216             step=step + 1,
217             step_count=self.num_steps(),
218             form=form,
219             previous_fields=previous_fields
220         ), context_instance=RequestContext(request))
221
222     def process_step(self, request, form, step):
223         """
224         Hook for modifying the FormWizard's internal state, given a fully
225         validated Form object. The Form is guaranteed to have clean, valid
226         data.
227
228         This method should *not* modify any of that data. Rather, it might want
229         to set self.extra_context or dynamically alter self.form_list, based on
230         previously submitted forms.
231
232         Note that this method is called every time a page is rendered for *all*
233         submitted steps.
234         """
235         pass
236
237     # METHODS SUBCLASSES MUST OVERRIDE ########################################
238
239     def done(self, request, form_list):
240         """
241         Hook for doing something with the validated data. This is responsible
242         for the final processing.
243
244         form_list is a list of Form instances, each containing clean, valid
245         data.
246         """
247         raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
Note: See TracBrowser for help on using the browser.