Ticket #9200: session_wizard_patch.diff

File session_wizard_patch.diff, 42.1 KB (added by ElliottM, 7 years ago)

Same patch but in diff format

  • home/david/work/django/django-trunk/django/contrib/formtools/wizard.py

     
    1 """
    2 FormWizard class -- implements a multi-page form, validating between each
    3 step and storing the form's state as HTML hidden fields so that no state is
    4 stored on the server side.
    5 """
    6 
    71import cPickle as pickle
    82
    93from django import forms
    104from django.conf import settings
    115from django.http import Http404
     6from django.http import HttpResponseRedirect
     7from django.http import HttpResponse
    128from django.shortcuts import render_to_response
    139from django.template.context import RequestContext
    1410from django.utils.hashcompat import md5_constructor
    1511from django.utils.translation import ugettext_lazy as _
    1612from django.contrib.formtools.utils import security_hash
     13from django.utils.datastructures import SortedDict
    1714
    18 class FormWizard(object):
     15class BaseWizard(object):
     16    pass
     17 
     18class 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    """   
    1924    # Dictionary of extra template context variables.
    2025    extra_context = {}
    2126
     
    239244        data.
    240245        """
    241246        raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
     247   
     248
     249class 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

     
    11from django import forms
     2from django.db import models
    23from django.contrib.formtools import preview, wizard
    34from django import http
    45from django.test import TestCase
     6from django.test.client import Client
    57
     8
    69success_string = "Done was called!"
    710
    811class TestFormPreview(preview.FormPreview):
     
    141144        request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"})
    142145        response = wizard(request)
    143146        self.assertEquals(1, wizard.step)
     147       
    144148
     149#
     150# SessionWizard tests
     151#
     152class SessionWizardModel(models.Model):
     153    field = models.CharField(max_length=10)
     154   
     155class SessionWizardPageOneForm(forms.Form):
     156    field = forms.CharField(required=True)
     157
     158class SessionWizardPageTwoForm(forms.ModelForm):
     159    class Meta:
     160        model = SessionWizardModel
     161
     162class SessionWizardPageThreeForm(forms.Form):
     163    field = forms.CharField()
     164
     165class SessionWizardDynamicPageForm(forms.Form):
     166    field = forms.CharField()
     167       
     168class 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
     199class 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

     
    99
    1010urlpatterns = patterns('',
    1111                       (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),)))
    1221                      )
  • home/david/work/django/django-trunk/docs/ref/contrib/formtools/session-wizard.txt

     
     1.. _ref-contrib-formtools-session-wizard:
     2
     3==============
     4Session 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
     12Django comes with an optional "session wizard" application that splits
     13:ref:`forms <topics-forms-index>` across multiple Web pages. It maintains
     14state in users' sessions incurring additional resource costs on a server
     15but also creating a smoother workflow for users.
     16
     17Note that SessionWizard is similar to :ref:`FormWizard <ref-contrib-formtools-form-wizard>`
     18and some of these examples and documentation mirror FormWizard examples and
     19documentation exactly.
     20
     21You might want to use this if you have a workflow or lengthy form and want to
     22provide navigation to various pages in the wizard.
     23
     24The 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
     29How it works
     30============
     31
     32Here'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
     44Usage
     45=====
     46
     47This application handles as much machinery for you as possible. Generally, you
     48just 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
     64Defining ``Form`` classes
     65=========================
     66
     67The 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
     72These classes can live anywhere in your codebase, but convention is to put them
     73in a file called :file:`forms.py` in your application.
     74
     75For example, let's write a "contact form" wizard, where the first page's form
     76collects the sender's e-mail address and subject, and the second page collects
     77the 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
     89data between pages, you should seriously consider whether or not it
     90makes sense to include :class:`~django.forms.fields.FileField` in any forms.
     91
     92Creating a ``SessionWizard`` class
     93==================================
     94
     95The next step is to create a :class:`~django.contrib.formtools.wizard.SessionWizard`
     96class, which should be a subclass of ``django.contrib.formtools.wizard.SessionWizard``.
     97
     98As your :class:`~django.forms.forms.Form` classes, this
     99:class:`~django.contrib.formtools.wizard.SessionWizard` class can live anywhere
     100in your codebase, but convention is to put it in :file:`forms.py`.
     101
     102The only requirement on this subclass is that it implement a
     103:meth:`~django.contrib.formtools.wizard.SessionWizard.done()` method,
     104which specifies what should happen when the data for *every* form is submitted
     105and validated. This method is passed one argument:
     106
     107    * ``request`` -- an :class:`~django.http.HttpRequest` object
     108
     109In this simplistic example, rather than perform any database operation, the
     110method 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
     123Note that this method will be called via ``POST``, so it really ought to be a
     124good Web citizen and redirect after processing the data. Here's another
     125example::
     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
     137See the section `Advanced SessionWizard methods`_ below to learn about more
     138:class:`~django.contrib.formtools.wizard.SessionWizard` hooks.
     139
     140Creating templates for the forms
     141================================
     142
     143Next, you'll need to create a template that renders the wizard's forms. By
     144default, 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
     147below. This hook also allows you to use a different template for each form.)
     148
     149This 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
     177Here'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
     205Note that ``page_key`` is required for the wizard to work properly.
     206
     207Hooking the wizard into a URLconf
     208=================================
     209
     210Finally, give your new :class:`~django.contrib.formtools.wizard.SessionWizard`
     211object a URL in ``urls.py``. The wizard has two types of initialization.  The
     212first takes a list of your form objects as arguments, and the seconds takes a
     213sequence of 2-tuples in the form (page_key, form_class).  The two types are
     214illustrated 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   
     228In the first type of SessionWizard initialization, a list of form classes is
     229provided.  The ``page_key`` values matched from a URL are auto-generated zero-based
     230digits.  Note these values are stored as strings not integers, which is
     231something to keep in mind while referencing ``page_key`` values in any SessionWizard
     232hooks described below.
     233
     234In the second style of initialization, the ``page_key`` values from a URL are
     235matched exactly with ``page_key`` values provided in a sequence of 2-tuples.
     236
     237Advanced 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

     
    1010
    1111   form-preview
    1212   form-wizard
     13   session-wizard
Back to Top