1 | Index: django-trunk/django/contrib/formtools/wizard.py |
---|
2 | =================================================================== |
---|
3 | --- django-trunk/django/contrib/formtools/wizard.py (revision 9084) |
---|
4 | +++ django-trunk/django/contrib/formtools/wizard.py (working copy) |
---|
5 | @@ -1,21 +1,23 @@ |
---|
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 | +from django.utils.datastructures import SortedDict |
---|
24 | |
---|
25 | + |
---|
26 | class FormWizard(object): |
---|
27 | + """ |
---|
28 | + FormWizard class -- implements a multi-page form, validating between each |
---|
29 | + step and storing the form's state as HTML hidden fields so that no state is |
---|
30 | + stored on the server side. |
---|
31 | + """ |
---|
32 | # Dictionary of extra template context variables. |
---|
33 | extra_context = {} |
---|
34 | |
---|
35 | @@ -239,3 +241,423 @@ |
---|
36 | data. |
---|
37 | """ |
---|
38 | raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) |
---|
39 | + |
---|
40 | + |
---|
41 | +class SessionWizard(object): |
---|
42 | + """ |
---|
43 | + SessionWizard class -- implements multi-page forms with the following |
---|
44 | + characteristics: |
---|
45 | + |
---|
46 | + 1) easily supports navigation to arbitrary pages in the wizard |
---|
47 | + 2) uses GETs to display forms (caveat validation errors) and POSTs for |
---|
48 | + form submissions |
---|
49 | + |
---|
50 | + Pros are support for back-button and arbitrary navigation within pages |
---|
51 | + (including the oddity of someone clicking on the refresh button) |
---|
52 | + |
---|
53 | + The major Con is use of the session scope. In particular, zero |
---|
54 | + consideration has been given to multipart form data. |
---|
55 | + """ |
---|
56 | + |
---|
57 | + # keys used to store wizard data in sessions |
---|
58 | + __form_classes_key = 'form_classes' |
---|
59 | + __cleaned_data_key = 'cleaned_data' |
---|
60 | + __POST_data_key = 'POST_data' |
---|
61 | + __pages_key = 'pages' |
---|
62 | + |
---|
63 | + def __init__(self, forms): |
---|
64 | + """ |
---|
65 | + A form_classes can be a list of form classes or a list of 2-Tuples in |
---|
66 | + the form (page_key, form_class). |
---|
67 | + """ |
---|
68 | + self.base_forms = SortedDict() |
---|
69 | + if forms: |
---|
70 | + if type(forms[0]) == tuple: |
---|
71 | + self.contains_named_pages = True |
---|
72 | + for page_key, form_class in forms: |
---|
73 | + self.base_forms[page_key] = form_class |
---|
74 | + else: |
---|
75 | + i = 0 |
---|
76 | + for form_class in forms: |
---|
77 | + self.base_forms[str(i)] = form_class |
---|
78 | + i = i + 1 |
---|
79 | + |
---|
80 | + def _init_wizard_data(self, request): |
---|
81 | + """ |
---|
82 | + Copy self.base_forms to the session scope so that subclasses can |
---|
83 | + manipulate the form_classes for individual users and initialize |
---|
84 | + the pages dict. |
---|
85 | + """ |
---|
86 | + session_key = self.get_wizard_data_key(request) |
---|
87 | + if session_key not in request.session: |
---|
88 | + pages = {} |
---|
89 | + for page_key in self.base_forms.keys(): |
---|
90 | + pages[page_key] = { |
---|
91 | + 'valid' : False, |
---|
92 | + 'visited' : False, |
---|
93 | + 'title' : self.get_page_title(request.session, page_key) |
---|
94 | + } |
---|
95 | + request.session[session_key] = { |
---|
96 | + self.__form_classes_key : self.base_forms.copy(), |
---|
97 | + self.__cleaned_data_key : {}, |
---|
98 | + self.__POST_data_key : {}, |
---|
99 | + self.__pages_key : pages, |
---|
100 | + } |
---|
101 | + |
---|
102 | + def __call__(self, request, *args, **kwargs): |
---|
103 | + """ |
---|
104 | + Initialize the form_classes for a session if needed and call GET or |
---|
105 | + POST depending on the provided request's method. |
---|
106 | + """ |
---|
107 | + self._init_wizard_data(request) |
---|
108 | + |
---|
109 | + if request.method == 'POST': |
---|
110 | + return self.POST(request) |
---|
111 | + else: |
---|
112 | + return self.GET(request, kwargs['page_key']) |
---|
113 | + |
---|
114 | + def GET(self, request, page_key): |
---|
115 | + """ |
---|
116 | + Initialize a form if necessary, and display the form/page identified by |
---|
117 | + page_key. |
---|
118 | + """ |
---|
119 | + page_data = self._get_cleaned_data(request.session, page_key) |
---|
120 | + if page_data is None: |
---|
121 | + form = self._get_form_classes(request.session)[page_key]() |
---|
122 | + else: |
---|
123 | + form_class = self._get_form_classes(request.session)[page_key] |
---|
124 | + if issubclass(form_class, forms.ModelForm): |
---|
125 | + form = form_class(instance=form_class.Meta.model(**page_data)) |
---|
126 | + else: |
---|
127 | + form = form_class(initial=page_data) |
---|
128 | + |
---|
129 | + return self._show_form(request, page_key, form) |
---|
130 | + |
---|
131 | + def POST(self, request): |
---|
132 | + """ |
---|
133 | + Validate form submission, and redirect to GET the next form or return |
---|
134 | + the response from self.done(). Note that the request.POST data must |
---|
135 | + contain a value for the key 'page_key', and this value must reference |
---|
136 | + a form in the form_classes collection for this wizard. |
---|
137 | + """ |
---|
138 | + form_classes = self._get_form_classes(request.session) |
---|
139 | + page_key = request.POST['page_key'] |
---|
140 | + page0 = form_classes.keys().index(page_key) |
---|
141 | + URL_base = self.get_URL_base(request, page_key) |
---|
142 | + self._set_POST_data(request.session, page_key, request.POST) |
---|
143 | + form = form_classes[page_key](request.POST) |
---|
144 | + new_page_key = self.preprocess_submit_form(request, page_key, form) |
---|
145 | + |
---|
146 | + if new_page_key is not None: |
---|
147 | + return HttpResponseRedirect(URL_base + new_page_key) |
---|
148 | + else: |
---|
149 | + if form.is_valid(): |
---|
150 | + self._set_cleaned_data(request.session, page_key, |
---|
151 | + form.cleaned_data) |
---|
152 | + self._set_page(request.session, page_key, True, True) |
---|
153 | + is_done = self.process_submit_form(request, page_key, form) |
---|
154 | + if (is_done is None or is_done == False) and \ |
---|
155 | + len(form_classes) > page0 + 1: |
---|
156 | + return HttpResponseRedirect(URL_base + |
---|
157 | + self._get_next_page_key(request.session, page_key)) |
---|
158 | + else: |
---|
159 | + first_broken_page, form = \ |
---|
160 | + self._validate_all_forms(request.session) |
---|
161 | + if first_broken_page is not None: |
---|
162 | + return self._show_form(request, first_broken_page, |
---|
163 | + form) |
---|
164 | + else: |
---|
165 | + return self.done(request) |
---|
166 | + else: |
---|
167 | + self._set_page(request.session, page_key, False) |
---|
168 | + |
---|
169 | + return self._show_form(request, page_key, form) |
---|
170 | + |
---|
171 | + |
---|
172 | + # form util methods # |
---|
173 | + def _validate_all_forms(self, session): |
---|
174 | + """ |
---|
175 | + Iterate through the session form list and validate based on 1) the |
---|
176 | + 'valid' attribute of the page data and 2) the POST data stored in the |
---|
177 | + session for this wizard. Return the page key and the form of the first |
---|
178 | + invalid form or None, None if all forms are valid. |
---|
179 | + """ |
---|
180 | + for page_key, form_class in self._get_form_classes(session).iteritems(): |
---|
181 | + if not self._get_pages(session)[page_key]['valid']: |
---|
182 | + form = form_class(self._get_POST_data(session, page_key)) |
---|
183 | + if not form.is_valid(): |
---|
184 | + return page_key, form |
---|
185 | + return None, None |
---|
186 | + |
---|
187 | + def _show_form(self, request, page_key, form): |
---|
188 | + """ |
---|
189 | + Show the form associated with indicated page index. |
---|
190 | + """ |
---|
191 | + URL_base = self.get_URL_base(request, page_key) |
---|
192 | + extra_context = self.process_show_form(request, page_key, form) |
---|
193 | + self._set_current_page(request.session, page_key) |
---|
194 | + pages = self._get_pages(request.session) |
---|
195 | + context = {'page_key' : page_key, |
---|
196 | + 'form' : form, |
---|
197 | + 'pages' : pages, |
---|
198 | + 'URL_base' : URL_base, |
---|
199 | + 'extra_context' : extra_context } |
---|
200 | + return render_to_response(self.get_template(page_key), context, |
---|
201 | + RequestContext(request)) |
---|
202 | + |
---|
203 | + def _get_form_classes(self, session): |
---|
204 | + """ |
---|
205 | + Return the collection of form classes stored in the provided session. |
---|
206 | + """ |
---|
207 | + return session[self.get_wizard_data_key(session)][self.__form_classes_key] |
---|
208 | + |
---|
209 | + def _insert_form(self, session, index, page_key, form_class): |
---|
210 | + """ |
---|
211 | + Insert a form class into the provided session's form list at the |
---|
212 | + provided index. |
---|
213 | + """ |
---|
214 | + form_classes = self._get_form_classes(session) |
---|
215 | + form_classes.insert(index, page_key, form_class) |
---|
216 | + self._insert_wizard_data(session, self.__form_classes_key, form_classes) |
---|
217 | + |
---|
218 | + def _remove_form(self, session, page_key): |
---|
219 | + """ |
---|
220 | + Remove the form at index page_key from the provided sessions form list. |
---|
221 | + """ |
---|
222 | + self._del_wizard_data(session, self.__form_classes_key, page_key) |
---|
223 | + # end form util methods # |
---|
224 | + |
---|
225 | + |
---|
226 | + # Form data methods # |
---|
227 | + def _get_POST_data(self, session, page_key): |
---|
228 | + """ |
---|
229 | + Return the POST data for a page_key stored in the provided session. |
---|
230 | + """ |
---|
231 | + post_data = self._get_all_POST_data(session) |
---|
232 | + if page_key in post_data: |
---|
233 | + return post_data[page_key] |
---|
234 | + else: |
---|
235 | + return {} |
---|
236 | + |
---|
237 | + def _set_POST_data(self, session, page_key, data): |
---|
238 | + """ |
---|
239 | + Set the POST data for a given page_key and session to the 'data' |
---|
240 | + provided. |
---|
241 | + """ |
---|
242 | + post_data = self._get_all_POST_data(session) |
---|
243 | + post_data[page_key] = data |
---|
244 | + self._insert_wizard_data(session, self.__POST_data_key, post_data) |
---|
245 | + |
---|
246 | + def _remove_POST_data(self, session, page_key): |
---|
247 | + """ |
---|
248 | + Remove the POST data stored in the session at index page_key. |
---|
249 | + """ |
---|
250 | + self._del_wizard_data(session, self.__POST_data_key, page_key) |
---|
251 | + |
---|
252 | + def _get_all_POST_data(self, session): |
---|
253 | + """ |
---|
254 | + Return the dict of all POST data for this wizard from the provided |
---|
255 | + session. |
---|
256 | + """ |
---|
257 | + return session[self.get_wizard_data_key(session)][self.__POST_data_key] |
---|
258 | + |
---|
259 | + def _get_cleaned_data(self, session, page_key): |
---|
260 | + """ |
---|
261 | + Return the cleaned data from the provided session for this wizard based |
---|
262 | + on the provided page_key. |
---|
263 | + """ |
---|
264 | + cleaned_data = self._get_all_cleaned_data(session) |
---|
265 | + if page_key in cleaned_data: |
---|
266 | + return cleaned_data[page_key] |
---|
267 | + else: |
---|
268 | + return {} |
---|
269 | + |
---|
270 | + def _set_cleaned_data(self, session, page_key, data): |
---|
271 | + """ |
---|
272 | + Assign the cleaned data for this wizard in the session at index |
---|
273 | + page_key. |
---|
274 | + """ |
---|
275 | + cleaned_data = self._get_all_cleaned_data(session) |
---|
276 | + cleaned_data[page_key] = data |
---|
277 | + self._insert_wizard_data(session, self.__cleaned_data_key, cleaned_data) |
---|
278 | + |
---|
279 | + def _get_all_cleaned_data(self, session): |
---|
280 | + """ |
---|
281 | + Return a list of all the cleaned data in the session for this wizard. |
---|
282 | + """ |
---|
283 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
284 | + return wizard_data[self.__cleaned_data_key] |
---|
285 | + |
---|
286 | + def _remove_cleaned_data(self, session, page_key): |
---|
287 | + """ |
---|
288 | + Remove the cleaned data at index page_key for this wizard from the |
---|
289 | + provided session. |
---|
290 | + """ |
---|
291 | + self._del_wizard_data(session, self.__cleaned_data_key, page_key) |
---|
292 | + # end Form data methods # |
---|
293 | + |
---|
294 | + |
---|
295 | + # page methods # |
---|
296 | + def _get_next_page_key(self, session, page_key): |
---|
297 | + """ |
---|
298 | + Return the next page_key after the provided page_key in the sequence of |
---|
299 | + pages. If this is a named pages wizard, this method iterates |
---|
300 | + through keys. Otherwise it will simply iterate the page_key. |
---|
301 | + This method must return a String. |
---|
302 | + """ |
---|
303 | + form_classes_keys = self._get_form_classes(session).keys() |
---|
304 | + return form_classes_keys[form_classes_keys.index(page_key) + 1] |
---|
305 | + |
---|
306 | + def _set_current_page(self, session, page_key): |
---|
307 | + """ |
---|
308 | + Iterate through the page dicts in the session and set 'current_page' to |
---|
309 | + True for the page corresponding to page_key and False for all others. |
---|
310 | + """ |
---|
311 | + for key, page in self._get_pages(session).iteritems(): |
---|
312 | + if key == page_key: |
---|
313 | + page['current_page'] = True |
---|
314 | + else: |
---|
315 | + page['current_page'] = False |
---|
316 | + |
---|
317 | + def _get_pages(self, session): |
---|
318 | + """ |
---|
319 | + Return the list of page info dicts stored in the provided session for |
---|
320 | + this wizard. |
---|
321 | + """ |
---|
322 | + return session[self.get_wizard_data_key(session)][self.__pages_key] |
---|
323 | + |
---|
324 | + def _remove_page_data(self, session, page_key): |
---|
325 | + """ |
---|
326 | + Remove page data from the provided session for this wizard based on a |
---|
327 | + given page_key. This removes page information, form_class and form |
---|
328 | + data. |
---|
329 | + """ |
---|
330 | + self._remove_form(session, page_key) |
---|
331 | + self._remove_page(session, page_key) |
---|
332 | + self._remove_cleaned_data(session, page_key) |
---|
333 | + self._remove_POST_data(session, page_key) |
---|
334 | + |
---|
335 | + def _remove_page(self, session, page_key): |
---|
336 | + """ |
---|
337 | + Remove the page info dict for this wizard stored at a given page_key |
---|
338 | + from the provided session. |
---|
339 | + """ |
---|
340 | + self._del_wizard_data(session, self.__pages_key, page_key) |
---|
341 | + |
---|
342 | + def _insert_page(self, session, index, page_key, form_class): |
---|
343 | + """ |
---|
344 | + Insert a page into this wizard at the provided form_class index, storing |
---|
345 | + required associated data. |
---|
346 | + """ |
---|
347 | + self._insert_form(session, index, page_key, form_class) |
---|
348 | + self._set_page(session, page_key, False) |
---|
349 | + self._set_cleaned_data(session, page_key, {}) |
---|
350 | + self._set_POST_data(session, page_key, {}) |
---|
351 | + |
---|
352 | + def _set_page(self, session, page_key, valid=False, visited=False): |
---|
353 | + """ |
---|
354 | + Set the page info in this wizard for a page at index page_key and stored |
---|
355 | + in the provided session. |
---|
356 | + """ |
---|
357 | + page_info = { |
---|
358 | + 'valid' : valid, |
---|
359 | + 'visited' : visited, |
---|
360 | + 'title' : self.get_page_title(session, page_key) |
---|
361 | + } |
---|
362 | + pages = self._get_pages(session) |
---|
363 | + pages[page_key] = page_info |
---|
364 | + self._insert_wizard_data(session, self.__pages_key, pages) |
---|
365 | + # end page methods # |
---|
366 | + |
---|
367 | + # start wizard data utils # |
---|
368 | + def _clear_wizard_data_from_session(self, session): |
---|
369 | + """ |
---|
370 | + Clear the session data used by this wizard from the provided session. |
---|
371 | + """ |
---|
372 | + del session[self.get_wizard_data_key(session)] |
---|
373 | + |
---|
374 | + def _insert_wizard_data(self, session, key, data): |
---|
375 | + """ |
---|
376 | + Inserts wizard data into the provided session at the provided key. |
---|
377 | + """ |
---|
378 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
379 | + wizard_data[key] = data |
---|
380 | + session[self.get_wizard_data_key(session)] = wizard_data |
---|
381 | + |
---|
382 | + def _del_wizard_data(self, session, key, page_key): |
---|
383 | + """ |
---|
384 | + Deletes wizard data from the provided session based on a page_key. |
---|
385 | + """ |
---|
386 | + wizard_data = session[self.get_wizard_data_key(session)] |
---|
387 | + sub_set = wizard_data[key] |
---|
388 | + if page_key in sub_set: |
---|
389 | + del sub_set[page_key] |
---|
390 | + wizard_data[key] = sub_set |
---|
391 | + session[self.get_wizard_data_key(session)] = wizard_data |
---|
392 | + |
---|
393 | + # end wizard data utils # |
---|
394 | + |
---|
395 | + # typically overriden methods # |
---|
396 | + def get_wizard_data_key(self, request): |
---|
397 | + """ |
---|
398 | + Return a session key for this wizard. The provided request could be |
---|
399 | + used to prevent overlapping keys in the case that someone needs |
---|
400 | + multiple instances of this wizard at one time. |
---|
401 | + """ |
---|
402 | + return 'session_wizard_data' |
---|
403 | + |
---|
404 | + def get_URL_base(self, request, page_key): |
---|
405 | + """ |
---|
406 | + Return the URL to this wizard minus the "page_key" part of the URL. |
---|
407 | + This value is passed to template as URL_base. |
---|
408 | + """ |
---|
409 | + return request.path.replace("/" + page_key, "/") |
---|
410 | + |
---|
411 | + def get_page_title(self, session, page_key): |
---|
412 | + """ |
---|
413 | + Return a user friendly title for the page at index page_key. |
---|
414 | + """ |
---|
415 | + if self.contains_named_pages: |
---|
416 | + return page_key.replace("_", " ").title() |
---|
417 | + else: |
---|
418 | + return 'Page %s' % str(int(page_key) + 1) |
---|
419 | + |
---|
420 | + def process_show_form(self, request, page_key, form): |
---|
421 | + """ |
---|
422 | + Called before rendering a form either from a GET or when a form submit |
---|
423 | + is invalid. |
---|
424 | + """ |
---|
425 | + |
---|
426 | + def preprocess_submit_form(self, request, page_key, form): |
---|
427 | + """ |
---|
428 | + Called when a form is POSTed, but before the form is validated. If this |
---|
429 | + function returns None then form submission continues, else it should |
---|
430 | + return a new page index that will be redirected to as a GET. |
---|
431 | + """ |
---|
432 | + |
---|
433 | + def process_submit_form(self, request, page_key, form): |
---|
434 | + """ |
---|
435 | + Called when a form is POSTed. This is only called if the form data is |
---|
436 | + valid. If this method returns True, the done() method is called, |
---|
437 | + otherwise the wizard continues. Note that it is possible that this |
---|
438 | + method would not return True, and done() would still be called because |
---|
439 | + there are no more forms left in the form_classes. |
---|
440 | + """ |
---|
441 | + |
---|
442 | + def get_template(self, page_key): |
---|
443 | + """ |
---|
444 | + Hook for specifying the name of the template to use for a given page. |
---|
445 | + Note that this can return a tuple of template names if you'd like to |
---|
446 | + use the template system's select_template() hook. |
---|
447 | + """ |
---|
448 | + return 'forms/session_wizard.html' |
---|
449 | + |
---|
450 | + def done(self, request): |
---|
451 | + """ |
---|
452 | + Hook for doing something with the validated data. This is responsible |
---|
453 | + for the final processing including clearing the session scope of items |
---|
454 | + created by this wizard. |
---|
455 | + """ |
---|
456 | + raise NotImplementedError("Your %s class has not defined a done() " + \ |
---|
457 | + "method, which is required." \ |
---|
458 | + % self.__class__.__name__) |
---|
459 | Index: django-trunk/django/contrib/formtools/tests.py |
---|
460 | =================================================================== |
---|
461 | --- django-trunk/django/contrib/formtools/tests.py (revision 9084) |
---|
462 | +++ django-trunk/django/contrib/formtools/tests.py (working copy) |
---|
463 | @@ -1,8 +1,11 @@ |
---|
464 | from django import forms |
---|
465 | +from django.db import models |
---|
466 | from django.contrib.formtools import preview, wizard |
---|
467 | from django import http |
---|
468 | from django.test import TestCase |
---|
469 | +from django.test.client import Client |
---|
470 | |
---|
471 | + |
---|
472 | success_string = "Done was called!" |
---|
473 | |
---|
474 | class TestFormPreview(preview.FormPreview): |
---|
475 | @@ -141,4 +144,168 @@ |
---|
476 | request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"}) |
---|
477 | response = wizard(request) |
---|
478 | self.assertEquals(1, wizard.step) |
---|
479 | + |
---|
480 | |
---|
481 | +# |
---|
482 | +# SessionWizard tests |
---|
483 | +# |
---|
484 | +class SessionWizardModel(models.Model): |
---|
485 | + field = models.CharField(max_length=10) |
---|
486 | + |
---|
487 | +class SessionWizardPageOneForm(forms.Form): |
---|
488 | + field = forms.CharField(required=True) |
---|
489 | + |
---|
490 | +class SessionWizardPageTwoForm(forms.ModelForm): |
---|
491 | + class Meta: |
---|
492 | + model = SessionWizardModel |
---|
493 | + |
---|
494 | +class SessionWizardPageThreeForm(forms.Form): |
---|
495 | + field = forms.CharField() |
---|
496 | + |
---|
497 | +class SessionWizardDynamicPageForm(forms.Form): |
---|
498 | + field = forms.CharField() |
---|
499 | + |
---|
500 | +class SessionWizardClass(wizard.SessionWizard): |
---|
501 | + def get_page_title(self, session, page_key): |
---|
502 | + try: |
---|
503 | + return "Custom Page Title: %s" % str(int(page_key) + 1) |
---|
504 | + except ValueError: |
---|
505 | + return super(SessionWizardClass, self).get_page_title(session, |
---|
506 | + page_key) |
---|
507 | + |
---|
508 | + def process_show_form(self, request, page_key, form): |
---|
509 | + try: |
---|
510 | + return {'form_title' : 'Form %s' % str(int(page_key) + 1)} |
---|
511 | + except ValueError: |
---|
512 | + return super(SessionWizardClass, self).process_show_form(request, |
---|
513 | + page_key, form) |
---|
514 | + |
---|
515 | + def preprocess_submit_form(self, request, page_key, form): |
---|
516 | + if page_key == "1" and request.POST['field'] == "": |
---|
517 | + self._remove_page(request.session, page_key) |
---|
518 | + return str(int(page_key) - 1) |
---|
519 | + |
---|
520 | + def process_submit_form(self, request, page_key, form): |
---|
521 | + if page_key == '2': |
---|
522 | + self._insert_page(request.session, 3, str(int(page_key) + 1), |
---|
523 | + SessionWizardDynamicPageForm) |
---|
524 | + |
---|
525 | + def get_template(self, page_key): |
---|
526 | + return "formtools/form.html" |
---|
527 | + |
---|
528 | + def done(self, request): |
---|
529 | + return http.HttpResponse(success_string) |
---|
530 | + |
---|
531 | +class SessionWizardTests(TestCase): |
---|
532 | + urls = 'django.contrib.formtools.test_urls' |
---|
533 | + |
---|
534 | + def test_named_pages_wizard_get(self): |
---|
535 | + """ |
---|
536 | + Tests that a wizard is created properly based on it's initialization |
---|
537 | + argument, which could be a sequence or dictionary-like object. |
---|
538 | + """ |
---|
539 | + response = self.client.get('/named_pages_wizard/first_page_form') |
---|
540 | + self.assertEquals(200, response.status_code) |
---|
541 | + self.assertEquals('First Page Form', |
---|
542 | + response.context[0]['pages']['first_page_form']['title']) |
---|
543 | + |
---|
544 | + |
---|
545 | + def test_valid_POST(self): |
---|
546 | + """ |
---|
547 | + Tests that a post containing valid data will set session values |
---|
548 | + correctly and redirect to the next page. |
---|
549 | + """ |
---|
550 | + response = self.client.post('/sessionwizard/', {"page_key":"0", |
---|
551 | + "field":"test"}) |
---|
552 | + self.assertEquals(302, response.status_code) |
---|
553 | + self.assertEquals("http://testserver/sessionwizard/1", |
---|
554 | + response['Location']) |
---|
555 | + session = self.client.session |
---|
556 | + cleaned_data = session['session_wizard_data']['cleaned_data'] |
---|
557 | + post_data = session['session_wizard_data']['POST_data'] |
---|
558 | + self.assertEquals('test', cleaned_data['0']['field']) |
---|
559 | + self.assertEquals('test', post_data['0']['field']) |
---|
560 | + |
---|
561 | + def test_invalid_POST(self): |
---|
562 | + """ |
---|
563 | + Tests that a post containing invalid data will set session values |
---|
564 | + correctly and redisplay the form. |
---|
565 | + """ |
---|
566 | + response = self.client.post('/sessionwizard/', {"page_key":"0", |
---|
567 | + "field":""}) |
---|
568 | + self.assertEquals(200, response.status_code) |
---|
569 | + session = self.client.session |
---|
570 | + post_data = session['session_wizard_data']['POST_data'] |
---|
571 | + self.assertEquals('', post_data['0']['field']) |
---|
572 | + |
---|
573 | + def test_GET(self): |
---|
574 | + """ |
---|
575 | + Tests that a get will display a page properly. |
---|
576 | + """ |
---|
577 | + response = self.client.get('/sessionwizard/0') |
---|
578 | + self.assertEquals(200, response.status_code) |
---|
579 | + |
---|
580 | + def test_preprocess_submit_form(self): |
---|
581 | + """ |
---|
582 | + Tests the preprocess_submit_form hook of SessionWizard POSTs. |
---|
583 | + The SessionWizardClass is coded to short-circuit a POST for page index 1 |
---|
584 | + when form.cleaned_data['field'] == '' by returning a reference to page_key |
---|
585 | + index 0. |
---|
586 | + """ |
---|
587 | + response = self.client.post('/sessionwizard/', {"page_key":"1", |
---|
588 | + "field":""}) |
---|
589 | + self.assertEquals(302, response.status_code) |
---|
590 | + self.assertEquals("http://testserver/sessionwizard/0", |
---|
591 | + response['Location']) |
---|
592 | + |
---|
593 | + def test_process_submit_form(self): |
---|
594 | + """ |
---|
595 | + Tests the process_submit_form hook of SessionWizard POSTs. |
---|
596 | + The SessionWizardClass is coded to insert a new page at index 3 on a |
---|
597 | + POST for page index 2. |
---|
598 | + """ |
---|
599 | + response = self.client.post('/sessionwizard/', {"page_key":"2", |
---|
600 | + "field":"test"}) |
---|
601 | + self.assertEquals(302, response.status_code) |
---|
602 | + self.assertEquals("http://testserver/sessionwizard/3", |
---|
603 | + response['Location']) |
---|
604 | + self.assertEquals({"0":SessionWizardPageOneForm, |
---|
605 | + "1":SessionWizardPageTwoForm, |
---|
606 | + "2":SessionWizardPageThreeForm, |
---|
607 | + "3":SessionWizardDynamicPageForm}, |
---|
608 | + self.client.session['session_wizard_data']['form_classes']) |
---|
609 | + |
---|
610 | + def test_process_show_form(self): |
---|
611 | + """ |
---|
612 | + Tests the process_show_form hook. SessionWizardClass is coded to |
---|
613 | + return a extra_context having a specific 'form_title' attribute. |
---|
614 | + """ |
---|
615 | + response = self.client.get('/sessionwizard/0') |
---|
616 | + self.assertEquals(200, response.status_code) |
---|
617 | + self.assertEquals("Form 1", |
---|
618 | + response.context[0]['extra_context']['form_title']) |
---|
619 | + |
---|
620 | + def test_validate_all(self): |
---|
621 | + """ |
---|
622 | + Submits all forms, with one of them being invalid, and tests that |
---|
623 | + submitting the last form will catch an invalid form earlier in the |
---|
624 | + workflow and redisplay it. |
---|
625 | + """ |
---|
626 | + response = self.client.post('/sessionwizard/', {"page_key":"0", "field":""}) |
---|
627 | + self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"}) |
---|
628 | + self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"}) |
---|
629 | + response = self.client.post('/sessionwizard/3', |
---|
630 | + {"page_key":"3", "field":"test3"}) |
---|
631 | + self.assertEquals(True, response.context[0]['pages']['1']['visited']) |
---|
632 | + self.assertEquals(True, response.context[0]['pages']['1']['valid']) |
---|
633 | + |
---|
634 | + self.assertEquals("Form 1", |
---|
635 | + response.context[0]['extra_context']['form_title']) |
---|
636 | + |
---|
637 | + def test_done(self): |
---|
638 | + self.client.post('/sessionwizard/', {"page_key":"0", "field":"test0"}) |
---|
639 | + self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"}) |
---|
640 | + self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"}) |
---|
641 | + response = self.client.post('/sessionwizard/', |
---|
642 | + {"page_key":"3", "field":"test3"}) |
---|
643 | + self.assertEqual(response.content, success_string) |
---|
644 | Index: django-trunk/django/contrib/formtools/test_urls.py |
---|
645 | =================================================================== |
---|
646 | --- django-trunk/django/contrib/formtools/test_urls.py (revision 9084) |
---|
647 | +++ django-trunk/django/contrib/formtools/test_urls.py (working copy) |
---|
648 | @@ -9,4 +9,13 @@ |
---|
649 | |
---|
650 | urlpatterns = patterns('', |
---|
651 | (r'^test1/', TestFormPreview(TestForm)), |
---|
652 | + (r'^sessionwizard/(?P<page_key>\d*)$', |
---|
653 | + SessionWizardClass([SessionWizardPageOneForm, |
---|
654 | + SessionWizardPageTwoForm, |
---|
655 | + SessionWizardPageThreeForm])), |
---|
656 | + (r'^named_pages_wizard/(?P<page_key>\w*)$', |
---|
657 | + SessionWizardClass(( |
---|
658 | + ('first_page_form', SessionWizardPageOneForm), |
---|
659 | + ('page2', SessionWizardPageTwoForm), |
---|
660 | + ('page3', SessionWizardPageThreeForm),))) |
---|
661 | ) |
---|
662 | Index: django-trunk/docs/ref/contrib/formtools/session-wizard.txt |
---|
663 | =================================================================== |
---|
664 | --- django-trunk/docs/ref/contrib/formtools/session-wizard.txt (revision 0) |
---|
665 | +++ django-trunk/docs/ref/contrib/formtools/session-wizard.txt (revision 0) |
---|
666 | @@ -0,0 +1,345 @@ |
---|
667 | +.. _ref-contrib-formtools-session-wizard: |
---|
668 | + |
---|
669 | +============== |
---|
670 | +Session wizard |
---|
671 | +============== |
---|
672 | + |
---|
673 | +.. module:: django.contrib.formtools.wizard |
---|
674 | + :synopsis: Splits forms across multiple Web pages using users' sessions to store form and page data. |
---|
675 | + |
---|
676 | +.. versionadded:: 1.x |
---|
677 | + |
---|
678 | +Django comes with an optional "session wizard" application that splits |
---|
679 | +:ref:`forms <topics-forms-index>` across multiple Web pages. It maintains |
---|
680 | +state in users' sessions incurring additional resource costs on a server |
---|
681 | +but also creating a smoother workflow for users. |
---|
682 | + |
---|
683 | +Note that SessionWizard is similar to :ref:`FormWizard <ref-contrib-formtools-form-wizard>` |
---|
684 | +and some of these examples and documentation mirror FormWizard examples and |
---|
685 | +documentation exactly. |
---|
686 | + |
---|
687 | +You might want to use this if you have a workflow or lengthy form and want to |
---|
688 | +provide navigation to various pages in the wizard. |
---|
689 | + |
---|
690 | +The term "wizard," in this context, is `explained on Wikipedia`_. |
---|
691 | + |
---|
692 | +.. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29 |
---|
693 | +.. _forms: ../forms/ |
---|
694 | + |
---|
695 | +How it works |
---|
696 | +============ |
---|
697 | + |
---|
698 | +Here's the basic workflow for how a user would use a wizard: |
---|
699 | + |
---|
700 | + 1. The user visits the first page of the wizard, fills in the form and |
---|
701 | + submits it. |
---|
702 | + 2. The server validates the data. If it's invalid, the form is displayed |
---|
703 | + again, with error messages. If it's valid, the server stores this data |
---|
704 | + in a user's session and sends an HTTP redirect to GET the next page. |
---|
705 | + 3. Step 1 and 2 repeat, for every subsequent form in the wizard. |
---|
706 | + 4. Once the user has submitted all the forms and all the data has been |
---|
707 | + validated, the wizard processes the data -- saving it to the database, |
---|
708 | + sending an e-mail, or whatever the application needs to do. |
---|
709 | + |
---|
710 | +Usage |
---|
711 | +===== |
---|
712 | + |
---|
713 | +This application handles as much machinery for you as possible. Generally, you |
---|
714 | +just have to do these things: |
---|
715 | + |
---|
716 | + 1. Define a number of :mod:`django.forms` |
---|
717 | + :class:`~django.forms.forms.Form` classes -- one per wizard page. |
---|
718 | + |
---|
719 | + 2. Create a :class:`~django.contrib.formtools.wizard.SessionWizard` class |
---|
720 | + that specifies what to do once all of your forms have been submitted |
---|
721 | + and validated. This also lets you override some of the wizard's behavior. |
---|
722 | + |
---|
723 | + 3. Create some templates that render the forms. You can define a single, |
---|
724 | + generic template to handle every one of the forms, or you can define a |
---|
725 | + specific template for each form. |
---|
726 | + |
---|
727 | + 4. Point your URLconf at your |
---|
728 | + :class:`~django.contrib.formtools.wizard.SessionWizard` class. |
---|
729 | + |
---|
730 | +Defining ``Form`` classes |
---|
731 | +========================= |
---|
732 | + |
---|
733 | +The first step in creating a form wizard is to create the |
---|
734 | +:class:`~django.forms.forms.Form` classes. These should be standard |
---|
735 | +:mod:`django.forms` :class:`~django.forms.forms.Form` classes, covered in the |
---|
736 | +:ref:`forms documentation <topics-forms-index>`. |
---|
737 | + |
---|
738 | +These classes can live anywhere in your codebase, but convention is to put them |
---|
739 | +in a file called :file:`forms.py` in your application. |
---|
740 | + |
---|
741 | +For example, let's write a "contact form" wizard, where the first page's form |
---|
742 | +collects the sender's e-mail address and subject, and the second page collects |
---|
743 | +the message itself. Here's what the :file:`forms.py` might look like:: |
---|
744 | + |
---|
745 | + from django import forms |
---|
746 | + |
---|
747 | + class ContactForm1(forms.Form): |
---|
748 | + subject = forms.CharField(max_length=100) |
---|
749 | + sender = forms.EmailField() |
---|
750 | + |
---|
751 | + class ContactForm2(forms.Form): |
---|
752 | + message = forms.CharField(widget=forms.Textarea) |
---|
753 | + |
---|
754 | +**Important limitation:** Because the wizard uses users' sessions to store |
---|
755 | +data between pages, you should seriously consider whether or not it |
---|
756 | +makes sense to include :class:`~django.forms.fields.FileField` in any forms. |
---|
757 | + |
---|
758 | +Creating a ``SessionWizard`` class |
---|
759 | +================================== |
---|
760 | + |
---|
761 | +The next step is to create a :class:`~django.contrib.formtools.wizard.SessionWizard` |
---|
762 | +class, which should be a subclass of ``django.contrib.formtools.wizard.SessionWizard``. |
---|
763 | + |
---|
764 | +As your :class:`~django.forms.forms.Form` classes, this |
---|
765 | +:class:`~django.contrib.formtools.wizard.SessionWizard` class can live anywhere |
---|
766 | +in your codebase, but convention is to put it in :file:`forms.py`. |
---|
767 | + |
---|
768 | +The only requirement on this subclass is that it implement a |
---|
769 | +:meth:`~django.contrib.formtools.wizard.SessionWizard.done()` method, |
---|
770 | +which specifies what should happen when the data for *every* form is submitted |
---|
771 | +and validated. This method is passed one argument: |
---|
772 | + |
---|
773 | + * ``request`` -- an :class:`~django.http.HttpRequest` object |
---|
774 | + |
---|
775 | +In this simplistic example, rather than perform any database operation, the |
---|
776 | +method simply renders a template of the validated data:: |
---|
777 | + |
---|
778 | + from django.shortcuts import render_to_response |
---|
779 | + from django.contrib.formtools.wizard import SessionWizard |
---|
780 | + |
---|
781 | + class ContactWizard(SessionWizard): |
---|
782 | + def done(self, request): |
---|
783 | + form_data = self._get_all_cleaned_data(request.session) |
---|
784 | + self._clear_wizard_data_from_session(request.session) |
---|
785 | + return render_to_response('done.html', { |
---|
786 | + 'form_data': form_data, |
---|
787 | + }) |
---|
788 | + |
---|
789 | +Note that this method will be called via ``POST``, so it really ought to be a |
---|
790 | +good Web citizen and redirect after processing the data. Here's another |
---|
791 | +example:: |
---|
792 | + |
---|
793 | + from django.http import HttpResponseRedirect |
---|
794 | + from django.contrib.formtools.wizard import SessionWizard |
---|
795 | + |
---|
796 | + class ContactWizard(SessionWizard): |
---|
797 | + def done(self, request): |
---|
798 | + form_data = self._get_all_cleaned_data(request.session) |
---|
799 | + self._clear_wizard_data_from_session(request.session) |
---|
800 | + do_something_with_the_form_data(form_data) |
---|
801 | + return HttpResponseRedirect('/page-to-redirect-to-when-done/') |
---|
802 | + |
---|
803 | +See the section `Advanced SessionWizard methods`_ below to learn about more |
---|
804 | +:class:`~django.contrib.formtools.wizard.SessionWizard` hooks. |
---|
805 | + |
---|
806 | +Creating templates for the forms |
---|
807 | +================================ |
---|
808 | + |
---|
809 | +Next, you'll need to create a template that renders the wizard's forms. By |
---|
810 | +default, every form uses a template called :file:`forms/session_wizard.html`. |
---|
811 | +(You can change this template name by overriding |
---|
812 | +:meth:`~django.contrib.formtools.wizard..get_template()`, which is documented |
---|
813 | +below. This hook also allows you to use a different template for each form.) |
---|
814 | + |
---|
815 | +This template expects the following context: |
---|
816 | + |
---|
817 | + * ``page_key`` -- A string representation of the current page in this |
---|
818 | + wizard. Depending on how a wizard is created, this could be a page name |
---|
819 | + or a zero-based page index. |
---|
820 | + * ``form`` -- The :class:`~django.forms.forms.Form` instance for the |
---|
821 | + current page (empty, populated or populated with errors). |
---|
822 | + * ``pages`` -- The current list of pages for this wizard. This is a dict of |
---|
823 | + dict objects in the form:: |
---|
824 | + |
---|
825 | + {'page_key1' : {'title' : 'page1', |
---|
826 | + 'visited': True, |
---|
827 | + 'valid' : True, |
---|
828 | + 'current_page' : False |
---|
829 | + }, |
---|
830 | + 'page_key2' : {'title' : 'page2', |
---|
831 | + 'visited': False, |
---|
832 | + 'valid' : False, |
---|
833 | + 'current_page' : True |
---|
834 | + }, |
---|
835 | + .. |
---|
836 | + } |
---|
837 | + * ``URL_base`` -- The base URL used to generate links to pages in this |
---|
838 | + wizard. By default, it is the request.path value minus the ``page_key``. |
---|
839 | + * 'extra_context' -- A dict returned from the |
---|
840 | + :meth:`~django.contrib.formtools.wizard.SessionWizard.process_show_form()` |
---|
841 | + hook. |
---|
842 | + |
---|
843 | +Here's a full example template: |
---|
844 | + |
---|
845 | +.. code-block:: html+django |
---|
846 | + |
---|
847 | + {% extends "base.html" %} |
---|
848 | + |
---|
849 | + {% block content %} |
---|
850 | + <ul> |
---|
851 | + {% for page_key,page in pages.items %} |
---|
852 | + <li class="{% if page.valid %}valid{% endif %} |
---|
853 | + {% if page.current_page %}current{% endif %}"> |
---|
854 | + {% if page.visited %}visited{% endif %} |
---|
855 | + <a href="{{ URL_base }}{{ page_key }}">{{ page.title }}</a> |
---|
856 | + {% else %} |
---|
857 | + {{ page.title }} |
---|
858 | + {% end if %} |
---|
859 | + </li> |
---|
860 | + {% endfor %} |
---|
861 | + </ul> |
---|
862 | + <form action="." method="post"> |
---|
863 | + <table> |
---|
864 | + {{ form }} |
---|
865 | + </table> |
---|
866 | + <input type="hidden" name="page_key" value="{{ page_key }}"/> |
---|
867 | + <input type="submit"> |
---|
868 | + </form> |
---|
869 | + {% endblock %} |
---|
870 | + |
---|
871 | +Note that ``page_key`` is required for the wizard to work properly. |
---|
872 | + |
---|
873 | +Hooking the wizard into a URLconf |
---|
874 | +================================= |
---|
875 | + |
---|
876 | +Finally, give your new :class:`~django.contrib.formtools.wizard.SessionWizard` |
---|
877 | +object a URL in ``urls.py``. The wizard has two types of initialization. The |
---|
878 | +first takes a list of your form objects as arguments, and the seconds takes a |
---|
879 | +sequence of 2-tuples in the form (page_key, form_class). The two types are |
---|
880 | +illustrated below:: |
---|
881 | + |
---|
882 | + from django.conf.urls.defaults import * |
---|
883 | + from mysite.testapp.forms import ContactForm1, ContactForm2, ContactWizard |
---|
884 | + |
---|
885 | + urlpatterns = patterns('', |
---|
886 | + ## First form - a list of form classes |
---|
887 | + (r'^contact/(?P<page_key>\d*)$', ContactWizard([ContactForm1, ContactForm2])), |
---|
888 | + |
---|
889 | + ## Second form - a sequence of 2-tuples |
---|
890 | + (r'^contact/(?P<page_key>\w*)$', ContactWizard((("subject_and_sender", ContactForm1), |
---|
891 | + ("message", ContactForm2)))), |
---|
892 | + ) |
---|
893 | + |
---|
894 | +In the first type of SessionWizard initialization, a list of form classes is |
---|
895 | +provided. The ``page_key`` values matched from a URL are auto-generated zero-based |
---|
896 | +digits. Note these values are stored as strings not integers, which is |
---|
897 | +something to keep in mind while referencing ``page_key`` values in any SessionWizard |
---|
898 | +hooks described below. |
---|
899 | + |
---|
900 | +In the second style of initialization, the ``page_key`` values from a URL are |
---|
901 | +matched exactly with ``page_key`` values provided in a sequence of 2-tuples. |
---|
902 | + |
---|
903 | +Advanced SessionWizard methods |
---|
904 | +============================== |
---|
905 | + |
---|
906 | +.. class:: SessionWizard |
---|
907 | + |
---|
908 | + Aside from the :meth:`~django.contrib.formtools.wizard.SessionWizard.done()` |
---|
909 | + method, :class:`~django.contrib.formtools.wizard.SessionWizard` offers a few |
---|
910 | + advanced method hooks that let you customize how your wizard works. |
---|
911 | + |
---|
912 | + Some of these methods take an argument ``page_key``, which is a string |
---|
913 | + representing the current page. As noted above, if a wizard is created |
---|
914 | + from a list of form classes, then this string is a zero-based auto-incremented |
---|
915 | + value. Otherwise, if a wizard is created from a sequence of 2-tuples, |
---|
916 | + the ``page_key`` is the name of the page. |
---|
917 | + |
---|
918 | +.. method:: SessionWizard.get_wizard_data_key |
---|
919 | + |
---|
920 | + Given a user's session, returns a value to be used as a key for storing and |
---|
921 | + retrieving a wizard's data. Note that a request is provided so that a |
---|
922 | + wizard could potentially avoid namespace collision in the event that |
---|
923 | + multiple instances of a wizard are required concurrently for a single user. |
---|
924 | + |
---|
925 | + Default implementation:: |
---|
926 | + |
---|
927 | + def get_wizard_data_key(self, request): |
---|
928 | + return "session_wizard_data" |
---|
929 | + |
---|
930 | +.. method:: SessionWizard.get_URL_base |
---|
931 | + |
---|
932 | + Returns a URL that will be used when generating redirects. To |
---|
933 | + generate a redirect to GET the next page in a wizard, the SessionWizard |
---|
934 | + class appends a ``page_key`` to the value returned from this function. |
---|
935 | + |
---|
936 | + Default implementation:: |
---|
937 | + |
---|
938 | + def get_URL_base(self, request, page_key): |
---|
939 | + return request.path.replace("/" + page_key, "/") |
---|
940 | + |
---|
941 | +.. method:: SessionWizard.get_page_title |
---|
942 | + |
---|
943 | + Return a title that will be placed in the ``pages`` template context dict. |
---|
944 | + |
---|
945 | + Default implementation:: |
---|
946 | + |
---|
947 | + def get_page_title(self, session, page_key): |
---|
948 | + if self.contains_named_pages: |
---|
949 | + return page_key.replace("_", " ").title() |
---|
950 | + else: |
---|
951 | + return 'Page %s' % str(int(page_key) + 1) |
---|
952 | + |
---|
953 | +.. method:: SessionWizard.process_show_form |
---|
954 | + |
---|
955 | + A hook for providing ``extra_context`` for a page. |
---|
956 | + |
---|
957 | + By default, this does nothing. |
---|
958 | + |
---|
959 | + Example:: |
---|
960 | + |
---|
961 | + def process_show_form(self, request, page_key, form): |
---|
962 | + return {'form_title' : '%s Form ' % page_key} |
---|
963 | + |
---|
964 | +.. method:: SessionWizard.get_template |
---|
965 | + |
---|
966 | + Return the name of the template that should be used for a given ``page_key``. |
---|
967 | + |
---|
968 | + By default, this returns :file:`'forms/session_wizard.html'`, regardless of |
---|
969 | + ``page_key``. |
---|
970 | + |
---|
971 | + Example:: |
---|
972 | + |
---|
973 | + def get_template(self, page_key): |
---|
974 | + return 'myapp/wizard_%s.html' % page_key |
---|
975 | + |
---|
976 | + If :meth:`~SessionWizard.get_template` returns a list of strings, then the |
---|
977 | + wizard will use the template system's :func:`~django.template.loader.select_template()` |
---|
978 | + function, :ref:`explained in the template docs <ref-templates-api-the-python-api>`. |
---|
979 | + This means the system will use the first template that exists on the |
---|
980 | + filesystem. For example:: |
---|
981 | + |
---|
982 | + def get_template(self, step): |
---|
983 | + return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html'] |
---|
984 | + |
---|
985 | + .. _explained in the template docs: ../templates_python/#the-python-api |
---|
986 | + |
---|
987 | +.. method:: SessionWizard.preprocess_submit_form |
---|
988 | + |
---|
989 | + Provides a means to short-circuit form posts and do something different |
---|
990 | + than the normal flow of validating the form and proceeding to the next page. |
---|
991 | + For instance, a wizard could present the user with a "Delete this page" |
---|
992 | + button, and use this hook to remove the stored data associated with the |
---|
993 | + provided ``page_key`` and redirect to a specific ``page_key``. |
---|
994 | + |
---|
995 | + Example:: |
---|
996 | + |
---|
997 | + def preprocess_submit_form(self, request, page_key, form): |
---|
998 | + if request.POST['submit'] == "Delete this page": |
---|
999 | + self._remove_page(request.session, page_key) |
---|
1000 | + return "next_page" |
---|
1001 | + |
---|
1002 | +.. method:: SessionWizard.process_submit_form |
---|
1003 | + |
---|
1004 | + This is a hook for doing something after a valid form submission. For |
---|
1005 | + instance, a wizard could store the state of the wizard after each submission |
---|
1006 | + and allow users to resume their work after a logout or system failure. |
---|
1007 | + |
---|
1008 | + The function signature:: |
---|
1009 | + |
---|
1010 | + def process_submit_form(self, request, page_key, form): |
---|
1011 | + # ... |
---|
1012 | Index: django-trunk/docs/ref/contrib/formtools/index.txt |
---|
1013 | =================================================================== |
---|
1014 | --- django-trunk/docs/ref/contrib/formtools/index.txt (revision 9084) |
---|
1015 | +++ django-trunk/docs/ref/contrib/formtools/index.txt (working copy) |
---|
1016 | @@ -10,3 +10,4 @@ |
---|
1017 | |
---|
1018 | form-preview |
---|
1019 | form-wizard |
---|
1020 | + session-wizard |
---|