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