1 | Index: /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py |
---|
2 | =================================================================== |
---|
3 | --- /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py (revision 9084) |
---|
4 | +++ /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py (working copy) |
---|
5 | @@ -1,21 +1,22 @@ |
---|
6 | -""" |
---|
7 | -FormWizard class -- implements a multi-page form, validating between each |
---|
8 | -step and storing the form's state as HTML hidden fields so that no state is |
---|
9 | -stored on the server side. |
---|
10 | -""" |
---|
11 | - |
---|
12 | import cPickle as pickle |
---|
13 | |
---|
14 | from django import forms |
---|
15 | from django.conf import settings |
---|
16 | from django.http import Http404 |
---|
17 | +from django.http import HttpResponseRedirect |
---|
18 | from django.shortcuts import render_to_response |
---|
19 | from django.template.context import RequestContext |
---|
20 | from django.utils.hashcompat import md5_constructor |
---|
21 | from django.utils.translation import ugettext_lazy as _ |
---|
22 | from django.contrib.formtools.utils import security_hash |
---|
23 | |
---|
24 | + |
---|
25 | class FormWizard(object): |
---|
26 | + """ |
---|
27 | + FormWizard class -- implements a multi-page form, validating between each |
---|
28 | + step and storing the form's state as HTML hidden fields so that no state is |
---|
29 | + stored on the server side. |
---|
30 | + """ |
---|
31 | # Dictionary of extra template context variables. |
---|
32 | extra_context = {} |
---|
33 | |
---|
34 | @@ -239,3 +240,399 @@ |
---|
35 | data. |
---|
36 | """ |
---|
37 | raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) |
---|
38 | + |
---|
39 | + |
---|
40 | +class SessionWizard(object): |
---|
41 | + """ |
---|
42 | + SessionWizard class -- implements multi-page forms with the following |
---|
43 | + characteristics: |
---|
44 | + |
---|
45 | + 1) easily supports navigation to arbitrary pages in the wizard |
---|
46 | + 2) uses GETs to display forms (caveat validation errors) and POSTs for |
---|
47 | + form submissions |
---|
48 | + |
---|
49 | + Pros are support for back-button and arbitrary navigation within pages |
---|
50 | + (including the oddity of someone clicking on the refresh button) |
---|
51 | + |
---|
52 | + The major Con is use of the session scope. In particular, zero |
---|
53 | + consideration has been given to multipart form data. |
---|
54 | + """ |
---|
55 | + |
---|
56 | + # keys used to store wizard data in sessions |
---|
57 | + __form_list_key = 'form_list' |
---|
58 | + __cleaned_data_key = 'cleaned_data' |
---|
59 | + __POST_data_key = 'POST_data' |
---|
60 | + __page_infos_key = 'page_infos' |
---|
61 | + |
---|
62 | + def __init__(self, form_list): |
---|
63 | + """form_list should be a list of Form classes (not instances).""" |
---|
64 | + self.base_form_list = form_list[:] |
---|
65 | + |
---|
66 | + def _init_form_list(self, request): |
---|
67 | + """ |
---|
68 | + Copy self.base_form_list to the session scope so that subclasses can |
---|
69 | + manipulate the form_list for individual users. |
---|
70 | + """ |
---|
71 | + session_key = self.get_wizard_data_key(request) |
---|
72 | + if session_key not in request.session: |
---|
73 | + request.session[session_key] = { |
---|
74 | + self.__form_list_key : self.base_form_list[:], |
---|
75 | + self.__cleaned_data_key : [], |
---|
76 | + self.__POST_data_key : [], |
---|
77 | + self.__page_infos_key : [], |
---|
78 | + } |
---|
79 | + |
---|
80 | + def __call__(self, request, *args, **kwargs): |
---|
81 | + """ |
---|
82 | + Initialize the form_list for a session if needed and call GET or |
---|
83 | + POST depending on the http method. |
---|
84 | + """ |
---|
85 | + self._init_form_list(request) |
---|
86 | + page0 = int(kwargs['page0']) |
---|
87 | + |
---|
88 | + if request.method == 'POST': |
---|
89 | + return self.POST(request) |
---|
90 | + else: |
---|
91 | + return self.GET(request, page0) |
---|
92 | + |
---|
93 | + |
---|
94 | + def GET(self, request, page0): |
---|
95 | + """ |
---|
96 | + Display the form/page for the page identified by page0 |
---|
97 | + """ |
---|
98 | + page_data = self._get_cleaned_data(request.session, page0) |
---|
99 | + if page_data is None: |
---|
100 | + form = self._get_form_list(request.session)[page0]() |
---|
101 | + else: |
---|
102 | + form_class = self._get_form_list(request.session)[page0] |
---|
103 | + if issubclass(form_class, forms.ModelForm): |
---|
104 | + form = form_class(instance=form_class.Meta.model(**page_data)) |
---|
105 | + else: |
---|
106 | + form = form_class(initial=page_data) |
---|
107 | + return self._show_form(request, page0, form) |
---|
108 | + |
---|
109 | + def POST(self, request): |
---|
110 | + """ |
---|
111 | + Validate form submission, and redirect to GET the next form or return |
---|
112 | + the response from self.done(). |
---|
113 | + """ |
---|
114 | + page0 = int(request.POST['page0']) |
---|
115 | + url_base = self.get_URL_base(request, page0) |
---|
116 | + self._set_POST_data(request.session, request.POST, page0) |
---|
117 | + form_list = self._get_form_list(request.session) |
---|
118 | + form = form_list[page0](request.POST) |
---|
119 | + new_page0 = self.preprocess_submit_form(request, page0, form) |
---|
120 | + |
---|
121 | + if new_page0 is not None: |
---|
122 | + return HttpResponseRedirect(url_base + str(new_page0)) |
---|
123 | + else: |
---|
124 | + if form.is_valid(): |
---|
125 | + self._set_cleaned_data(request.session, page0, |
---|
126 | + form.cleaned_data) |
---|
127 | + self._set_page_info(request.session, page0, True) |
---|
128 | + is_done = self.process_submit_form(request, page0, form) |
---|
129 | + if (is_done is None or is_done == False) and \ |
---|
130 | + len(form_list) > page0 + 1: |
---|
131 | + return HttpResponseRedirect(url_base + str(page0 + 1)) |
---|
132 | + else: |
---|
133 | + first_broken_page, form = \ |
---|
134 | + self._validate_all_forms(request.session) |
---|
135 | + if first_broken_page is not None: |
---|
136 | + return self._show_form(request, first_broken_page, |
---|
137 | + form) |
---|
138 | + else: |
---|
139 | + return self.done(request) |
---|
140 | + else: |
---|
141 | + self._set_page_info(request.session, page0, False) |
---|
142 | + |
---|
143 | + return self._show_form(request, page0, form) |
---|
144 | + |
---|
145 | + |
---|
146 | + # form util methods # |
---|
147 | + def _validate_all_forms(self, session): |
---|
148 | + """ |
---|
149 | + Iterate through the session form list and validate based on the POST |
---|
150 | + data stored in the session for this wizard. Return the page index and |
---|
151 | + the form of the first invalid form or None, None if all forms are valid. |
---|
152 | + """ |
---|
153 | + i = 0 |
---|
154 | + for form_class in self._get_form_list(session): |
---|
155 | + form = form_class(self._get_POST_data(session, i)) |
---|
156 | + if not form.is_valid(): |
---|
157 | + return i, form |
---|
158 | + else: |
---|
159 | + i = i + 1 |
---|
160 | + return None, None |
---|
161 | + |
---|
162 | + def _show_form(self, request, page0, form): |
---|
163 | + """ |
---|
164 | + Show the form associated with indicated page index. |
---|
165 | + """ |
---|
166 | + url_base = self.get_URL_base(request, page0) |
---|
167 | + extra_context = self.process_show_form(request, page0, form) |
---|
168 | + self._set_current_page(request.session, page0) |
---|
169 | + page_infos = self._get_page_infos(request.session) |
---|
170 | + return render_to_response(self.get_template(page0), |
---|
171 | + {'page0' : page0, |
---|
172 | + 'page' : page0 + 1, |
---|
173 | + 'form' : form, |
---|
174 | + 'page_infos' : page_infos, |
---|
175 | + 'url_base' : url_base, |
---|
176 | + 'extra_context' : extra_context |
---|
177 | + }, RequestContext(request)) |
---|
178 | + |
---|
179 | + def _get_form_list(self, session): |
---|
180 | + """ |
---|
181 | + Return the list of form classes stored in the provided session. |
---|
182 | + """ |
---|
183 | + return session[self.get_wizard_data_key(session)][self.__form_list_key] |
---|
184 | + |
---|
185 | + def _insert_form(self, session, page0, form_class): |
---|
186 | + """ |
---|
187 | + Insert a form class into the provided session's form list at index |
---|
188 | + page0. |
---|
189 | + """ |
---|
190 | + form_list = self._get_form_list(session) |
---|
191 | + form_list.insert(page0, form_class) |
---|
192 | + self._insert_wizard_data(session, self.__form_list_key, form_list) |
---|
193 | + |
---|
194 | + def _remove_form(self, session, page0): |
---|
195 | + """ |
---|
196 | + Remove the form at index page0 from the provided sessions form list. |
---|
197 | + """ |
---|
198 | + self._del_wizard_data(session, self.__form_list_key, page0) |
---|
199 | + # end form util methods # |
---|
200 | + |
---|
201 | + |
---|
202 | + # Form data methods # |
---|
203 | + def _get_POST_data(self, session, page0): |
---|
204 | + """ |
---|
205 | + Return the POST data for a given page index page0, stored in the |
---|
206 | + provided session. |
---|
207 | + """ |
---|
208 | + post_data = self._get_all_POST_data(session) |
---|
209 | + if len(post_data) > page0: |
---|
210 | + return post_data[page0] |
---|
211 | + else: |
---|
212 | + return {} |
---|
213 | + |
---|
214 | + def _set_POST_data(self, session, data, page0, force_insert=False): |
---|
215 | + """ |
---|
216 | + Set the POST data for a given page index and session to the 'data' |
---|
217 | + provided. If force_insert is True then the data assignment is forced |
---|
218 | + as an list.insert(page0, data) call. |
---|
219 | + """ |
---|
220 | + post_data = self._get_all_POST_data(session) |
---|
221 | + if force_insert or len(post_data) <= page0: |
---|
222 | + post_data.insert(page0, data) |
---|
223 | + else: |
---|
224 | + post_data[page0] = data |
---|
225 | + self._insert_wizard_data(session, self.__POST_data_key, post_data) |
---|
226 | + |
---|
227 | + def _remove_POST_data(self, session, page0): |
---|
228 | + """ |
---|
229 | + Remove the POST data stored in the session at index page0. |
---|
230 | + """ |
---|
231 | + self._del_wizard_data(session, self.__POST_data_key, page0) |
---|
232 | + |
---|
233 | + def _get_all_POST_data(self, session): |
---|
234 | + """ |
---|
235 | + Return the list of all POST data for this wizard from the provided |
---|
236 | + session. |
---|
237 | + """ |
---|
238 | + return session[self.get_wizard_data_key(session)][self.__POST_data_key] |
---|
239 | + |
---|
240 | + def _get_cleaned_data(self, session, page0): |
---|
241 | + """ |
---|
242 | + Return all cleaned data for this wizard from the provided session. |
---|
243 | + """ |
---|
244 | + cleaned_data = self._get_all_cleaned_data(session) |
---|
245 | + if len(cleaned_data) > page0: |
---|
246 | + return cleaned_data[page0] |
---|
247 | + else: |
---|
248 | + return {} |
---|
249 | + |
---|
250 | + def _set_cleaned_data(self, session, page0, data, force_insert=False): |
---|
251 | + """ |
---|
252 | + Assign the cleaned data for this wizard in the session at index page0, |
---|
253 | + optionally forcing a call a list insert call based on the |
---|
254 | + 'force_insert' argument. |
---|
255 | + """ |
---|
256 | + cleaned_data = self._get_all_cleaned_data(session) |
---|
257 | + if force_insert or len(cleaned_data) <= page0: |
---|
258 | + cleaned_data.insert(page0, data) |
---|
259 | + else: |
---|
260 | + cleaned_data[page0] = data |
---|
261 | + self._insert_wizard_data(session, self.__cleaned_data_key, cleaned_data) |
---|
262 | + |
---|
263 | + |
---|
264 | + def _get_all_cleaned_data(self, session): |
---|
265 | + """ |
---|
266 | + Return a list of all the cleaned data in the session for this wizard. |
---|
267 | + """ |
---|
268 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
269 | + return wizard_data[self.__cleaned_data_key] |
---|
270 | + |
---|
271 | + def _remove_cleaned_data(self, session, page0): |
---|
272 | + """ |
---|
273 | + Remove the cleaned data at index page0 for this wizard from the |
---|
274 | + provided session. |
---|
275 | + """ |
---|
276 | + self._del_wizard_data(session, self.__cleaned_data_key, page0) |
---|
277 | + # end Form data methods # |
---|
278 | + |
---|
279 | + |
---|
280 | + # page methods # |
---|
281 | + def _set_current_page(self, session, page0): |
---|
282 | + """ |
---|
283 | + Iterate through the page info dicts in the session and set |
---|
284 | + 'current_page' to True for the page_info corresponding to page0 and |
---|
285 | + False for all others. |
---|
286 | + """ |
---|
287 | + page_infos = self._get_page_infos(session) |
---|
288 | + for i in range(len(page_infos)): |
---|
289 | + if i == page0: |
---|
290 | + page_infos[i]['current_page'] = True |
---|
291 | + else: |
---|
292 | + page_infos[i]['current_page'] = False |
---|
293 | + |
---|
294 | + def _get_page_infos(self, session): |
---|
295 | + """ |
---|
296 | + Return the list of page info dicts stored in the provided session for |
---|
297 | + this wizard. |
---|
298 | + """ |
---|
299 | + return session[self.get_wizard_data_key(session)][self.__page_infos_key] |
---|
300 | + |
---|
301 | + def _remove_page(self, session, page0): |
---|
302 | + """ |
---|
303 | + Remove the page for this wizard indicated by the page0 argument from |
---|
304 | + the provided session. |
---|
305 | + """ |
---|
306 | + self._remove_form(session, page0) |
---|
307 | + self._remove_page_info(session, page0) |
---|
308 | + self._remove_cleaned_data(session, page0) |
---|
309 | + self._remove_POST_data(session, page0) |
---|
310 | + |
---|
311 | + def _remove_page_info(self, session, page0): |
---|
312 | + """ |
---|
313 | + Remove the page info dict for this wizard stored at the page0 index |
---|
314 | + from the provided session. |
---|
315 | + """ |
---|
316 | + self._del_wizard_data(session, self.__page_infos_key, page0) |
---|
317 | + |
---|
318 | + def _insert_page(self, session, page0, form_class): |
---|
319 | + """ |
---|
320 | + Insert a page into this wizard, storing required session structures. |
---|
321 | + """ |
---|
322 | + self._insert_form(session, page0, form_class) |
---|
323 | + self._set_page_info(session, page0, False, True) |
---|
324 | + self._set_cleaned_data(session, page0, {}, True) |
---|
325 | + self._set_POST_data(session, {}, page0, True) |
---|
326 | + |
---|
327 | + def _set_page_info(self, session, page0, valid, force_insert=False): |
---|
328 | + """ |
---|
329 | + Set the page info in this wizard for a page at index page0 and stored |
---|
330 | + in the provided session. |
---|
331 | + """ |
---|
332 | + page_info = { |
---|
333 | + 'valid' : valid, |
---|
334 | + 'title' : self.get_page_title(session, page0) |
---|
335 | + } |
---|
336 | + page_infos = self._get_page_infos(session) |
---|
337 | + if force_insert or len(page_infos) <= page0: |
---|
338 | + page_infos.insert(page0, page_info) |
---|
339 | + else: |
---|
340 | + page_infos[page0] = page_info |
---|
341 | + self._insert_wizard_data(session, self.__page_infos_key, page_infos) |
---|
342 | + # end page methods # |
---|
343 | + |
---|
344 | + # start wizard data utils # |
---|
345 | + def _clear_wizard_data_from_session(self, session): |
---|
346 | + """ |
---|
347 | + Clear the session data used by this wizard from the provided session. |
---|
348 | + """ |
---|
349 | + del session[self.get_wizard_data_key(session)] |
---|
350 | + |
---|
351 | + def _insert_wizard_data(self, session, key, data): |
---|
352 | + """ |
---|
353 | + Inserts wizard data into the provided session at the provided key. |
---|
354 | + """ |
---|
355 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
356 | + wizard_data[key] = data |
---|
357 | + session[self.get_wizard_data_key(session)] = wizard_data |
---|
358 | + |
---|
359 | + def _del_wizard_data(self, session, key, page0): |
---|
360 | + """ |
---|
361 | + Deletes wizard data from the provided session at the key and page0 |
---|
362 | + index. |
---|
363 | + """ |
---|
364 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
365 | + sub_set = wizard_data[key] |
---|
366 | + if len(sub_set) > page0: |
---|
367 | + del sub_set[page0] |
---|
368 | + wizard_data[key] = sub_set |
---|
369 | + session[self.get_wizard_data_key(session)] = wizard_data |
---|
370 | + |
---|
371 | + # end wizard data utils # |
---|
372 | + |
---|
373 | + # typically overriden methods # |
---|
374 | + def get_wizard_data_key(self, session): |
---|
375 | + """ |
---|
376 | + Return a session key for this wizard. The provided session could be |
---|
377 | + used to prevent overlapping keys in the case that someone needs |
---|
378 | + multiple instances of this wizard at one time. |
---|
379 | + """ |
---|
380 | + return 'session_wizard_data' |
---|
381 | + |
---|
382 | + def get_URL_base(self, request, page0): |
---|
383 | + """ |
---|
384 | + Return the URL to this wizard minus the "page0" parto of the URL. This |
---|
385 | + value is passed to template as url_base. |
---|
386 | + """ |
---|
387 | + return request.path.replace("/" + str(page0), "/") |
---|
388 | + |
---|
389 | + def get_page_title(self, session, page0): |
---|
390 | + """ |
---|
391 | + Return a user friendly title for the page at index page0. |
---|
392 | + """ |
---|
393 | + return 'Page %s' % str(page0 + 1) |
---|
394 | + |
---|
395 | + def process_show_form(self, request, page0, form): |
---|
396 | + """ |
---|
397 | + Called before rendering a form either from a GET or when a form submit |
---|
398 | + is invalid. |
---|
399 | + """ |
---|
400 | + |
---|
401 | + def preprocess_submit_form(self, request, page0, form): |
---|
402 | + """ |
---|
403 | + Called when a form is POSTed, but before form is validated. If this |
---|
404 | + function returns None then form submission continues, else it should |
---|
405 | + return a new page index that will be redirected to as a GET. |
---|
406 | + """ |
---|
407 | + |
---|
408 | + def process_submit_form(self, request, page0, form): |
---|
409 | + """ |
---|
410 | + Called when a form is POSTed. This is only called if the form data is |
---|
411 | + valid. If this method returns True, the done() method is called, |
---|
412 | + otherwise the wizard continues. Note that it is possible that this |
---|
413 | + method would not return True, and done() would still be called because |
---|
414 | + there are no more forms left in the form_list. |
---|
415 | + """ |
---|
416 | + |
---|
417 | + def get_template(self, page0): |
---|
418 | + """ |
---|
419 | + Hook for specifying the name of the template to use for a given page. |
---|
420 | + Note that this can return a tuple of template names if you'd like to |
---|
421 | + use the template system's select_template() hook. |
---|
422 | + """ |
---|
423 | + return 'forms/session_wizard.html' |
---|
424 | + |
---|
425 | + def done(self, request): |
---|
426 | + """ |
---|
427 | + Hook for doing something with the validated data. This is responsible |
---|
428 | + for the final processing including clearing the session scope of items |
---|
429 | + created by this wizard. |
---|
430 | + """ |
---|
431 | + raise NotImplementedError("Your %s class has not defined a done() " + \ |
---|
432 | + "method, which is required." \ |
---|
433 | + % self.__class__.__name__) |
---|