Django

Code

root/django/branches/newforms-admin/django/contrib/formtools/wizard.py

Revision 7669, 10.2 kB (checked in by brosner, 6 months ago)

newforms-admin: Merged from trunk up to [7668].

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