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 | # 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__)
|
---|