summaryrefslogtreecommitdiff
path: root/django/contrib/formtools/preview.py
blob: 351d991762ef84efd540c9d9148b2bc094eb9374 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""
Formtools Preview application.

This is an abstraction of the following workflow:

    "Display an HTML form, force a preview, then do something with the submission."

Given a django.newforms.Form object that you define, this takes care of the
following:

    * Displays the form as HTML on a Web page.
    * Validates the form data once it's submitted via POST.
        * If it's valid, displays a preview page.
        * If it's not valid, redisplays the form with error messages.
    * At the preview page, if the preview confirmation button is pressed, calls
      a hook that you define -- a done() method.

The framework enforces the required preview by passing a shared-secret hash to
the preview page. If somebody tweaks the form parameters on the preview page,
the form submission will fail the hash comparison test.

Usage
=====

Subclass FormPreview and define a done() method:

    def done(self, request, cleaned_data):
        # ...

This method takes an HttpRequest object and a dictionary of the form data after
it has been validated and cleaned. It should return an HttpResponseRedirect.

Then, just instantiate your FormPreview subclass by passing it a Form class,
and pass that to your URLconf, like so:

    (r'^post/$', MyFormPreview(MyForm)),

The FormPreview class has a few other hooks. See the docstrings in the source
code below.

The framework also uses two templates: 'formtools/preview.html' and
'formtools/form.html'. You can override these by setting 'preview_template' and
'form_template' attributes on your FormPreview subclass. See
django/contrib/formtools/templates for the default templates.
"""

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404
from django.shortcuts import render_to_response
from django.template.context import RequestContext
import cPickle as pickle
import md5

AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.

class FormPreview(object):
    preview_template = 'formtools/preview.html'
    form_template = 'formtools/form.html'

    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################

    def __init__(self, form):
        # form should be a Form class, not an instance.
        self.form, self.state = form, {}

    def __call__(self, request, *args, **kwargs):
        stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview')
        self.parse_params(*args, **kwargs)
        try:
            method = getattr(self, stage + '_' + request.method.lower())
        except AttributeError:
            raise Http404
        return method(request)

    def unused_name(self, name):
        """
        Given a first-choice name, adds an underscore to the name until it
        reaches a name that isn't claimed by any field in the form.

        This is calculated rather than being hard-coded so that no field names
        are off-limits for use in the form.
        """
        while 1:
            try:
                f = self.form.fields[name]
            except KeyError:
                break # This field name isn't being used by the form.
            name += '_'
        return name

    def preview_get(self, request):
        "Displays the form"
        f = self.form(auto_id=AUTO_ID)
        return render_to_response(self.form_template,
            {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state},
            context_instance=RequestContext(request))

    def preview_post(self, request):
        "Validates the POST data. If valid, displays the preview page. Else, redisplays form."
        f = self.form(request.POST, auto_id=AUTO_ID)
        context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}
        if f.is_valid():
            context['hash_field'] = self.unused_name('hash')
            context['hash_value'] = self.security_hash(request, f)
            return render_to_response(self.preview_template, context, context_instance=RequestContext(request))
        else:
            return render_to_response(self.form_template, context, context_instance=RequestContext(request))

    def post_post(self, request):
        "Validates the POST data. If valid, calls done(). Else, redisplays form."
        f = self.form(request.POST, auto_id=AUTO_ID)
        if f.is_valid():
            if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')):
                return self.failed_hash(request) # Security hash failed.
            return self.done(request, f.cleaned_data)
        else:
            return render_to_response(self.form_template,
                {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state},
                context_instance=RequestContext(request))

    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################

    def parse_params(self, *args, **kwargs):
        """
        Given captured args and kwargs from the URLconf, saves something in
        self.state and/or raises Http404 if necessary.

        For example, this URLconf captures a user_id variable:

            (r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)),

        In this case, the kwargs variable in parse_params would be
        {'user_id': 32} for a request to '/contact/32/'. You can use that
        user_id to make sure it's a valid user and/or save it for later, for
        use in done().
        """
        pass

    def security_hash(self, request, form):
        """
        Calculates the security hash for the given Form instance.

        This creates a list of the form field names/values in a deterministic
        order, pickles the result with the SECRET_KEY setting and takes an md5
        hash of that.

        Subclasses may want to take into account request-specific information
        such as the IP address.
        """
        data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
        # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
        # Python 2.3, but Django requires 2.3 anyway, so that's OK.
        pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
        return md5.new(pickled).hexdigest()

    def failed_hash(self, request):
        "Returns an HttpResponse in the case of an invalid security hash."
        return self.preview_post(request)

    # METHODS SUBCLASSES MUST OVERRIDE ########################################

    def done(self, request, cleaned_data):
        """
        Does something with the cleaned_data and returns an
        HttpResponseRedirect.
        """
        raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)