Ticket #9200: ticket9200-2.diff

File ticket9200-2.diff, 140.3 KB (added by Jannis Leidel, 13 years ago)
  • django/contrib/formtools/tests/__init__.py

    diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
    index be0372a..5796852 100644
    a b from django.test import TestCase  
    88from django.test.utils import get_warnings_state, restore_warnings_state
    99from django.utils import unittest
    1010
     11from django.contrib.formtools.wizard.tests import *
     12
     13warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
     14                        module='django.contrib.formtools.wizard')
     15
    1116
    1217success_string = "Done was called!"
    1318
  • 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..11645b1
    - +  
     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.contrib.formtools.utils import form_hmac
     8from django.http import Http404
     9from django.shortcuts import render_to_response
     10from django.template.context import RequestContext
     11from django.utils.crypto import constant_time_compare
     12from django.utils.translation import ugettext_lazy as _
     13from django.utils.decorators import method_decorator
     14from django.views.decorators.csrf import csrf_protect
     15
     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/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/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..24fda5e
    - +  
     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_add_extra_context(self):
     106        request = get_request({'test_wizard-current_step': 'start'})
     107
     108        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     109        response, instance = testform(
     110            request, extra_context={'key1': 'value1'})
     111        self.assertEqual(instance.get_extra_data(), {'key1': 'value1'})
     112
     113        request.method = 'POST'
     114        response, instance = testform(
     115            request, extra_context={'key1': 'value1'})
     116        self.assertEqual(instance.get_extra_data(), {'key1': 'value1'})
     117
     118    def test_form_prefix(self):
     119        request = get_request()
     120
     121        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     122        response, instance = testform(request)
     123
     124        self.assertEqual(instance.get_form_prefix(), 'start')
     125        self.assertEqual(instance.get_form_prefix('another'), 'another')
     126
     127    def test_form_initial(self):
     128        request = get_request()
     129
     130        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)],
     131            initial_dict={'start': {'name': 'value1'}})
     132        response, instance = testform(request)
     133
     134        self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'})
     135        self.assertEqual(instance.get_form_initial('step2'), {})
     136
     137    def test_form_instance(self):
     138        request = get_request()
     139        the_instance = User()
     140        testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)],
     141            instance_dict={'start': the_instance})
     142        response, instance = testform(request)
     143
     144        self.assertEqual(
     145            instance.get_form_instance('start'),
     146            the_instance)
     147        self.assertEqual(
     148            instance.get_form_instance('non_exist_instance'),
     149            None)
     150
     151    def test_formset_instance(self):
     152        request = get_request()
     153        the_instance1, created = User.objects.get_or_create(
     154            username='testuser1')
     155        the_instance2, created = User.objects.get_or_create(
     156            username='testuser2')
     157        testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)],
     158            instance_dict={'start': User.objects.filter(username='testuser1')})
     159        response, instance = testform(request)
     160
     161        self.assertEqual(list(instance.get_form_instance('start')), [the_instance1])
     162        self.assertEqual(instance.get_form_instance('non_exist_instance'), None)
     163
     164        self.assertEqual(instance.get_form().initial_form_count(), 1)
     165
     166    def test_done(self):
     167        request = get_request()
     168
     169        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     170        response, instance = testform(request)
     171
     172        self.assertRaises(NotImplementedError, instance.done, None)
     173
     174    def test_revalidation(self):
     175        request = get_request()
     176
     177        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
     178        response, instance = testform(request)
     179        instance.render_done(None)
     180        self.assertEqual(instance.storage.current_step, 'start')
     181
     182
     183class SessionFormTests(TestCase):
     184    def test_init(self):
     185        request = get_request()
     186        testform = SessionWizardView.as_view([('start', Step1)])
     187        self.assertTrue(isinstance(testform(request), TemplateResponse))
     188
     189
     190class CookieFormTests(TestCase):
     191    def test_init(self):
     192        request = get_request()
     193        testform = CookieWizardView.as_view([('start', Step1)])
     194        self.assertTrue(isinstance(testform(request), TemplateResponse))
     195
  • 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..f0e9c0c
    - +  
     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_add_extra_context(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
     331        response, instance = testform(request,
     332                                      step='form1',
     333                                      extra_context={'key1': 'value1'})
     334        self.assertEqual(instance.get_extra_data(), {'key1': 'value1'})
     335
     336        instance.storage.reset()
     337
     338        response, instance = testform(request,
     339                                      extra_context={'key2': 'value2'})
     340        self.assertEqual(instance.get_extra_data(), {'key2': 'value2'})
     341
     342    def test_revalidation(self):
     343        request = get_request()
     344
     345        testform = self.formwizard_class.as_view(
     346            [('start', Step1), ('step2', Step2)],
     347            url_name=self.wizard_urlname)
     348        response, instance = testform(request, step='done')
     349
     350        instance.render_done(None)
     351        self.assertEqual(instance.storage.current_step, 'start')
     352
     353class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView):
     354
     355    def dispatch(self, request, *args, **kwargs):
     356        response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs)
     357        return response, self
     358
     359class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView):
     360
     361    def dispatch(self, request, *args, **kwargs):
     362        response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs)
     363        return response, self
     364
     365
     366class NamedSessionFormTests(NamedFormTests, TestCase):
     367    formwizard_class = TestNamedUrlSessionFormWizard
     368    wizard_urlname = 'nwiz_session'
     369
     370
     371class NamedCookieFormTests(NamedFormTests, TestCase):
     372    formwizard_class = TestNamedUrlCookieFormWizard
     373    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..9cd5261
    - +  
     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        # if there is an extra_context item in the kwargs,
     236        # pass the data to the storage engine.
     237        self.update_extra_data(kwargs.get('extra_context', {}))
     238
     239        # reset the current step to the first step.
     240        self.storage.current_step = self.steps.first
     241        return self.render(self.get_form())
     242
     243    def post(self, *args, **kwargs):
     244        """
     245        This method handles POST requests.
     246
     247        The wizard will render either the current step (if form validation
     248        wasn't successful), the next step (if the current step was stored
     249        successful) or the done view (if no more steps are available)
     250        """
     251        # if there is an extra_context item in the kwargs,
     252        # pass the data to the storage engine.
     253        self.update_extra_data(kwargs.get('extra_context', {}))
     254
     255        # Look for a wizard_prev_step element in the posted data which
     256        # contains a valid step name. If one was found, render the requested
     257        # form. (This makes stepping back a lot easier).
     258        wizard_prev_step = self.request.POST.get('wizard_prev_step', None)
     259        if wizard_prev_step and wizard_prev_step in self.get_form_list():
     260            self.storage.current_step = wizard_prev_step
     261            form = self.get_form(
     262                data=self.storage.get_step_data(self.steps.current),
     263                files=self.storage.get_step_files(self.steps.current))
     264            return self.render(form)
     265
     266        # Check if form was refreshed
     267        management_form = ManagementForm(self.request.POST, prefix=self.prefix)
     268        if not management_form.is_valid():
     269            raise ValidationError(
     270                'ManagementForm data is missing or has been tampered.')
     271
     272        form_current_step = management_form.cleaned_data['current_step']
     273        if (form_current_step != self.steps.current and
     274                self.storage.current_step is not None):
     275            # form refreshed, change current step
     276            self.storage.current_step = form_current_step
     277
     278        # get the form for the current step
     279        form = self.get_form(data=self.request.POST, files=self.request.FILES)
     280
     281        # and try to validate
     282        if form.is_valid():
     283            # if the form is valid, store the cleaned data and files.
     284            self.storage.set_step_data(self.steps.current, self.process_step(form))
     285            self.storage.set_step_files(self.steps.current, self.process_step_files(form))
     286
     287            # check if the current step is the last step
     288            if self.steps.current == self.steps.last:
     289                # no more steps, render done view
     290                return self.render_done(form, **kwargs)
     291            else:
     292                # proceed to the next step
     293                return self.render_next_step(form)
     294        return self.render(form)
     295
     296    def render_next_step(self, form, **kwargs):
     297        """
     298        THis method gets called when the next step/form should be rendered.
     299        `form` contains the last/current form.
     300        """
     301        # get the form instance based on the data from the storage backend
     302        # (if available).
     303        next_step = self.steps.next
     304        new_form = self.get_form(next_step,
     305            data=self.storage.get_step_data(next_step),
     306            files=self.storage.get_step_files(next_step))
     307
     308        # change the stored current step
     309        self.storage.current_step = next_step
     310        return self.render(new_form, **kwargs)
     311
     312    def render_done(self, form, **kwargs):
     313        """
     314        This method gets called when all forms passed. The method should also
     315        re-validate all steps to prevent manipulation. If any form don't
     316        validate, `render_revalidation_failure` should get called.
     317        If everything is fine call `done`.
     318        """
     319        final_form_list = []
     320        # walk through the form list and try to validate the data again.
     321        for form_key in self.get_form_list():
     322            form_obj = self.get_form(step=form_key,
     323                data=self.storage.get_step_data(form_key),
     324                files=self.storage.get_step_files(form_key))
     325            if not form_obj.is_valid():
     326                return self.render_revalidation_failure(form_key, form_obj, **kwargs)
     327            final_form_list.append(form_obj)
     328
     329        # render the done view and reset the wizard before returning the
     330        # response. This is needed to prevent from rendering done with the
     331        # same data twice.
     332        done_response = self.done(final_form_list, **kwargs)
     333        self.storage.reset()
     334        return done_response
     335
     336    def get_form_prefix(self, step=None, form=None):
     337        """
     338        Returns the prefix which will be used when calling the actual form for
     339        the given step. `step` contains the step-name, `form` the form which
     340        will be called with the returned prefix.
     341
     342        If no step is given, the form_prefix will determine the current step
     343        automatically.
     344        """
     345        if step is None:
     346            step = self.steps.current
     347        return str(step)
     348
     349    def get_form_initial(self, step):
     350        """
     351        Returns a dictionary which will be passed to the form for `step`
     352        as `initial`. If no initial data was provied while initializing the
     353        form wizard, a empty dictionary will be returned.
     354        """
     355        return self.initial_dict.get(step, {})
     356
     357    def get_form_instance(self, step):
     358        """
     359        Returns a object which will be passed to the form for `step`
     360        as `instance`. If no instance object was provied while initializing
     361        the form wizard, None be returned.
     362        """
     363        return self.instance_dict.get(step, None)
     364
     365    def get_form(self, step=None, data=None, files=None):
     366        """
     367        Constructs the form for a given `step`. If no `step` is defined, the
     368        current step will be determined automatically.
     369
     370        The form will be initialized using the `data` argument to prefill the
     371        new form. If needed, instance or queryset (for `ModelForm` or
     372        `ModelFormSet`) will be added too.
     373        """
     374        if step is None:
     375            step = self.steps.current
     376        # prepare the kwargs for the form instance.
     377        kwargs = {
     378            'data': data,
     379            'files': files,
     380            'prefix': self.get_form_prefix(step, self.form_list[step]),
     381            'initial': self.get_form_initial(step),
     382        }
     383        if issubclass(self.form_list[step], forms.ModelForm):
     384            # If the form is based on ModelForm, add instance if available.
     385            kwargs.update({'instance': self.get_form_instance(step)})
     386        elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
     387            # If the form is based on ModelFormSet, add queryset if available.
     388            kwargs.update({'queryset': self.get_form_instance(step)})
     389        return self.form_list[step](**kwargs)
     390
     391    def process_step(self, form):
     392        """
     393        This method is used to postprocess the form data. By default, it
     394        returns the raw `form.data` dictionary.
     395        """
     396        return self.get_form_step_data(form)
     397
     398    def process_step_files(self, form):
     399        """
     400        This method is used to postprocess the form files. By default, it
     401        returns the raw `form.files` dictionary.
     402        """
     403        return self.get_form_step_files(form)
     404
     405    def render_revalidation_failure(self, step, form, **kwargs):
     406        """
     407        Gets called when a form doesn't validate when rendering the done
     408        view. By default, it changed the current step to failing forms step
     409        and renders the form.
     410        """
     411        self.storage.current_step = step
     412        return self.render(form, **kwargs)
     413
     414    def get_form_step_data(self, form):
     415        """
     416        Is used to return the raw form data. You may use this method to
     417        manipulate the data.
     418        """
     419        return form.data
     420
     421    def get_form_step_files(self, form):
     422        """
     423        Is used to return the raw form files. You may use this method to
     424        manipulate the data.
     425        """
     426        return form.files
     427
     428    def get_all_cleaned_data(self):
     429        """
     430        Returns a merged dictionary of all step cleaned_data dictionaries.
     431        If a step contains a `FormSet`, the key will be prefixed with formset
     432        and contain a list of the formset' cleaned_data dictionaries.
     433        """
     434        cleaned_data = {}
     435        for form_key in self.get_form_list():
     436            form_obj = self.get_form(
     437                step=form_key,
     438                data=self.storage.get_step_data(form_key),
     439                files=self.storage.get_step_files(form_key)
     440            )
     441            if form_obj.is_valid():
     442                if isinstance(form_obj.cleaned_data, (tuple, list)):
     443                    cleaned_data.update({
     444                        'formset-%s' % form_key: form_obj.cleaned_data
     445                    })
     446                else:
     447                    cleaned_data.update(form_obj.cleaned_data)
     448        return cleaned_data
     449
     450    def get_cleaned_data_for_step(self, step):
     451        """
     452        Returns the cleaned data for a given `step`. Before returning the
     453        cleaned data, the stored values are being revalidated through the
     454        form. If the data doesn't validate, None will be returned.
     455        """
     456        if step in self.form_list:
     457            form_obj = self.get_form(step=step,
     458                data=self.storage.get_step_data(step),
     459                files=self.storage.get_step_files(step))
     460            if form_obj.is_valid():
     461                return form_obj.cleaned_data
     462        return None
     463
     464    def get_next_step(self, step=None):
     465        """
     466        Returns the next step after the given `step`. If no more steps are
     467        available, None will be returned. If the `step` argument is None, the
     468        current step will be determined automatically.
     469        """
     470        if step is None:
     471            step = self.steps.current
     472        form_list = self.get_form_list()
     473        key = form_list.keyOrder.index(step) + 1
     474        if len(form_list.keyOrder) > key:
     475            return form_list.keyOrder[key]
     476        return None
     477
     478    def get_prev_step(self, step=None):
     479        """
     480        Returns the previous step before the given `step`. If there are no
     481        steps available, None will be returned. If the `step` argument is
     482        None, the current step will be determined automatically.
     483        """
     484        if step is None:
     485            step = self.steps.current
     486        form_list = self.get_form_list()
     487        key = form_list.keyOrder.index(step) - 1
     488        if key >= 0:
     489            return form_list.keyOrder[key]
     490        return None
     491
     492    def get_step_index(self, step=None):
     493        """
     494        Returns the index for the given `step` name. If no step is given,
     495        the current step will be used to get the index.
     496        """
     497        if step is None:
     498            step = self.steps.current
     499        return self.get_form_list().keyOrder.index(step)
     500
     501    def get_context_data(self, form, *args, **kwargs):
     502        """
     503        Returns the template context for a step. You can overwrite this method
     504        to add more data for all or some steps. This method returns a
     505        dictionary containing the rendered form step. Available template
     506        context variables are:
     507
     508         * `extra_data` - current extra data
     509         * `form` - form instance of the current step
     510         * `wizard` - the wizard instance itself
     511
     512        Example:
     513
     514        .. code-block:: python
     515
     516            class MyWizard(FormWizard):
     517                def get_context_data(self, form, **kwargs):
     518                    context = super(MyWizard, self).get_context_data(form, **kwargs)
     519                    if self.steps.current == 'my_step_name':
     520                        context.update({'another_var': True})
     521                    return context
     522        """
     523        context = super(WizardView, self).get_context_data(*args, **kwargs)
     524        context.update(self.get_extra_data())
     525        context['wizard'] = {
     526            'form': form,
     527            'steps': self.steps,
     528            'managenent_form': ManagementForm(prefix=self.prefix, initial={
     529                'current_step': self.steps.current,
     530            }),
     531        }
     532        return context
     533
     534    def get_extra_data(self):
     535        """
     536        Returns the extra data currently stored in the storage backend.
     537        """
     538        return self.storage.extra_data
     539
     540    def update_extra_data(self, data):
     541        """
     542        Updates the currently stored extra data. Already stored extra
     543        context will be kept!
     544        """
     545        new_extra_data = copy.copy(self.get_extra_data())
     546        new_extra_data.update(data)
     547        self.storage.extra_data = new_extra_data
     548
     549    def render(self, form=None, **kwargs):
     550        """
     551        Returns a ``HttpResponse`` containing a all needed context data.
     552        """
     553        form = form or self.get_form()
     554        context = self.get_context_data(form, **kwargs)
     555        return self.render_to_response(context)
     556
     557    def done(self, form_list, **kwargs):
     558        """
     559        This method muss be overrided by a subclass to process to form data
     560        after processing all steps.
     561        """
     562        raise NotImplementedError("Your %s class has not defined a done() "
     563            "method, which is required." % self.__class__.__name__)
     564
     565
     566class SessionWizardView(WizardView):
     567    """
     568    A WizardView with pre-configured SessionStorage backend.
     569    """
     570    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     571
     572
     573class CookieWizardView(WizardView):
     574    """
     575    A WizardView with pre-configured CookieStorage backend.
     576    """
     577    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     578
     579
     580class NamedUrlWizardView(WizardView):
     581    """
     582    A WizardView with URL named steps support.
     583    """
     584    url_name = None
     585    done_step_name = None
     586
     587    @classmethod
     588    def get_initkwargs(cls, *args, **kwargs):
     589        """
     590        We require a url_name to reverse URLs later. Additionally users can
     591        pass a done_step_name to change the URL name of the "done" view.
     592        """
     593        extra_kwargs = {
     594            'done_step_name': 'done'
     595        }
     596        assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs'
     597        extra_kwargs['url_name'] = kwargs.pop('url_name')
     598
     599        if 'done_step_name' in kwargs:
     600            extra_kwargs['done_step_name'] = kwargs.pop('done_step_name')
     601
     602        initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs)
     603        initkwargs.update(extra_kwargs)
     604
     605        assert initkwargs['done_step_name'] not in initkwargs['form_list'], \
     606            'step name "%s" is reserved for "done" view' % initkwargs['done_step_name']
     607
     608        return initkwargs
     609
     610    def get(self, *args, **kwargs):
     611        """
     612        This renders the form or, if needed, does the http redirects.
     613        """
     614        self.update_extra_data(kwargs.get('extra_context', {}))
     615        step_url = kwargs.get('step', None)
     616        if step_url is None:
     617            if 'reset' in self.request.GET:
     618                self.storage.reset()
     619                self.storage.current_step = self.steps.first
     620            if self.request.GET:
     621                query_string = "?%s" % self.request.GET.urlencode()
     622            else:
     623                query_string = ""
     624            next_step_url = reverse(self.url_name, kwargs={
     625                'step': self.steps.current,
     626            }) + query_string
     627            return redirect(next_step_url)
     628
     629        # is the current step the "done" name/view?
     630        elif step_url == self.done_step_name:
     631            last_step = self.steps.last
     632            return self.render_done(self.get_form(step=last_step,
     633                data=self.storage.get_step_data(last_step),
     634                files=self.storage.get_step_files(last_step)
     635            ), **kwargs)
     636
     637        # is the url step name not equal to the step in the storage?
     638        # if yes, change the step in the storage (if name exists)
     639        elif step_url == self.steps.current:
     640            # URL step name and storage step name are equal, render!
     641            return self.render(self.get_form(
     642                data=self.storage.current_step_data,
     643                files=self.storage.current_step_data,
     644            ), **kwargs)
     645
     646        elif step_url in self.get_form_list():
     647            self.storage.current_step = step_url
     648            return self.render(self.get_form(
     649                data=self.storage.current_step_data,
     650                files=self.storage.current_step_data,
     651            ), **kwargs)
     652
     653        # invalid step name, reset to first and redirect.
     654        else:
     655            self.storage.current_step = self.steps.first
     656            return redirect(self.url_name, step=self.steps.first)
     657
     658    def post(self, *args, **kwargs):
     659        """
     660        Do a redirect if user presses the prev. step button. The rest of this
     661        is super'd from FormWizard.
     662        """
     663        prev_step = self.request.POST.get('wizard_prev_step', None)
     664        if prev_step and prev_step in self.get_form_list():
     665            self.storage.current_step = prev_step
     666            return redirect(self.url_name, step=prev_step)
     667        return super(NamedUrlWizardView, self).post(*args, **kwargs)
     668
     669    def render_next_step(self, form, **kwargs):
     670        """
     671        When using the NamedUrlFormWizard, we have to redirect to update the
     672        browser's URL to match the shown step.
     673        """
     674        next_step = self.get_next_step()
     675        self.storage.current_step = next_step
     676        return redirect(self.url_name, step=next_step)
     677
     678    def render_revalidation_failure(self, failed_step, form, **kwargs):
     679        """
     680        When a step fails, we have to redirect the user to the first failing
     681        step.
     682        """
     683        self.storage.current_step = failed_step
     684        return redirect(self.url_name, step=failed_step)
     685
     686    def render_done(self, form, **kwargs):
     687        """
     688        When rendering the done view, we have to redirect first (if the URL
     689        name doesn't fit).
     690        """
     691        if kwargs.get('step', None) != self.done_step_name:
     692            return redirect(self.url_name, step=self.done_step_name)
     693        return super(NamedUrlWizardView, self).render_done(form, **kwargs)
     694
     695
     696class NamedUrlSessionWizardView(NamedUrlWizardView):
     697    """
     698    A NamedUrlWizardView with pre-configured SessionStorage backend.
     699    """
     700    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
     701
     702
     703class NamedUrlCookieWizardView(NamedUrlWizardView):
     704    """
     705    A NamedUrlFormWizard with pre-configured CookieStorageBackend.
     706    """
     707    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
     708
  • django/utils/functional.py

    diff --git a/django/utils/functional.py b/django/utils/functional.py
    index 21463bd..7d52794 100644
    a b class SimpleLazyObject(LazyObject):  
    267267
    268268    def _setup(self):
    269269        self._wrapped = self._setupfunc()
     270
     271
     272class lazy_property(property):
     273    """
     274    A property that works with subclasses by wrapping the decorated
     275    functions of the base class.
     276    """
     277    def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
     278        if fget is not None:
     279            @wraps(fget)
     280            def fget(instance, instance_type=None, name=fget.__name__):
     281                return getattr(instance, name)()
     282        if fset is not None:
     283            @wraps(fset)
     284            def fset(instance, value, name=fset.__name__):
     285                return getattr(instance, name)(value)
     286        if fdel is not None:
     287            @wraps(fdel)
     288            def fdel(instance, name=fdel.__name__):
     289                return getattr(instance, name)()
     290        return property(fget, fset, fdel, doc)
  • 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..8539842 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.
    8582
    86 Creating a ``FormWizard`` class
    87 ===============================
     83.. note::
     84
     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 what
     87    to do.
     88
     89Creating a ``WizardView`` class
     90-------------------------------
    8891
    8992The 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`.
     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 (session-based or cookie-based).
     96
     97We will use the :class:`SessionWizardView` in all examples but is is completly
     98fine to use the :class:`CookieWizardView` instead. As with your
     99:class:`~django.forms.Form` classes, this :class:`WizardView` class can live
     100anywhere in your codebase, but convention is to put it in :file:`views.py`.
    93101
    94102The only requirement on this subclass is that it implement a
    95 :meth:`~FormWizard.done()` method.
     103:meth:`~WizardView.done()` method.
    96104
    97 .. method:: FormWizard.done
     105.. method:: WizardView.done(form_list)
    98106
    99107    This method specifies what should happen when the data for *every* form is
    100     submitted and validated.  This method is passed two arguments:
    101 
    102         * ``request`` -- an :class:`~django.http.HttpRequest` object
    103         * ``form_list`` -- a list of :class:`~django.forms.Form` classes
     108    submitted and validated. This method is passed a list of validated
     109    :class:`~django.forms.Form` instances.
    104110
    105 In this simplistic example, rather than perform any database operation, the
    106 method simply renders a template of the validated data::
     111    In this simplistic example, rather than performing any database operation,
     112    the method simply renders a template of the validated data::
    107113
    108     from django.shortcuts import render_to_response
    109     from django.contrib.formtools.wizard import FormWizard
     114        from django.shortcuts import render_to_response
     115        from django.contrib.formtools.wizard.views import SessionWizardView
    110116
    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             })
     117        class ContactWizard(SessionWizardView):
     118            def done(self, form_list, **kwargs):
     119                return render_to_response('done.html', {
     120                    'form_data': [form.cleaned_data for form in form_list],
     121                })
    116122
    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::
     123    Note that this method will be called via ``POST``, so it really ought to be a
     124    good Web citizen and redirect after processing the data. Here's another
     125    example::
    120126
    121     from django.http import HttpResponseRedirect
    122     from django.contrib.formtools.wizard import FormWizard
     127        from django.http import HttpResponseRedirect
     128        from django.contrib.formtools.wizard.views import SessionWizardView
    123129
    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/')
     130        class ContactWizard(SessionWizardView):
     131            def done(self, form_list, **kwargs):
     132                do_something_with_the_form_data(form_list)
     133                return HttpResponseRedirect('/page-to-redirect-to-when-done/')
    128134
    129 See the section `Advanced FormWizard methods`_ below to learn about more
    130 :class:`FormWizard` hooks.
     135See the section :ref:`Advanced WizardView methods <wizardview-advanced-methods>`
     136below to learn about more :class:`WizardView` hooks.
    131137
    132138Creating templates for the forms
    133 ================================
     139--------------------------------
    134140
    135141Next, 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.)
    140 
    141 This template expects the following context:
    142 
    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.
     142default, every form uses a template called
     143:file:`formtools/wizard/wizard_form.html`. You can change this template name
     144by overriding either the :attr:`~WizardView.template_name` attribute or the
     145:meth:`~WizardView.get_template_names()` method, which is documented below.
     146This hook also allows you to use a different template for each form.
     147
     148This template expects a ``wizard`` object that has various items attached to
     149it:
     150
     151    * ``form`` -- The :class:`~django.forms.Form` instance for the current
     152      step (either empty or with errors).
     153
     154    * ``steps`` -- A helper object to access the various steps related data:
     155
     156        * ``step0`` -- The current step (zero-based).
     157        * ``step1`` -- The current step (one-based).
     158        * ``count`` -- The total number of steps.
     159        * ``first`` -- The first step.
     160        * ``last`` -- The last step.
     161        * ``current`` -- The current (or first) step.
     162        * ``next`` -- The next step.
     163        * ``prev`` -- The previous step.
     164        * ``index`` -- The index of the current step.
     165        * ``all`` -- A list of all steps of the wizard.
    153166
    154167You can supply extra context to this template in two ways:
    155168
    156169    * Set the :attr:`~FormWizard.extra_context` attribute on your
    157170      :class:`FormWizard` subclass to a dictionary.
    158171
    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`.
     172    * Pass a dictionary as a parameter named ``extra_context`` to your
     173      wizard's URL pattern in your URLconf.
     174
     175      See :ref:`Hooking the wizard into a URLconf <wizard-urlconf>`.
    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::
     206
     207    Note that ``management_form`` **must be used** for the wizard to work properly.
    182208
    183 .. _hooking-wizard-into-urlconf:
     209.. _wizard-urlconf:
    184210
    185211Hooking the wizard into a URLconf
    186 =================================
     212---------------------------------
    187213
    188214Finally, 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::
     215deploy the new :class:`WizardView` object a URL in the ``urls.py``. The
     216wizard's :meth:`as_view` method takes a list of your
     217:class:`~django.forms.Form` classes as an argument during instantiation::
    192218
    193     from django.conf.urls.defaults import *
    194     from testapp.forms import ContactForm1, ContactForm2, ContactWizard
     219    from django.conf.urls.defaults import patterns
     220
     221    from myapp.forms import ContactForm1, ContactForm2
     222    from myapp.views import ContactWizard
    195223
    196224    urlpatterns = patterns('',
    197         (r'^contact/$', ContactWizard([ContactForm1, ContactForm2])),
     225        (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
    198226    )
    199227
    200 Advanced ``FormWizard`` methods
     228.. _wizardview-advanced-methods:
     229
     230Advanced ``WizardView`` methods
    201231===============================
    202232
    203 .. class:: FormWizard
     233.. class:: WizardView
    204234
    205     Aside from the :meth:`~done()` method, :class:`FormWizard` offers a few
     235    Aside from the :meth:`~done()` method, :class:`WizardView` offers a few
    206236    advanced method hooks that let you customize how your wizard works.
    207237
    208238    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``.)
     239    counter as string representing the current step of the wizard. (E.g., the
     240    first form is ``'0'`` and the second form is ``'1'``)
    211241
    212 .. method:: FormWizard.prefix_for_step
     242.. method:: WizardView.get_form_prefix(step)
    213243
    214     Given the step, returns a form prefix to use.  By default, this simply uses
     244    Given the step, returns a form prefix to use. By default, this simply uses
    215245    the step itself. For more, see the :ref:`form prefix documentation
    216246    <form-prefix>`.
    217247
     248.. method:: WizardView.process_step(form)
     249
     250    Hook for modifying the wizard's internal state, given a fully validated
     251    :class:`~django.forms.Form` object. The Form is guaranteed to have clean,
     252    valid data.
     253
     254    Note that this method is called every time a page is rendered for *all*
     255    submitted steps.
     256
     257    The default implementation::
     258
     259        def process_step(self, form):
     260            return self.get_form_step_data(form)
     261
     262.. method:: WizardView.get_form_initial(step)
     263
     264    Returns a dictionary which will be passed to the form for ``step`` as
     265    ``initial``. If no initial data was provied while initializing the
     266    form wizard, a empty dictionary should be returned.
     267
     268    The default implementation::
     269
     270        def get_form_initial(self, step):
     271            return self.initial_dict.get(step, {})
     272
     273.. method:: WizardView.get_form_instance(step)
     274
     275    Returns a object which will be passed to the form for ``step`` as
     276    ``instance``. If no instance object was provied while initializing
     277    the form wizard, None be returned.
     278
     279    The default implementation::
     280
     281        def get_form_instance(self, step):
     282            return self.instance_dict.get(step, None)
     283
     284.. method:: WizardView.get_context_data(form, **kwargs)
     285
     286    Returns the template context for a step. You can overwrite this method
     287    to add more data for all or some steps. This method returns a dictionary
     288    containing the rendered form step.
     289
     290    The default template context variables are:
     291
     292    * Any data that :attr:`WizardView.get_extra_data` returns.
     293    * ``form`` -- form instance of the current step
     294    * ``wizard`` -- the wizard instance itself
     295
     296    Example to add extra variables for a specific step::
     297
     298        def get_context_data(self, form, **kwargs):
     299            context = super(MyWizard, self).get_context_data(form, **kwargs)
     300            if self.steps.current == 'my_step_name':
     301                context.update({'another_var': True})
     302            return context
     303
     304.. method:: WizardView.get_wizard_name()
     305
     306    This method can be used to change the wizard's internal name.
     307
    218308    Default implementation::
    219309
    220         def prefix_for_step(self, step):
    221             return str(step)
     310        def get_wizard_name(self):
     311            return normalize_name(self.__class__.__name__)
     312
     313.. method:: WizardView.get_prefix()
    222314
    223 .. method:: FormWizard.render_hash_failure
     315    This method returns a prefix for the storage backends. These backends use
     316    the prefix to fetch the correct data for the wizard. (Multiple wizards
     317    could save their data in one session)
    224318
    225     Renders a template if the hash check fails. It's rare that you'd need to
    226     override this.
     319    You can change this method to make the wizard data prefix more unique to,
     320    e.g. have multiple instances of one wizard in one session.
    227321
    228322    Default implementation::
    229323
    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.'})
     324        def get_prefix(self):
     325            return self.wizard_name
    235326
    236 .. method:: FormWizard.security_hash
     327.. method:: WizardView.get_form(step=None, data=None, files=None)
    237328
    238     Calculates the security hash for the given request object and
    239     :class:`~django.forms.Form` instance.
     329    This method constructs the form for a given ``step``. If no ``step`` is
     330    defined, the current step will be determined automatically.
     331    The method gets three arguments:
    240332
    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.
     333    * ``step`` -- The step for which the form instance should be generated.
     334    * ``data`` -- Gets passed to the form's data argument
     335    * ``files`` -- Gets passed to the form's files argument
    244336
    245     Example::
     337    You can override this method to add extra arguments to the form instance.
    246338
    247         def security_hash(self, request, form):
    248             return my_hash_function(request, form)
     339    Example code to add a user attribute to the form on step 2::
    249340
    250 .. method:: FormWizard.parse_params
     341        def get_form(self, step=None, data=None, files=None):
     342            form = super(MyWizard, self).get_form(step, data, files)
     343            if step == '1':
     344                form.user = self.request.user
     345            return form
    251346
    252     A hook for saving state from the request object and ``args`` / ``kwargs``
    253     that were captured from the URL by your URLconf.
     347.. method:: WizardView.process_step(form)
    254348
    255     By default, this does nothing.
     349    This method gives you a way to post-process the form data before the data
     350    gets stored within the storage backend. By default it just passed the
     351    form.data dictionary. You should not manipulate the data here but you can
     352    use the data to do some extra work if needed (e.g. set extra_data).
    256353
    257     Example::
     354    Default implementation::
    258355
    259         def parse_params(self, request, *args, **kwargs):
    260             self.my_state = args[0]
     356        def process_step(self, form):
     357            return self.get_form_step_data(form)
    261358
    262 .. method:: FormWizard.get_template
     359.. method:: WizardView.process_step_files(form)
    263360
    264     Returns the name of the template that should be used for the given step.
     361    This method gives you a way to post-process the form files before the
     362    files gets stored within the storage backend. By default it just passed
     363    the ``form.files`` dictionary. You should not manipulate the data here
     364    but you can use the data to do some extra work if needed (e.g. set extra
     365    data).
    265366
    266     By default, this returns :file:`'forms/wizard.html'`, regardless of step.
     367    Default implementation::
    267368
    268     Example::
     369        def process_step_files(self, form):
     370            return self.get_form_step_files(form)
    269371
    270         def get_template(self, step):
    271             return 'myapp/wizard_%s.html' % step
     372.. method:: WizardView.render_revalidation_failure(step, form, **kwargs)
    272373
    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::
     374    When the wizard thinks, all steps passed it revalidates all forms with the
     375    data from the backend storage.
    278376
    279         def get_template(self, step):
    280             return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
     377    If any of the forms don't validate correctly, this method gets called.
     378    This method expects two arguments, ``step`` and ``form``.
    281379
    282 .. method:: FormWizard.render_template
     380    The default implementation resets the current step to the first failing
     381    form and redirects the user to the invalid form.
    283382
    284     Renders the template for the given step, returning an
    285     :class:`~django.http.HttpResponse` object.
     383    Default implementation::
    286384
    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.
     385        def render_revalidation_failure(self, step, form, **kwargs):
     386            self.storage.current_step = step
     387            return self.render(form, **kwargs)
    290388
    291     The template will be rendered with the context documented in the
    292     "Creating templates for the forms" section above.
     389.. method:: WizardView.get_form_step_data(form)
    293390
    294 .. method:: FormWizard.process_step
     391    This method fetches the form data from and returns the dictionary. You
     392    can use this method to manipulate the values before the data gets stored
     393    in the storage backend.
    295394
    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.
     395    Default implementation::
    299396
    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.
     397        def get_form_step_data(self, form):
     398            return form.data
    303399
    304     Note that this method is called every time a page is rendered for *all*
    305     submitted steps.
     400.. method:: WizardView.get_form_step_files(form)
     401
     402    This method returns the form files. You can use this method to manipulate
     403    the files before the data gets stored in the storage backend.
     404
     405    Default implementation::
     406
     407        def get_form_step_files(self, form):
     408            return form.files
     409
     410.. method:: WizardView.get_extra_data
     411
     412    This method returns the content of the stored extra data. You can override
     413    this method to change the extra data which gets passed to the template.
     414    The default implementation passes the extra data dictionary from the
     415    storage backend.
     416
     417    Default implementation::
     418
     419        def get_extra_data(self):
     420            return self.storage.extra_data
     421
     422.. method:: WizardView.update_extra_data(data)
     423
     424    This method expects one argument ``data``. The method will fetch the
     425    current extra data, update the dictionary with the passed data and store
     426    the content back to storage backend.
     427
     428    You could change this method to protect the extra_data dictionary from
     429    external changes by just doing nothing.
    306430
    307     The function signature::
     431    Example code::
    308432
    309         def process_step(self, request, form, step):
    310             # ...
     433        def update_extra_data(self, data):
     434            pass
     435
     436.. method:: WizardView.render(form, **kwargs)
     437
     438    This method gets called after the get or post request was handled. You can
     439    hook in this method to, e.g. change the type of http response.
     440
     441    Default implementation::
     442
     443        def render(self, form=None, **kwargs):
     444            form = form or self.get_form()
     445            context = self.get_context_data(form, **kwargs)
     446            return self.render_to_response(context)
    311447
    312448Providing initial data for the forms
    313449====================================
    314450
    315 .. attribute:: FormWizard.initial
     451.. attribute:: WizardView.initial_dict
    316452
    317453    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
     454    provided using the optional :attr:`~Wizard.initial_dict` keyword argument.
     455    This argument should be a dictionary mapping the steps to dictionaries
     456    containing the initial data for each step. The dictionary of initial data
    321457    will be passed along to the constructor of the step's
    322458    :class:`~django.forms.Form`::
    323459
    324         >>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard
     460        >>> from myapp.forms import ContactForm1, ContactForm2
     461        >>> from myapp.views import ContactWizard
    325462        >>> initial = {
    326         ...     0: {'subject': 'Hello', 'sender': 'user@example.com'},
    327         ...     1: {'message': 'Hi there!'}
     463        ...     '0': {'subject': 'Hello', 'sender': 'user@example.com'},
     464        ...     '1': {'message': 'Hi there!'}
    328465        ... }
    329         >>> wiz = ContactWizard([ContactForm1, ContactForm2], initial=initial)
    330         >>> form1 = wiz.get_form(0)
    331         >>> form2 = wiz.get_form(1)
     466        >>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)
     467        >>> form1 = wiz.get_form('0')
     468        >>> form2 = wiz.get_form('1')
    332469        >>> form1.initial
    333470        {'sender': 'user@example.com', 'subject': 'Hello'}
    334471        >>> form2.initial
    335472        {'message': 'Hi there!'}
     473
     474    The ``initial_dict`` can also take a list of dictionaries for a specific
     475    step if the step is a ``FormSet``.
     476
     477.. _wizard-files:
     478
     479Handling files
     480==============
     481
     482To handle :class:`~django.forms.FileField` within any step form of the wizard,
     483you have to add a :attr:`file_storage` to your :class:`WizardView` subclass.
     484
     485This storage will temporarilyy store the uploaded files for the wizard. The
     486:attr:`file_storage` attribute should be a
     487:class:`~django.core.files.storage.Storage` subclass.
     488
     489.. warning::
     490
     491    Please remember to take care of removing old files as the
     492    :class:`WizardView` won't remove any files, whether the wizard gets
     493    finished corretly or not.
     494
     495Conditionally view/skip specific steps
     496======================================
     497
     498.. attribute:: WizardView.condition_dict
     499
     500The :meth:`~WizardView.as_view` accepts a ``condition_dict`` argument. You can pass a
     501dictionary of boolean values or callables. The key should match the steps
     502name (e.g. '0', '1').
     503
     504If the value of a specific step is callable it will be called with the
     505:class:`WizardView` instance as the only argument. If the return value is true,
     506the step's form will be used.
     507
     508This example provides a contact form including a condition. The condition is
     509used to show a message from only if a checkbox in the first step was checked.
     510
     511The steps are defined in a ``forms.py``::
     512
     513    from django import forms
     514
     515    class ContactForm1(forms.Form):
     516        subject = forms.CharField(max_length=100)
     517        sender = forms.EmailField()
     518        leave_message = forms.BooleanField(required=False)
     519
     520    class ContactForm2(forms.Form):
     521        message = forms.CharField(widget=forms.Textarea)
     522
     523We define our wizard in a ``views.py``::
     524
     525    from django.shortcuts import render_to_response
     526    from django.contrib.formtools.wizard.views import SessionWizardView
     527
     528    def show_message_form_condition(wizard):
     529        # try to get the cleaned data of step 1
     530        cleaned_data = wizard.get_cleaned_data_for_step('0') or {}
     531        # check if the field ``leave_message`` was checked.
     532        return cleaned_data.get('leave_message', True)
     533
     534    class ContactWizard(SessionWizardView):
     535
     536        def done(self, form_list, **kwargs):
     537            return render_to_response('done.html', {
     538                'form_data': [form.cleaned_data for form in form_list],
     539            })
     540
     541We need to add the ``ContactWizard`` to our ``urls.py`` file::
     542
     543    from django.conf.urls.defaults import pattern
     544
     545    from myapp.forms import ContactForm1, ContactForm2
     546    from myapp.views import ContactWizard, show_message_form_condition
     547
     548    contact_forms = [ContactForm1, ContactForm2]
     549
     550    urlpatterns = patterns('',
     551        (r'^contact/$', ContactWizard.as_view(contact_forms,
     552            condition_dict={'1': show_message_form_condition}
     553        )),
     554    )
     555
     556As you can see, we defined a ``show_message_form_condition`` next to our
     557:class:`WizardView` subclass and added a ``condition_dict`` argument to the
     558:meth:`~WizardView.as_view` method. The key refers to the second wizard step
     559(because of the zero based step index).
     560
     561How to work with ModelForm and ModelFormSet
     562===========================================
     563
     564The WizardView supports :class:`~django.forms.ModelForm` and
     565:class:`~django.forms.ModelFormSet`. Additionally to the ``initial_dict``,
     566the :meth:`~WizardView.as_view` method takes a ``instance_dict`` argument
     567with a list of instances for the ``ModelForm`` and ``ModelFormSet``.
     568
     569Usage of NamedUrlWizardView
     570===========================
     571
     572.. class:: NamedUrlWizardView
     573
     574There is a :class:`WizardView` subclass which adds named-urls support to the wizard.
     575By doing this, you can have single urls for every step.
     576
     577To use the named urls, you have to change the ``urls.py``.
     578
     579Below you will see an example of a contact wizard with two steps, step 1 with
     580"contactdata" as its name and step 2 with "leavemessage" as its name.
     581
     582Additionally you have to pass two more arguments to the
     583:meth:`~WizardView.as_view` method:
     584
     585   * ``url_name`` -- the name of the url (as provided in the urls.py)
     586   * ``done_step_name`` -- the name in the url for the done step
     587
     588Example code for the changed ``urls.py`` file::
     589
     590    from django.conf.urls.defaults import url, patterns
     591
     592    from myapp.forms import ContactForm1, ContactForm2
     593    from myapp.views import ContactWizard
     594
     595    named_contact_forms = (
     596        ('contactdata', ContactForm1),
     597        ('leavemessage', ContactForm2),
     598    )
     599
     600    contact_wizard = ContactWizard.as_view(named_contact_forms,
     601        url_name='contact_step', done_step_name='finished')
     602
     603    urlpatterns = patterns('',
     604        url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'),
     605        url(r'^contact/$', contact_wizard, name='contact'),
     606    )
  • tests/regressiontests/formwizard/tests.py

    diff --git a/tests/regressiontests/formwizard/tests.py b/tests/regressiontests/formwizard/tests.py
    index 0c94d2e..944569d 100644
    a b  
    11import re
     2import warnings
    23from django import forms
    34from django.test import TestCase
    45
     6warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
     7                        module='django.contrib.formtools.wizard')
     8
     9
    510class FormWizardWithNullBooleanField(TestCase):
    611    urls = 'regressiontests.formwizard.urls'
    712
  • 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