Ticket #3218: wizard.5.py

File wizard.5.py, 8.5 KB (added by Honza Král, 17 years ago)

added current_step and step_count

Line 
1"""
2USAGE:
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"""
29from django.conf import settings
30from django.http import Http404
31from django.shortcuts import render_to_response
32from django.template.context import RequestContext
33
34from django import newforms as forms
35import cPickle as pickle
36import md5
37
38class 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 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
125
126 def failed_hash(self, request, i):
127 """
128 One of the hashes verifying old data doesn't match.
129 """
130 self.step = i
131 return self.render(self.get_form(self.step), request)
132
133 def security_hash(self, request, form):
134 """
135 Calculates the security hash for the given Form instance.
136
137 This creates a list of the form field names/values in a deterministic
138 order, pickles the result with the SECRET_KEY setting and takes an md5
139 hash of that.
140
141 Subclasses may want to take into account request-specific information
142 such as the IP address.
143 """
144 data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
145 # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
146 # Python 2.3, but Django requires 2.3 anyway, so that's OK.
147 pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
148 return md5.new(pickled).hexdigest()
149
150 def parse_params(self, request, *args, **kwargs):
151 """
152 Set self.step, process any additional info from parameters and/or form data
153 """
154 if request.POST:
155 self.step = int(request.POST.get(self.STEP_FIELD, 0))
156 else:
157 self.step = 0
158
159 def get_template(self):
160 """
161 Return name of the template to be rendered, use self.step to get the step number.
162 """
163 return "wizard.html"
164
165 def render_template(self, request, form, previous_fields):
166 """
167 Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
168
169 If you only wish to override the template name, use get_template
170
171 Some additional items are added to the context:
172 'step_field' is the name of the hidden field containing step
173 'step' holds the current step
174 'form' containing the current form to be processed (either empty or with errors)
175 'previous_data' contains all the addtitional information, including
176 hashes for finished forms and old data in form of hidden fields
177 any additional data stored in self.extra_context
178 """
179 return render_to_response(self.get_template(), dict(
180 step_field=self.STEP_FIELD,
181 step=self.step,
182 current_step=self.step + 1,
183 step_count=self.step_count(),
184 form=form,
185 previous_fields=previous_fields,
186 ** self.extra_context
187 ), context_instance=RequestContext(request))
188
189 def process_step(self, request, form, step):
190 """
191 This should not modify any data, it is only a hook to modify wizard's internal state
192 (such as dynamically generating form_list based on previously submited forms).
193 It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
194
195 Note that this method is called every time a page is rendered for ALL submitted steps.
196
197 Only valid data enter here.
198 """
199 pass
200
201 def step_count(self):
202 """Return number of steps"""
203 return len(self.form_list)
204
205 # METHODS SUBCLASSES MUST OVERRIDE ########################################
206
207 def done(self, request, form_list):
208 """
209 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
210 """
211 raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
Back to Top