Ticket #16174: cbv-formpreview3.diff

File cbv-formpreview3.diff, 19.4 KB (added by Ryan Kaskel, 13 years ago)
  • django/contrib/formtools/preview.py

    diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py
    index b4cdeba..0acca94 100644
    a b  
    11"""
    22Formtools Preview application.
    33"""
    4 
    5 try:
    6     import cPickle as pickle
    7 except ImportError:
    8     import pickle
    9 
    104from 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
    145from django.utils.crypto import constant_time_compare
    156from django.contrib.formtools.utils import form_hmac
     7from django.views.generic import FormView
    168
     9PREVIEW_STAGE = 'preview'
     10POST_STAGE = 'post'
    1711AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
     12STAGE_FIELD = 'stage'
     13HASH_FIELD = 'hash'
    1814
    19 class FormPreview(object):
     15class FormPreview(FormView):
    2016    preview_template = 'formtools/preview.html'
    2117    form_template = 'formtools/form.html'
    2218
    2319    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
    2420
    25     def __init__(self, form):
     21    def __init__(self, form_class, *args, **kwargs):
     22        super(FormPreview, self).__init__(*args, **kwargs)
    2623        # form should be a Form class, not an instance.
    27         self.form, self.state = form, {}
     24        self.form_class = form_class
     25        self._preview_stage = PREVIEW_STAGE
     26        self._post_stage = POST_STAGE
     27        self._stages = {'1': self._preview_stage, '2': self._post_stage}
     28        # A relic of the past; override get_context_data to pass extra context
     29        # to the template. Left in for backwards compatibility.
     30        self.state = {}
    2831
    2932    def __call__(self, request, *args, **kwargs):
    30         stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview')
     33        return self.dispatch(request, *args, **kwargs)
     34
     35    def dispatch(self, request, *args, **kwargs):
     36        posted_stage = request.POST.get(self.unused_name(STAGE_FIELD))
     37        self._stage = self._stages.get(posted_stage, self._preview_stage)
     38
     39        # For backwards compatiblity
    3140        self.parse_params(*args, **kwargs)
    32         try:
    33             method = getattr(self, stage + '_' + request.method.lower())
    34         except AttributeError:
    35             raise Http404
    36         return method(request)
     41
     42        return super(FormPreview, self).dispatch(request, *args, **kwargs)
    3743
    3844    def unused_name(self, name):
    3945        """
    class FormPreview(object):  
    4551        """
    4652        while 1:
    4753            try:
    48                 f = self.form.base_fields[name]
     54                self.form_class.base_fields[name]
    4955            except KeyError:
    5056                break # This field name isn't being used by the form.
    5157            name += '_'
    5258        return name
    5359
    54     def preview_get(self, request):
     60    def _get_context_data(self, form):
     61        """ For backwards compatiblity. """
     62        context = self.get_context_data(form=form)
     63        context.update(self.get_context(self.request, form))
     64        return context
     65
     66    def get(self, request, *args, **kwargs):
    5567        "Displays the form"
    56         f = self.form(auto_id=self.get_auto_id(), initial=self.get_initial(request))
    57         return render_to_response(self.form_template,
    58             self.get_context(request, f),
    59             context_instance=RequestContext(request))
     68        form_class = self.get_form_class()
     69        form = self.get_form(form_class)
     70        context = self._get_context_data(form)
     71        self.template_name = self.form_template
     72        return self.render_to_response(context)
     73
     74    def _check_security_hash(self, token, form):
     75        expected = self.security_hash(self.request, form)
     76        return constant_time_compare(token, expected)
    6077
    6178    def preview_post(self, request):
    62         "Validates the POST data. If valid, displays the preview page. Else, redisplays form."
    63         f = self.form(request.POST, auto_id=self.get_auto_id())
    64         context = self.get_context(request, f)
    65         if f.is_valid():
    66             self.process_preview(request, f, context)
    67             context['hash_field'] = self.unused_name('hash')
    68             context['hash_value'] = self.security_hash(request, f)
    69             return render_to_response(self.preview_template, context, context_instance=RequestContext(request))
     79        """ For backwards compatibility. failed_hash calls this method by
     80        default. """
     81        self._stage = self._preview_stage
     82        return self.post(request)
     83
     84    def form_valid(self, form):
     85        context = self._get_context_data(form)
     86        if self._stage == self._preview_stage:
     87            self.process_preview(self.request, form, context)
     88            context['hash_field'] = self.unused_name(HASH_FIELD)
     89            context['hash_value'] = self.security_hash(self.request, form)
     90            self.template_name = self.preview_template
     91            return self.render_to_response(context)
    7092        else:
    71             return render_to_response(self.form_template, context, context_instance=RequestContext(request))
     93            form_hash = self.request.POST.get(self.unused_name(HASH_FIELD), '')
     94            if not self._check_security_hash(form_hash, form):
     95                return self.failed_hash(self.request) # Security hash failed.
     96            return self.done(self.request, form.cleaned_data)
    7297
    73     def _check_security_hash(self, token, request, form):
    74         expected = self.security_hash(request, form)
    75         return constant_time_compare(token, expected)
    76 
    77     def post_post(self, request):
    78         "Validates the POST data. If valid, calls done(). Else, redisplays form."
    79         f = self.form(request.POST, auto_id=self.get_auto_id())
    80         if f.is_valid():
    81             if not self._check_security_hash(request.POST.get(self.unused_name('hash'), ''),
    82                                              request, f):
    83                 return self.failed_hash(request) # Security hash failed.
    84             return self.done(request, f.cleaned_data)
    85         else:
    86             return render_to_response(self.form_template,
    87                 self.get_context(request, f),
    88                 context_instance=RequestContext(request))
     98    def form_invalid(self, form):
     99        context = self._get_context_data(form)
     100        self.template_name = self.form_template
     101        return self.render_to_response(context)
    89102
    90103    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
    91104
    class FormPreview(object):  
    96109        """
    97110        return AUTO_ID
    98111
    99     def get_initial(self, request):
     112    def get_initial(self, request=None):
    100113        """
    101114        Takes a request argument and returns a dictionary to pass to the form's
    102115        ``initial`` kwarg when the form is being created from an HTTP get.
    103116        """
    104         return {}
     117        return self.initial
    105118
    106119    def get_context(self, request, form):
    107120        "Context for template rendering."
    108         return {'form': form, 'stage_field': self.unused_name('stage'), 'state': self.state}
    109 
     121        context = {
     122            'form': form,
     123            'stage_field': self.unused_name(STAGE_FIELD),
     124            'state': self.state
     125        }
     126        return context
     127
     128    def get_form_kwargs(self):
     129        """ This is overriden to maintain backward compatibility and pass
     130        the request to get_initial. """
     131        kwargs = {
     132            'initial': self.get_initial(self.request),
     133            'auto_id': self.get_auto_id()
     134        }
     135        if self.request.method in ('POST', 'PUT'):
     136            kwargs.update({
     137                'data': self.request.POST,
     138                'files': self.request.FILES,
     139            })
     140        return kwargs
    110141
    111142    def parse_params(self, *args, **kwargs):
    112143        """
    113         Given captured args and kwargs from the URLconf, saves something in
    114         self.state and/or raises Http404 if necessary.
     144        Called in dispatch() prior to delegating the request to get() or post().
     145        Given captured args and kwargs from the URLconf, allows the ability to
     146        save something on the instance and/or raises Http404 if necessary.
    115147
    116148        For example, this URLconf captures a user_id variable:
    117149
  • django/contrib/formtools/tests/__init__.py

    diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
    index 7084386..4fba189 100644
    a b warnings.filterwarnings('ignore', category=PendingDeprecationWarning,  
    1717
    1818success_string = "Done was called!"
    1919
     20
    2021class TestFormPreview(preview.FormPreview):
    2122    def get_context(self, request, form):
    2223        context = super(TestFormPreview, self).get_context(request, form)
    2324        context.update({'custom_context': True})
    2425        return context
    2526
     27    def get_context_data(self, **kwargs):
     28        context = super(TestFormPreview, self).get_context_data(**kwargs)
     29        context['is_bound_form'] = context['form'].is_bound
     30        return context
     31
    2632    def get_initial(self, request):
    2733        return {'field1': 'Works!'}
    2834
    class PreviewTests(TestCase):  
    6773        stage = self.input % 1
    6874        self.assertContains(response, stage, 1)
    6975        self.assertEqual(response.context['custom_context'], True)
     76        self.assertEqual(response.context['is_bound_form'], False)
    7077        self.assertEqual(response.context['form'].initial, {'field1': 'Works!'})
    7178
     79    def test_invalid_form(self):
     80        """ Verifies that an invalid form displays the correct template. """
     81        self.test_data.pop('field1')
     82        response = self.client.post('/preview/', self.test_data)
     83        self.assertTemplateUsed(response, 'formtools/form.html')
     84
    7285    def test_form_preview(self):
    7386        """
    7487        Test contrib.formtools.preview form preview rendering.
    class PreviewTests(TestCase):  
    8699        stage = self.input % 2
    87100        self.assertContains(response, stage, 1)
    88101
     102        # Check that the correct context was passed to the template
     103        self.assertEqual(response.context['custom_context'], True)
     104        self.assertEqual(response.context['is_bound_form'], True)
     105
    89106    def test_form_submit(self):
    90107        """
    91108        Test contrib.formtools.preview form submittal.
    class PreviewTests(TestCase):  
    140157        response = self.client.post('/preview/', self.test_data)
    141158        self.assertEqual(response.content, success_string)
    142159
    143 
    144160    def test_form_submit_bad_hash(self):
    145161        """
    146162        Test contrib.formtools.preview form submittal does not proceed
    class PreviewTests(TestCase):  
    154170        self.assertNotEqual(response.content, success_string)
    155171        hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
    156172        self.test_data.update({'hash': hash})
    157         response = self.client.post('/previewpreview/', self.test_data)
     173        response = self.client.post('/preview/', self.test_data)
     174        self.assertTemplateUsed(response, 'formtools/preview.html')
    158175        self.assertNotEqual(response.content, success_string)
    159176
    160177
  • docs/ref/contrib/formtools/form-preview.txt

    diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt
    index c5d8b9a..d40ee97 100644
    a b Python class.  
    1717Overview
    1818=========
    1919
    20 Given a :class:`django.forms.Form` subclass that you define, this
     20Given a :class:`~django.forms.Form` subclass that you define, this
    2121application takes care of the following workflow:
    2222
    2323   1. Displays the form as HTML on a Web page.
    on the preview page, the form submission will fail the hash-comparison test.  
    3636How to use ``FormPreview``
    3737==========================
    3838
    39     1. Point Django at the default FormPreview templates. There are two ways to
     39    1. Point Django to the default FormPreview templates. There are two ways to
    4040       do this:
    4141
    4242            * Add ``'django.contrib.formtools'`` to your
    How to use ``FormPreview``  
    8989
    9090.. class:: FormPreview
    9191
    92 A :class:`~django.contrib.formtools.preview.FormPreview` class is a simple Python class
    93 that represents the preview workflow.
    94 :class:`~django.contrib.formtools.preview.FormPreview` classes must subclass
     92.. versionchanged:: 1.4
     93   :class:`~django.contrib.formtools.preview.FormPreview` is now based on
     94   :doc:`generic class-based views </ref/class-based-views>`.
     95
     96:class:`~django.contrib.formtools.preview.FormPreview` is a subclass
     97of the class-based generic :class:`~django.views.generic.edit.FormView` and
     98implements the preview workflow described above. To use this workflow
     99in your application, subclass
    95100``django.contrib.formtools.preview.FormPreview`` and override the
    96 :meth:`~django.contrib.formtools.preview.FormPreview.done()` method. They can live
    97 anywhere in your codebase.
     101:meth:`~django.contrib.formtools.preview.FormPreview.done()`
     102method. In addition to the
     103:meth:`~django.contrib.formtools.preview.FormPreview.done()` method,
     104there are other helpful methods you might wish to override described
     105below. The class should live in the application's ``views.py`` file.
    98106
    99107``FormPreview`` templates
    100108=========================
    101109
    102 By default, the form is rendered via the template :file:`formtools/form.html`,
    103 and the preview page is rendered via the template :file:`formtools/preview.html`.
    104 These values can be overridden for a particular form preview by setting
    105 :attr:`~django.contrib.formtools.preview.FormPreview.preview_template` and
    106 :attr:`~django.contrib.formtools.preview.FormPreview.form_template` attributes on the
    107 FormPreview subclass. See :file:`django/contrib/formtools/templates` for the
    108 default templates.
     110During the form preview process, two templates will be displayed:
     111
     112    1. The template specified by the attribute
     113       :attr:`~django.contrib.foremtools.preview.FormPreview.form_template`,
     114       displayed after the initial ``GET`` request. The default
     115       template is :file:`formtools/form.html`.
     116
     117    2. The template specified by the attribute
     118       :attr:`~django.contrib.formtools.preview.FormPreview.preview_template`,
     119       used to display the preview page. The default template is
     120       :file:`formtools/preview.html`.
     121
     122The template specified by
     123:attr:`~django.contrib.formtools.preview.FormPreview.form_template` is
     124passed ``stage_field`` in the context. This special field needs to
     125be included in the template:
     126
     127.. code-block:: html+django
     128
     129    <form action="." method="post">{% csrf_token %}
     130    <table>
     131    {{ form }}
     132    </table>
     133    <input type="hidden" name="{{ stage_field }}" value="1" />
     134    <input type="submit" value="Preview" />
     135    </form>
     136
     137Note that the value of ``stage_field`` is set to ``1`` to indicate
     138that this is the first time the data is being submitted and that the
     139view should display a preview.
     140
     141The template specified by
     142:attr:`~django.contrib.formtoolspreview.FormPreview.preview_template`
     143is used to preview the user's form selection and provide a way to
     144submit the form again. ``stage_field`` will be passed to the template
     145and its value should be set to ``2``, telling the
     146:class:`~django.contrib.formtools.preview.FormPreview` view that the
     147form is being posted a second and final time and that the
     148:meth:`~django.contrib.formtools.preview.FormPreview.done()` method
     149should be called. Two items related to the security hash of the form
     150are also passed to the template and must be submitted:
     151
     152    1. ``hash_field`` - The name to use for the ``<input>`` which
     153       will store the calculated hash value of the
     154       :class:`~django.forms.Form` instance.
     155
     156    2. ``hash_value`` - The actual value of the security hash.
     157
     158The ``<form>`` part of the preview template might look like:
     159
     160.. code-block:: html+django
     161
     162    <form action="" method="post">{% csrf_token %}
     163    {% for field in form %}{{ field.as_hidden }}
     164    {% endfor %}
     165    <input type="hidden" name="{{ stage_field }}" value="2" />
     166    <input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" />
     167    <p><input type="submit" value="Submit" /></p>
     168    </form>
     169
     170The above template will hide the form fields from the user although
     171this isn't required.
     172
     173The values of ``stage_field`` and ``hash_field`` are guaranteed not to
     174conflict with any of the field names of your
     175:class:`~django.forms.Form`.
     176
    109177
    110178Advanced ``FormPreview`` methods
    111179================================
    112180
     181:meth:`~done()` is the only method you are required to implement on
     182your subclass of
     183:class:`~django.contrib.formtools.preview.FormPreview`. Aside from
     184the methods provided by the
     185:class:`~django.views.generic.edit.FormView`, you can override a
     186number of other methods to customize the form preview process.
     187
     188.. note::
     189
     190    Many of :class:`~django.contrib.formtools.preview.FormPreview`'s
     191    methods take an :class:`~.django.http.HttpRequest` object as an
     192    argument. This is to maintain backward compatibility with previous
     193    versions of the class. Instances of
     194    :class:`~django.contrib.formtools.preview.FormPreview`, like all
     195    class-based generic views, have a ``request`` attribute containing
     196    the current request.
     197
     198.. method:: FormPreview.get_auto_id()
     199
     200    Returns a value that is passed to the form as the ``auto_id``
     201    keyword argument. See the :doc:`Forms API </ref/forms/api>` for
     202    more information on ``auto_id.``. The default value is
     203    ``formtools``.
     204
     205.. method:: FormPreview.get_initial(request)
     206
     207    Returns a dictionary that is passed to the form as ``initial``. The
     208    default value is an empty dictionary. Note that unlike the
     209    ``get_initial`` method on other class-based generic views, this
     210    method takes an :class:`~.django.http.HttpRequest` object as an
     211    argument. This is to maintain backwards compatibility with
     212    previous versions of the method.
     213
     214.. method:: FormPreview.parse_params(*args, **kwargs)
     215
     216    Called prior to dispatching the ``get`` or ``post`` methods, allows
     217    you to set attributes on the
     218    :class:`~django.contrib.formtools.preview.FormPreview` in a
     219    convenient way. It takes the captured ``args`` and ``kwargs`` from
     220    the URLconf as arguments. For example, let's say you had the
     221    following in your URLconf::
     222
     223        (r'^contact/(?P<user_id>\d+)/$', MyFormPreview(MyForm)),
     224
     225    You could do the following to save the user's ID for use when
     226    :meth:`~django.contrib.formtools.preview.FormPreview.done()` is
     227    called::
     228
     229        def parse_params(*args, **kwargs):
     230            self.user_id = kwargs.get('user_id')
     231
     232    This method is empty by default.
     233
    113234.. versionadded:: 1.2
    114235
    115 .. method:: FormPreview.process_preview
     236.. method:: FormPreview.process_preview(request, form, context)
    116237
    117238    Given a validated form, performs any extra processing before displaying the
    118239    preview page, and saves any extra data in context.
    119240
    120241    By default, this method is empty.  It is called after the form is validated,
    121242    but before the context is modified with hash information and rendered.
     243
     244.. method:: FormPreview.security_hash(request, form)
     245
     246    Calculates the security hash for the given instances of the
     247    :class:`~.django.http.HttpRequest` and :class:`~django.forms.Form`.
     248
     249    Subclasses may want to take into account request-specific information,
     250    such as the IP address but this method is rarely overriden.
     251
     252    By default, this returns a generated SHA1 HMAC using the form
     253    instance and your :setting:`SECRET_KEY`.
     254
     255.. method:: FormPreview.failed_hash(request)
     256
     257   Returns an :class:`~django.http.HttpResponse` when the security hash
     258   check fails. By default this returns the user to the preview stage
     259   of the process.
Back to Top