Django

Code

Ticket #3218: wizard.4.py

File wizard.4.py, 8.3 kB (added by Honza_Kral, 3 years ago)

validating all forms before invoking done()

Line 
1 """
2 USAGE:
3     views:
4         class MyWizard(Wizard):
5             def done(self, request, form_list):
6                 return render_to_response('done.html', {'form_data' : [ form.cleaned_data for form in form_list ] })
7     urls:
8         (r'^$', MyWizard([MyForm, MyForm, MyForm])),
9  
10     form template:
11         <form action="." method="POST">
12         FORM({{ step }}): {{ form }}
13         
14         step_info : <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
15  
16         previous_fields: {{ previous_fields }}
17  
18         <input type="submit">
19         </form>
20
21     done.html:
22         {% for data in form_data %}
23             {% for item in data.items %}
24                 {{ item.0 }} : {{ item.1 }}<br />
25             {% endfor %}
26         {% endfor %}
27         
28 """
29 from django.conf import settings
30 from django.http import Http404
31 from django.shortcuts import render_to_response
32 from django.template.context import RequestContext
33  
34 from django import newforms as forms
35 import cPickle as pickle
36 import md5
37  
38 class Wizard(object):
39     PREFIX="%d"
40     STEP_FIELD="wizard_step"
41  
42     # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
43     def __init__(self, form_list, initial=None):
44         " Pass list of Form classes (not instances !) "
45         self.form_list = form_list[:]
46         self.initial = initial or {}
47  
48     def __repr__(self):
49         return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
50    
51     def get_form(self, step, data=None):
52         " Shortcut to return form instance. "
53         return self.form_list[step](data, prefix=self.PREFIX % step, initial=self.initial.get(step, None))
54
55     def __call__(self, request, *args, **kwargs):
56         """
57         Main function that does all the hard work:
58             - initializes the wizard object (via parse_params())
59             - veryfies (using security_hash()) that noone has tempered with the data since we last saw them
60                 calls failed_hash() if it is so
61                 calls process_step() for every previously submitted form
62             - validates current form and
63                 returns it again if errors were found
64                 returns done() if it was the last form
65                 returns next form otherwise
66         """
67         # add extra_context, we don't care if somebody overrides it, as long as it remains a dict
68         self.extra_context = getattr(self, 'extra_context', {})
69         if 'extra_context' in kwargs:
70             self.extra_context.update(kwargs['extra_context'])
71  
72         self.parse_params(request, *args, **kwargs)
73  
74         # we only accept POST method for form delivery  no POST, no data
75         if not request.POST:
76             self.step = 0
77             return self.render(self.get_form(0), request)
78  
79         # verify old steps' hashes
80         for i in range(self.step):
81             form = self.get_form(i, request.POST)
82             # somebody is trying to corrupt our data
83             if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
84                 # revert to the corrupted step
85                 return self.failed_hash(request, i)
86             self.process_step(request, form, i)
87  
88         # process current step
89         form = self.get_form(self.step, request.POST)
90         if form.is_valid():
91             self.process_step(request, form, self.step)
92             self.step += 1
93             # this was the last step
94             if self.step == len(self.form_list):
95                 form_list = [self.get_form(i, request.POST) for i in range(len(self.form_list))]
96                 # validate all the forms befora passing it to done()
97                 for f in form_list: f.is_valid()
98                 return self.done(request, form_list)
99             form = self.get_form(self.step)
100         return self.render(form, request)
101
102     def render(self, form, request):
103         """
104         Prepare the form and call the render_template() method to do tha actual rendering.
105         """
106         if self.step >= len(self.form_list):
107             raise Http404
108  
109         old_data = request.POST
110         prev_fields = ''
111         if old_data:
112             # old data
113             prev_fields = '\n'.join(
114                     bf.as_hidden() for i in range(self.step) for bf in self.get_form(i, old_data)
115                 )
116             # hashes for old forms
117             hidden = forms.widgets.HiddenInput()
118             prev_fields += '\n'.join(
119                     hidden.render("hash_%d" % i, old_data.get("hash_%d" % i, self.security_hash(request, self.get_form(i, old_data))))
120                         for i in range(self.step)
121                 )
122         return self.render_template(request, form, prev_fields)
123  
124        
125     # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
126  
127     def failed_hash(self, request, i):
128         """
129         One of the hashes verifying old data doesn't match.
130         """
131         self.step = i
132         return self.render(self.get_form(self.step), request)
133  
134     def security_hash(self, request, form):
135         """
136         Calculates the security hash for the given Form instance.
137  
138         This creates a list of the form field names/values in a deterministic
139         order, pickles the result with the SECRET_KEY setting and takes an md5
140         hash of that.
141  
142         Subclasses may want to take into account request-specific information
143         such as the IP address.
144         """
145         data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
146         # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
147         # Python 2.3, but Django requires 2.3 anyway, so that's OK.
148         pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
149         return md5.new(pickled).hexdigest()
150  
151     def parse_params(self, request, *args, **kwargs):
152         """
153         Set self.step, process any additional info from parameters and/or form data
154         """
155         if request.POST:
156             self.step = int(request.POST.get(self.STEP_FIELD, 0))
157         else:
158             self.step = 0
159
160     def get_template(self):
161         """
162         Return name of the template to be rendered, use self.step to get the step number.
163         """
164         return "wizard.html"
165  
166     def render_template(self, request, form, previous_fields):
167         """
168         Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
169  
170         If you only wish to override the template name, use get_template
171  
172         Some additional items are added to the context:
173             'step_field' is the name of the hidden field containing step
174             'step' holds the current step
175             'form' containing the current form to be processed (either empty or with errors)
176             'previous_data' contains all the addtitional information, including
177                 hashes for finished forms and old data in form of hidden fields
178             any additional data stored in self.extra_context
179         """
180         return render_to_response(self.get_template(), dict(
181                     step_field=self.STEP_FIELD,
182                     step=self.step,
183                     form=form,
184                     previous_fields=previous_fields,
185                     ** self.extra_context
186                 ), context_instance=RequestContext(request))
187  
188     def process_step(self, request, form, step):
189         """
190         This should not modify any data, it is only a hook to modify wizard's internal state
191         (such as dynamically generating form_list based on previously submited forms).
192         It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
193  
194         Note that this method is called every time a page is rendered for ALL submitted steps.
195  
196         Only valid data enter here.
197         """
198         pass
199  
200     # METHODS SUBCLASSES MUST OVERRIDE ########################################
201  
202     def done(self, request, form_list):
203         """
204         this method must be overriden, it is responsible for the end processing - it will be called with instances of all form_list with their data
205         """
206         raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)