Ticket #9200: session_wizard_patch.diff
File session_wizard_patch.diff, 42.1 KB (added by , 16 years ago) |
---|
-
home/david/work/django/django-trunk/django/contrib/formtools/wizard.py
1 """2 FormWizard class -- implements a multi-page form, validating between each3 step and storing the form's state as HTML hidden fields so that no state is4 stored on the server side.5 """6 7 1 import cPickle as pickle 8 2 9 3 from django import forms 10 4 from django.conf import settings 11 5 from django.http import Http404 6 from django.http import HttpResponseRedirect 7 from django.http import HttpResponse 12 8 from django.shortcuts import render_to_response 13 9 from django.template.context import RequestContext 14 10 from django.utils.hashcompat import md5_constructor 15 11 from django.utils.translation import ugettext_lazy as _ 16 12 from django.contrib.formtools.utils import security_hash 13 from django.utils.datastructures import SortedDict 17 14 18 class FormWizard(object): 15 class BaseWizard(object): 16 pass 17 18 class FormWizard(BaseWizard): 19 """ 20 FormWizard class -- implements a multi-page form, validating between each 21 step and storing the form's state as HTML hidden fields so that no state is 22 stored on the server side. 23 """ 19 24 # Dictionary of extra template context variables. 20 25 extra_context = {} 21 26 … … 239 244 data. 240 245 """ 241 246 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) 247 248 249 class SessionWizard(BaseWizard): 250 """ 251 SessionWizard class -- implements multi-page forms with the following 252 characteristics: 253 254 1) easily supports navigation to arbitrary pages in the wizard 255 2) uses GETs to display forms (caveat validation errors) and POSTs for 256 form submissions 257 258 Pros are support for back-button and arbitrary navigation within pages 259 (including the oddity of someone clicking on the refresh button) 260 261 The major Con is use of the session scope. In particular, zero 262 consideration has been given to multipart form data. 263 """ 264 265 # keys used to store wizard data in sessions 266 __form_classes_key = 'form_classes' 267 __cleaned_data_key = 'cleaned_data' 268 __POST_data_key = 'POST_data' 269 __pages_key = 'pages' 270 271 def __init__(self, forms): 272 """ 273 A form_classes can be a list of form classes or a list of 2-Tuples in 274 the form (page_key, form_class). 275 """ 276 self.base_forms = SortedDict() 277 if forms: 278 if type(forms[0]) == tuple: 279 self.contains_named_pages = True 280 for page_key, form_class in forms: 281 self.base_forms[page_key] = form_class 282 else: 283 self.contains_named_pages = False 284 i = 0 285 for form_class in forms: 286 self.base_forms[str(i)] = form_class 287 i = i + 1 288 289 def _init_wizard_data(self, request): 290 """ 291 Copy self.base_forms to the session scope so that subclasses can 292 manipulate the form_classes for individual users. Also, initialize 293 the pages dict. 294 """ 295 wizard_key = self.get_wizard_data_key(request) 296 if wizard_key not in request.session: 297 pages = SortedDict() 298 for page_key in self.base_forms.keys(): 299 pages[page_key] = { 300 'valid' : False, 301 'visited' : False, 302 'title' : self.get_page_title(request, page_key) 303 } 304 request.session[wizard_key] = { 305 self.__form_classes_key : self.base_forms.copy(), 306 self.__cleaned_data_key : {}, 307 self.__POST_data_key : {}, 308 self.__pages_key : pages, 309 } 310 311 def __call__(self, request, *args, **kwargs): 312 """ 313 Initialize the form_classes for a session if needed and call GET or 314 POST depending on the provided request's method. 315 """ 316 self._init_wizard_data(request) 317 318 if request.method == 'POST': 319 return self.POST(request) 320 else: 321 return self.GET(request, kwargs['page_key']) 322 323 def GET(self, request, page_key): 324 """ 325 Initialize a form if necessary, and display the form/page identified by 326 page_key. 327 """ 328 page_data = self._get_cleaned_data(request, page_key) 329 if page_data is None: 330 form = self._get_form_classes(request)[page_key]() 331 else: 332 form_class = self._get_form_classes(request)[page_key] 333 if issubclass(form_class, forms.ModelForm): 334 form = form_class(instance=form_class.Meta.model(**page_data)) 335 else: 336 form = form_class(initial=page_data) 337 338 return self._show_form(request, page_key, form) 339 340 def POST(self, request): 341 """ 342 Validate form submission, and redirect to GET the next form or return 343 the response from self.done(). Note that the request.POST data must 344 contain a value for the key 'page_key', and this value must reference 345 a form in the form_classes collection for this wizard. 346 """ 347 form_classes = self._get_form_classes(request) 348 page_key = request.POST['page_key'] 349 page0 = form_classes.keys().index(page_key) 350 URL_base = self.get_URL_base(request, page_key) 351 self._set_POST_data(request, page_key, request.POST) 352 form = form_classes[page_key](request.POST) 353 new_page = self.preprocess_submit_form(request, page_key, form) 354 355 if isinstance(new_page, HttpResponse): 356 return new_page 357 elif new_page: 358 return HttpResponseRedirect(URL_base + new_page) 359 else: 360 if form.is_valid(): 361 self._set_cleaned_data(request, page_key, form.cleaned_data) 362 self._set_page(request, page_key, True, True) 363 is_done = self.process_submit_form(request, page_key, form) 364 if (not is_done) and len(form_classes) > page0 + 1: 365 return HttpResponseRedirect(URL_base + 366 self._get_next_page_key(request, page_key)) 367 else: 368 first_broken_page, form = self._validate_all_forms(request) 369 if first_broken_page is not None: 370 return self._show_form(request, first_broken_page, 371 form) 372 else: 373 return self.done(request) 374 else: 375 self._set_page(request, page_key, False) 376 377 return self._show_form(request, page_key, form) 378 379 380 # form util methods # 381 def _validate_all_forms(self, request): 382 """ 383 Iterate through the session form list and validate based on 1) the 384 'valid' attribute of the page data and 2) the POST data stored in the 385 session for this wizard. Return the page key and the form of the first 386 invalid form or None, None if all forms are valid. 387 """ 388 for page_key, form_class in self._get_form_classes(request).iteritems(): 389 if not self._get_pages(request)[page_key]['valid']: 390 form = form_class(self._get_POST_data(request, page_key)) 391 if not form.is_valid(): 392 return page_key, form 393 return None, None 394 395 def _show_form(self, request, page_key, form): 396 """ 397 Show the form associated with indicated page index. 398 """ 399 URL_base = self.get_URL_base(request, page_key) 400 extra_context = self.process_show_form(request, page_key, form) 401 self._set_current_page(request, page_key) 402 pages = self._get_pages(request) 403 context = {'page_key' : page_key, 404 'form' : form, 405 'pages' : pages, 406 'URL_base' : URL_base, 407 'extra_context' : extra_context } 408 return render_to_response(self.get_template(page_key), context, 409 RequestContext(request)) 410 411 def _get_form_classes(self, request): 412 """ 413 Return the collection of form classes stored in the provided request's 414 session. 415 """ 416 return request.session[self.get_wizard_data_key(request)]\ 417 [self.__form_classes_key] 418 419 def _insert_form(self, request, index, page_key, form_class): 420 """ 421 Insert a form class into the provided session's form list at the 422 provided index. 423 """ 424 form_classes = self._get_form_classes(request) 425 form_classes.insert(index, page_key, form_class) 426 self._insert_wizard_data(request, self.__form_classes_key, form_classes) 427 428 def _remove_form(self, request, page_key): 429 """ 430 Remove the form at index page_key from the provided sessions form list. 431 """ 432 self._del_wizard_data(request, self.__form_classes_key, page_key) 433 # end form util methods # 434 435 436 # Form data methods # 437 def _get_POST_data(self, request, page_key): 438 """ 439 Return the POST data for a page_key stored in the provided session. 440 """ 441 post_data = self._get_all_POST_data(request) 442 if page_key in post_data: 443 return post_data[page_key] 444 else: 445 return {} 446 447 def _set_POST_data(self, request, page_key, data): 448 """ 449 Set the POST data for a given page_key and session to the 'data' 450 provided. 451 """ 452 post_data = self._get_all_POST_data(request) 453 post_data[page_key] = data 454 self._insert_wizard_data(request, self.__POST_data_key, post_data) 455 456 def _remove_POST_data(self, request, page_key): 457 """ 458 Remove the POST data stored in the session at index page_key. 459 """ 460 self._del_wizard_data(request, self.__POST_data_key, page_key) 461 462 def _get_all_POST_data(self, request): 463 """ 464 Return the dict of all POST data for this wizard from the provided 465 session. 466 """ 467 return request.session[self.get_wizard_data_key(request)]\ 468 [self.__POST_data_key] 469 470 def _get_cleaned_data(self, request, page_key): 471 """ 472 Return the cleaned data from the provided session for this wizard based 473 on the provided page_key. 474 """ 475 cleaned_data = self._get_all_cleaned_data(request) 476 if page_key in cleaned_data: 477 return cleaned_data[page_key] 478 else: 479 return {} 480 481 def _set_cleaned_data(self, request, page_key, data): 482 """ 483 Assign the cleaned data for this wizard in the session at index 484 page_key. 485 """ 486 cleaned_data = self._get_all_cleaned_data(request) 487 cleaned_data[page_key] = data 488 self._insert_wizard_data(request, self.__cleaned_data_key, cleaned_data) 489 490 def _get_all_cleaned_data(self, request): 491 """ 492 Return a list of all the cleaned data in the session for this wizard. 493 """ 494 wizard_data = request.session[self.get_wizard_data_key(request)] 495 return wizard_data[self.__cleaned_data_key] 496 497 def _remove_cleaned_data(self, request, page_key): 498 """ 499 Remove the cleaned data at index page_key for this wizard from the 500 provided session. 501 """ 502 self._del_wizard_data(request, self.__cleaned_data_key, page_key) 503 # end Form data methods # 504 505 506 # page methods # 507 def _get_next_page_key(self, request, page_key): 508 """ 509 Return the next page_key after the provided page_key in the sequence of 510 pages. If this is a named pages wizard, this method iterates 511 through keys. Otherwise it will simply iterate the page_key. 512 This method must return a String. 513 """ 514 form_classes_keys = self._get_form_classes(request).keys() 515 return form_classes_keys[form_classes_keys.index(page_key) + 1] 516 517 def _set_current_page(self, request, page_key): 518 """ 519 Iterate through the page dicts in the session and set 'current_page' to 520 True for the page corresponding to page_key and False for all others. 521 """ 522 for key, page in self._get_pages(request).iteritems(): 523 if key == page_key: 524 page['current_page'] = True 525 else: 526 page['current_page'] = False 527 528 def _get_pages(self, request): 529 """ 530 Return the list of page info dicts stored in the provided session for 531 this wizard. 532 """ 533 return request.session[self.get_wizard_data_key(request)]\ 534 [self.__pages_key] 535 536 def _remove_page_data(self, request, page_key): 537 """ 538 Remove page data from the provided session for this wizard based on a 539 given page_key. This removes page information, form_class and form 540 data. 541 """ 542 self._remove_form(request, page_key) 543 self._remove_page(request, page_key) 544 self._remove_cleaned_data(request, page_key) 545 self._remove_POST_data(request, page_key) 546 547 def _remove_page(self, request, page_key): 548 """ 549 Remove the page info dict for this wizard stored at a given page_key 550 from the provided session. 551 """ 552 self._del_wizard_data(request, self.__pages_key, page_key) 553 554 def _insert_page(self, request, index, page_key, form_class): 555 """ 556 Insert a page into this wizard at the provided form_class index, storing 557 required associated data. 558 """ 559 self._insert_form(request, index, page_key, form_class) 560 self._set_page(request, page_key, False) 561 self._set_cleaned_data(request, page_key, {}) 562 self._set_POST_data(request, page_key, {}) 563 564 def _set_page(self, request, page_key, valid=False, visited=False): 565 """ 566 Set the page info in this wizard for a page at index page_key and stored 567 in the provided session. 568 """ 569 page_info = { 570 'valid' : valid, 571 'visited' : visited, 572 'title' : self.get_page_title(request, page_key) 573 } 574 pages = self._get_pages(request) 575 pages[page_key] = page_info 576 self._insert_wizard_data(request, self.__pages_key, pages) 577 # end page methods # 578 579 # start wizard data utils # 580 def _clear_wizard_data_from_session(self, request): 581 """ 582 Clear the session data used by this wizard from the provided session. 583 """ 584 del request.session[self.get_wizard_data_key(request)] 585 586 def _insert_wizard_data(self, request, key, data): 587 """ 588 Inserts wizard data into the provided session at the provided key. 589 """ 590 wizard_data = request.session[self.get_wizard_data_key(request)] 591 wizard_data[key] = data 592 request.session[self.get_wizard_data_key(request)] = wizard_data 593 594 def _del_wizard_data(self, request, key, page_key): 595 """ 596 Deletes wizard data from the provided session based on a page_key. 597 """ 598 wizard_data = request.session[self.get_wizard_data_key(request)] 599 sub_set = wizard_data[key] 600 if page_key in sub_set: 601 del sub_set[page_key] 602 wizard_data[key] = sub_set 603 request.session[self.get_wizard_data_key(request)] = wizard_data 604 605 # end wizard data utils # 606 607 # typically overriden methods # 608 def get_wizard_data_key(self, request): 609 """ 610 Return a session key for this wizard. The provided request could be 611 used to prevent overlapping keys in the case that someone needs 612 multiple instances of this wizard at one time. 613 """ 614 return 'session_wizard_data' 615 616 def get_URL_base(self, request, page_key): 617 """ 618 Return the URL to this wizard minus the "page_key" part of the URL. 619 This value is passed to template as URL_base. 620 """ 621 return request.path.replace("/" + page_key, "/") 622 623 def get_page_title(self, request, page_key): 624 """ 625 Return a user friendly title for the page at index page_key. 626 """ 627 if self.contains_named_pages: 628 return page_key.replace("_", " ").title() 629 else: 630 return 'Page %s' % str(int(page_key) + 1) 631 632 def process_show_form(self, request, page_key, form): 633 """ 634 Called before rendering a form either from a GET or when a form submit 635 is invalid. 636 """ 637 638 def preprocess_submit_form(self, request, page_key, form): 639 """ 640 Called when a form is POSTed, but before the form is validated. If this 641 function returns None then form submission continues, else it should 642 return either a Response object or a new page index that will be 643 redirected to as a GET. 644 """ 645 646 def process_submit_form(self, request, page_key, form): 647 """ 648 Called when a form is POSTed. This is only called if the form data is 649 valid. If this method returns True, the done() method is called, 650 otherwise the wizard continues. Note that it is possible that this 651 method would not return True, and done() would still be called because 652 there are no more forms left in the form_classes. 653 """ 654 655 def get_template(self, page_key): 656 """ 657 Hook for specifying the name of the template to use for a given page. 658 Note that this can return a tuple of template names if you'd like to 659 use the template system's select_template() hook. 660 """ 661 return 'forms/session_wizard.html' 662 663 def done(self, request): 664 """ 665 Hook for doing something with the validated data. This is responsible 666 for the final processing including clearing the session scope of items 667 created by this wizard. 668 """ 669 raise NotImplementedError("Your %s class has not defined a done() " + \ 670 "method, which is required." \ 671 % self.__class__.__name__) -
home/david/work/django/django-trunk/django/contrib/formtools/tests.py
1 1 from django import forms 2 from django.db import models 2 3 from django.contrib.formtools import preview, wizard 3 4 from django import http 4 5 from django.test import TestCase 6 from django.test.client import Client 5 7 8 6 9 success_string = "Done was called!" 7 10 8 11 class TestFormPreview(preview.FormPreview): … … 141 144 request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"}) 142 145 response = wizard(request) 143 146 self.assertEquals(1, wizard.step) 147 144 148 149 # 150 # SessionWizard tests 151 # 152 class SessionWizardModel(models.Model): 153 field = models.CharField(max_length=10) 154 155 class SessionWizardPageOneForm(forms.Form): 156 field = forms.CharField(required=True) 157 158 class SessionWizardPageTwoForm(forms.ModelForm): 159 class Meta: 160 model = SessionWizardModel 161 162 class SessionWizardPageThreeForm(forms.Form): 163 field = forms.CharField() 164 165 class SessionWizardDynamicPageForm(forms.Form): 166 field = forms.CharField() 167 168 class SessionWizardClass(wizard.SessionWizard): 169 def get_page_title(self, request, page_key): 170 try: 171 return "Custom Page Title: %s" % str(int(page_key) + 1) 172 except ValueError: 173 return super(SessionWizardClass, self).get_page_title(request, 174 page_key) 175 176 def process_show_form(self, request, page_key, form): 177 try: 178 return {'form_title' : 'Form %s' % str(int(page_key) + 1)} 179 except ValueError: 180 return super(SessionWizardClass, self).process_show_form(request, 181 page_key, form) 182 183 def preprocess_submit_form(self, request, page_key, form): 184 if page_key == "1" and request.POST['field'] == "": 185 self._remove_page(request, page_key) 186 return str(int(page_key) - 1) 187 188 def process_submit_form(self, request, page_key, form): 189 if page_key == '2': 190 self._insert_page(request, 3, str(int(page_key) + 1), 191 SessionWizardDynamicPageForm) 192 193 def get_template(self, page_key): 194 return "formtools/form.html" 195 196 def done(self, request): 197 return http.HttpResponse(success_string) 198 199 class SessionWizardTests(TestCase): 200 urls = 'django.contrib.formtools.test_urls' 201 202 def test_named_pages_wizard_get(self): 203 """ 204 Tests that a wizard is created properly based on it's initialization 205 argument, which could be a sequence or dictionary-like object. 206 """ 207 response = self.client.get('/named_pages_wizard/first_page_form') 208 self.assertEquals(200, response.status_code) 209 self.assertEquals('First Page Form', 210 response.context[0]['pages']['first_page_form']['title']) 211 212 213 def test_valid_POST(self): 214 """ 215 Tests that a post containing valid data will set session values 216 correctly and redirect to the next page. 217 """ 218 response = self.client.post('/sessionwizard/', {"page_key":"0", 219 "field":"test"}) 220 self.assertEquals(302, response.status_code) 221 self.assertEquals("http://testserver/sessionwizard/1", 222 response['Location']) 223 session = self.client.session 224 cleaned_data = session['session_wizard_data']['cleaned_data'] 225 post_data = session['session_wizard_data']['POST_data'] 226 self.assertEquals('test', cleaned_data['0']['field']) 227 self.assertEquals('test', post_data['0']['field']) 228 229 def test_invalid_POST(self): 230 """ 231 Tests that a post containing invalid data will set session values 232 correctly and redisplay the form. 233 """ 234 response = self.client.post('/sessionwizard/', {"page_key":"0", 235 "field":""}) 236 self.assertEquals(200, response.status_code) 237 session = self.client.session 238 post_data = session['session_wizard_data']['POST_data'] 239 self.assertEquals('', post_data['0']['field']) 240 241 def test_GET(self): 242 """ 243 Tests that a get will display a page properly. 244 """ 245 response = self.client.get('/sessionwizard/0') 246 self.assertEquals(200, response.status_code) 247 248 def test_preprocess_submit_form(self): 249 """ 250 Tests the preprocess_submit_form hook of SessionWizard POSTs. 251 The SessionWizardClass is coded to short-circuit a POST for page index 1 252 when form.cleaned_data['field'] == '' by returning a reference to page_key 253 index 0. 254 """ 255 response = self.client.post('/sessionwizard/', {"page_key":"1", 256 "field":""}) 257 self.assertEquals(302, response.status_code) 258 self.assertEquals("http://testserver/sessionwizard/0", 259 response['Location']) 260 261 def test_process_submit_form(self): 262 """ 263 Tests the process_submit_form hook of SessionWizard POSTs. 264 The SessionWizardClass is coded to insert a new page at index 3 on a 265 POST for page index 2. 266 """ 267 response = self.client.post('/sessionwizard/', {"page_key":"2", 268 "field":"test"}) 269 self.assertEquals(302, response.status_code) 270 self.assertEquals("http://testserver/sessionwizard/3", 271 response['Location']) 272 self.assertEquals({"0":SessionWizardPageOneForm, 273 "1":SessionWizardPageTwoForm, 274 "2":SessionWizardPageThreeForm, 275 "3":SessionWizardDynamicPageForm}, 276 self.client.session['session_wizard_data']['form_classes']) 277 278 def test_process_show_form(self): 279 """ 280 Tests the process_show_form hook. SessionWizardClass is coded to 281 return a extra_context having a specific 'form_title' attribute. 282 """ 283 response = self.client.get('/sessionwizard/0') 284 self.assertEquals(200, response.status_code) 285 self.assertEquals("Form 1", 286 response.context[0]['extra_context']['form_title']) 287 288 def test_validate_all(self): 289 """ 290 Submits all forms, with one of them being invalid, and tests that 291 submitting the last form will catch an invalid form earlier in the 292 workflow and redisplay it. 293 """ 294 response = self.client.post('/sessionwizard/', {"page_key":"0", "field":""}) 295 self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"}) 296 self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"}) 297 response = self.client.post('/sessionwizard/3', 298 {"page_key":"3", "field":"test3"}) 299 self.assertEquals(True, response.context[0]['pages']['1']['visited']) 300 self.assertEquals(True, response.context[0]['pages']['1']['valid']) 301 302 self.assertEquals("Form 1", 303 response.context[0]['extra_context']['form_title']) 304 305 def test_done(self): 306 self.client.post('/sessionwizard/', {"page_key":"0", "field":"test0"}) 307 self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"}) 308 self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"}) 309 response = self.client.post('/sessionwizard/', 310 {"page_key":"3", "field":"test3"}) 311 self.assertEqual(response.content, success_string) -
home/david/work/django/django-trunk/django/contrib/formtools/test_urls.py
9 9 10 10 urlpatterns = patterns('', 11 11 (r'^test1/', TestFormPreview(TestForm)), 12 (r'^sessionwizard/(?P<page_key>\d*)$', 13 SessionWizardClass([SessionWizardPageOneForm, 14 SessionWizardPageTwoForm, 15 SessionWizardPageThreeForm])), 16 (r'^named_pages_wizard/(?P<page_key>\w*)$', 17 SessionWizardClass(( 18 ('first_page_form', SessionWizardPageOneForm), 19 ('page2', SessionWizardPageTwoForm), 20 ('page3', SessionWizardPageThreeForm),))) 12 21 ) -
home/david/work/django/django-trunk/docs/ref/contrib/formtools/session-wizard.txt
1 .. _ref-contrib-formtools-session-wizard: 2 3 ============== 4 Session wizard 5 ============== 6 7 .. module:: django.contrib.formtools.wizard 8 :synopsis: Splits forms across multiple Web pages using users' sessions to store form and page data. 9 10 .. versionadded:: 1.x 11 12 Django comes with an optional "session wizard" application that splits 13 :ref:`forms <topics-forms-index>` across multiple Web pages. It maintains 14 state in users' sessions incurring additional resource costs on a server 15 but also creating a smoother workflow for users. 16 17 Note that SessionWizard is similar to :ref:`FormWizard <ref-contrib-formtools-form-wizard>` 18 and some of these examples and documentation mirror FormWizard examples and 19 documentation exactly. 20 21 You might want to use this if you have a workflow or lengthy form and want to 22 provide navigation to various pages in the wizard. 23 24 The term "wizard," in this context, is `explained on Wikipedia`_. 25 26 .. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29 27 .. _forms: ../forms/ 28 29 How it works 30 ============ 31 32 Here's the basic workflow for how a user would use a wizard: 33 34 1. The user visits the first page of the wizard, fills in the form and 35 submits it. 36 2. The server validates the data. If it's invalid, the form is displayed 37 again, with error messages. If it's valid, the server stores this data 38 in a user's session and sends an HTTP redirect to GET the next page. 39 3. Step 1 and 2 repeat, for every subsequent form in the wizard. 40 4. Once the user has submitted all the forms and all the data has been 41 validated, the wizard processes the data -- saving it to the database, 42 sending an e-mail, or whatever the application needs to do. 43 44 Usage 45 ===== 46 47 This application handles as much machinery for you as possible. Generally, you 48 just have to do these things: 49 50 1. Define a number of :mod:`django.forms` 51 :class:`~django.forms.forms.Form` classes -- one per wizard page. 52 53 2. Create a :class:`~django.contrib.formtools.wizard.SessionWizard` class 54 that specifies what to do once all of your forms have been submitted 55 and validated. This also lets you override some of the wizard's behavior. 56 57 3. Create some templates that render the forms. You can define a single, 58 generic template to handle every one of the forms, or you can define a 59 specific template for each form. 60 61 4. Point your URLconf at your 62 :class:`~django.contrib.formtools.wizard.SessionWizard` class. 63 64 Defining ``Form`` classes 65 ========================= 66 67 The first step in creating a form wizard is to create the 68 :class:`~django.forms.forms.Form` classes. These should be standard 69 :mod:`django.forms` :class:`~django.forms.forms.Form` classes, covered in the 70 :ref:`forms documentation <topics-forms-index>`. 71 72 These classes can live anywhere in your codebase, but convention is to put them 73 in a file called :file:`forms.py` in your application. 74 75 For example, let's write a "contact form" wizard, where the first page's form 76 collects the sender's e-mail address and subject, and the second page collects 77 the message itself. Here's what the :file:`forms.py` might look like:: 78 79 from django import forms 80 81 class ContactForm1(forms.Form): 82 subject = forms.CharField(max_length=100) 83 sender = forms.EmailField() 84 85 class ContactForm2(forms.Form): 86 message = forms.CharField(widget=forms.Textarea) 87 88 **Important limitation:** Because the wizard uses users' sessions to store 89 data between pages, you should seriously consider whether or not it 90 makes sense to include :class:`~django.forms.fields.FileField` in any forms. 91 92 Creating a ``SessionWizard`` class 93 ================================== 94 95 The next step is to create a :class:`~django.contrib.formtools.wizard.SessionWizard` 96 class, which should be a subclass of ``django.contrib.formtools.wizard.SessionWizard``. 97 98 As your :class:`~django.forms.forms.Form` classes, this 99 :class:`~django.contrib.formtools.wizard.SessionWizard` class can live anywhere 100 in your codebase, but convention is to put it in :file:`forms.py`. 101 102 The only requirement on this subclass is that it implement a 103 :meth:`~django.contrib.formtools.wizard.SessionWizard.done()` method, 104 which specifies what should happen when the data for *every* form is submitted 105 and validated. This method is passed one argument: 106 107 * ``request`` -- an :class:`~django.http.HttpRequest` object 108 109 In this simplistic example, rather than perform any database operation, the 110 method simply renders a template of the validated data:: 111 112 from django.shortcuts import render_to_response 113 from django.contrib.formtools.wizard import SessionWizard 114 115 class ContactWizard(SessionWizard): 116 def done(self, request): 117 form_data = self._get_all_cleaned_data(request.session) 118 self._clear_wizard_data_from_session(request.session) 119 return render_to_response('done.html', { 120 'form_data': form_data, 121 }) 122 123 Note that this method will be called via ``POST``, so it really ought to be a 124 good Web citizen and redirect after processing the data. Here's another 125 example:: 126 127 from django.http import HttpResponseRedirect 128 from django.contrib.formtools.wizard import SessionWizard 129 130 class ContactWizard(SessionWizard): 131 def done(self, request): 132 form_data = self._get_all_cleaned_data(request.session) 133 self._clear_wizard_data_from_session(request.session) 134 do_something_with_the_form_data(form_data) 135 return HttpResponseRedirect('/page-to-redirect-to-when-done/') 136 137 See the section `Advanced SessionWizard methods`_ below to learn about more 138 :class:`~django.contrib.formtools.wizard.SessionWizard` hooks. 139 140 Creating templates for the forms 141 ================================ 142 143 Next, you'll need to create a template that renders the wizard's forms. By 144 default, every form uses a template called :file:`forms/session_wizard.html`. 145 (You can change this template name by overriding 146 :meth:`~django.contrib.formtools.wizard..get_template()`, which is documented 147 below. This hook also allows you to use a different template for each form.) 148 149 This template expects the following context: 150 151 * ``page_key`` -- A string representation of the current page in this 152 wizard. Depending on how a wizard is created, this could be a page name 153 or a zero-based page index. 154 * ``form`` -- The :class:`~django.forms.forms.Form` instance for the 155 current page (empty, populated or populated with errors). 156 * ``pages`` -- The current list of pages for this wizard. This is a dict of 157 dict objects in the form:: 158 159 {'page_key1' : {'title' : 'page1', 160 'visited': True, 161 'valid' : True, 162 'current_page' : False 163 }, 164 'page_key2' : {'title' : 'page2', 165 'visited': False, 166 'valid' : False, 167 'current_page' : True 168 }, 169 .. 170 } 171 * ``URL_base`` -- The base URL used to generate links to pages in this 172 wizard. By default, it is the request.path value minus the ``page_key``. 173 * 'extra_context' -- A dict returned from the 174 :meth:`~django.contrib.formtools.wizard.SessionWizard.process_show_form()` 175 hook. 176 177 Here's a full example template: 178 179 .. code-block:: html+django 180 181 {% extends "base.html" %} 182 183 {% block content %} 184 <ul> 185 {% for page_key,page in pages.items %} 186 <li class="{% if page.valid %}valid{% endif %} 187 {% if page.current_page %}current{% endif %}"> 188 {% if page.visited %} 189 <a href="{{ URL_base }}{{ page_key }}">{{ page.title }}</a> 190 {% else %} 191 {{ page.title }} 192 {% endif %} 193 </li> 194 {% endfor %} 195 </ul> 196 <form action="." method="post"> 197 <table> 198 {{ form }} 199 </table> 200 <input type="hidden" name="page_key" value="{{ page_key }}"/> 201 <input type="submit"> 202 </form> 203 {% endblock %} 204 205 Note that ``page_key`` is required for the wizard to work properly. 206 207 Hooking the wizard into a URLconf 208 ================================= 209 210 Finally, give your new :class:`~django.contrib.formtools.wizard.SessionWizard` 211 object a URL in ``urls.py``. The wizard has two types of initialization. The 212 first takes a list of your form objects as arguments, and the seconds takes a 213 sequence of 2-tuples in the form (page_key, form_class). The two types are 214 illustrated below:: 215 216 from django.conf.urls.defaults import * 217 from mysite.testapp.forms import ContactForm1, ContactForm2, ContactWizard 218 219 urlpatterns = patterns('', 220 ## First form - a list of form classes 221 (r'^contact/(?P<page_key>\d*)$', ContactWizard([ContactForm1, ContactForm2])), 222 223 ## Second form - a sequence of 2-tuples 224 (r'^contact/(?P<page_key>\w*)$', ContactWizard((("subject_and_sender", ContactForm1), 225 ("message", ContactForm2)))), 226 ) 227 228 In the first type of SessionWizard initialization, a list of form classes is 229 provided. The ``page_key`` values matched from a URL are auto-generated zero-based 230 digits. Note these values are stored as strings not integers, which is 231 something to keep in mind while referencing ``page_key`` values in any SessionWizard 232 hooks described below. 233 234 In the second style of initialization, the ``page_key`` values from a URL are 235 matched exactly with ``page_key`` values provided in a sequence of 2-tuples. 236 237 Advanced SessionWizard methods 238 ============================== 239 240 .. class:: SessionWizard 241 242 Aside from the :meth:`~django.contrib.formtools.wizard.SessionWizard.done()` 243 method, :class:`~django.contrib.formtools.wizard.SessionWizard` offers a few 244 advanced method hooks that let you customize how your wizard works. 245 246 Some of these methods take an argument ``page_key``, which is a string 247 representing the current page. As noted above, if a wizard is created 248 from a list of form classes, then this string is a zero-based auto-incremented 249 value. Otherwise, if a wizard is created from a sequence of 2-tuples, 250 the ``page_key`` is the name of the page. 251 252 .. method:: SessionWizard.get_wizard_data_key 253 254 Given a user's session, returns a value to be used as a key for storing and 255 retrieving a wizard's data. Note that a request is provided so that a 256 wizard could potentially avoid namespace collision in the event that 257 multiple instances of a wizard are required concurrently for a single user. 258 259 Default implementation:: 260 261 def get_wizard_data_key(self, request): 262 return "session_wizard_data" 263 264 .. method:: SessionWizard.get_URL_base 265 266 Returns a URL that will be used when generating redirects. To 267 generate a redirect to GET the next page in a wizard, the SessionWizard 268 class appends a ``page_key`` to the value returned from this function. 269 270 Default implementation:: 271 272 def get_URL_base(self, request, page_key): 273 return request.path.replace("/" + page_key, "/") 274 275 .. method:: SessionWizard.get_page_title 276 277 Return a title that will be placed in the ``pages`` template context dict. 278 279 Default implementation:: 280 281 def get_page_title(self, request, page_key): 282 if self.contains_named_pages: 283 return page_key.replace("_", " ").title() 284 else: 285 return 'Page %s' % str(int(page_key) + 1) 286 287 .. method:: SessionWizard.process_show_form 288 289 A hook for providing ``extra_context`` for a page. 290 291 By default, this does nothing. 292 293 Example:: 294 295 def process_show_form(self, request, page_key, form): 296 return {'form_title' : '%s Form ' % page_key} 297 298 .. method:: SessionWizard.get_template 299 300 Return the name of the template that should be used for a given ``page_key``. 301 302 By default, this returns :file:`'forms/session_wizard.html'`, regardless of 303 ``page_key``. 304 305 Example:: 306 307 def get_template(self, page_key): 308 return 'myapp/wizard_%s.html' % page_key 309 310 If :meth:`~SessionWizard.get_template` returns a list of strings, then the 311 wizard will use the template system's :func:`~django.template.loader.select_template()` 312 function, :ref:`explained in the template docs <ref-templates-api-the-python-api>`. 313 This means the system will use the first template that exists on the 314 filesystem. For example:: 315 316 def get_template(self, step): 317 return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html'] 318 319 .. _explained in the template docs: ../templates_python/#the-python-api 320 321 .. method:: SessionWizard.preprocess_submit_form 322 323 Provides a means to short-circuit form posts and do something different 324 than the normal flow of validating the form and proceeding to the next page. 325 For instance, a wizard could present the user with a "Delete this page" 326 button, and use this hook to remove the stored data associated with the 327 provided ``page_key`` and redirect to a specific ``page_key``. 328 329 The return value can be either an HttpResponse or a ``page_key`` string. 330 331 Example:: 332 333 def preprocess_submit_form(self, request, page_key, form): 334 if request.POST['submit'] == "Delete this page": 335 self._remove_page(request, page_key) 336 return "next_page" 337 338 .. method:: SessionWizard.process_submit_form 339 340 This is a hook for doing something after a valid form submission. For 341 instance, a wizard could persist the wizard's state after each submission 342 and later allow users to resume their work after a session timeout or 343 system failure. 344 345 The function signature:: 346 347 def process_submit_form(self, request, page_key, form): 348 # ... -
home/david/work/django/django-trunk/docs/ref/contrib/formtools/index.txt
10 10 11 11 form-preview 12 12 form-wizard 13 session-wizard