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