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