Ticket #3218: wizard.py

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