Django

Code

Ticket #3218: wizard.2.py

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

matching current trunk (6158)

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                 return self.done(  request, [ self.get_form( i, request.POST ) for i in range( len( self.form_list ) ) ] )
96             form = self.get_form( self.step )
97         return self.render( form, request )
98
99     def render( self, form, request ):
100         """
101         Prepare the form and call the render_template() method to do tha actual rendering.
102         """
103         if self.step >= len( self.form_list ):
104             raise Http404
105  
106         old_data = request.POST
107         prev_fields = ''
108         if old_data:
109             # old data
110             prev_fields = '\n'.join(
111                     bf.as_hidden() for i in range(self.step) for bf in self.get_form( i, old_data )
112                 )
113             # hashes for old forms
114             hidden = forms.widgets.HiddenInput()
115             prev_fields += '\n'.join(
116                     hidden.render( "hash_%d" % i, old_data.get( "hash_%d" % i, self.security_hash( request, self.get_form( i, old_data ) ) ) )
117                         for i in range( self.step)
118                 )
119         return self.render_template( request, form, prev_fields )
120  
121        
122     # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
123  
124     def failed_hash( self, request, i ):
125         """
126         One of the hashes verifying old data doesn't match.
127         """
128         self.step = i
129         return self.render( self.get_form(self.step), request )
130  
131     def security_hash(self, request, form):
132         """
133         Calculates the security hash for the given Form instance.
134  
135         This creates a list of the form field names/values in a deterministic
136         order, pickles the result with the SECRET_KEY setting and takes an md5
137         hash of that.
138  
139         Subclasses may want to take into account request-specific information
140         such as the IP address.
141         """
142         data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
143         # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
144         # Python 2.3, but Django requires 2.3 anyway, so that's OK.
145         pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
146         return md5.new(pickled).hexdigest()
147  
148     def parse_params( self, request, *args, **kwargs ):
149         """
150         Set self.step, process any additional info from parameters and/or form data
151         """
152         if request.POST:
153             self.step = int( request.POST.get( self.STEP_FIELD, 0 ) )
154         else:
155             self.step = 0
156
157     def get_template( self ):
158         """
159         Return name of the template to be rendered, use self.step to get the step number.
160         """
161         return "wizard.html"
162  
163     def render_template( self, request, form, previous_fields ):
164         """
165         Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
166  
167         If you only wish to override the template name, use get_template
168  
169         Some additional items are added to the context:
170             'step_field' is the name of the hidden field containing step
171             'step' holds the current step
172             'form' containing the current form to be processed (either empty or with errors)
173             'previous_data' contains all the addtitional information, including
174                 hashes for finished forms and old data in form of hidden fields
175             any additional data stored in self.extra_context
176         """
177         return render_to_response( self.get_template(), dict(
178                     step_field=self.STEP_FIELD,
179                     step=self.step,
180                     form=form,
181                     previous_fields=previous_fields,
182                     ** self.extra_context
183                 ), context_instance=RequestContext( request ) )
184  
185     def process_step( self, request, form, step ):
186         """
187         This should not modify any data, it is only a hook to modify wizard's internal state
188         (such as dynamically generating form_list based on previously submited forms).
189         It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
190  
191         Note that this method is called every time a page is rendered for ALL submitted steps.
192  
193         Only valid data enter here.
194         """
195         pass
196  
197     # METHODS SUBCLASSES MUST OVERRIDE ########################################
198  
199     def done( self, request, form_list ):
200         """
201         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
202         """
203         raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)