Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #3218 -- Implemented django.contrib.formtools.wizard. Thanks, H…

…onza and Oyvind. Note that there are no docs yet

git-svn-id: http://code.djangoproject.com/svn/django/trunk@7236 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 48d7a6fb300a8ccf0aab081c4205278b0cc5ccf3 1 parent 79abd05
Adrian Holovaty authored March 13, 2008

Showing 1 changed file with 235 additions and 0 deletions. Show diff stats Hide diff stats

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

0 notes on commit 48d7a6f

Please sign in to comment.
Something went wrong with that request. Please try again.