Ticket #9200: 9200-3.diff

File 9200-3.diff, 156.8 KB (added by Jannis Leidel, 14 years ago)

Updates as requested.

  • django/contrib/formtools/tests/__init__.py

    diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
    index be0372a..7084386 100644
    a b  
    11import os
     2import re
    23import warnings
    34
    4 from django import forms, http
     5from django import http
    56from django.conf import settings
    67from django.contrib.formtools import preview, wizard, utils
    78from django.test import TestCase
    89from django.test.utils import get_warnings_state, restore_warnings_state
    910from django.utils import unittest
    1011
     12from django.contrib.formtools.wizard.tests import *
     13from django.contrib.formtools.tests.forms import *
     14
     15warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
     16                        module='django.contrib.formtools.wizard')
    1117
    1218success_string = "Done was called!"
    1319
    class TestFormPreview(preview.FormPreview):  
    2430        return http.HttpResponse(success_string)
    2531
    2632
    27 class TestForm(forms.Form):
    28     field1 = forms.CharField()
    29     field1_ = forms.CharField()
    30     bool1 = forms.BooleanField(required=False)
    31 
    32 
    3333class PreviewTests(TestCase):
    3434    urls = 'django.contrib.formtools.tests.urls'
    3535
    class PreviewTests(TestCase):  
    6363        is created to manage the stage.
    6464
    6565        """
    66         response = self.client.get('/test1/')
     66        response = self.client.get('/preview/')
    6767        stage = self.input % 1
    6868        self.assertContains(response, stage, 1)
    6969        self.assertEqual(response.context['custom_context'], True)
    class PreviewTests(TestCase):  
    8181        # Pass strings for form submittal and add stage variable to
    8282        # show we previously saw first stage of the form.
    8383        self.test_data.update({'stage': 1})
    84         response = self.client.post('/test1/', self.test_data)
     84        response = self.client.post('/preview/', self.test_data)
    8585        # Check to confirm stage is set to 2 in output form.
    8686        stage = self.input % 2
    8787        self.assertContains(response, stage, 1)
    class PreviewTests(TestCase):  
    9999        # Pass strings for form submittal and add stage variable to
    100100        # show we previously saw first stage of the form.
    101101        self.test_data.update({'stage':2})
    102         response = self.client.post('/test1/', self.test_data)
     102        response = self.client.post('/preview/', self.test_data)
    103103        self.assertNotEqual(response.content, success_string)
    104104        hash = self.preview.security_hash(None, TestForm(self.test_data))
    105105        self.test_data.update({'hash': hash})
    106         response = self.client.post('/test1/', self.test_data)
     106        response = self.client.post('/preview/', self.test_data)
    107107        self.assertEqual(response.content, success_string)
    108108
    109109    def test_bool_submit(self):
    class PreviewTests(TestCase):  
    122122        self.test_data.update({'stage':2})
    123123        hash = self.preview.security_hash(None, TestForm(self.test_data))
    124124        self.test_data.update({'hash':hash, 'bool1':u'False'})
    125         response = self.client.post('/test1/', self.test_data)
     125        response = self.client.post('/preview/', self.test_data)
    126126        self.assertEqual(response.content, success_string)
    127127
    128128    def test_form_submit_good_hash(self):
    class PreviewTests(TestCase):  
    133133        # Pass strings for form submittal and add stage variable to
    134134        # show we previously saw first stage of the form.
    135135        self.test_data.update({'stage':2})
    136         response = self.client.post('/test1/', self.test_data)
     136        response = self.client.post('/preview/', self.test_data)
    137137        self.assertNotEqual(response.content, success_string)
    138138        hash = utils.form_hmac(TestForm(self.test_data))
    139139        self.test_data.update({'hash': hash})
    140         response = self.client.post('/test1/', self.test_data)
     140        response = self.client.post('/preview/', self.test_data)
    141141        self.assertEqual(response.content, success_string)
    142142
    143143
    class PreviewTests(TestCase):  
    149149        # Pass strings for form submittal and add stage variable to
    150150        # show we previously saw first stage of the form.
    151151        self.test_data.update({'stage':2})
    152         response = self.client.post('/test1/', self.test_data)
     152        response = self.client.post('/preview/', self.test_data)
    153153        self.assertEqual(response.status_code, 200)
    154154        self.assertNotEqual(response.content, success_string)
    155155        hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
    156156        self.test_data.update({'hash': hash})
    157         response = self.client.post('/test1/', self.test_data)
     157        response = self.client.post('/previewpreview/', self.test_data)
    158158        self.assertNotEqual(response.content, success_string)
    159159
    160160
    class FormHmacTests(unittest.TestCase):  
    220220        self.assertEqual(hash1, hash2)
    221221
    222222
    223 class HashTestForm(forms.Form):
    224     name = forms.CharField()
    225     bio = forms.CharField()
    226 
    227 
    228 class HashTestBlankForm(forms.Form):
    229     name = forms.CharField(required=False)
    230     bio = forms.CharField(required=False)
    231 
    232223#
    233224# FormWizard tests
    234225#
    235226
    236 
    237 class WizardPageOneForm(forms.Form):
    238     field = forms.CharField()
    239 
    240 
    241 class WizardPageTwoForm(forms.Form):
    242     field = forms.CharField()
    243 
    244 class WizardPageTwoAlternativeForm(forms.Form):
    245     field = forms.CharField()
    246 
    247 class WizardPageThreeForm(forms.Form):
    248     field = forms.CharField()
    249 
    250 
    251 class WizardClass(wizard.FormWizard):
     227class TestWizardClass(wizard.FormWizard):
    252228
    253229    def get_template(self, step):
    254         return 'formwizard/wizard.html'
     230        return 'forms/wizard.html'
    255231
    256232    def done(self, request, cleaned_data):
    257233        return http.HttpResponse(success_string)
    class DummyRequest(http.HttpRequest):  
    269245
    270246class WizardTests(TestCase):
    271247    urls = 'django.contrib.formtools.tests.urls'
     248    input_re = re.compile('name="([^"]+)" value="([^"]+)"')
     249    wizard_step_data = (
     250        {
     251            '0-name': 'Pony',
     252            '0-thirsty': '2',
     253        },
     254        {
     255            '1-address1': '123 Main St',
     256            '1-address2': 'Djangoland',
     257        },
     258        {
     259            '2-random_crap': 'blah blah',
     260        }
     261    )
    272262
    273263    def setUp(self):
    274264        self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
    class WizardTests(TestCase):  
    290280        """
    291281        step should be zero for the first form
    292282        """
    293         response = self.client.get('/wizard/')
     283        response = self.client.get('/wizard1/')
    294284        self.assertEqual(0, response.context['step0'])
    295285
    296286    def test_step_increments(self):
    297287        """
    298288        step should be incremented when we go to the next page
    299289        """
    300         response = self.client.post('/wizard/', {"0-field":"test", "wizard_step":"0"})
     290        response = self.client.post('/wizard1/', {"0-field":"test", "wizard_step":"0"})
    301291        self.assertEqual(1, response.context['step0'])
    302292
    303293    def test_bad_hash(self):
    304294        """
    305295        Form should not advance if the hash is missing or bad
    306296        """
    307         response = self.client.post('/wizard/',
     297        response = self.client.post('/wizard1/',
    308298                                    {"0-field":"test",
    309299                                     "1-field":"test2",
    310300                                     "wizard_step": "1"})
    class WizardTests(TestCase):  
    319309                "1-field": "test2",
    320310                "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c",
    321311                "wizard_step": "1"}
    322         response = self.client.post('/wizard/', data)
     312        response = self.client.post('/wizard1/', data)
    323313        self.assertEqual(2, response.context['step0'])
    324314
    325315    def test_11726(self):
    class WizardTests(TestCase):  
    330320        reached = [False]
    331321        that = self
    332322
    333         class WizardWithProcessStep(WizardClass):
     323        class WizardWithProcessStep(TestWizardClass):
    334324            def process_step(self, request, form, step):
    335325                if step == 0:
    336326                    if self.num_steps() < 2:
    class WizardTests(TestCase):  
    362352        reached = [False]
    363353        that = self
    364354
    365         class WizardWithProcessStep(WizardClass):
     355        class WizardWithProcessStep(TestWizardClass):
    366356            def process_step(self, request, form, step):
    367357                that.assertTrue(hasattr(form, 'cleaned_data'))
    368358                reached[0] = True
    class WizardTests(TestCase):  
    386376        reached = [False]
    387377        that = self
    388378
    389         class Wizard(WizardClass):
     379        class Wizard(TestWizardClass):
    390380            def done(self, request, form_list):
    391381                reached[0] = True
    392382                that.assertTrue(len(form_list) == 2)
    class WizardTests(TestCase):  
    409399        reached = [False]
    410400        that = self
    411401
    412         class WizardWithProcessStep(WizardClass):
     402        class WizardWithProcessStep(TestWizardClass):
    413403            def process_step(self, request, form, step):
    414404                if step == 0:
    415405                    self.form_list[1] = WizardPageTwoAlternativeForm
    class WizardTests(TestCase):  
    426416                "wizard_step": "1"}
    427417        wizard(DummyRequest(POST=data))
    428418        self.assertTrue(reached[0])
     419
     420    def grab_field_data(self, response):
     421        """
     422        Pull the appropriate field data from the context to pass to the next wizard step
     423        """
     424        previous_fields = response.context['previous_fields']
     425        fields = {'wizard_step': response.context['step0']}
     426
     427        def grab(m):
     428            fields[m.group(1)] = m.group(2)
     429            return ''
     430
     431        self.input_re.sub(grab, previous_fields)
     432        return fields
     433
     434    def check_wizard_step(self, response, step_no):
     435        """
     436        Helper function to test each step of the wizard
     437        - Make sure the call succeeded
     438        - Make sure response is the proper step number
     439        - return the result from the post for the next step
     440        """
     441        step_count = len(self.wizard_step_data)
     442
     443        self.assertEqual(response.status_code, 200)
     444        self.assertContains(response, 'Step %d of %d' % (step_no, step_count))
     445
     446        data = self.grab_field_data(response)
     447        data.update(self.wizard_step_data[step_no - 1])
     448
     449        return self.client.post('/wizard2/', data)
     450
     451    def test_9473(self):
     452        response = self.client.get('/wizard2/')
     453        for step_no in range(1, len(self.wizard_step_data) + 1):
     454            response = self.check_wizard_step(response, step_no)
  • new file django/contrib/formtools/tests/forms.py

    diff --git a/django/contrib/formtools/tests/forms.py b/django/contrib/formtools/tests/forms.py
    new file mode 100644
    index 0000000..93bad92
    - +  
     1from django import forms
     2from django.contrib.formtools.wizard import FormWizard
     3from django.http import HttpResponse
     4
     5class Page1(forms.Form):
     6    name = forms.CharField(max_length=100)
     7    thirsty = forms.NullBooleanField()
     8
     9class Page2(forms.Form):
     10    address1 = forms.CharField(max_length=100)
     11    address2 = forms.CharField(max_length=100)
     12   
     13class Page3(forms.Form):
     14    random_crap = forms.CharField(max_length=100)
     15   
     16class ContactWizard(FormWizard):
     17    def done(self, request, form_list):
     18        return HttpResponse("")
     19
     20class TestForm(forms.Form):
     21    field1 = forms.CharField()
     22    field1_ = forms.CharField()
     23    bool1 = forms.BooleanField(required=False)
     24
     25class HashTestForm(forms.Form):
     26    name = forms.CharField()
     27    bio = forms.CharField()
     28
     29class HashTestBlankForm(forms.Form):
     30    name = forms.CharField(required=False)
     31    bio = forms.CharField(required=False)
     32
     33class WizardPageOneForm(forms.Form):
     34    field = forms.CharField()
     35
     36class WizardPageTwoForm(forms.Form):
     37    field = forms.CharField()
     38
     39class WizardPageTwoAlternativeForm(forms.Form):
     40    field = forms.CharField()
     41
     42class WizardPageThreeForm(forms.Form):
     43    field = forms.CharField()
  • new file django/contrib/formtools/tests/templates/forms/wizard.html

    diff --git a/django/contrib/formtools/tests/templates/forms/wizard.html b/django/contrib/formtools/tests/templates/forms/wizard.html
    new file mode 100644
    index 0000000..c911c3c
    - +  
     1<html>
     2  <body>
     3    <p>Step {{ step }} of {{ step_count }}</p>
     4    <form action="." method="post">
     5    <table>
     6    {{ form }}
     7    </table>
     8    <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
     9    {{ previous_fields|safe }}
     10    <input type="submit">
     11    </form>
     12  </body>
     13</html>
  • deleted file django/contrib/formtools/tests/templates/formwizard/wizard.html

    diff --git a/django/contrib/formtools/tests/templates/formwizard/wizard.html b/django/contrib/formtools/tests/templates/formwizard/wizard.html
    deleted file mode 100644
    index 42b6e78..0000000
    + -  
    1 <p>Step {{ step }} of {{ step_count }}</p>
    2 <form action="." method="post">{% csrf_token %}
    3 <table>
    4 {{ form }}
    5 </table>
    6 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
    7 {{ previous_fields|safe }}
    8 <input type="submit">
    9 </form>
  • django/contrib/formtools/tests/urls.py

    diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py
    index 6fc1e4e..f058335 100644
    a b This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.  
    33"""
    44
    55from django.conf.urls.defaults import *
    6 from django.contrib.formtools.tests import *
     6from django.contrib.formtools.tests import TestFormPreview, TestWizardClass
     7
     8from forms import (ContactWizard, Page1, Page2, Page3, TestForm,
     9    WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm)
    710
    811urlpatterns = patterns('',
    9                        (r'^test1/', TestFormPreview(TestForm)),
    10                        (r'^wizard/$', WizardClass([WizardPageOneForm,
    11                                                    WizardPageTwoForm,
    12                                                    WizardPageThreeForm])),
    13                       )
     12    url(r'^preview/', TestFormPreview(TestForm)),
     13    url(r'^wizard1/$', TestWizardClass(
     14        [WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm])),
     15    url(r'^wizard2/$', ContactWizard([Page1, Page2, Page3])),
     16)
  • deleted file django/contrib/formtools/wizard.py

    diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py
    deleted file mode 100644
    index c19578c..0000000
    + -  
    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 
    7 try:
    8     import cPickle as pickle
    9 except ImportError:
    10     import pickle
    11 
    12 from django import forms
    13 from django.conf import settings
    14 from django.contrib.formtools.utils import form_hmac
    15 from django.http import Http404
    16 from django.shortcuts import render_to_response
    17 from django.template.context import RequestContext
    18 from django.utils.crypto import constant_time_compare
    19 from django.utils.translation import ugettext_lazy as _
    20 from django.utils.decorators import method_decorator
    21 from django.views.decorators.csrf import csrf_protect
    22 
    23 
    24 class FormWizard(object):
    25     # The HTML (and POST data) field name for the "step" variable.
    26     step_field_name="wizard_step"
    27 
    28     # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
    29 
    30     def __init__(self, form_list, initial=None):
    31         """
    32         Start a new wizard with a list of forms.
    33 
    34         form_list should be a list of Form classes (not instances).
    35         """
    36         self.form_list = form_list[:]
    37         self.initial = initial or {}
    38 
    39         # Dictionary of extra template context variables.
    40         self.extra_context = {}
    41 
    42         # A zero-based counter keeping track of which step we're in.
    43         self.step = 0
    44 
    45     def __repr__(self):
    46         return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
    47 
    48     def get_form(self, step, data=None):
    49         "Helper method that returns the Form instance for the given step."
    50         # Sanity check.
    51         if step >= self.num_steps():
    52             raise Http404('Step %s does not exist' % step)
    53         return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
    54 
    55     def num_steps(self):
    56         "Helper method that returns the number of steps."
    57         # You might think we should just set "self.num_steps = len(form_list)"
    58         # in __init__(), but this calculation needs to be dynamic, because some
    59         # hook methods might alter self.form_list.
    60         return len(self.form_list)
    61 
    62     def _check_security_hash(self, token, request, form):
    63         expected = self.security_hash(request, form)
    64         return constant_time_compare(token, expected)
    65 
    66     @method_decorator(csrf_protect)
    67     def __call__(self, request, *args, **kwargs):
    68         """
    69         Main method that does all the hard work, conforming to the Django view
    70         interface.
    71         """
    72         if 'extra_context' in kwargs:
    73             self.extra_context.update(kwargs['extra_context'])
    74         current_step = self.determine_step(request, *args, **kwargs)
    75         self.parse_params(request, *args, **kwargs)
    76 
    77         # Validate and process all the previous forms before instantiating the
    78         # current step's form in case self.process_step makes changes to
    79         # self.form_list.
    80 
    81         # If any of them fails validation, that must mean the validator relied
    82         # on some other input, such as an external Web site.
    83 
    84         # It is also possible that alidation might fail under certain attack
    85         # situations: an attacker might be able to bypass previous stages, and
    86         # generate correct security hashes for all the skipped stages by virtue
    87         # of:
    88         #  1) having filled out an identical form which doesn't have the
    89         #     validation (and does something different at the end),
    90         #  2) or having filled out a previous version of the same form which
    91         #     had some validation missing,
    92         #  3) or previously having filled out the form when they had more
    93         #     privileges than they do now.
    94         #
    95         # Since the hashes only take into account values, and not other other
    96         # validation the form might do, we must re-do validation now for
    97         # security reasons.
    98         previous_form_list = []
    99         for i in range(current_step):
    100             f = self.get_form(i, request.POST)
    101             if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),
    102                                              request, f):
    103                 return self.render_hash_failure(request, i)
    104 
    105             if not f.is_valid():
    106                 return self.render_revalidation_failure(request, i, f)
    107             else:
    108                 self.process_step(request, f, i)
    109                 previous_form_list.append(f)
    110 
    111         # Process the current step. If it's valid, go to the next step or call
    112         # done(), depending on whether any steps remain.
    113         if request.method == 'POST':
    114             form = self.get_form(current_step, request.POST)
    115         else:
    116             form = self.get_form(current_step)
    117 
    118         if form.is_valid():
    119             self.process_step(request, form, current_step)
    120             next_step = current_step + 1
    121 
    122             if next_step == self.num_steps():
    123                 return self.done(request, previous_form_list + [form])
    124             else:
    125                 form = self.get_form(next_step)
    126                 self.step = current_step = next_step
    127 
    128         return self.render(form, request, current_step)
    129 
    130     def render(self, form, request, step, context=None):
    131         "Renders the given Form object, returning an HttpResponse."
    132         old_data = request.POST
    133         prev_fields = []
    134         if old_data:
    135             hidden = forms.HiddenInput()
    136             # Collect all data from previous steps and render it as HTML hidden fields.
    137             for i in range(step):
    138                 old_form = self.get_form(i, old_data)
    139                 hash_name = 'hash_%s' % i
    140                 prev_fields.extend([bf.as_hidden() for bf in old_form])
    141                 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
    142         return self.render_template(request, form, ''.join(prev_fields), step, context)
    143 
    144     # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
    145 
    146     def prefix_for_step(self, step):
    147         "Given the step, returns a Form prefix to use."
    148         return str(step)
    149 
    150     def render_hash_failure(self, request, step):
    151         """
    152         Hook for rendering a template if a hash check failed.
    153 
    154         step is the step that failed. Any previous step is guaranteed to be
    155         valid.
    156 
    157         This default implementation simply renders the form for the given step,
    158         but subclasses may want to display an error message, etc.
    159         """
    160         return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')})
    161 
    162     def render_revalidation_failure(self, request, step, form):
    163         """
    164         Hook for rendering a template if final revalidation failed.
    165 
    166         It is highly unlikely that this point would ever be reached, but See
    167         the comment in __call__() for an explanation.
    168         """
    169         return self.render(form, request, step)
    170 
    171     def security_hash(self, request, form):
    172         """
    173         Calculates the security hash for the given HttpRequest and Form instances.
    174 
    175         Subclasses may want to take into account request-specific information,
    176         such as the IP address.
    177         """
    178         return form_hmac(form)
    179 
    180     def determine_step(self, request, *args, **kwargs):
    181         """
    182         Given the request object and whatever *args and **kwargs were passed to
    183         __call__(), returns the current step (which is zero-based).
    184 
    185         Note that the result should not be trusted. It may even be a completely
    186         invalid number. It's not the job of this method to validate it.
    187         """
    188         if not request.POST:
    189             return 0
    190         try:
    191             step = int(request.POST.get(self.step_field_name, 0))
    192         except ValueError:
    193             return 0
    194         return step
    195 
    196     def parse_params(self, request, *args, **kwargs):
    197         """
    198         Hook for setting some state, given the request object and whatever
    199         *args and **kwargs were passed to __call__(), sets some state.
    200 
    201         This is called at the beginning of __call__().
    202         """
    203         pass
    204 
    205     def get_template(self, step):
    206         """
    207         Hook for specifying the name of the template to use for a given step.
    208 
    209         Note that this can return a tuple of template names if you'd like to
    210         use the template system's select_template() hook.
    211         """
    212         return 'forms/wizard.html'
    213 
    214     def render_template(self, request, form, previous_fields, step, context=None):
    215         """
    216         Renders the template for the given step, returning an HttpResponse object.
    217 
    218         Override this method if you want to add a custom context, return a
    219         different MIME type, etc. If you only need to override the template
    220         name, use get_template() instead.
    221 
    222         The template will be rendered with the following context:
    223             step_field -- The name of the hidden field containing the step.
    224             step0      -- The current step (zero-based).
    225             step       -- The current step (one-based).
    226             step_count -- The total number of steps.
    227             form       -- The Form instance for the current step (either empty
    228                           or with errors).
    229             previous_fields -- A string representing every previous data field,
    230                           plus hashes for completed forms, all in the form of
    231                           hidden fields. Note that you'll need to run this
    232                           through the "safe" template filter, to prevent
    233                           auto-escaping, because it's raw HTML.
    234         """
    235         context = context or {}
    236         context.update(self.extra_context)
    237         return render_to_response(self.get_template(step), dict(context,
    238             step_field=self.step_field_name,
    239             step0=step,
    240             step=step + 1,
    241             step_count=self.num_steps(),
    242             form=form,
    243             previous_fields=previous_fields
    244         ), context_instance=RequestContext(request))
    245 
    246     def process_step(self, request, form, step):
    247         """
    248         Hook for modifying the FormWizard's internal state, given a fully
    249         validated Form object. The Form is guaranteed to have clean, valid
    250         data.
    251 
    252         This method should *not* modify any of that data. Rather, it might want
    253         to set self.extra_context or dynamically alter self.form_list, based on
    254         previously submitted forms.
    255 
    256         Note that this method is called every time a page is rendered for *all*
    257         submitted steps.
    258         """
    259         pass
    260 
    261     # METHODS SUBCLASSES MUST OVERRIDE ########################################
    262 
    263     def done(self, request, form_list):
    264         """
    265         Hook for doing something with the validated data. This is responsible
    266         for the final processing.
    267 
    268         form_list is a list of Form instances, each containing clean, valid
    269         data.
    270         """
    271         raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
  • new file django/contrib/formtools/wizard/__init__.py

    diff --git a/django/contrib/formtools/wizard/__init__.py b/django/contrib/formtools/wizard/__init__.py
    new file mode 100644
    index 0000000..8e51a31
    - +  
     1from django.contrib.formtools.wizard.legacy import FormWizard
  • new file django/contrib/formtools/wizard/forms.py

    diff --git a/django/contrib/formtools/wizard/forms.py b/django/contrib/formtools/wizard/forms.py
    new file mode 100644
    index 0000000..bf46c5c
    - +  
     1from django import forms
     2
     3class ManagementForm(forms.Form):
     4    """
     5    ``ManagementForm`` is used to keep track of the current wizard step.
     6    """
     7    current_step = forms.CharField(widget=forms.HiddenInput)
  • new file django/contrib/formtools/wizard/legacy.py

    diff --git a/django/contrib/formtools/wizard/legacy.py b/django/contrib/formtools/wizard/legacy.py
    new file mode 100644
    index 0000000..532635a
    - +  
     1"""
     2FormWizard class -- implements a multi-page form, validating between each
     3step and storing the form's state as HTML hidden fields so that no state is
     4stored on the server side.
     5"""
     6from django.forms import HiddenInput
     7from django.http import Http404
     8from django.shortcuts import render_to_response
     9from django.template.context import RequestContext
     10from django.utils.crypto import constant_time_compare
     11from django.utils.translation import ugettext_lazy as _
     12from django.utils.decorators import method_decorator
     13from django.views.decorators.csrf import csrf_protect
     14
     15from django.contrib.formtools.utils import form_hmac
     16
     17class FormWizard(object):
     18    # The HTML (and POST data) field name for the "step" variable.
     19    step_field_name="wizard_step"
     20
     21    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
     22
     23    def __init__(self, form_list, initial=None):
     24        """
     25        Start a new wizard with a list of forms.
     26
     27        form_list should be a list of Form classes (not instances).
     28        """
     29        self.form_list = form_list[:]
     30        self.initial = initial or {}
     31
     32        # Dictionary of extra template context variables.
     33        self.extra_context = {}
     34
     35        # A zero-based counter keeping track of which step we're in.
     36        self.step = 0
     37
     38        import warnings
     39        warnings.warn(
     40            'Old-style form wizards have been deprecated; use the class-based '
     41            'views in django.contrib.formtools.wizard.views instead.',
     42            PendingDeprecationWarning)
     43
     44    def __repr__(self):
     45        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
     46
     47    def get_form(self, step, data=None):
     48        "Helper method that returns the Form instance for the given step."
     49        # Sanity check.
     50        if step >= self.num_steps():
     51            raise Http404('Step %s does not exist' % step)
     52        return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
     53
     54    def num_steps(self):
     55        "Helper method that returns the number of steps."
     56        # You might think we should just set "self.num_steps = len(form_list)"
     57        # in __init__(), but this calculation needs to be dynamic, because some
     58        # hook methods might alter self.form_list.
     59        return len(self.form_list)
     60
     61    def _check_security_hash(self, token, request, form):
     62        expected = self.security_hash(request, form)
     63        return constant_time_compare(token, expected)
     64
     65    @method_decorator(csrf_protect)
     66    def __call__(self, request, *args, **kwargs):
     67        """
     68        Main method that does all the hard work, conforming to the Django view
     69        interface.
     70        """
     71        if 'extra_context' in kwargs:
     72            self.extra_context.update(kwargs['extra_context'])
     73        current_step = self.get_current_or_first_step(request, *args, **kwargs)
     74        self.parse_params(request, *args, **kwargs)
     75
     76        # Validate and process all the previous forms before instantiating the
     77        # current step's form in case self.process_step makes changes to
     78        # self.form_list.
     79
     80        # If any of them fails validation, that must mean the validator relied
     81        # on some other input, such as an external Web site.
     82
     83        # It is also possible that alidation might fail under certain attack
     84        # situations: an attacker might be able to bypass previous stages, and
     85        # generate correct security hashes for all the skipped stages by virtue
     86        # of:
     87        #  1) having filled out an identical form which doesn't have the
     88        #     validation (and does something different at the end),
     89        #  2) or having filled out a previous version of the same form which
     90        #     had some validation missing,
     91        #  3) or previously having filled out the form when they had more
     92        #     privileges than they do now.
     93        #
     94        # Since the hashes only take into account values, and not other other
     95        # validation the form might do, we must re-do validation now for
     96        # security reasons.
     97        previous_form_list = []
     98        for i in range(current_step):
     99            f = self.get_form(i, request.POST)
     100            if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),
     101                                             request, f):
     102                return self.render_hash_failure(request, i)
     103
     104            if not f.is_valid():
     105                return self.render_revalidation_failure(request, i, f)
     106            else:
     107                self.process_step(request, f, i)
     108                previous_form_list.append(f)
     109
     110        # Process the current step. If it's valid, go to the next step or call
     111        # done(), depending on whether any steps remain.
     112        if request.method == 'POST':
     113            form = self.get_form(current_step, request.POST)
     114        else:
     115            form = self.get_form(current_step)
     116
     117        if form.is_valid():
     118            self.process_step(request, form, current_step)
     119            next_step = current_step + 1
     120
     121            if next_step == self.num_steps():
     122                return self.done(request, previous_form_list + [form])
     123            else:
     124                form = self.get_form(next_step)
     125                self.step = current_step = next_step
     126
     127        return self.render(form, request, current_step)
     128
     129    def render(self, form, request, step, context=None):
     130        "Renders the given Form object, returning an HttpResponse."
     131        old_data = request.POST
     132        prev_fields = []
     133        if old_data:
     134            hidden = HiddenInput()
     135            # Collect all data from previous steps and render it as HTML hidden fields.
     136            for i in range(step):
     137                old_form = self.get_form(i, old_data)
     138                hash_name = 'hash_%s' % i
     139                prev_fields.extend([bf.as_hidden() for bf in old_form])
     140                prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
     141        return self.render_template(request, form, ''.join(prev_fields), step, context)
     142
     143    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
     144
     145    def prefix_for_step(self, step):
     146        "Given the step, returns a Form prefix to use."
     147        return str(step)
     148
     149    def render_hash_failure(self, request, step):
     150        """
     151        Hook for rendering a template if a hash check failed.
     152
     153        step is the step that failed. Any previous step is guaranteed to be
     154        valid.
     155
     156        This default implementation simply renders the form for the given step,
     157        but subclasses may want to display an error message, etc.
     158        """
     159        return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')})
     160
     161    def render_revalidation_failure(self, request, step, form):
     162        """
     163        Hook for rendering a template if final revalidation failed.
     164
     165        It is highly unlikely that this point would ever be reached, but See
     166        the comment in __call__() for an explanation.
     167        """
     168        return self.render(form, request, step)
     169
     170    def security_hash(self, request, form):
     171        """
     172        Calculates the security hash for the given HttpRequest and Form instances.
     173
     174        Subclasses may want to take into account request-specific information,
     175        such as the IP address.
     176        """
     177        return form_hmac(form)
     178
     179    def get_current_or_first_step(self, request, *args, **kwargs):
     180        """
     181        Given the request object and whatever *args and **kwargs were passed to
     182        __call__(), returns the current step (which is zero-based).
     183
     184        Note that the result should not be trusted. It may even be a completely
     185        invalid number. It's not the job of this method to validate it.
     186        """
     187        if not request.POST:
     188            return 0
     189        try:
     190            step = int(request.POST.get(self.step_field_name, 0))
     191        except ValueError:
     192            return 0
     193        return step
     194
     195    def parse_params(self, request, *args, **kwargs):
     196        """
     197        Hook for setting some state, given the request object and whatever
     198        *args and **kwargs were passed to __call__(), sets some state.
     199
     200        This is called at the beginning of __call__().
     201        """
     202        pass
     203
     204    def get_template(self, step):
     205        """
     206        Hook for specifying the name of the template to use for a given step.
     207
     208        Note that this can return a tuple of template names if you'd like to
     209        use the template system's select_template() hook.
     210        """
     211        return 'forms/wizard.html'
     212
     213    def render_template(self, request, form, previous_fields, step, context=None):
     214        """
     215        Renders the template for the given step, returning an HttpResponse object.
     216
     217        Override this method if you want to add a custom context, return a
     218        different MIME type, etc. If you only need to override the template
     219        name, use get_template() instead.
     220
     221        The template will be rendered with the following context:
     222            step_field -- The name of the hidden field containing the step.
     223            step0      -- The current step (zero-based).
     224            step       -- The current step (one-based).
     225            step_count -- The total number of steps.
     226            form       -- The Form instance for the current step (either empty
     227                          or with errors).
     228            previous_fields -- A string representing every previous data field,
     229                          plus hashes for completed forms, all in the form of
     230                          hidden fields. Note that you'll need to run this
     231                          through the "safe" template filter, to prevent
     232                          auto-escaping, because it's raw HTML.
     233        """
     234        context = context or {}
     235        context.update(self.extra_context)
     236        return render_to_response(self.get_template(step), dict(context,
     237            step_field=self.step_field_name,
     238            step0=step,
     239            step=step + 1,
     240            step_count=self.num_steps(),
     241            form=form,
     242            previous_fields=previous_fields
     243        ), context_instance=RequestContext(request))
     244
     245    def process_step(self, request, form, step):
     246        """
     247        Hook for modifying the FormWizard's internal state, given a fully
     248        validated Form object. The Form is guaranteed to have clean, valid
     249        data.
     250
     251        This method should *not* modify any of that data. Rather, it might want
     252        to set self.extra_context or dynamically alter self.form_list, based on
     253        previously submitted forms.
     254
     255        Note that this method is called every time a page is rendered for *all*
     256        submitted steps.
     257        """
     258        pass
     259
     260    # METHODS SUBCLASSES MUST OVERRIDE ########################################
     261
     262    def done(self, request, form_list):
     263        """
     264        Hook for doing something with the validated data. This is responsible
     265        for the final processing.
     266
     267        form_list is a list of Form instances, each containing clean, valid
     268        data.
     269        """
     270        raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
  • new file django/contrib/formtools/wizard/storage/__init__.py

    diff --git a/django/contrib/formtools/wizard/storage/__init__.py b/django/contrib/formtools/wizard/storage/__init__.py
    new file mode 100644
    index 0000000..b88ccc7
    - +  
     1from django.utils.importlib import import_module
     2
     3from django.contrib.formtools.wizard.storage.base import BaseStorage
     4from django.contrib.formtools.wizard.storage.exceptions import (
     5    MissingStorageModule, MissingStorageClass, NoFileStorageConfigured)
     6
     7
     8def get_storage(path, *args, **kwargs):
     9    i = path.rfind('.')
     10    module, attr = path[:i], path[i+1:]
     11    try:
     12        mod = import_module(module)
     13    except ImportError, e:
     14        raise MissingStorageModule(
     15            'Error loading storage %s: "%s"' % (module, e))
     16    try:
     17        storage_class = getattr(mod, attr)
     18    except AttributeError:
     19        raise MissingStorageClass(
     20            'Module "%s" does not define a storage named "%s"' % (module, attr))
     21    return storage_class(*args, **kwargs)
     22
  • new file django/contrib/formtools/wizard/storage/base.py

    diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py
    new file mode 100644
    index 0000000..475b39d
    - +  
     1from django.core.files.uploadedfile import UploadedFile
     2from django.utils.functional import lazy_property
     3from django.utils.encoding import smart_str
     4
     5from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured
     6
     7class BaseStorage(object):
     8    step_key = 'step'
     9    step_data_key = 'step_data'
     10    step_files_key = 'step_files'
     11    extra_data_key = 'extra_data'
     12
     13    def __init__(self, prefix, request=None, file_storage=None):
     14        self.prefix = 'wizard_%s' % prefix
     15        self.request = request
     16        self.file_storage = file_storage
     17
     18    def init_data(self):
     19        self.data = {
     20            self.step_key: None,
     21            self.step_data_key: {},
     22            self.step_files_key: {},
     23            self.extra_data_key: {},
     24        }
     25
     26    def reset(self):
     27        self.init_data()
     28
     29    def _get_current_step(self):
     30        return self.data[self.step_key]
     31
     32    def _set_current_step(self, step):
     33        self.data[self.step_key] = step
     34
     35    current_step = lazy_property(_get_current_step, _set_current_step)
     36
     37    def _get_extra_data(self):
     38        return self.data[self.extra_data_key] or {}
     39
     40    def _set_extra_data(self, extra_data):
     41        self.data[self.extra_data_key] = extra_data
     42
     43    extra_data = lazy_property(_get_extra_data, _set_extra_data)
     44
     45    def get_step_data(self, step):
     46        return self.data[self.step_data_key].get(step, None)
     47
     48    def set_step_data(self, step, cleaned_data):
     49        self.data[self.step_data_key][step] = cleaned_data
     50
     51    @property
     52    def current_step_data(self):
     53        return self.get_step_data(self.current_step)
     54
     55    def get_step_files(self, step):
     56        wizard_files = self.data[self.step_files_key].get(step, {})
     57
     58        if wizard_files and not self.file_storage:
     59            raise NoFileStorageConfigured
     60
     61        files = {}
     62        for field, field_dict in wizard_files.iteritems():
     63            field_dict = dict((smart_str(k), v)
     64                              for k, v in field_dict.iteritems())
     65            tmp_name = field_dict.pop('tmp_name')
     66            files[field] = UploadedFile(
     67                file=self.file_storage.open(tmp_name), **field_dict)
     68        return files or None
     69
     70    def set_step_files(self, step, files):
     71        if files and not self.file_storage:
     72            raise NoFileStorageConfigured
     73
     74        if step not in self.data[self.step_files_key]:
     75            self.data[self.step_files_key][step] = {}
     76
     77        for field, field_file in (files or {}).iteritems():
     78            tmp_filename = self.file_storage.save(field_file.name, field_file)
     79            file_dict = {
     80                'tmp_name': tmp_filename,
     81                'name': field_file.name,
     82                'content_type': field_file.content_type,
     83                'size': field_file.size,
     84                'charset': field_file.charset
     85            }
     86            self.data[self.step_files_key][step][field] = file_dict
     87
     88    @property
     89    def current_step_files(self):
     90        return self.get_step_files(self.current_step)
     91
     92    def update_response(self, response):
     93        pass
  • new file django/contrib/formtools/wizard/storage/cookie.py

    diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py
    new file mode 100644
    index 0000000..af26e01
    - +  
     1from django.core.exceptions import SuspiciousOperation
     2from django.core.signing import BadSignature
     3from django.utils import simplejson as json
     4
     5from django.contrib.formtools.wizard import storage
     6
     7
     8class CookieStorage(storage.BaseStorage):
     9    encoder = json.JSONEncoder(separators=(',', ':'))
     10
     11    def __init__(self, *args, **kwargs):
     12        super(CookieStorage, self).__init__(*args, **kwargs)
     13        self.data = self.load_data()
     14        if self.data is None:
     15            self.init_data()
     16
     17    def load_data(self):
     18        try:
     19            data = self.request.get_signed_cookie(self.prefix)
     20        except KeyError:
     21            data = None
     22        except BadSignature:
     23            raise SuspiciousOperation('FormWizard cookie manipulated')
     24        if data is None:
     25            return None
     26        return json.loads(data, cls=json.JSONDecoder)
     27
     28    def update_response(self, response):
     29        if self.data:
     30            response.set_signed_cookie(self.prefix, self.encoder.encode(self.data))
     31        else:
     32            response.delete_cookie(self.prefix)
  • new file django/contrib/formtools/wizard/storage/exceptions.py

    diff --git a/django/contrib/formtools/wizard/storage/exceptions.py b/django/contrib/formtools/wizard/storage/exceptions.py
    new file mode 100644
    index 0000000..eab9030
    - +  
     1from django.core.exceptions import ImproperlyConfigured
     2
     3class MissingStorageModule(ImproperlyConfigured):
     4    pass
     5
     6class MissingStorageClass(ImproperlyConfigured):
     7    pass
     8
     9class NoFileStorageConfigured(ImproperlyConfigured):
     10    pass
  • new file django/contrib/formtools/wizard/storage/session.py

    diff --git a/django/contrib/formtools/wizard/storage/session.py b/django/contrib/formtools/wizard/storage/session.py
    new file mode 100644
    index 0000000..84a3848
    - +  
     1from django.core.files.uploadedfile import UploadedFile
     2from django.contrib.formtools.wizard import storage
     3
     4
     5class SessionStorage(storage.BaseStorage):
     6
     7    def __init__(self, *args, **kwargs):
     8        super(SessionStorage, self).__init__(*args, **kwargs)
     9        if self.prefix not in self.request.session:
     10            self.init_data()
     11
     12    def _get_data(self):
     13        self.request.session.modified = True
     14        return self.request.session[self.prefix]
     15
     16    def _set_data(self, value):
     17        self.request.session[self.prefix] = value
     18        self.request.session.modified = True
     19
     20    data = property(_get_data, _set_data)
  • new file django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html

    diff --git a/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html b/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
    new file mode 100644
    index 0000000..b98e58d
    - +  
     1{% load i18n %}
     2{% csrf_token %}
     3{{ wizard.management_form }}
     4{% if wizard.form.forms %}
     5    {{ wizard.form.management_form }}
     6    {% for form in wizard.form.forms %}
     7        {{ form.as_p }}
     8    {% endfor %}
     9{% else %}
     10    {{ wizard.form.as_p }}
     11{% endif %}
     12
     13{% if wizard.steps.prev %}
     14<button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button>
     15<button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
     16{% endif %}
     17<input type="submit" name="submit" value="{% trans "submit" %}" />
  • new file django/contrib/formtools/wizard/tests/__init__.py

    diff --git a/django/contrib/formtools/wizard/tests/__init__.py b/django/contrib/formtools/wizard/tests/__init__.py
    new file mode 100644
    index 0000000..7c66c82
    - +  
     1from django.contrib.formtools.wizard.tests.formtests import *
     2from django.contrib.formtools.wizard.tests.sessionstoragetests import *
     3from django.contrib.formtools.wizard.tests.cookiestoragetests import *
     4from django.contrib.formtools.wizard.tests.loadstoragetests import *
     5from django.contrib.formtools.wizard.tests.wizardtests import *
     6from django.contrib.formtools.wizard.tests.namedwizardtests import *
  • new file django/contrib/formtools/wizard/tests/cookiestoragetests.py

    diff --git a/django/contrib/formtools/wizard/tests/cookiestoragetests.py b/django/contrib/formtools/wizard/tests/cookiestoragetests.py
    new file mode 100644
    index 0000000..74c7e82
    - +  
     1from django.test import TestCase
     2from django.core import signing
     3from django.core.exceptions import SuspiciousOperation
     4from django.http import HttpResponse
     5
     6from django.contrib.formtools.wizard.storage.cookie import CookieStorage
     7from django.contrib.formtools.wizard.tests.storagetests import get_request, TestStorage
     8
     9class TestCookieStorage(TestStorage, TestCase):
     10    def get_storage(self):
     11        return CookieStorage
     12
     13    def test_manipulated_cookie(self):
     14        request = get_request()
     15        storage = self.get_storage()('wizard1', request, None)
     16
     17        cookie_signer = signing.get_cookie_signer(storage.prefix)
     18
     19        storage.request.COOKIES[storage.prefix] = cookie_signer.sign(
     20            storage.encoder.encode({'key1': 'value1'}))
     21
     22        self.assertEqual(storage.load_data(), {'key1': 'value1'})
     23
     24        storage.request.COOKIES[storage.prefix] = 'i_am_manipulated'
     25        self.assertRaises(SuspiciousOperation, storage.load_data)
     26
     27    def test_reset_cookie(self):
     28        request = get_request()
     29        storage = self.get_storage()('wizard1', request, None)
     30
     31        storage.data = {'key1': 'value1'}
     32
     33        response = HttpResponse()
     34        storage.update_response(response)
     35
     36        cookie_signer = signing.get_cookie_signer(storage.prefix)
     37        signed_cookie_data = cookie_signer.sign(storage.encoder.encode(storage.data))
     38        self.assertEqual(response.cookies[storage.prefix].value, signed_cookie_data)
     39
     40        storage.init_data()
     41        storage.update_response(response)
     42        unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value)
     43        self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}')
  • new file django/contrib/formtools/wizard/tests/formtests.py

    diff --git a/django/contrib/formtools/wizard/tests/formtests.py b/django/contrib/formtools/wizard/tests/formtests.py
    new file mode 100644
    index 0000000..111981f
    - +  
     1from django import forms, http
     2from django.conf import settings
     3from django.test import TestCase
     4from django.template.response import TemplateResponse
     5from django.utils.importlib import import_module
     6
     7from django.contrib.auth.models import User
     8
     9from django.contrib.formtools.wizard.views import (WizardView,
     10                                                   SessionWizardView,
     11                                                   CookieWizardView)
     12
     13
     14class DummyRequest(http.HttpRequest):
     15    def __init__(self, POST=None):
     16        super(DummyRequest, self).__init__()
     17        self.method = POST and "POST" or "GET"
     18        if POST is not None:
     19            self.POST.update(POST)
     20        self.session = {}
     21        self._dont_enforce_csrf_checks = True
     22
     23def get_request(*args, **kwargs):
     24    request = DummyRequest(*args, **kwargs)
     25    engine = import_module(settings.SESSION_ENGINE)
     26    request.session = engine.SessionStore(None)
     27    return request
     28
     29class Step1(forms.Form):
     30    name = forms.CharField()
     31
     32class Step2(forms.Form):
     33    name = forms.CharField()
     34
     35class Step3(forms.Form):
     36    data = forms.CharField()
     37
     38class UserForm(forms.ModelForm):
     39    class Meta:
     40        model = User
     41
     42UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2)
     43
     44class TestWizard(WizardView):
     45    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     46
     47    def dispatch(self, request, *args, **kwargs):
     48        response = super(TestWizard, self).dispatch(request, *args, **kwargs)
     49        return response, self
     50
     51class FormTests(TestCase):
     52    def test_form_init(self):
     53        testform = TestWizard.get_initkwargs([Step1, Step2])
     54        self.assertEquals(testform['form_list'], {u'0': Step1, u'1': Step2})
     55
     56        testform = TestWizard.get_initkwargs([('start', Step1), ('step2', Step2)])
     57        self.assertEquals(
     58            testform['form_list'], {u'start': Step1, u'step2': Step2})
     59
     60        testform = TestWizard.get_initkwargs([Step1, Step2, ('finish', Step3)])
     61        self.assertEquals(
     62            testform['form_list'], {u'0': Step1, u'1': Step2, u'finish': Step3})
     63
     64    def test_first_step(self):
     65        request = get_request()
     66
     67        testform = TestWizard.as_view([Step1, Step2])
     68        response, instance = testform(request)
     69        self.assertEquals(instance.steps.current, u'0')
     70
     71        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     72        response, instance = testform(request)
     73
     74        self.assertEquals(instance.steps.current, 'start')
     75
     76    def test_persistence(self):
     77        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     78        request = get_request({'test_wizard-current_step': 'start',
     79                               'name': 'data1'})
     80        response, instance = testform(request)
     81        self.assertEquals(instance.steps.current, 'start')
     82
     83        instance.storage.current_step = 'step2'
     84
     85        testform2 = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     86        request.POST = {'test_wizard-current_step': 'step2'}
     87        response, instance = testform2(request)
     88        self.assertEquals(instance.steps.current, 'step2')
     89
     90    def test_form_condition(self):
     91        request = get_request()
     92
     93        testform = TestWizard.as_view(
     94            [('start', Step1), ('step2', Step2), ('step3', Step3)],
     95            condition_dict={'step2': True})
     96        response, instance = testform(request)
     97        self.assertEquals(instance.get_next_step(), 'step2')
     98
     99        testform = TestWizard.as_view(
     100            [('start', Step1), ('step2', Step2), ('step3', Step3)],
     101            condition_dict={'step2': False})
     102        response, instance = testform(request)
     103        self.assertEquals(instance.get_next_step(), 'step3')
     104
     105    def test_form_prefix(self):
     106        request = get_request()
     107
     108        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     109        response, instance = testform(request)
     110
     111        self.assertEqual(instance.get_form_prefix(), 'start')
     112        self.assertEqual(instance.get_form_prefix('another'), 'another')
     113
     114    def test_form_initial(self):
     115        request = get_request()
     116
     117        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)],
     118            initial_dict={'start': {'name': 'value1'}})
     119        response, instance = testform(request)
     120
     121        self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'})
     122        self.assertEqual(instance.get_form_initial('step2'), {})
     123
     124    def test_form_instance(self):
     125        request = get_request()
     126        the_instance = User()
     127        testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)],
     128            instance_dict={'start': the_instance})
     129        response, instance = testform(request)
     130
     131        self.assertEqual(
     132            instance.get_form_instance('start'),
     133            the_instance)
     134        self.assertEqual(
     135            instance.get_form_instance('non_exist_instance'),
     136            None)
     137
     138    def test_formset_instance(self):
     139        request = get_request()
     140        the_instance1, created = User.objects.get_or_create(
     141            username='testuser1')
     142        the_instance2, created = User.objects.get_or_create(
     143            username='testuser2')
     144        testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)],
     145            instance_dict={'start': User.objects.filter(username='testuser1')})
     146        response, instance = testform(request)
     147
     148        self.assertEqual(list(instance.get_form_instance('start')), [the_instance1])
     149        self.assertEqual(instance.get_form_instance('non_exist_instance'), None)
     150
     151        self.assertEqual(instance.get_form().initial_form_count(), 1)
     152
     153    def test_done(self):
     154        request = get_request()
     155
     156        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     157        response, instance = testform(request)
     158
     159        self.assertRaises(NotImplementedError, instance.done, None)
     160
     161    def test_revalidation(self):
     162        request = get_request()
     163
     164        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     165        response, instance = testform(request)
     166        instance.render_done(None)
     167        self.assertEqual(instance.storage.current_step, 'start')
     168
     169
     170class SessionFormTests(TestCase):
     171    def test_init(self):
     172        request = get_request()
     173        testform = SessionWizardView.as_view([('start', Step1)])
     174        self.assertTrue(isinstance(testform(request), TemplateResponse))
     175
     176
     177class CookieFormTests(TestCase):
     178    def test_init(self):
     179        request = get_request()
     180        testform = CookieWizardView.as_view([('start', Step1)])
     181        self.assertTrue(isinstance(testform(request), TemplateResponse))
     182
  • new file django/contrib/formtools/wizard/tests/loadstoragetests.py

    diff --git a/django/contrib/formtools/wizard/tests/loadstoragetests.py b/django/contrib/formtools/wizard/tests/loadstoragetests.py
    new file mode 100644
    index 0000000..267dee0
    - +  
     1from django.test import TestCase
     2
     3from django.contrib.formtools.wizard.storage import (get_storage,
     4                                                     MissingStorageModule,
     5                                                     MissingStorageClass)
     6from django.contrib.formtools.wizard.storage.base import BaseStorage
     7
     8
     9class TestLoadStorage(TestCase):
     10    def test_load_storage(self):
     11        self.assertEqual(
     12            type(get_storage('django.contrib.formtools.wizard.storage.base.BaseStorage', 'wizard1')),
     13            BaseStorage)
     14
     15    def test_missing_module(self):
     16        self.assertRaises(MissingStorageModule, get_storage,
     17            'django.contrib.formtools.wizard.storage.idontexist.IDontExistStorage', 'wizard1')
     18
     19    def test_missing_class(self):
     20        self.assertRaises(MissingStorageClass, get_storage,
     21            'django.contrib.formtools.wizard.storage.base.IDontExistStorage', 'wizard1')
     22
  • new file django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py

    diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py b/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
    new file mode 100644
    index 0000000..4387356
    - +  
     1from django.contrib.formtools.wizard.tests.namedwizardtests.tests import *
     2 No newline at end of file
  • new file django/contrib/formtools/wizard/tests/namedwizardtests/forms.py

    diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py b/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
    new file mode 100644
    index 0000000..ae98126
    - +  
     1from django import forms
     2from django.forms.formsets import formset_factory
     3from django.http import HttpResponse
     4from django.template import Template, Context
     5
     6from django.contrib.auth.models import User
     7
     8from django.contrib.formtools.wizard.views import NamedUrlWizardView
     9
     10class Page1(forms.Form):
     11    name = forms.CharField(max_length=100)
     12    user = forms.ModelChoiceField(queryset=User.objects.all())
     13    thirsty = forms.NullBooleanField()
     14
     15class Page2(forms.Form):
     16    address1 = forms.CharField(max_length=100)
     17    address2 = forms.CharField(max_length=100)
     18
     19class Page3(forms.Form):
     20    random_crap = forms.CharField(max_length=100)
     21
     22Page4 = formset_factory(Page3, extra=2)
     23
     24class ContactWizard(NamedUrlWizardView):
     25    def done(self, form_list, **kwargs):
     26        c = Context({
     27            'form_list': [x.cleaned_data for x in form_list],
     28            'all_cleaned_data': self.get_all_cleaned_data()
     29        })
     30
     31        for form in self.form_list.keys():
     32            c[form] = self.get_cleaned_data_for_step(form)
     33
     34        c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
     35        return HttpResponse(Template('').render(c))
     36
     37class SessionContactWizard(ContactWizard):
     38    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     39
     40class CookieContactWizard(ContactWizard):
     41    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     42
  • new file django/contrib/formtools/wizard/tests/namedwizardtests/tests.py

    diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py b/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
    new file mode 100644
    index 0000000..cc442d7
    - +  
     1import os
     2
     3from django.core.urlresolvers import reverse
     4from django.http import QueryDict
     5from django.test import TestCase
     6from django.conf import settings
     7
     8from django.contrib.auth.models import User
     9
     10from django.contrib.formtools import wizard
     11
     12from django.contrib.formtools.wizard.views import (NamedUrlSessionWizardView,
     13                                                   NamedUrlCookieWizardView)
     14from django.contrib.formtools.wizard.tests.formtests import (get_request,
     15                                                             Step1,
     16                                                             Step2)
     17
     18class NamedWizardTests(object):
     19    urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
     20
     21    def setUp(self):
     22        self.testuser, created = User.objects.get_or_create(username='testuser1')
     23        self.wizard_step_data[0]['form1-user'] = self.testuser.pk
     24
     25        wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
     26        settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
     27
     28    def tearDown(self):
     29        del settings.TEMPLATE_DIRS[-1]
     30
     31    def test_initial_call(self):
     32        response = self.client.get(reverse('%s_start' % self.wizard_urlname))
     33        self.assertEqual(response.status_code, 302)
     34        response = self.client.get(response['Location'])
     35        self.assertEqual(response.status_code, 200)
     36        wizard = response.context['wizard']
     37        self.assertEqual(wizard['steps'].current, 'form1')
     38        self.assertEqual(wizard['steps'].step0, 0)
     39        self.assertEqual(wizard['steps'].step1, 1)
     40        self.assertEqual(wizard['steps'].last, 'form4')
     41        self.assertEqual(wizard['steps'].prev, None)
     42        self.assertEqual(wizard['steps'].next, 'form2')
     43        self.assertEqual(wizard['steps'].count, 4)
     44
     45    def test_initial_call_with_params(self):
     46        get_params = {'getvar1': 'getval1', 'getvar2': 'getval2'}
     47        response = self.client.get(reverse('%s_start' % self.wizard_urlname),
     48                                   get_params)
     49        self.assertEqual(response.status_code, 302)
     50
     51        # Test for proper redirect GET parameters
     52        location = response['Location']
     53        self.assertNotEqual(location.find('?'), -1)
     54        querydict = QueryDict(location[location.find('?') + 1:])
     55        self.assertEqual(dict(querydict.items()), get_params)
     56
     57    def test_form_post_error(self):
     58        response = self.client.post(
     59            reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
     60            self.wizard_step_1_data)
     61
     62        self.assertEqual(response.status_code, 200)
     63        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     64        self.assertEqual(response.context['wizard']['form'].errors,
     65                         {'name': [u'This field is required.'],
     66                          'user': [u'This field is required.']})
     67
     68    def test_form_post_success(self):
     69        response = self.client.post(
     70            reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
     71            self.wizard_step_data[0])
     72        response = self.client.get(response['Location'])
     73
     74        self.assertEqual(response.status_code, 200)
     75        wizard = response.context['wizard']
     76        self.assertEqual(wizard['steps'].current, 'form2')
     77        self.assertEqual(wizard['steps'].step0, 1)
     78        self.assertEqual(wizard['steps'].prev, 'form1')
     79        self.assertEqual(wizard['steps'].next, 'form3')
     80
     81    def test_form_stepback(self):
     82        response = self.client.get(
     83            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
     84
     85        self.assertEqual(response.status_code, 200)
     86        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     87
     88        response = self.client.post(
     89            reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
     90            self.wizard_step_data[0])
     91        response = self.client.get(response['Location'])
     92
     93        self.assertEqual(response.status_code, 200)
     94        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     95
     96        response = self.client.post(
     97            reverse(self.wizard_urlname, kwargs={
     98                'step': response.context['wizard']['steps'].current
     99            }), {'wizard_prev_step': response.context['wizard']['steps'].prev})
     100        response = self.client.get(response['Location'])
     101
     102        self.assertEqual(response.status_code, 200)
     103        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     104
     105    def test_form_jump(self):
     106        response = self.client.get(
     107            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
     108
     109        self.assertEqual(response.status_code, 200)
     110        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     111
     112        response = self.client.get(
     113            reverse(self.wizard_urlname, kwargs={'step': 'form3'}))
     114        self.assertEqual(response.status_code, 200)
     115        self.assertEqual(response.context['wizard']['steps'].current, 'form3')
     116
     117    def test_form_finish(self):
     118        response = self.client.get(
     119            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
     120
     121        self.assertEqual(response.status_code, 200)
     122        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     123
     124        response = self.client.post(
     125            reverse(self.wizard_urlname,
     126                    kwargs={'step': response.context['wizard']['steps'].current}),
     127            self.wizard_step_data[0])
     128        response = self.client.get(response['Location'])
     129
     130        self.assertEqual(response.status_code, 200)
     131        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     132
     133        response = self.client.post(
     134            reverse(self.wizard_urlname,
     135                    kwargs={'step': response.context['wizard']['steps'].current}),
     136            self.wizard_step_data[1])
     137        response = self.client.get(response['Location'])
     138
     139        self.assertEqual(response.status_code, 200)
     140        self.assertEqual(response.context['wizard']['steps'].current, 'form3')
     141
     142        response = self.client.post(
     143            reverse(self.wizard_urlname,
     144                    kwargs={'step': response.context['wizard']['steps'].current}),
     145            self.wizard_step_data[2])
     146        response = self.client.get(response['Location'])
     147
     148        self.assertEqual(response.status_code, 200)
     149        self.assertEqual(response.context['wizard']['steps'].current, 'form4')
     150
     151        response = self.client.post(
     152            reverse(self.wizard_urlname,
     153                    kwargs={'step': response.context['wizard']['steps'].current}),
     154            self.wizard_step_data[3])
     155        response = self.client.get(response['Location'])
     156        self.assertEqual(response.status_code, 200)
     157
     158        self.assertEqual(response.context['form_list'], [
     159            {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
     160            {'address1': u'123 Main St', 'address2': u'Djangoland'},
     161            {'random_crap': u'blah blah'},
     162            [{'random_crap': u'blah blah'}, {'random_crap': u'blah blah'}]])
     163
     164    def test_cleaned_data(self):
     165        response = self.client.get(
     166            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
     167        self.assertEqual(response.status_code, 200)
     168
     169        response = self.client.post(
     170            reverse(self.wizard_urlname,
     171                    kwargs={'step': response.context['wizard']['steps'].current}),
     172            self.wizard_step_data[0])
     173        response = self.client.get(response['Location'])
     174        self.assertEqual(response.status_code, 200)
     175
     176        response = self.client.post(
     177            reverse(self.wizard_urlname,
     178                    kwargs={'step': response.context['wizard']['steps'].current}),
     179            self.wizard_step_data[1])
     180        response = self.client.get(response['Location'])
     181        self.assertEqual(response.status_code, 200)
     182
     183        response = self.client.post(
     184            reverse(self.wizard_urlname,
     185                    kwargs={'step': response.context['wizard']['steps'].current}),
     186            self.wizard_step_data[2])
     187        response = self.client.get(response['Location'])
     188        self.assertEqual(response.status_code, 200)
     189
     190        response = self.client.post(
     191            reverse(self.wizard_urlname,
     192                    kwargs={'step': response.context['wizard']['steps'].current}),
     193            self.wizard_step_data[3])
     194        response = self.client.get(response['Location'])
     195        self.assertEqual(response.status_code, 200)
     196
     197        self.assertEqual(
     198            response.context['all_cleaned_data'],
     199            {'name': u'Pony', 'thirsty': True, 'user': self.testuser,
     200             'address1': u'123 Main St', 'address2': u'Djangoland',
     201             'random_crap': u'blah blah', 'formset-form4': [
     202                 {'random_crap': u'blah blah'},
     203                 {'random_crap': u'blah blah'}
     204             ]})
     205
     206    def test_manipulated_data(self):
     207        response = self.client.get(
     208            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
     209        self.assertEqual(response.status_code, 200)
     210
     211        response = self.client.post(
     212            reverse(self.wizard_urlname,
     213                    kwargs={'step': response.context['wizard']['steps'].current}),
     214            self.wizard_step_data[0])
     215        response = self.client.get(response['Location'])
     216        self.assertEqual(response.status_code, 200)
     217
     218        response = self.client.post(
     219            reverse(self.wizard_urlname,
     220                    kwargs={'step': response.context['wizard']['steps'].current}),
     221            self.wizard_step_data[1])
     222        response = self.client.get(response['Location'])
     223        self.assertEqual(response.status_code, 200)
     224
     225        response = self.client.post(
     226            reverse(self.wizard_urlname,
     227                    kwargs={'step': response.context['wizard']['steps'].current}),
     228            self.wizard_step_data[2])
     229        loc = response['Location']
     230        response = self.client.get(loc)
     231        self.assertEqual(response.status_code, 200, loc)
     232
     233        self.client.cookies.pop('sessionid', None)
     234        self.client.cookies.pop('wizard_cookie_contact_wizard', None)
     235
     236        response = self.client.post(
     237            reverse(self.wizard_urlname,
     238                    kwargs={'step': response.context['wizard']['steps'].current}),
     239            self.wizard_step_data[3])
     240
     241        self.assertEqual(response.status_code, 200)
     242        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     243
     244    def test_form_reset(self):
     245        response = self.client.post(
     246            reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
     247            self.wizard_step_data[0])
     248        response = self.client.get(response['Location'])
     249        self.assertEqual(response.status_code, 200)
     250        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     251
     252        response = self.client.get(
     253            '%s?reset=1' % reverse('%s_start' % self.wizard_urlname))
     254        self.assertEqual(response.status_code, 302)
     255
     256        response = self.client.get(response['Location'])
     257        self.assertEqual(response.status_code, 200)
     258        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     259
     260class NamedSessionWizardTests(NamedWizardTests, TestCase):
     261    wizard_urlname = 'nwiz_session'
     262    wizard_step_1_data = {
     263        'session_contact_wizard-current_step': 'form1',
     264    }
     265    wizard_step_data = (
     266        {
     267            'form1-name': 'Pony',
     268            'form1-thirsty': '2',
     269            'session_contact_wizard-current_step': 'form1',
     270        },
     271        {
     272            'form2-address1': '123 Main St',
     273            'form2-address2': 'Djangoland',
     274            'session_contact_wizard-current_step': 'form2',
     275        },
     276        {
     277            'form3-random_crap': 'blah blah',
     278            'session_contact_wizard-current_step': 'form3',
     279        },
     280        {
     281            'form4-INITIAL_FORMS': '0',
     282            'form4-TOTAL_FORMS': '2',
     283            'form4-MAX_NUM_FORMS': '0',
     284            'form4-0-random_crap': 'blah blah',
     285            'form4-1-random_crap': 'blah blah',
     286            'session_contact_wizard-current_step': 'form4',
     287        }
     288    )
     289
     290class NamedCookieWizardTests(NamedWizardTests, TestCase):
     291    wizard_urlname = 'nwiz_cookie'
     292    wizard_step_1_data = {
     293        'cookie_contact_wizard-current_step': 'form1',
     294    }
     295    wizard_step_data = (
     296        {
     297            'form1-name': 'Pony',
     298            'form1-thirsty': '2',
     299            'cookie_contact_wizard-current_step': 'form1',
     300        },
     301        {
     302            'form2-address1': '123 Main St',
     303            'form2-address2': 'Djangoland',
     304            'cookie_contact_wizard-current_step': 'form2',
     305        },
     306        {
     307            'form3-random_crap': 'blah blah',
     308            'cookie_contact_wizard-current_step': 'form3',
     309        },
     310        {
     311            'form4-INITIAL_FORMS': '0',
     312            'form4-TOTAL_FORMS': '2',
     313            'form4-MAX_NUM_FORMS': '0',
     314            'form4-0-random_crap': 'blah blah',
     315            'form4-1-random_crap': 'blah blah',
     316            'cookie_contact_wizard-current_step': 'form4',
     317        }
     318    )
     319
     320
     321class NamedFormTests(object):
     322    urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
     323
     324    def test_revalidation(self):
     325        request = get_request()
     326
     327        testform = self.formwizard_class.as_view(
     328            [('start', Step1), ('step2', Step2)],
     329            url_name=self.wizard_urlname)
     330        response, instance = testform(request, step='done')
     331
     332        instance.render_done(None)
     333        self.assertEqual(instance.storage.current_step, 'start')
     334
     335class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView):
     336
     337    def dispatch(self, request, *args, **kwargs):
     338        response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs)
     339        return response, self
     340
     341class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView):
     342
     343    def dispatch(self, request, *args, **kwargs):
     344        response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs)
     345        return response, self
     346
     347
     348class NamedSessionFormTests(NamedFormTests, TestCase):
     349    formwizard_class = TestNamedUrlSessionFormWizard
     350    wizard_urlname = 'nwiz_session'
     351
     352
     353class NamedCookieFormTests(NamedFormTests, TestCase):
     354    formwizard_class = TestNamedUrlCookieFormWizard
     355    wizard_urlname = 'nwiz_cookie'
  • new file django/contrib/formtools/wizard/tests/namedwizardtests/urls.py

    diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py b/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
    new file mode 100644
    index 0000000..a97ca98
    - +  
     1from django.conf.urls.defaults import *
     2from django.contrib.formtools.wizard.tests.namedwizardtests.forms import (
     3    SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
     4
     5def get_named_session_wizard():
     6    return SessionContactWizard.as_view(
     7        [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
     8        url_name='nwiz_session',
     9        done_step_name='nwiz_session_done'
     10    )
     11
     12def get_named_cookie_wizard():
     13    return CookieContactWizard.as_view(
     14        [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
     15        url_name='nwiz_cookie',
     16        done_step_name='nwiz_cookie_done'
     17    )
     18
     19urlpatterns = patterns('',
     20    url(r'^nwiz_session/(?P<step>.+)/$', get_named_session_wizard(), name='nwiz_session'),
     21    url(r'^nwiz_session/$', get_named_session_wizard(), name='nwiz_session_start'),
     22    url(r'^nwiz_cookie/(?P<step>.+)/$', get_named_cookie_wizard(), name='nwiz_cookie'),
     23    url(r'^nwiz_cookie/$', get_named_cookie_wizard(), name='nwiz_cookie_start'),
     24)
  • new file django/contrib/formtools/wizard/tests/sessionstoragetests.py

    diff --git a/django/contrib/formtools/wizard/tests/sessionstoragetests.py b/django/contrib/formtools/wizard/tests/sessionstoragetests.py
    new file mode 100644
    index 0000000..c643921
    - +  
     1from django.test import TestCase
     2
     3from django.contrib.formtools.wizard.tests.storagetests import TestStorage
     4from django.contrib.formtools.wizard.storage.session import SessionStorage
     5
     6class TestSessionStorage(TestStorage, TestCase):
     7    def get_storage(self):
     8        return SessionStorage
  • new file django/contrib/formtools/wizard/tests/storagetests.py

    diff --git a/django/contrib/formtools/wizard/tests/storagetests.py b/django/contrib/formtools/wizard/tests/storagetests.py
    new file mode 100644
    index 0000000..fec4fae
    - +  
     1from datetime import datetime
     2
     3from django.http import HttpRequest
     4from django.conf import settings
     5from django.utils.importlib import import_module
     6
     7from django.contrib.auth.models import User
     8
     9def get_request():
     10    request = HttpRequest()
     11    engine = import_module(settings.SESSION_ENGINE)
     12    request.session = engine.SessionStore(None)
     13    return request
     14
     15class TestStorage(object):
     16    def setUp(self):
     17        self.testuser, created = User.objects.get_or_create(username='testuser1')
     18
     19    def test_current_step(self):
     20        request = get_request()
     21        storage = self.get_storage()('wizard1', request, None)
     22        my_step = 2
     23
     24        self.assertEqual(storage.current_step, None)
     25
     26        storage.current_step = my_step
     27        self.assertEqual(storage.current_step, my_step)
     28
     29        storage.reset()
     30        self.assertEqual(storage.current_step, None)
     31
     32        storage.current_step = my_step
     33        storage2 = self.get_storage()('wizard2', request, None)
     34        self.assertEqual(storage2.current_step, None)
     35
     36    def test_step_data(self):
     37        request = get_request()
     38        storage = self.get_storage()('wizard1', request, None)
     39        step1 = 'start'
     40        step_data1 = {'field1': 'data1',
     41                      'field2': 'data2',
     42                      'field3': datetime.now(),
     43                      'field4': self.testuser}
     44
     45        self.assertEqual(storage.get_step_data(step1), None)
     46
     47        storage.set_step_data(step1, step_data1)
     48        self.assertEqual(storage.get_step_data(step1), step_data1)
     49
     50        storage.reset()
     51        self.assertEqual(storage.get_step_data(step1), None)
     52
     53        storage.set_step_data(step1, step_data1)
     54        storage2 = self.get_storage()('wizard2', request, None)
     55        self.assertEqual(storage2.get_step_data(step1), None)
     56
     57    def test_extra_context(self):
     58        request = get_request()
     59        storage = self.get_storage()('wizard1', request, None)
     60        extra_context = {'key1': 'data1',
     61                         'key2': 'data2',
     62                         'key3': datetime.now(),
     63                         'key4': self.testuser}
     64
     65        self.assertEqual(storage.extra_data, {})
     66
     67        storage.extra_data = extra_context
     68        self.assertEqual(storage.extra_data, extra_context)
     69
     70        storage.reset()
     71        self.assertEqual(storage.extra_data, {})
     72
     73        storage.extra_data = extra_context
     74        storage2 = self.get_storage()('wizard2', request, None)
     75        self.assertEqual(storage2.extra_data, {})
     76
  • new file django/contrib/formtools/wizard/tests/wizardtests/__init__.py

    diff --git a/django/contrib/formtools/wizard/tests/wizardtests/__init__.py b/django/contrib/formtools/wizard/tests/wizardtests/__init__.py
    new file mode 100644
    index 0000000..9173cd8
    - +  
     1from django.contrib.formtools.wizard.tests.wizardtests.tests import *
     2 No newline at end of file
  • new file django/contrib/formtools/wizard/tests/wizardtests/forms.py

    diff --git a/django/contrib/formtools/wizard/tests/wizardtests/forms.py b/django/contrib/formtools/wizard/tests/wizardtests/forms.py
    new file mode 100644
    index 0000000..726d74a
    - +  
     1import tempfile
     2
     3from django import forms
     4from django.core.files.storage import FileSystemStorage
     5from django.forms.formsets import formset_factory
     6from django.http import HttpResponse
     7from django.template import Template, Context
     8
     9from django.contrib.auth.models import User
     10
     11from django.contrib.formtools.wizard.views import WizardView
     12
     13temp_storage_location = tempfile.mkdtemp()
     14temp_storage = FileSystemStorage(location=temp_storage_location)
     15
     16class Page1(forms.Form):
     17    name = forms.CharField(max_length=100)
     18    user = forms.ModelChoiceField(queryset=User.objects.all())
     19    thirsty = forms.NullBooleanField()
     20
     21class Page2(forms.Form):
     22    address1 = forms.CharField(max_length=100)
     23    address2 = forms.CharField(max_length=100)
     24    file1 = forms.FileField()
     25
     26class Page3(forms.Form):
     27    random_crap = forms.CharField(max_length=100)
     28
     29Page4 = formset_factory(Page3, extra=2)
     30
     31class ContactWizard(WizardView):
     32    file_storage = temp_storage
     33
     34    def done(self, form_list, **kwargs):
     35        c = Context({
     36            'form_list': [x.cleaned_data for x in form_list],
     37            'all_cleaned_data': self.get_all_cleaned_data()
     38        })
     39
     40        for form in self.form_list.keys():
     41            c[form] = self.get_cleaned_data_for_step(form)
     42
     43        c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
     44        return HttpResponse(Template('').render(c))
     45
     46    def get_context_data(self, form, **kwargs):
     47        context = super(ContactWizard, self).get_context_data(form, **kwargs)
     48        if self.storage.current_step == 'form2':
     49            context.update({'another_var': True})
     50        return context
     51
     52class SessionContactWizard(ContactWizard):
     53    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     54
     55class CookieContactWizard(ContactWizard):
     56    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     57
  • new file django/contrib/formtools/wizard/tests/wizardtests/tests.py

    diff --git a/django/contrib/formtools/wizard/tests/wizardtests/tests.py b/django/contrib/formtools/wizard/tests/wizardtests/tests.py
    new file mode 100644
    index 0000000..f64b2ba
    - +  
     1import os
     2
     3from django.test import TestCase
     4from django.conf import settings
     5from django.contrib.auth.models import User
     6
     7from django.contrib.formtools import wizard
     8
     9class WizardTests(object):
     10    urls = 'django.contrib.formtools.wizard.tests.wizardtests.urls'
     11
     12    def setUp(self):
     13        self.testuser, created = User.objects.get_or_create(username='testuser1')
     14        self.wizard_step_data[0]['form1-user'] = self.testuser.pk
     15
     16        wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
     17        settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
     18
     19    def tearDown(self):
     20        del settings.TEMPLATE_DIRS[-1]
     21
     22    def test_initial_call(self):
     23        response = self.client.get(self.wizard_url)
     24        wizard = response.context['wizard']
     25        self.assertEqual(response.status_code, 200)
     26        self.assertEqual(wizard['steps'].current, 'form1')
     27        self.assertEqual(wizard['steps'].step0, 0)
     28        self.assertEqual(wizard['steps'].step1, 1)
     29        self.assertEqual(wizard['steps'].last, 'form4')
     30        self.assertEqual(wizard['steps'].prev, None)
     31        self.assertEqual(wizard['steps'].next, 'form2')
     32        self.assertEqual(wizard['steps'].count, 4)
     33
     34    def test_form_post_error(self):
     35        response = self.client.post(self.wizard_url, self.wizard_step_1_data)
     36        self.assertEqual(response.status_code, 200)
     37        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     38        self.assertEqual(response.context['wizard']['form'].errors,
     39                         {'name': [u'This field is required.'],
     40                          'user': [u'This field is required.']})
     41
     42    def test_form_post_success(self):
     43        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     44        wizard = response.context['wizard']
     45        self.assertEqual(response.status_code, 200)
     46        self.assertEqual(wizard['steps'].current, 'form2')
     47        self.assertEqual(wizard['steps'].step0, 1)
     48        self.assertEqual(wizard['steps'].prev, 'form1')
     49        self.assertEqual(wizard['steps'].next, 'form3')
     50
     51    def test_form_stepback(self):
     52        response = self.client.get(self.wizard_url)
     53        self.assertEqual(response.status_code, 200)
     54        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     55
     56        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     57        self.assertEqual(response.status_code, 200)
     58        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     59
     60        response = self.client.post(self.wizard_url, {
     61            'wizard_prev_step': response.context['wizard']['steps'].prev})
     62        self.assertEqual(response.status_code, 200)
     63        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     64
     65    def test_template_context(self):
     66        response = self.client.get(self.wizard_url)
     67        self.assertEqual(response.status_code, 200)
     68        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     69        self.assertEqual(response.context.get('another_var', None), None)
     70
     71        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     72        self.assertEqual(response.status_code, 200)
     73        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     74        self.assertEqual(response.context.get('another_var', None), True)
     75
     76    def test_form_finish(self):
     77        response = self.client.get(self.wizard_url)
     78        self.assertEqual(response.status_code, 200)
     79        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     80
     81        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     82        self.assertEqual(response.status_code, 200)
     83        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     84
     85        post_data = self.wizard_step_data[1]
     86        post_data['form2-file1'] = open(__file__)
     87        response = self.client.post(self.wizard_url, post_data)
     88        self.assertEqual(response.status_code, 200)
     89        self.assertEqual(response.context['wizard']['steps'].current, 'form3')
     90
     91        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
     92        self.assertEqual(response.status_code, 200)
     93        self.assertEqual(response.context['wizard']['steps'].current, 'form4')
     94
     95        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
     96        self.assertEqual(response.status_code, 200)
     97
     98        all_data = response.context['form_list']
     99        self.assertEqual(all_data[1]['file1'].read(), open(__file__).read())
     100        del all_data[1]['file1']
     101        self.assertEqual(all_data, [
     102            {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
     103            {'address1': u'123 Main St', 'address2': u'Djangoland'},
     104            {'random_crap': u'blah blah'},
     105            [{'random_crap': u'blah blah'},
     106             {'random_crap': u'blah blah'}]])
     107
     108    def test_cleaned_data(self):
     109        response = self.client.get(self.wizard_url)
     110        self.assertEqual(response.status_code, 200)
     111
     112        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     113        self.assertEqual(response.status_code, 200)
     114
     115        post_data = self.wizard_step_data[1]
     116        post_data['form2-file1'] = open(__file__)
     117        response = self.client.post(self.wizard_url, post_data)
     118        self.assertEqual(response.status_code, 200)
     119
     120        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
     121        self.assertEqual(response.status_code, 200)
     122
     123        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
     124        self.assertEqual(response.status_code, 200)
     125
     126        all_data = response.context['all_cleaned_data']
     127        self.assertEqual(all_data['file1'].read(), open(__file__).read())
     128        del all_data['file1']
     129        self.assertEqual(all_data, {
     130            'name': u'Pony', 'thirsty': True, 'user': self.testuser,
     131            'address1': u'123 Main St', 'address2': u'Djangoland',
     132            'random_crap': u'blah blah', 'formset-form4': [
     133                {'random_crap': u'blah blah'},
     134                {'random_crap': u'blah blah'}]})
     135
     136    def test_manipulated_data(self):
     137        response = self.client.get(self.wizard_url)
     138        self.assertEqual(response.status_code, 200)
     139
     140        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     141        self.assertEqual(response.status_code, 200)
     142
     143        post_data = self.wizard_step_data[1]
     144        post_data['form2-file1'] = open(__file__)
     145        response = self.client.post(self.wizard_url, post_data)
     146        self.assertEqual(response.status_code, 200)
     147
     148        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
     149        self.assertEqual(response.status_code, 200)
     150        self.client.cookies.pop('sessionid', None)
     151        self.client.cookies.pop('wizard_cookie_contact_wizard', None)
     152
     153        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
     154        self.assertEqual(response.status_code, 200)
     155        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     156
     157    def test_form_refresh(self):
     158        response = self.client.get(self.wizard_url)
     159        self.assertEqual(response.status_code, 200)
     160        self.assertEqual(response.context['wizard']['steps'].current, 'form1')
     161
     162        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     163        self.assertEqual(response.status_code, 200)
     164        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     165
     166        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     167        self.assertEqual(response.status_code, 200)
     168        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     169
     170        post_data = self.wizard_step_data[1]
     171        post_data['form2-file1'] = open(__file__)
     172        response = self.client.post(self.wizard_url, post_data)
     173        self.assertEqual(response.status_code, 200)
     174        self.assertEqual(response.context['wizard']['steps'].current, 'form3')
     175
     176        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
     177        self.assertEqual(response.status_code, 200)
     178        self.assertEqual(response.context['wizard']['steps'].current, 'form4')
     179
     180        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
     181        self.assertEqual(response.status_code, 200)
     182        self.assertEqual(response.context['wizard']['steps'].current, 'form2')
     183
     184        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
     185        self.assertEqual(response.status_code, 200)
     186
     187
     188class SessionWizardTests(WizardTests, TestCase):
     189    wizard_url = '/wiz_session/'
     190    wizard_step_1_data = {
     191        'session_contact_wizard-current_step': 'form1',
     192    }
     193    wizard_step_data = (
     194        {
     195            'form1-name': 'Pony',
     196            'form1-thirsty': '2',
     197            'session_contact_wizard-current_step': 'form1',
     198        },
     199        {
     200            'form2-address1': '123 Main St',
     201            'form2-address2': 'Djangoland',
     202            'session_contact_wizard-current_step': 'form2',
     203        },
     204        {
     205            'form3-random_crap': 'blah blah',
     206            'session_contact_wizard-current_step': 'form3',
     207        },
     208        {
     209            'form4-INITIAL_FORMS': '0',
     210            'form4-TOTAL_FORMS': '2',
     211            'form4-MAX_NUM_FORMS': '0',
     212            'form4-0-random_crap': 'blah blah',
     213            'form4-1-random_crap': 'blah blah',
     214            'session_contact_wizard-current_step': 'form4',
     215        }
     216    )
     217
     218class CookieWizardTests(WizardTests, TestCase):
     219    wizard_url = '/wiz_cookie/'
     220    wizard_step_1_data = {
     221        'cookie_contact_wizard-current_step': 'form1',
     222    }
     223    wizard_step_data = (
     224        {
     225            'form1-name': 'Pony',
     226            'form1-thirsty': '2',
     227            'cookie_contact_wizard-current_step': 'form1',
     228        },
     229        {
     230            'form2-address1': '123 Main St',
     231            'form2-address2': 'Djangoland',
     232            'cookie_contact_wizard-current_step': 'form2',
     233        },
     234        {
     235            'form3-random_crap': 'blah blah',
     236            'cookie_contact_wizard-current_step': 'form3',
     237        },
     238        {
     239            'form4-INITIAL_FORMS': '0',
     240            'form4-TOTAL_FORMS': '2',
     241            'form4-MAX_NUM_FORMS': '0',
     242            'form4-0-random_crap': 'blah blah',
     243            'form4-1-random_crap': 'blah blah',
     244            'cookie_contact_wizard-current_step': 'form4',
     245        }
     246    )
     247
     248
  • new file django/contrib/formtools/wizard/tests/wizardtests/urls.py

    diff --git a/django/contrib/formtools/wizard/tests/wizardtests/urls.py b/django/contrib/formtools/wizard/tests/wizardtests/urls.py
    new file mode 100644
    index 0000000..e305397
    - +  
     1from django.conf.urls.defaults import *
     2from django.contrib.formtools.wizard.tests.wizardtests.forms import (
     3    SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
     4
     5urlpatterns = patterns('',
     6    url(r'^wiz_session/$', SessionContactWizard.as_view(
     7        [('form1', Page1),
     8         ('form2', Page2),
     9         ('form3', Page3),
     10         ('form4', Page4)])),
     11    url(r'^wiz_cookie/$', CookieContactWizard.as_view(
     12        [('form1', Page1),
     13         ('form2', Page2),
     14         ('form3', Page3),
     15         ('form4', Page4)])),
     16)
  • new file django/contrib/formtools/wizard/views.py

    diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
    new file mode 100644
    index 0000000..c0f8b3b
    - +  
     1import copy
     2import re
     3
     4from django import forms
     5from django.shortcuts import redirect
     6from django.core.urlresolvers import reverse
     7from django.forms import formsets, ValidationError
     8from django.views.generic import TemplateView
     9from django.utils.datastructures import SortedDict
     10from django.utils.decorators import classonlymethod
     11
     12from django.contrib.formtools.wizard.storage import get_storage
     13from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured
     14from django.contrib.formtools.wizard.forms import ManagementForm
     15
     16
     17def normalize_name(name):
     18    new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
     19    return new.lower().strip('_')
     20
     21class StepsHelper(object):
     22
     23    def __init__(self, wizard):
     24        self._wizard = wizard
     25
     26    def __dir__(self):
     27        return self.all
     28
     29    def __len__(self):
     30        return self.count
     31
     32    def __repr__(self):
     33        return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all)
     34
     35    @property
     36    def all(self):
     37        "Returns the names of all steps/forms."
     38        return self._wizard.get_form_list().keys()
     39
     40    @property
     41    def count(self):
     42        "Returns the total number of steps/forms in this the wizard."
     43        return len(self.all)
     44
     45    @property
     46    def current(self):
     47        """
     48        Returns the current step. If no current step is stored in the
     49        storage backend, the first step will be returned.
     50        """
     51        return self._wizard.storage.current_step or self.first
     52
     53    @property
     54    def first(self):
     55        "Returns the name of the first step."
     56        return self.all[0]
     57
     58    @property
     59    def last(self):
     60        "Returns the name of the last step."
     61        return self.all[-1]
     62
     63    @property
     64    def next(self):
     65        "Returns the next step."
     66        return self._wizard.get_next_step()
     67
     68    @property
     69    def prev(self):
     70        "Returns the previous step."
     71        return self._wizard.get_prev_step()
     72
     73    @property
     74    def index(self):
     75        "Returns the index for the current step."
     76        return self._wizard.get_step_index()
     77
     78    @property
     79    def step0(self):
     80        return int(self.index)
     81
     82    @property
     83    def step1(self):
     84        return int(self.index) + 1
     85
     86
     87class WizardView(TemplateView):
     88    """
     89    The WizardView is used to create multi-page forms and handles all the
     90    storage and validation stuff. The wizard is based on Django's generic
     91    class based views.
     92    """
     93    storage_name = None
     94    form_list = None
     95    initial_dict = None
     96    instance_dict = None
     97    condition_dict = None
     98    template_name = 'formtools/wizard/wizard_form.html'
     99
     100    def __repr__(self):
     101        return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list)
     102
     103    @classonlymethod
     104    def as_view(cls, *args, **kwargs):
     105        """
     106        This method is used within urls.py to create unique formwizard
     107        instances for every request. We need to override this method because
     108        we add some kwargs which are needed to make the formwizard usable.
     109        """
     110        initkwargs = cls.get_initkwargs(*args, **kwargs)
     111        return super(WizardView, cls).as_view(**initkwargs)
     112
     113    @classmethod
     114    def get_initkwargs(cls, form_list,
     115            initial_dict=None, instance_dict=None, condition_dict=None):
     116        """
     117        Creates a dict with all needed parameters for the form wizard instances.
     118
     119        * `form_list` - is a list of forms. The list entries can be single form
     120          classes or tuples of (`step_name`, `form_class`). If you pass a list
     121          of forms, the formwizard will convert the class list to
     122          (`zero_based_counter`, `form_class`). This is needed to access the
     123          form for a specific step.
     124        * `initial_dict` - contains a dictionary of initial data dictionaries.
     125          The key should be equal to the `step_name` in the `form_list` (or
     126          the str of the zero based counter - if no step_names added in the
     127          `form_list`)
     128        * `instance_dict` - contains a dictionary of instance objects. This list
     129          is only used when `ModelForm`s are used. The key should be equal to
     130          the `step_name` in the `form_list`. Same rules as for `initial_dict`
     131          apply.
     132        * `condition_dict` - contains a dictionary of boolean values or
     133          callables. If the value of for a specific `step_name` is callable it
     134          will be called with the formwizard instance as the only argument.
     135          If the return value is true, the step's form will be used.
     136        """
     137        kwargs = {
     138            'initial_dict': initial_dict or {},
     139            'instance_dict': instance_dict or {},
     140            'condition_dict': condition_dict or {},
     141        }
     142        init_form_list = SortedDict()
     143
     144        assert len(form_list) > 0, 'at least one form is needed'
     145
     146        # walk through the passed form list
     147        for i, form in enumerate(form_list):
     148            if isinstance(form, (list, tuple)):
     149                # if the element is a tuple, add the tuple to the new created
     150                # sorted dictionary.
     151                init_form_list[unicode(form[0])] = form[1]
     152            else:
     153                # if not, add the form with a zero based counter as unicode
     154                init_form_list[unicode(i)] = form
     155
     156        # walk through the ne created list of forms
     157        for form in init_form_list.itervalues():
     158            if issubclass(form, formsets.BaseFormSet):
     159                # if the element is based on BaseFormSet (FormSet/ModelFormSet)
     160                # we need to override the form variable.
     161                form = form.form
     162            # check if any form contains a FileField, if yes, we need a
     163            # file_storage added to the formwizard (by subclassing).
     164            for field in form.base_fields.itervalues():
     165                if (isinstance(field, forms.FileField) and
     166                        not hasattr(cls, 'file_storage')):
     167                    raise NoFileStorageConfigured
     168
     169        # build the kwargs for the formwizard instances
     170        kwargs['form_list'] = init_form_list
     171        return kwargs
     172
     173    def get_wizard_name(self):
     174        return normalize_name(self.__class__.__name__)
     175
     176    def get_prefix(self):
     177        # TODO: Add some kind of unique id to prefix
     178        return self.wizard_name
     179
     180    def get_form_list(self):
     181        """
     182        This method returns a form_list based on the initial form list but
     183        checks if there is a condition method/value in the condition_list.
     184        If an entry exists in the condition list, it will call/read the value
     185        and respect the result. (True means add the form, False means ignore
     186        the form)
     187
     188        The form_list is always generated on the fly because condition methods
     189        could use data from other (maybe previous forms).
     190        """
     191        form_list = SortedDict()
     192        for form_key, form_class in self.form_list.iteritems():
     193            # try to fetch the value from condition list, by default, the form
     194            # gets passed to the new list.
     195            condition = self.condition_dict.get(form_key, True)
     196            if callable(condition):
     197                # call the value if needed, passes the current instance.
     198                condition = condition(self)
     199            if condition:
     200                form_list[form_key] = form_class
     201        return form_list
     202
     203    def dispatch(self, request, *args, **kwargs):
     204        """
     205        This method gets called by the routing engine. The first argument is
     206        `request` which contains a `HttpRequest` instance.
     207        The request is stored in `self.request` for later use. The storage
     208        instance is stored in `self.storage`.
     209
     210        After processing the request using the `dispatch` method, the
     211        response gets updated by the storage engine (for example add cookies).
     212        """
     213        # add the storage engine to the current formwizard instance
     214        self.wizard_name = self.get_wizard_name()
     215        self.prefix = self.get_prefix()
     216        self.storage = get_storage(self.storage_name, self.prefix, request,
     217            getattr(self, 'file_storage', None))
     218        self.steps = StepsHelper(self)
     219        response = super(WizardView, self).dispatch(request, *args, **kwargs)
     220
     221        # update the response (e.g. adding cookies)
     222        self.storage.update_response(response)
     223        return response
     224
     225    def get(self, request, *args, **kwargs):
     226        """
     227        This method handles GET requests.
     228
     229        If a GET request reaches this point, the wizard assumes that the user
     230        just starts at the first step or wants to restart the process.
     231        The data of the wizard will be resetted before rendering the first step.
     232        """
     233        self.storage.reset()
     234
     235        # reset the current step to the first step.
     236        self.storage.current_step = self.steps.first
     237        return self.render(self.get_form())
     238
     239    def post(self, *args, **kwargs):
     240        """
     241        This method handles POST requests.
     242
     243        The wizard will render either the current step (if form validation
     244        wasn't successful), the next step (if the current step was stored
     245        successful) or the done view (if no more steps are available)
     246        """
     247        # Look for a wizard_prev_step element in the posted data which
     248        # contains a valid step name. If one was found, render the requested
     249        # form. (This makes stepping back a lot easier).
     250        wizard_prev_step = self.request.POST.get('wizard_prev_step', None)
     251        if wizard_prev_step and wizard_prev_step in self.get_form_list():
     252            self.storage.current_step = wizard_prev_step
     253            form = self.get_form(
     254                data=self.storage.get_step_data(self.steps.current),
     255                files=self.storage.get_step_files(self.steps.current))
     256            return self.render(form)
     257
     258        # Check if form was refreshed
     259        management_form = ManagementForm(self.request.POST, prefix=self.prefix)
     260        if not management_form.is_valid():
     261            raise ValidationError(
     262                'ManagementForm data is missing or has been tampered.')
     263
     264        form_current_step = management_form.cleaned_data['current_step']
     265        if (form_current_step != self.steps.current and
     266                self.storage.current_step is not None):
     267            # form refreshed, change current step
     268            self.storage.current_step = form_current_step
     269
     270        # get the form for the current step
     271        form = self.get_form(data=self.request.POST, files=self.request.FILES)
     272
     273        # and try to validate
     274        if form.is_valid():
     275            # if the form is valid, store the cleaned data and files.
     276            self.storage.set_step_data(self.steps.current, self.process_step(form))
     277            self.storage.set_step_files(self.steps.current, self.process_step_files(form))
     278
     279            # check if the current step is the last step
     280            if self.steps.current == self.steps.last:
     281                # no more steps, render done view
     282                return self.render_done(form, **kwargs)
     283            else:
     284                # proceed to the next step
     285                return self.render_next_step(form)
     286        return self.render(form)
     287
     288    def render_next_step(self, form, **kwargs):
     289        """
     290        THis method gets called when the next step/form should be rendered.
     291        `form` contains the last/current form.
     292        """
     293        # get the form instance based on the data from the storage backend
     294        # (if available).
     295        next_step = self.steps.next
     296        new_form = self.get_form(next_step,
     297            data=self.storage.get_step_data(next_step),
     298            files=self.storage.get_step_files(next_step))
     299
     300        # change the stored current step
     301        self.storage.current_step = next_step
     302        return self.render(new_form, **kwargs)
     303
     304    def render_done(self, form, **kwargs):
     305        """
     306        This method gets called when all forms passed. The method should also
     307        re-validate all steps to prevent manipulation. If any form don't
     308        validate, `render_revalidation_failure` should get called.
     309        If everything is fine call `done`.
     310        """
     311        final_form_list = []
     312        # walk through the form list and try to validate the data again.
     313        for form_key in self.get_form_list():
     314            form_obj = self.get_form(step=form_key,
     315                data=self.storage.get_step_data(form_key),
     316                files=self.storage.get_step_files(form_key))
     317            if not form_obj.is_valid():
     318                return self.render_revalidation_failure(form_key, form_obj, **kwargs)
     319            final_form_list.append(form_obj)
     320
     321        # render the done view and reset the wizard before returning the
     322        # response. This is needed to prevent from rendering done with the
     323        # same data twice.
     324        done_response = self.done(final_form_list, **kwargs)
     325        self.storage.reset()
     326        return done_response
     327
     328    def get_form_prefix(self, step=None, form=None):
     329        """
     330        Returns the prefix which will be used when calling the actual form for
     331        the given step. `step` contains the step-name, `form` the form which
     332        will be called with the returned prefix.
     333
     334        If no step is given, the form_prefix will determine the current step
     335        automatically.
     336        """
     337        if step is None:
     338            step = self.steps.current
     339        return str(step)
     340
     341    def get_form_initial(self, step):
     342        """
     343        Returns a dictionary which will be passed to the form for `step`
     344        as `initial`. If no initial data was provied while initializing the
     345        form wizard, a empty dictionary will be returned.
     346        """
     347        return self.initial_dict.get(step, {})
     348
     349    def get_form_instance(self, step):
     350        """
     351        Returns a object which will be passed to the form for `step`
     352        as `instance`. If no instance object was provied while initializing
     353        the form wizard, None be returned.
     354        """
     355        return self.instance_dict.get(step, None)
     356
     357    def get_form(self, step=None, data=None, files=None):
     358        """
     359        Constructs the form for a given `step`. If no `step` is defined, the
     360        current step will be determined automatically.
     361
     362        The form will be initialized using the `data` argument to prefill the
     363        new form. If needed, instance or queryset (for `ModelForm` or
     364        `ModelFormSet`) will be added too.
     365        """
     366        if step is None:
     367            step = self.steps.current
     368        # prepare the kwargs for the form instance.
     369        kwargs = {
     370            'data': data,
     371            'files': files,
     372            'prefix': self.get_form_prefix(step, self.form_list[step]),
     373            'initial': self.get_form_initial(step),
     374        }
     375        if issubclass(self.form_list[step], forms.ModelForm):
     376            # If the form is based on ModelForm, add instance if available.
     377            kwargs.update({'instance': self.get_form_instance(step)})
     378        elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
     379            # If the form is based on ModelFormSet, add queryset if available.
     380            kwargs.update({'queryset': self.get_form_instance(step)})
     381        return self.form_list[step](**kwargs)
     382
     383    def process_step(self, form):
     384        """
     385        This method is used to postprocess the form data. By default, it
     386        returns the raw `form.data` dictionary.
     387        """
     388        return self.get_form_step_data(form)
     389
     390    def process_step_files(self, form):
     391        """
     392        This method is used to postprocess the form files. By default, it
     393        returns the raw `form.files` dictionary.
     394        """
     395        return self.get_form_step_files(form)
     396
     397    def render_revalidation_failure(self, step, form, **kwargs):
     398        """
     399        Gets called when a form doesn't validate when rendering the done
     400        view. By default, it changed the current step to failing forms step
     401        and renders the form.
     402        """
     403        self.storage.current_step = step
     404        return self.render(form, **kwargs)
     405
     406    def get_form_step_data(self, form):
     407        """
     408        Is used to return the raw form data. You may use this method to
     409        manipulate the data.
     410        """
     411        return form.data
     412
     413    def get_form_step_files(self, form):
     414        """
     415        Is used to return the raw form files. You may use this method to
     416        manipulate the data.
     417        """
     418        return form.files
     419
     420    def get_all_cleaned_data(self):
     421        """
     422        Returns a merged dictionary of all step cleaned_data dictionaries.
     423        If a step contains a `FormSet`, the key will be prefixed with formset
     424        and contain a list of the formset' cleaned_data dictionaries.
     425        """
     426        cleaned_data = {}
     427        for form_key in self.get_form_list():
     428            form_obj = self.get_form(
     429                step=form_key,
     430                data=self.storage.get_step_data(form_key),
     431                files=self.storage.get_step_files(form_key)
     432            )
     433            if form_obj.is_valid():
     434                if isinstance(form_obj.cleaned_data, (tuple, list)):
     435                    cleaned_data.update({
     436                        'formset-%s' % form_key: form_obj.cleaned_data
     437                    })
     438                else:
     439                    cleaned_data.update(form_obj.cleaned_data)
     440        return cleaned_data
     441
     442    def get_cleaned_data_for_step(self, step):
     443        """
     444        Returns the cleaned data for a given `step`. Before returning the
     445        cleaned data, the stored values are being revalidated through the
     446        form. If the data doesn't validate, None will be returned.
     447        """
     448        if step in self.form_list:
     449            form_obj = self.get_form(step=step,
     450                data=self.storage.get_step_data(step),
     451                files=self.storage.get_step_files(step))
     452            if form_obj.is_valid():
     453                return form_obj.cleaned_data
     454        return None
     455
     456    def get_next_step(self, step=None):
     457        """
     458        Returns the next step after the given `step`. If no more steps are
     459        available, None will be returned. If the `step` argument is None, the
     460        current step will be determined automatically.
     461        """
     462        if step is None:
     463            step = self.steps.current
     464        form_list = self.get_form_list()
     465        key = form_list.keyOrder.index(step) + 1
     466        if len(form_list.keyOrder) > key:
     467            return form_list.keyOrder[key]
     468        return None
     469
     470    def get_prev_step(self, step=None):
     471        """
     472        Returns the previous step before the given `step`. If there are no
     473        steps available, None will be returned. If the `step` argument is
     474        None, the current step will be determined automatically.
     475        """
     476        if step is None:
     477            step = self.steps.current
     478        form_list = self.get_form_list()
     479        key = form_list.keyOrder.index(step) - 1
     480        if key >= 0:
     481            return form_list.keyOrder[key]
     482        return None
     483
     484    def get_step_index(self, step=None):
     485        """
     486        Returns the index for the given `step` name. If no step is given,
     487        the current step will be used to get the index.
     488        """
     489        if step is None:
     490            step = self.steps.current
     491        return self.get_form_list().keyOrder.index(step)
     492
     493    def get_context_data(self, form, *args, **kwargs):
     494        """
     495        Returns the template context for a step. You can overwrite this method
     496        to add more data for all or some steps. This method returns a
     497        dictionary containing the rendered form step. Available template
     498        context variables are:
     499
     500         * all extra data stored in the storage backend
     501         * `form` - form instance of the current step
     502         * `wizard` - the wizard instance itself
     503
     504        Example:
     505
     506        .. code-block:: python
     507
     508            class MyWizard(FormWizard):
     509                def get_context_data(self, form, **kwargs):
     510                    context = super(MyWizard, self).get_context_data(form, **kwargs)
     511                    if self.steps.current == 'my_step_name':
     512                        context.update({'another_var': True})
     513                    return context
     514        """
     515        context = super(WizardView, self).get_context_data(*args, **kwargs)
     516        context.update(self.storage.extra_data)
     517        context['wizard'] = {
     518            'form': form,
     519            'steps': self.steps,
     520            'managenent_form': ManagementForm(prefix=self.prefix, initial={
     521                'current_step': self.steps.current,
     522            }),
     523        }
     524        return context
     525
     526    def render(self, form=None, **kwargs):
     527        """
     528        Returns a ``HttpResponse`` containing a all needed context data.
     529        """
     530        form = form or self.get_form()
     531        context = self.get_context_data(form, **kwargs)
     532        return self.render_to_response(context)
     533
     534    def done(self, form_list, **kwargs):
     535        """
     536        This method muss be overrided by a subclass to process to form data
     537        after processing all steps.
     538        """
     539        raise NotImplementedError("Your %s class has not defined a done() "
     540            "method, which is required." % self.__class__.__name__)
     541
     542
     543class SessionWizardView(WizardView):
     544    """
     545    A WizardView with pre-configured SessionStorage backend.
     546    """
     547    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     548
     549
     550class CookieWizardView(WizardView):
     551    """
     552    A WizardView with pre-configured CookieStorage backend.
     553    """
     554    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     555
     556
     557class NamedUrlWizardView(WizardView):
     558    """
     559    A WizardView with URL named steps support.
     560    """
     561    url_name = None
     562    done_step_name = None
     563
     564    @classmethod
     565    def get_initkwargs(cls, *args, **kwargs):
     566        """
     567        We require a url_name to reverse URLs later. Additionally users can
     568        pass a done_step_name to change the URL name of the "done" view.
     569        """
     570        extra_kwargs = {
     571            'done_step_name': 'done'
     572        }
     573        assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs'
     574        extra_kwargs['url_name'] = kwargs.pop('url_name')
     575
     576        if 'done_step_name' in kwargs:
     577            extra_kwargs['done_step_name'] = kwargs.pop('done_step_name')
     578
     579        initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs)
     580        initkwargs.update(extra_kwargs)
     581
     582        assert initkwargs['done_step_name'] not in initkwargs['form_list'], \
     583            'step name "%s" is reserved for "done" view' % initkwargs['done_step_name']
     584
     585        return initkwargs
     586
     587    def get(self, *args, **kwargs):
     588        """
     589        This renders the form or, if needed, does the http redirects.
     590        """
     591        step_url = kwargs.get('step', None)
     592        if step_url is None:
     593            if 'reset' in self.request.GET:
     594                self.storage.reset()
     595                self.storage.current_step = self.steps.first
     596            if self.request.GET:
     597                query_string = "?%s" % self.request.GET.urlencode()
     598            else:
     599                query_string = ""
     600            next_step_url = reverse(self.url_name, kwargs={
     601                'step': self.steps.current,
     602            }) + query_string
     603            return redirect(next_step_url)
     604
     605        # is the current step the "done" name/view?
     606        elif step_url == self.done_step_name:
     607            last_step = self.steps.last
     608            return self.render_done(self.get_form(step=last_step,
     609                data=self.storage.get_step_data(last_step),
     610                files=self.storage.get_step_files(last_step)
     611            ), **kwargs)
     612
     613        # is the url step name not equal to the step in the storage?
     614        # if yes, change the step in the storage (if name exists)
     615        elif step_url == self.steps.current:
     616            # URL step name and storage step name are equal, render!
     617            return self.render(self.get_form(
     618                data=self.storage.current_step_data,
     619                files=self.storage.current_step_data,
     620            ), **kwargs)
     621
     622        elif step_url in self.get_form_list():
     623            self.storage.current_step = step_url
     624            return self.render(self.get_form(
     625                data=self.storage.current_step_data,
     626                files=self.storage.current_step_data,
     627            ), **kwargs)
     628
     629        # invalid step name, reset to first and redirect.
     630        else:
     631            self.storage.current_step = self.steps.first
     632            return redirect(self.url_name, step=self.steps.first)
     633
     634    def post(self, *args, **kwargs):
     635        """
     636        Do a redirect if user presses the prev. step button. The rest of this
     637        is super'd from FormWizard.
     638        """
     639        prev_step = self.request.POST.get('wizard_prev_step', None)
     640        if prev_step and prev_step in self.get_form_list():
     641            self.storage.current_step = prev_step
     642            return redirect(self.url_name, step=prev_step)
     643        return super(NamedUrlWizardView, self).post(*args, **kwargs)
     644
     645    def render_next_step(self, form, **kwargs):
     646        """
     647        When using the NamedUrlFormWizard, we have to redirect to update the
     648        browser's URL to match the shown step.
     649        """
     650        next_step = self.get_next_step()
     651        self.storage.current_step = next_step
     652        return redirect(self.url_name, step=next_step)
     653
     654    def render_revalidation_failure(self, failed_step, form, **kwargs):
     655        """
     656        When a step fails, we have to redirect the user to the first failing
     657        step.
     658        """
     659        self.storage.current_step = failed_step
     660        return redirect(self.url_name, step=failed_step)
     661
     662    def render_done(self, form, **kwargs):
     663        """
     664        When rendering the done view, we have to redirect first (if the URL
     665        name doesn't fit).
     666        """
     667        if kwargs.get('step', None) != self.done_step_name:
     668            return redirect(self.url_name, step=self.done_step_name)
     669        return super(NamedUrlWizardView, self).render_done(form, **kwargs)
     670
     671
     672class NamedUrlSessionWizardView(NamedUrlWizardView):
     673    """
     674    A NamedUrlWizardView with pre-configured SessionStorage backend.
     675    """
     676    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     677
     678
     679class NamedUrlCookieWizardView(NamedUrlWizardView):
     680    """
     681    A NamedUrlFormWizard with pre-configured CookieStorageBackend.
     682    """
     683    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     684
  • django/utils/functional.py

    diff --git a/django/utils/functional.py b/django/utils/functional.py
    index b0233de..76f3639 100644
    a b class SimpleLazyObject(LazyObject):  
    265265
    266266    def _setup(self):
    267267        self._wrapped = self._setupfunc()
     268
     269
     270class lazy_property(property):
     271    """
     272    A property that works with subclasses by wrapping the decorated
     273    functions of the base class.
     274    """
     275    def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
     276        if fget is not None:
     277            @wraps(fget)
     278            def fget(instance, instance_type=None, name=fget.__name__):
     279                return getattr(instance, name)()
     280        if fset is not None:
     281            @wraps(fset)
     282            def fset(instance, value, name=fset.__name__):
     283                return getattr(instance, name)(value)
     284        if fdel is not None:
     285            @wraps(fdel)
     286            def fdel(instance, name=fdel.__name__):
     287                return getattr(instance, name)()
     288        return property(fget, fset, fdel, doc)
  • docs/internals/deprecation.txt

    diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
    index 01e4500..4d81cb7 100644
    a b their deprecation, as per the :ref:`Django deprecation policy  
    203203          settings have been superseded by :setting:`IGNORABLE_404_URLS` in
    204204          the 1.4 release. They will be removed.
    205205
     206        * The :doc:`form wizard </ref/contrib/formtools/form-wizard>` has been
     207          refactored to use class based views with pluggable backends in 1.4.
     208          The previous implementation will be deprecated.
     209
    206210    * 2.0
    207211        * ``django.views.defaults.shortcut()``. This function has been moved
    208212          to ``django.contrib.contenttypes.views.shortcut()`` as part of the
  • docs/ref/contrib/formtools/form-wizard.txt

    diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt
    index cbacd59..2434c8b 100644
    a b  
    22Form wizard
    33===========
    44
    5 .. module:: django.contrib.formtools.wizard
     5.. module:: django.contrib.formtools.wizard.views
    66    :synopsis: Splits forms across multiple Web pages.
    77
    88Django comes with an optional "form wizard" application that splits
    99:doc:`forms </topics/forms/index>` across multiple Web pages. It maintains
    10 state in hashed HTML :samp:`<input type="hidden">` fields so that the full
    11 server-side processing can be delayed until the submission of the final form.
     10state in one of the backends so that the full server-side processing can be
     11delayed until the submission of the final form.
    1212
    1313You might want to use this if you have a lengthy form that would be too
    1414unwieldy for display on a single page. The first page might ask the user for
    1515core information, the second page might ask for less important information,
    1616etc.
    1717
    18 The term "wizard," in this context, is `explained on Wikipedia`_.
     18The term "wizard", in this context, is `explained on Wikipedia`_.
    1919
    2020.. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29
    21 .. _forms: ../forms/
    2221
    2322How it works
    2423============
    Here's the basic workflow for how a user would use a wizard:  
    2827    1. The user visits the first page of the wizard, fills in the form and
    2928       submits it.
    3029    2. The server validates the data. If it's invalid, the form is displayed
    31        again, with error messages. If it's valid, the server calculates a
    32        secure hash of the data and presents the user with the next form,
    33        saving the validated data and hash in :samp:`<input type="hidden">`
    34        fields.
     30       again, with error messages. If it's valid, the server saves the current
     31       state of the wizard in the backend and redirects to the next step.
    3532    3. Step 1 and 2 repeat, for every subsequent form in the wizard.
    3633    4. Once the user has submitted all the forms and all the data has been
    3734       validated, the wizard processes the data -- saving it to the database,
    Here's the basic workflow for how a user would use a wizard:  
    4037Usage
    4138=====
    4239
    43 This application handles as much machinery for you as possible. Generally, you
    44 just have to do these things:
     40This application handles as much machinery for you as possible. Generally,
     41you just have to do these things:
    4542
    46     1. Define a number of :class:`~django.forms.Form` classes -- one per wizard
    47        page.
     43    1. Define a number of :class:`~django.forms.Form` classes -- one per
     44       wizard page.
    4845
    49     2. Create a :class:`FormWizard` class that specifies what to do once all of
    50        your forms have been submitted and validated. This also lets you
    51        override some of the wizard's behavior.
     46    2. Create a :class:`WizardView` subclass that specifies what to do once
     47       all of your forms have been submitted and validated. This also lets
     48       you override some of the wizard's behavior.
    5249
    5350    3. Create some templates that render the forms. You can define a single,
    5451       generic template to handle every one of the forms, or you can define a
    5552       specific template for each form.
    5653
    57     4. Point your URLconf at your :class:`FormWizard` class.
     54    4. Add ``django.contrib.formtools.wizard`` to your
     55       :setting:`INSTALLED_APPS` list in your settings file.
     56
     57    5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method.
    5858
    5959Defining ``Form`` classes
    60 =========================
     60-------------------------
    6161
    6262The first step in creating a form wizard is to create the
    6363:class:`~django.forms.Form` classes.  These should be standard
    6464:class:`django.forms.Form` classes, covered in the :doc:`forms documentation
    65 </topics/forms/index>`.  These classes can live anywhere in your codebase, but
    66 convention is to put them in a file called :file:`forms.py` in your
     65</topics/forms/index>`.  These classes can live anywhere in your codebase,
     66but convention is to put them in a file called :file:`forms.py` in your
    6767application.
    6868
    6969For example, let's write a "contact form" wizard, where the first page's form
    the message itself. Here's what the :file:`forms.py` might look like::  
    7979    class ContactForm2(forms.Form):
    8080        message = forms.CharField(widget=forms.Textarea)
    8181
    82 **Important limitation:** Because the wizard uses HTML hidden fields to store
    83 data between pages, you may not include a :class:`~django.forms.FileField`
    84 in any form except the last one.
    85 
    86 Creating a ``FormWizard`` class
    87 ===============================
    8882
    89 The next step is to create a
    90 :class:`django.contrib.formtools.wizard.FormWizard` subclass.  As with your
    91 :class:`~django.forms.Form` classes, this :class:`FormWizard` class can live
    92 anywhere in your codebase, but convention is to put it in :file:`forms.py`.
     83.. note::
    9384
    94 The only requirement on this subclass is that it implement a
    95 :meth:`~FormWizard.done()` method.
     85    In order to use :class:`~django.forms.FileField` in any form, see the
     86    section :ref:`Handling files <wizard-files>` below to learn more about
     87    what to do.
    9688
    97 .. method:: FormWizard.done
     89Creating a ``WizardView`` class
     90-------------------------------
    9891
    99     This method specifies what should happen when the data for *every* form is
    100     submitted and validated.  This method is passed two arguments:
     92The next step is to create a
     93:class:`django.contrib.formtools.wizard.view.WizardView` subclass. You can
     94also use the :class:`SessionWizardView` or :class:`CookieWizardView` class
     95which preselects the wizard storage backend.
    10196
    102         * ``request`` -- an :class:`~django.http.HttpRequest` object
    103         * ``form_list`` -- a list of :class:`~django.forms.Form` classes
     97.. note::
    10498
    105 In this simplistic example, rather than perform any database operation, the
    106 method simply renders a template of the validated data::
     99    To use the :class:`SessionWizardView` follow the instructions
     100    in the :doc:`sessions documentation </topics/http/sessions>` on
     101    how to enable sessions.
    107102
    108     from django.shortcuts import render_to_response
    109     from django.contrib.formtools.wizard import FormWizard
     103We will use the :class:`SessionWizardView` in all examples but is is completly
     104fine to use the :class:`CookieWizardView` instead. As with your
     105:class:`~django.forms.Form` classes, this :class:`WizardView` class can live
     106anywhere in your codebase, but convention is to put it in :file:`views.py`.
    110107
    111     class ContactWizard(FormWizard):
    112         def done(self, request, form_list):
    113             return render_to_response('done.html', {
    114                 'form_data': [form.cleaned_data for form in form_list],
    115             })
     108The only requirement on this subclass is that it implement a
     109:meth:`~WizardView.done()` method.
    116110
    117 Note that this method will be called via ``POST``, so it really ought to be a
    118 good Web citizen and redirect after processing the data. Here's another
    119 example::
     111.. method:: WizardView.done(form_list)
    120112
    121     from django.http import HttpResponseRedirect
    122     from django.contrib.formtools.wizard import FormWizard
     113    This method specifies what should happen when the data for *every* form is
     114    submitted and validated. This method is passed a list of validated
     115    :class:`~django.forms.Form` instances.
    123116
    124     class ContactWizard(FormWizard):
    125         def done(self, request, form_list):
    126             do_something_with_the_form_data(form_list)
    127             return HttpResponseRedirect('/page-to-redirect-to-when-done/')
     117    In this simplistic example, rather than performing any database operation,
     118    the method simply renders a template of the validated data::
    128119
    129 See the section `Advanced FormWizard methods`_ below to learn about more
    130 :class:`FormWizard` hooks.
     120        from django.shortcuts import render_to_response
     121        from django.contrib.formtools.wizard.views import SessionWizardView
    131122
    132 Creating templates for the forms
    133 ================================
     123        class ContactWizard(SessionWizardView):
     124            def done(self, form_list, **kwargs):
     125                return render_to_response('done.html', {
     126                    'form_data': [form.cleaned_data for form in form_list],
     127                })
    134128
    135 Next, you'll need to create a template that renders the wizard's forms. By
    136 default, every form uses a template called :file:`forms/wizard.html`. (You can
    137 change this template name by overriding :meth:`~FormWizard.get_template()`,
    138 which is documented below. This hook also allows you to use a different
    139 template for each form.)
     129    Note that this method will be called via ``POST``, so it really ought to be a
     130    good Web citizen and redirect after processing the data. Here's another
     131    example::
    140132
    141 This template expects the following context:
     133        from django.http import HttpResponseRedirect
     134        from django.contrib.formtools.wizard.views import SessionWizardView
    142135
    143     * ``step_field`` -- The name of the hidden field containing the step.
    144     * ``step0`` -- The current step (zero-based).
    145     * ``step`` -- The current step (one-based).
    146     * ``step_count`` -- The total number of steps.
    147     * ``form`` -- The :class:`~django.forms.Form` instance for the current step
    148       (either empty or with errors).
    149     * ``previous_fields`` -- A string representing every previous data field,
    150       plus hashes for completed forms, all in the form of hidden fields. Note
    151       that you'll need to run this through the :tfilter:`safe` template filter,
    152       to prevent auto-escaping, because it's raw HTML.
     136        class ContactWizard(SessionWizardView):
     137            def done(self, form_list, **kwargs):
     138                do_something_with_the_form_data(form_list)
     139                return HttpResponseRedirect('/page-to-redirect-to-when-done/')
    153140
    154 You can supply extra context to this template in two ways:
     141See the section :ref:`Advanced WizardView methods <wizardview-advanced-methods>`
     142below to learn about more :class:`WizardView` hooks.
    155143
    156     * Set the :attr:`~FormWizard.extra_context` attribute on your
    157       :class:`FormWizard` subclass to a dictionary.
     144Creating templates for the forms
     145--------------------------------
    158146
    159     * Pass a dictionary as a parameter named ``extra_context`` to your wizard's
    160       URL pattern in your URLconf.  See :ref:`hooking-wizard-into-urlconf`.
     147Next, you'll need to create a template that renders the wizard's forms. By
     148default, every form uses a template called
     149:file:`formtools/wizard/wizard_form.html`. You can change this template name
     150by overriding either the :attr:`~WizardView.template_name` attribute or the
     151:meth:`~WizardView.get_template_names()` method, which is documented below.
     152This hook also allows you to use a different template for each form.
     153
     154This template expects a ``wizard`` object that has various items attached to
     155it:
     156
     157    * ``form`` -- The :class:`~django.forms.Form` instance for the current
     158      step (either empty or with errors).
     159
     160    * ``steps`` -- A helper object to access the various steps related data:
     161
     162        * ``step0`` -- The current step (zero-based).
     163        * ``step1`` -- The current step (one-based).
     164        * ``count`` -- The total number of steps.
     165        * ``first`` -- The first step.
     166        * ``last`` -- The last step.
     167        * ``current`` -- The current (or first) step.
     168        * ``next`` -- The next step.
     169        * ``prev`` -- The previous step.
     170        * ``index`` -- The index of the current step.
     171        * ``all`` -- A list of all steps of the wizard.
     172
     173You can supply additional context variables by using the
     174:meth:`~FormWizard.get_context_data` method of your :class:`FormWizard`
     175subclass.
    161176
    162177Here's a full example template:
    163178
    Here's a full example template:  
    166181    {% extends "base.html" %}
    167182
    168183    {% block content %}
    169     <p>Step {{ step }} of {{ step_count }}</p>
     184    <p>Step {{ wizard.steps.current }} of {{ wizard.steps.count }}</p>
    170185    <form action="." method="post">{% csrf_token %}
    171186    <table>
    172     {{ form }}
     187    {{ wizard.management_form }}
     188    {% if wizard.form.forms %}
     189        {{ wizard.form.management_form }}
     190        {% for form in wizard.form.forms %}
     191            {{ form }}
     192        {% endfor %}
     193    {% else %}
     194        {{ wizard.form }}
     195    {% endif %}
     196    {% if wizard.steps.prev %}
     197    <button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button>
     198    <button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
     199    {% endif %}
    173200    </table>
    174     <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
    175     {{ previous_fields|safe }}
    176201    <input type="submit">
    177202    </form>
    178203    {% endblock %}
    179204
    180 Note that ``previous_fields``, ``step_field`` and ``step0`` are all required
    181 for the wizard to work properly.
     205.. note::
    182206
    183 .. _hooking-wizard-into-urlconf:
     207    Note that ``{{ wizard.management_form }}`` **must be used** for
     208    the wizard to work properly.
     209
     210.. _wizard-urlconf:
    184211
    185212Hooking the wizard into a URLconf
    186 =================================
     213---------------------------------
    187214
    188215Finally, we need to specify which forms to use in the wizard, and then
    189 deploy the new :class:`FormWizard` object a URL in ``urls.py``. The
    190 wizard takes a list of your :class:`~django.forms.Form` objects as
    191 arguments when you instantiate the Wizard::
     216deploy the new :class:`WizardView` object a URL in the ``urls.py``. The
     217wizard's :meth:`as_view` method takes a list of your
     218:class:`~django.forms.Form` classes as an argument during instantiation::
     219
     220    from django.conf.urls.defaults import patterns
    192221
    193     from django.conf.urls.defaults import *
    194     from testapp.forms import ContactForm1, ContactForm2, ContactWizard
     222    from myapp.forms import ContactForm1, ContactForm2
     223    from myapp.views import ContactWizard
    195224
    196225    urlpatterns = patterns('',
    197         (r'^contact/$', ContactWizard([ContactForm1, ContactForm2])),
     226        (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
    198227    )
    199228
    200 Advanced ``FormWizard`` methods
     229.. _wizardview-advanced-methods:
     230
     231Advanced ``WizardView`` methods
    201232===============================
    202233
    203 .. class:: FormWizard
     234.. class:: WizardView
    204235
    205     Aside from the :meth:`~done()` method, :class:`FormWizard` offers a few
     236    Aside from the :meth:`~done()` method, :class:`WizardView` offers a few
    206237    advanced method hooks that let you customize how your wizard works.
    207238
    208239    Some of these methods take an argument ``step``, which is a zero-based
    209     counter representing the current step of the wizard. (E.g., the first form
    210     is ``0`` and the second form is ``1``.)
     240    counter as string representing the current step of the wizard. (E.g., the
     241    first form is ``'0'`` and the second form is ``'1'``)
    211242
    212 .. method:: FormWizard.prefix_for_step
     243.. method:: WizardView.get_form_prefix(step)
    213244
    214     Given the step, returns a form prefix to use.  By default, this simply uses
     245    Given the step, returns a form prefix to use. By default, this simply uses
    215246    the step itself. For more, see the :ref:`form prefix documentation
    216247    <form-prefix>`.
    217248
     249.. method:: WizardView.process_step(form)
     250
     251    Hook for modifying the wizard's internal state, given a fully validated
     252    :class:`~django.forms.Form` object. The Form is guaranteed to have clean,
     253    valid data.
     254
     255    Note that this method is called every time a page is rendered for *all*
     256    submitted steps.
     257
     258    The default implementation::
     259
     260        def process_step(self, form):
     261            return self.get_form_step_data(form)
     262
     263.. method:: WizardView.get_form_initial(step)
     264
     265    Returns a dictionary which will be passed to the form for ``step`` as
     266    ``initial``. If no initial data was provied while initializing the
     267    form wizard, a empty dictionary should be returned.
     268
     269    The default implementation::
     270
     271        def get_form_initial(self, step):
     272            return self.initial_dict.get(step, {})
     273
     274.. method:: WizardView.get_form_instance(step)
     275
     276    Returns a object which will be passed to the form for ``step`` as
     277    ``instance``. If no instance object was provied while initializing
     278    the form wizard, None be returned.
     279
     280    The default implementation::
     281
     282        def get_form_instance(self, step):
     283            return self.instance_dict.get(step, None)
     284
     285.. method:: WizardView.get_context_data(form, **kwargs)
     286
     287    Returns the template context for a step. You can overwrite this method
     288    to add more data for all or some steps. This method returns a dictionary
     289    containing the rendered form step.
     290
     291    The default template context variables are:
     292
     293    * Any extra data the storage backend has stored
     294    * ``form`` -- form instance of the current step
     295    * ``wizard`` -- the wizard instance itself
     296
     297    Example to add extra variables for a specific step::
     298
     299        def get_context_data(self, form, **kwargs):
     300            context = super(MyWizard, self).get_context_data(form, **kwargs)
     301            if self.steps.current == 'my_step_name':
     302                context.update({'another_var': True})
     303            return context
     304
     305.. method:: WizardView.get_wizard_name()
     306
     307    This method can be used to change the wizard's internal name.
     308
    218309    Default implementation::
    219310
    220         def prefix_for_step(self, step):
    221             return str(step)
     311        def get_wizard_name(self):
     312            return normalize_name(self.__class__.__name__)
     313
     314.. method:: WizardView.get_prefix()
    222315
    223 .. method:: FormWizard.render_hash_failure
     316    This method returns a prefix for the storage backends. These backends use
     317    the prefix to fetch the correct data for the wizard. (Multiple wizards
     318    could save their data in one session)
    224319
    225     Renders a template if the hash check fails. It's rare that you'd need to
    226     override this.
     320    You can change this method to make the wizard data prefix more unique to,
     321    e.g. have multiple instances of one wizard in one session.
    227322
    228323    Default implementation::
    229324
    230         def render_hash_failure(self, request, step):
    231             return self.render(self.get_form(step), request, step,
    232                 context={'wizard_error':
    233                              'We apologize, but your form has expired. Please'
    234                              ' continue filling out the form from this page.'})
     325        def get_prefix(self):
     326            return self.wizard_name
    235327
    236 .. method:: FormWizard.security_hash
     328.. method:: WizardView.get_form(step=None, data=None, files=None)
    237329
    238     Calculates the security hash for the given request object and
    239     :class:`~django.forms.Form` instance.
     330    This method constructs the form for a given ``step``. If no ``step`` is
     331    defined, the current step will be determined automatically.
     332    The method gets three arguments:
    240333
    241     By default, this generates a SHA1 HMAC using your form data and your
    242     :setting:`SECRET_KEY` setting. It's rare that somebody would need to
    243     override this.
     334    * ``step`` -- The step for which the form instance should be generated.
     335    * ``data`` -- Gets passed to the form's data argument
     336    * ``files`` -- Gets passed to the form's files argument
    244337
    245     Example::
     338    You can override this method to add extra arguments to the form instance.
    246339
    247         def security_hash(self, request, form):
    248             return my_hash_function(request, form)
     340    Example code to add a user attribute to the form on step 2::
    249341
    250 .. method:: FormWizard.parse_params
     342        def get_form(self, step=None, data=None, files=None):
     343            form = super(MyWizard, self).get_form(step, data, files)
     344            if step == '1':
     345                form.user = self.request.user
     346            return form
    251347
    252     A hook for saving state from the request object and ``args`` / ``kwargs``
    253     that were captured from the URL by your URLconf.
     348.. method:: WizardView.process_step(form)
    254349
    255     By default, this does nothing.
     350    This method gives you a way to post-process the form data before the data
     351    gets stored within the storage backend. By default it just passed the
     352    form.data dictionary. You should not manipulate the data here but you can
     353    use the data to do some extra work if needed (e.g. set storage extra data).
    256354
    257     Example::
     355    Default implementation::
    258356
    259         def parse_params(self, request, *args, **kwargs):
    260             self.my_state = args[0]
     357        def process_step(self, form):
     358            return self.get_form_step_data(form)
    261359
    262 .. method:: FormWizard.get_template
     360.. method:: WizardView.process_step_files(form)
    263361
    264     Returns the name of the template that should be used for the given step.
     362    This method gives you a way to post-process the form files before the
     363    files gets stored within the storage backend. By default it just passed
     364    the ``form.files`` dictionary. You should not manipulate the data here
     365    but you can use the data to do some extra work if needed (e.g. set storage
     366    extra data).
    265367
    266     By default, this returns :file:`'forms/wizard.html'`, regardless of step.
     368    Default implementation::
    267369
    268     Example::
     370        def process_step_files(self, form):
     371            return self.get_form_step_files(form)
    269372
    270         def get_template(self, step):
    271             return 'myapp/wizard_%s.html' % step
     373.. method:: WizardView.render_revalidation_failure(step, form, **kwargs)
    272374
    273     If :meth:`~FormWizard.get_template` returns a list of strings, then the
    274     wizard will use the template system's
    275     :func:`~django.template.loader.select_template` function.
    276     This means the system will use the first template that exists on the
    277     filesystem. For example::
     375    When the wizard thinks, all steps passed it revalidates all forms with the
     376    data from the backend storage.
    278377
    279         def get_template(self, step):
    280             return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
     378    If any of the forms don't validate correctly, this method gets called.
     379    This method expects two arguments, ``step`` and ``form``.
    281380
    282 .. method:: FormWizard.render_template
     381    The default implementation resets the current step to the first failing
     382    form and redirects the user to the invalid form.
    283383
    284     Renders the template for the given step, returning an
    285     :class:`~django.http.HttpResponse` object.
     384    Default implementation::
    286385
    287     Override this method if you want to add a custom context, return a
    288     different MIME type, etc. If you only need to override the template name,
    289     use :meth:`~FormWizard.get_template` instead.
     386        def render_revalidation_failure(self, step, form, **kwargs):
     387            self.storage.current_step = step
     388            return self.render(form, **kwargs)
    290389
    291     The template will be rendered with the context documented in the
    292     "Creating templates for the forms" section above.
     390.. method:: WizardView.get_form_step_data(form)
    293391
    294 .. method:: FormWizard.process_step
     392    This method fetches the form data from and returns the dictionary. You
     393    can use this method to manipulate the values before the data gets stored
     394    in the storage backend.
    295395
    296     Hook for modifying the wizard's internal state, given a fully validated
    297     :class:`~django.forms.Form` object. The Form is guaranteed to have clean,
    298     valid data.
     396    Default implementation::
    299397
    300     This method should *not* modify any of that data. Rather, it might want to
    301     set ``self.extra_context`` or dynamically alter ``self.form_list``, based
    302     on previously submitted forms.
     398        def get_form_step_data(self, form):
     399            return form.data
    303400
    304     Note that this method is called every time a page is rendered for *all*
    305     submitted steps.
     401.. method:: WizardView.get_form_step_files(form)
     402
     403    This method returns the form files. You can use this method to manipulate
     404    the files before the data gets stored in the storage backend.
     405
     406    Default implementation::
     407
     408        def get_form_step_files(self, form):
     409            return form.files
    306410
    307     The function signature::
     411.. method:: WizardView.render(form, **kwargs)
    308412
    309         def process_step(self, request, form, step):
    310             # ...
     413    This method gets called after the get or post request was handled. You can
     414    hook in this method to, e.g. change the type of http response.
     415
     416    Default implementation::
     417
     418        def render(self, form=None, **kwargs):
     419            form = form or self.get_form()
     420            context = self.get_context_data(form, **kwargs)
     421            return self.render_to_response(context)
    311422
    312423Providing initial data for the forms
    313424====================================
    314425
    315 .. attribute:: FormWizard.initial
     426.. attribute:: WizardView.initial_dict
    316427
    317428    Initial data for a wizard's :class:`~django.forms.Form` objects can be
    318     provided using the optional :attr:`~FormWizard.initial` keyword argument.
    319     This argument should be a dictionary mapping a step to a dictionary
    320     containing the initial data for that step. The dictionary of initial data
     429    provided using the optional :attr:`~Wizard.initial_dict` keyword argument.
     430    This argument should be a dictionary mapping the steps to dictionaries
     431    containing the initial data for each step. The dictionary of initial data
    321432    will be passed along to the constructor of the step's
    322433    :class:`~django.forms.Form`::
    323434
    324         >>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard
     435        >>> from myapp.forms import ContactForm1, ContactForm2
     436        >>> from myapp.views import ContactWizard
    325437        >>> initial = {
    326         ...     0: {'subject': 'Hello', 'sender': 'user@example.com'},
    327         ...     1: {'message': 'Hi there!'}
     438        ...     '0': {'subject': 'Hello', 'sender': 'user@example.com'},
     439        ...     '1': {'message': 'Hi there!'}
    328440        ... }
    329         >>> wiz = ContactWizard([ContactForm1, ContactForm2], initial=initial)
    330         >>> form1 = wiz.get_form(0)
    331         >>> form2 = wiz.get_form(1)
     441        >>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)
     442        >>> form1 = wiz.get_form('0')
     443        >>> form2 = wiz.get_form('1')
    332444        >>> form1.initial
    333445        {'sender': 'user@example.com', 'subject': 'Hello'}
    334446        >>> form2.initial
    335447        {'message': 'Hi there!'}
     448
     449    The ``initial_dict`` can also take a list of dictionaries for a specific
     450    step if the step is a ``FormSet``.
     451
     452.. _wizard-files:
     453
     454Handling files
     455==============
     456
     457To handle :class:`~django.forms.FileField` within any step form of the wizard,
     458you have to add a :attr:`file_storage` to your :class:`WizardView` subclass.
     459
     460This storage will temporarilyy store the uploaded files for the wizard. The
     461:attr:`file_storage` attribute should be a
     462:class:`~django.core.files.storage.Storage` subclass.
     463
     464.. warning::
     465
     466    Please remember to take care of removing old files as the
     467    :class:`WizardView` won't remove any files, whether the wizard gets
     468    finished corretly or not.
     469
     470Conditionally view/skip specific steps
     471======================================
     472
     473.. attribute:: WizardView.condition_dict
     474
     475The :meth:`~WizardView.as_view` accepts a ``condition_dict`` argument. You can pass a
     476dictionary of boolean values or callables. The key should match the steps
     477name (e.g. '0', '1').
     478
     479If the value of a specific step is callable it will be called with the
     480:class:`WizardView` instance as the only argument. If the return value is true,
     481the step's form will be used.
     482
     483This example provides a contact form including a condition. The condition is
     484used to show a message from only if a checkbox in the first step was checked.
     485
     486The steps are defined in a ``forms.py``::
     487
     488    from django import forms
     489
     490    class ContactForm1(forms.Form):
     491        subject = forms.CharField(max_length=100)
     492        sender = forms.EmailField()
     493        leave_message = forms.BooleanField(required=False)
     494
     495    class ContactForm2(forms.Form):
     496        message = forms.CharField(widget=forms.Textarea)
     497
     498We define our wizard in a ``views.py``::
     499
     500    from django.shortcuts import render_to_response
     501    from django.contrib.formtools.wizard.views import SessionWizardView
     502
     503    def show_message_form_condition(wizard):
     504        # try to get the cleaned data of step 1
     505        cleaned_data = wizard.get_cleaned_data_for_step('0') or {}
     506        # check if the field ``leave_message`` was checked.
     507        return cleaned_data.get('leave_message', True)
     508
     509    class ContactWizard(SessionWizardView):
     510
     511        def done(self, form_list, **kwargs):
     512            return render_to_response('done.html', {
     513                'form_data': [form.cleaned_data for form in form_list],
     514            })
     515
     516We need to add the ``ContactWizard`` to our ``urls.py`` file::
     517
     518    from django.conf.urls.defaults import pattern
     519
     520    from myapp.forms import ContactForm1, ContactForm2
     521    from myapp.views import ContactWizard, show_message_form_condition
     522
     523    contact_forms = [ContactForm1, ContactForm2]
     524
     525    urlpatterns = patterns('',
     526        (r'^contact/$', ContactWizard.as_view(contact_forms,
     527            condition_dict={'1': show_message_form_condition}
     528        )),
     529    )
     530
     531As you can see, we defined a ``show_message_form_condition`` next to our
     532:class:`WizardView` subclass and added a ``condition_dict`` argument to the
     533:meth:`~WizardView.as_view` method. The key refers to the second wizard step
     534(because of the zero based step index).
     535
     536How to work with ModelForm and ModelFormSet
     537===========================================
     538
     539The WizardView supports :class:`~django.forms.ModelForm` and
     540:class:`~django.forms.ModelFormSet`. Additionally to the ``initial_dict``,
     541the :meth:`~WizardView.as_view` method takes a ``instance_dict`` argument
     542with a list of instances for the ``ModelForm`` and ``ModelFormSet``.
     543
     544Usage of NamedUrlWizardView
     545===========================
     546
     547.. class:: NamedUrlWizardView
     548
     549There is a :class:`WizardView` subclass which adds named-urls support to the wizard.
     550By doing this, you can have single urls for every step.
     551
     552To use the named urls, you have to change the ``urls.py``.
     553
     554Below you will see an example of a contact wizard with two steps, step 1 with
     555"contactdata" as its name and step 2 with "leavemessage" as its name.
     556
     557Additionally you have to pass two more arguments to the
     558:meth:`~WizardView.as_view` method:
     559
     560   * ``url_name`` -- the name of the url (as provided in the urls.py)
     561   * ``done_step_name`` -- the name in the url for the done step
     562
     563Example code for the changed ``urls.py`` file::
     564
     565    from django.conf.urls.defaults import url, patterns
     566
     567    from myapp.forms import ContactForm1, ContactForm2
     568    from myapp.views import ContactWizard
     569
     570    named_contact_forms = (
     571        ('contactdata', ContactForm1),
     572        ('leavemessage', ContactForm2),
     573    )
     574
     575    contact_wizard = ContactWizard.as_view(named_contact_forms,
     576        url_name='contact_step', done_step_name='finished')
     577
     578    urlpatterns = patterns('',
     579        url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'),
     580        url(r'^contact/$', contact_wizard, name='contact'),
     581    )
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 7fdf0d7..2a2090a 100644
    a b signing in Web applications.  
    5555
    5656See :doc:`cryptographic signing </topics/signing>` docs for more information.
    5757
     58New form wizard
     59~~~~~~~~~~~~~~~
     60
     61The previously shipped ``FormWizard`` of the formtools contrib app has been
     62replaced with a new implementation that is based on the class based views
     63introduced in Django 1.3. It features a pluggable storage API and doesn't
     64require the wizard to pass around hidden fields for every previous step.
     65
     66Django 1.4 ships with a session based storage backend and a cookie based
     67storage backend. The latter uses the tools for
     68:doc:`cryptographic signing </topics/signing>` also introduced in
     69Django 1.4 to store the wizard state in the user's cookies.
     70
     71See the :doc:`form wizard </ref/contrib/formtools/form-wizard>` docs for
     72more information.
     73
    5874Simple clickjacking protection
    5975~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    6076
  • deleted file tests/regressiontests/formwizard/forms.py

    diff --git a/tests/regressiontests/formwizard/__init__.py b/tests/regressiontests/formwizard/__init__.py
    deleted file mode 100644
    index e69de29..0000000
    diff --git a/tests/regressiontests/formwizard/forms.py b/tests/regressiontests/formwizard/forms.py
    deleted file mode 100644
    index f458eda..0000000
    + -  
    1 from django import forms
    2 from django.contrib.formtools.wizard import FormWizard
    3 from django.http import HttpResponse
    4 
    5 class Page1(forms.Form):
    6     name = forms.CharField(max_length=100)
    7     thirsty = forms.NullBooleanField()
    8 
    9 class Page2(forms.Form):
    10     address1 = forms.CharField(max_length=100)
    11     address2 = forms.CharField(max_length=100)
    12    
    13 class Page3(forms.Form):
    14     random_crap = forms.CharField(max_length=100)
    15    
    16 class ContactWizard(FormWizard):
    17     def done(self, request, form_list):
    18         return HttpResponse("")
  • deleted file tests/regressiontests/formwizard/templates/forms/wizard.html

    diff --git a/tests/regressiontests/formwizard/models.py b/tests/regressiontests/formwizard/models.py
    deleted file mode 100644
    index e69de29..0000000
    diff --git a/tests/regressiontests/formwizard/templates/forms/wizard.html b/tests/regressiontests/formwizard/templates/forms/wizard.html
    deleted file mode 100644
    index a31378f..0000000
    + -  
    1 <html>
    2   <body>
    3     <p>Step {{ step }} of {{ step_count }}</p>
    4     <form action="." method="post">
    5     <table>
    6     {{ form }}
    7     </table>
    8     <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
    9     {{ previous_fields|safe }}
    10     <input type="submit">
    11     </form>
    12   </body>
    13 </html>
    14  No newline at end of file
  • deleted file tests/regressiontests/formwizard/tests.py

    diff --git a/tests/regressiontests/formwizard/tests.py b/tests/regressiontests/formwizard/tests.py
    deleted file mode 100644
    index 0c94d2e..0000000
    + -  
    1 import re
    2 from django import forms
    3 from django.test import TestCase
    4 
    5 class FormWizardWithNullBooleanField(TestCase):
    6     urls = 'regressiontests.formwizard.urls'
    7 
    8     input_re = re.compile('name="([^"]+)" value="([^"]+)"')
    9 
    10     wizard_url = '/wiz/'
    11     wizard_step_data = (
    12         {
    13             '0-name': 'Pony',
    14             '0-thirsty': '2',
    15         },
    16         {
    17             '1-address1': '123 Main St',
    18             '1-address2': 'Djangoland',
    19         },
    20         {
    21             '2-random_crap': 'blah blah',
    22         }
    23     )
    24 
    25     def grabFieldData(self, response):
    26         """
    27         Pull the appropriate field data from the context to pass to the next wizard step
    28         """
    29         previous_fields = response.context['previous_fields']
    30         fields = {'wizard_step': response.context['step0']}
    31        
    32         def grab(m):
    33             fields[m.group(1)] = m.group(2)
    34             return ''
    35        
    36         self.input_re.sub(grab, previous_fields)
    37         return fields
    38 
    39     def checkWizardStep(self, response, step_no):
    40         """
    41         Helper function to test each step of the wizard
    42         - Make sure the call succeeded
    43         - Make sure response is the proper step number
    44         - return the result from the post for the next step
    45         """
    46         step_count = len(self.wizard_step_data)
    47 
    48         self.assertEqual(response.status_code, 200)
    49         self.assertContains(response, 'Step %d of %d' % (step_no, step_count))
    50 
    51         data = self.grabFieldData(response)
    52         data.update(self.wizard_step_data[step_no - 1])
    53 
    54         return self.client.post(self.wizard_url, data)
    55        
    56     def testWizard(self):
    57         response = self.client.get(self.wizard_url)
    58         for step_no in range(1, len(self.wizard_step_data) + 1):
    59             response = self.checkWizardStep(response, step_no)
  • deleted file tests/regressiontests/formwizard/urls.py

    diff --git a/tests/regressiontests/formwizard/urls.py b/tests/regressiontests/formwizard/urls.py
    deleted file mode 100644
    index d964bc6..0000000
    + -  
    1 from django.conf.urls.defaults import *
    2 from forms import ContactWizard, Page1, Page2, Page3
    3 
    4 urlpatterns = patterns('',
    5     url(r'^wiz/$', ContactWizard([Page1, Page2, Page3])),
    6     )
  • tests/regressiontests/utils/functional.py

    diff --git a/tests/regressiontests/utils/functional.py b/tests/regressiontests/utils/functional.py
    index 2784ddd..90a6f08 100644
    a b  
    11from django.utils import unittest
    2 from django.utils.functional import lazy
     2from django.utils.functional import lazy, lazy_property
    33
    44
    55class FunctionalTestCase(unittest.TestCase):
    class FunctionalTestCase(unittest.TestCase):  
    2020
    2121        t = lazy(lambda: Klazz(), Klazz)()
    2222        self.assertTrue('base_method' in dir(t))
     23
     24    def test_lazy_property(self):
     25
     26        class A(object):
     27
     28            def _get_do(self):
     29                raise NotImplementedError
     30            def _set_do(self, value):
     31                raise NotImplementedError
     32            do = lazy_property(_get_do, _set_do)
     33
     34        class B(A):
     35            def _get_do(self):
     36                return "DO IT"
     37
     38        self.assertRaises(NotImplementedError, lambda: A().do)
     39        self.assertEqual(B().do, 'DO IT')
Back to Top