Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added django.contrib.formtools, including the forced-preview application

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4164 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 311fadeee016177d6f85d3988ad4bb9dc675349e 1 parent 6c0219c
Adrian Holovaty authored December 05, 2006
160  django/contrib/formtools/preview.py
... ...
@@ -0,0 +1,160 @@
  1
+"""
  2
+Formtools Preview application.
  3
+
  4
+This is an abstraction of the following workflow:
  5
+
  6
+    "Display an HTML form, force a preview, then do something with the submission."
  7
+
  8
+Given a django.newforms.Form object that you define, this takes care of the
  9
+following:
  10
+
  11
+    * Displays the form as HTML on a Web page.
  12
+    * Validates the form data once it's submitted via POST.
  13
+        * If it's valid, displays a preview page.
  14
+        * If it's not valid, redisplays the form with error messages.
  15
+    * At the preview page, if the preview confirmation button is pressed, calls
  16
+      a hook that you define -- a done() method.
  17
+
  18
+The framework enforces the required preview by passing a shared-secret hash to
  19
+the preview page. If somebody tweaks the form parameters on the preview page,
  20
+the form submission will fail the hash comparison test.
  21
+
  22
+Usage
  23
+=====
  24
+
  25
+Subclass FormPreview and define a done() method:
  26
+
  27
+    def done(self, request, clean_data):
  28
+        # ...
  29
+
  30
+This method takes an HttpRequest object and a dictionary of the form data after
  31
+it has been validated and cleaned. It should return an HttpResponseRedirect.
  32
+
  33
+Then, just instantiate your FormPreview subclass by passing it a Form class,
  34
+and pass that to your URLconf, like so:
  35
+
  36
+    (r'^post/$', MyFormPreview(MyForm)),
  37
+
  38
+The FormPreview class has a few other hooks. See the docstrings in the source
  39
+code below.
  40
+
  41
+The framework also uses two templates: 'formtools/preview.html' and
  42
+'formtools/form.html'. You can override these by setting 'preview_template' and
  43
+'form_template' attributes on your FormPreview subclass. See
  44
+django/contrib/formtools/templates for the default templates.
  45
+"""
  46
+
  47
+from django.conf import settings
  48
+from django.core.exceptions import ImproperlyConfigured
  49
+from django.http import Http404
  50
+from django.shortcuts import render_to_response
  51
+import cPickle as pickle
  52
+import md5
  53
+
  54
+AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
  55
+
  56
+class FormPreview(object):
  57
+    preview_template = 'formtools/preview.html'
  58
+    form_template = 'formtools/form.html'
  59
+
  60
+    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
  61
+
  62
+    def __init__(self, form):
  63
+        # form should be a Form class, not an instance.
  64
+        self.form, self.state = form, {}
  65
+
  66
+    def __call__(self, request, *args, **kwargs):
  67
+        stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview')
  68
+        self.parse_params(*args, **kwargs)
  69
+        try:
  70
+            method = getattr(self, stage + '_' + request.method.lower())
  71
+        except AttributeError:
  72
+            raise Http404
  73
+        return method(request)
  74
+
  75
+    def unused_name(self, name):
  76
+        """
  77
+        Given a first-choice name, adds an underscore to the name until it
  78
+        reaches a name that isn't claimed by any field in the form.
  79
+
  80
+        This is calculated rather than being hard-coded so that no field names
  81
+        are off-limits for use in the form.
  82
+        """
  83
+        while 1:
  84
+            try:
  85
+                f = self.form.fields[name]
  86
+            except KeyError:
  87
+                break # This field name isn't being used by the form.
  88
+            name += '_'
  89
+        return name
  90
+
  91
+    def preview_get(self, request):
  92
+        "Displays the form"
  93
+        f = self.form(auto_id=AUTO_ID)
  94
+        return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state})
  95
+
  96
+    def preview_post(self, request):
  97
+        "Validates the POST data. If valid, displays the preview page. Else, redisplays form."
  98
+        f = self.form(request.POST, auto_id=AUTO_ID)
  99
+        context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}
  100
+        if f.is_valid():
  101
+            context['hash_field'] = self.unused_name('hash')
  102
+            context['hash_value'] = self.security_hash(request, f)
  103
+            return render_to_response(self.preview_template, context)
  104
+        else:
  105
+            return render_to_response(self.form_template, context)
  106
+
  107
+    def post_post(self, request):
  108
+        "Validates the POST data. If valid, calls done(). Else, redisplays form."
  109
+        f = self.form(request.POST, auto_id=AUTO_ID)
  110
+        if f.is_valid():
  111
+            if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')):
  112
+                return self.failed_hash(request) # Security hash failed.
  113
+            return self.done(request, f.clean_data)
  114
+        else:
  115
+            return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state})
  116
+
  117
+    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
  118
+
  119
+    def parse_params(self, *args, **kwargs):
  120
+        """
  121
+        Given captured args and kwargs from the URLconf, saves something in
  122
+        self.state and/or raises Http404 if necessary.
  123
+
  124
+        For example, this URLconf captures a user_id variable:
  125
+
  126
+            (r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)),
  127
+
  128
+        In this case, the kwargs variable in parse_params would be
  129
+        {'user_id': 32} for a request to '/contact/32/'. You can use that
  130
+        user_id to make sure it's a valid user and/or save it for later, for
  131
+        use in done().
  132
+        """
  133
+        pass
  134
+
  135
+    def security_hash(self, request, form):
  136
+        """
  137
+        Calculates the security hash for the given Form instance.
  138
+
  139
+        This creates a list of the form field names/values in a deterministic
  140
+        order, pickles the result with the SECRET_KEY setting and takes an md5
  141
+        hash of that.
  142
+
  143
+        Subclasses may want to take into account request-specific information
  144
+        such as the IP address.
  145
+        """
  146
+        data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
  147
+        # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
  148
+        # Python 2.3, but Django requires 2.3 anyway, so that's OK.
  149
+        pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
  150
+        return md5.new(pickled).hexdigest()
  151
+
  152
+    def failed_hash(self, request):
  153
+        "Returns an HttpResponse in the case of an invalid security hash."
  154
+        return self.preview_post(request)
  155
+
  156
+    # METHODS SUBCLASSES MUST OVERRIDE ########################################
  157
+
  158
+    def done(self, request, clean_data):
  159
+        "Does something with the clean_data and returns an HttpResponseRedirect."
  160
+        raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
15  django/contrib/formtools/templates/formtools/form.html
... ...
@@ -0,0 +1,15 @@
  1
+{% extends "base.html" %}
  2
+
  3
+{% block content %}
  4
+
  5
+{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %}
  6
+
  7
+<form action="" method="post">
  8
+<table>
  9
+{{ form }}
  10
+</table>
  11
+<input type="hidden" name="{{ stage_field }}" value="1" />
  12
+<p><input type="submit" value="Submit" /></p>
  13
+</form>
  14
+
  15
+{% endblock %}
36  django/contrib/formtools/templates/formtools/preview.html
... ...
@@ -0,0 +1,36 @@
  1
+{% extends "base.html" %}
  2
+
  3
+{% block content %}
  4
+
  5
+<h1>Preview your submission</h1>
  6
+
  7
+<table>
  8
+{% for field in form %}
  9
+<tr>
  10
+<th>{{ field.verbose_name }}:</th>
  11
+<td>{{ field.data|escape }}</td>
  12
+</tr>
  13
+{% endfor %}
  14
+</table>
  15
+
  16
+<p>Security hash: {{ hash_value }}</p>
  17
+
  18
+<form action="" method="post">
  19
+{% for field in form %}{{ field.as_hidden }}
  20
+{% endfor %}
  21
+<input type="hidden" name="{{ stage_field }}" value="2" />
  22
+<input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" />
  23
+<p><input type="submit" value="Submit" /></p>
  24
+</form>
  25
+
  26
+<h1>Or edit it again</h1>
  27
+
  28
+<form action="" method="post">
  29
+<table>
  30
+{{ form }}
  31
+</table>
  32
+<input type="hidden" name="{{ stage_field }}" value="1" />
  33
+<p><input type="submit" value="Submit changes" /></p>
  34
+</form>
  35
+
  36
+{% endblock %}
17  docs/add_ons.txt
@@ -48,6 +48,23 @@ See the `csrf documentation`_.
48 48
 
49 49
 .. _csrf documentation: http://www.djangoproject.com/documentation/csrf/
50 50
 
  51
+formtools
  52
+=========
  53
+
  54
+**New in Django development version**
  55
+
  56
+A set of high-level abstractions for Django forms (django.newforms).
  57
+
  58
+django.contrib.formtools.preview
  59
+--------------------------------
  60
+
  61
+An abstraction of the following workflow:
  62
+
  63
+"Display an HTML form, force a preview, then do something with the submission."
  64
+
  65
+Full documentation for this feature does not yet exist, but you can read the
  66
+code and docstrings in ``django/contrib/formtools/preview.py`` for a start.
  67
+
51 68
 humanize
52 69
 ========
53 70
 
0  formtools/__init__.py b/django/contrib/formtools/__init__.py
No changes.

0 notes on commit 311fade

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