Django

Code

Ticket #3218: wizard_20070111.py

File wizard_20070111.py, 7.8 kB (added by Honza Král <Honza.Kral@gmail.com>, 3 years ago)

updated version

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