Ticket #16174: cbv-formpreview3.diff
File cbv-formpreview3.diff, 19.4 KB (added by , 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 1 1 """ 2 2 Formtools Preview application. 3 3 """ 4 5 try:6 import cPickle as pickle7 except ImportError:8 import pickle9 10 4 from django.conf import settings 11 from django.http import Http40412 from django.shortcuts import render_to_response13 from django.template.context import RequestContext14 5 from django.utils.crypto import constant_time_compare 15 6 from django.contrib.formtools.utils import form_hmac 7 from django.views.generic import FormView 16 8 9 PREVIEW_STAGE = 'preview' 10 POST_STAGE = 'post' 17 11 AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. 12 STAGE_FIELD = 'stage' 13 HASH_FIELD = 'hash' 18 14 19 class FormPreview( object):15 class FormPreview(FormView): 20 16 preview_template = 'formtools/preview.html' 21 17 form_template = 'formtools/form.html' 22 18 23 19 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### 24 20 25 def __init__(self, form): 21 def __init__(self, form_class, *args, **kwargs): 22 super(FormPreview, self).__init__(*args, **kwargs) 26 23 # 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 = {} 28 31 29 32 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 31 40 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) 37 43 38 44 def unused_name(self, name): 39 45 """ … … class FormPreview(object): 45 51 """ 46 52 while 1: 47 53 try: 48 f = self.form.base_fields[name]54 self.form_class.base_fields[name] 49 55 except KeyError: 50 56 break # This field name isn't being used by the form. 51 57 name += '_' 52 58 return name 53 59 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): 55 67 "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) 60 77 61 78 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) 70 92 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) 72 97 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) 89 102 90 103 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## 91 104 … … class FormPreview(object): 96 109 """ 97 110 return AUTO_ID 98 111 99 def get_initial(self, request ):112 def get_initial(self, request=None): 100 113 """ 101 114 Takes a request argument and returns a dictionary to pass to the form's 102 115 ``initial`` kwarg when the form is being created from an HTTP get. 103 116 """ 104 return {}117 return self.initial 105 118 106 119 def get_context(self, request, form): 107 120 "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 110 141 111 142 def parse_params(self, *args, **kwargs): 112 143 """ 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. 115 147 116 148 For example, this URLconf captures a user_id variable: 117 149 -
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, 17 17 18 18 success_string = "Done was called!" 19 19 20 20 21 class TestFormPreview(preview.FormPreview): 21 22 def get_context(self, request, form): 22 23 context = super(TestFormPreview, self).get_context(request, form) 23 24 context.update({'custom_context': True}) 24 25 return context 25 26 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 26 32 def get_initial(self, request): 27 33 return {'field1': 'Works!'} 28 34 … … class PreviewTests(TestCase): 67 73 stage = self.input % 1 68 74 self.assertContains(response, stage, 1) 69 75 self.assertEqual(response.context['custom_context'], True) 76 self.assertEqual(response.context['is_bound_form'], False) 70 77 self.assertEqual(response.context['form'].initial, {'field1': 'Works!'}) 71 78 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 72 85 def test_form_preview(self): 73 86 """ 74 87 Test contrib.formtools.preview form preview rendering. … … class PreviewTests(TestCase): 86 99 stage = self.input % 2 87 100 self.assertContains(response, stage, 1) 88 101 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 89 106 def test_form_submit(self): 90 107 """ 91 108 Test contrib.formtools.preview form submittal. … … class PreviewTests(TestCase): 140 157 response = self.client.post('/preview/', self.test_data) 141 158 self.assertEqual(response.content, success_string) 142 159 143 144 160 def test_form_submit_bad_hash(self): 145 161 """ 146 162 Test contrib.formtools.preview form submittal does not proceed … … class PreviewTests(TestCase): 154 170 self.assertNotEqual(response.content, success_string) 155 171 hash = utils.form_hmac(TestForm(self.test_data)) + "bad" 156 172 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') 158 175 self.assertNotEqual(response.content, success_string) 159 176 160 177 -
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. 17 17 Overview 18 18 ========= 19 19 20 Given a :class:` django.forms.Form` subclass that you define, this20 Given a :class:`~django.forms.Form` subclass that you define, this 21 21 application takes care of the following workflow: 22 22 23 23 1. Displays the form as HTML on a Web page. … … on the preview page, the form submission will fail the hash-comparison test. 36 36 How to use ``FormPreview`` 37 37 ========================== 38 38 39 1. Point Django atthe default FormPreview templates. There are two ways to39 1. Point Django to the default FormPreview templates. There are two ways to 40 40 do this: 41 41 42 42 * Add ``'django.contrib.formtools'`` to your … … How to use ``FormPreview`` 89 89 90 90 .. class:: FormPreview 91 91 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 97 of the class-based generic :class:`~django.views.generic.edit.FormView` and 98 implements the preview workflow described above. To use this workflow 99 in your application, subclass 95 100 ``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()` 102 method. In addition to the 103 :meth:`~django.contrib.formtools.preview.FormPreview.done()` method, 104 there are other helpful methods you might wish to override described 105 below. The class should live in the application's ``views.py`` file. 98 106 99 107 ``FormPreview`` templates 100 108 ========================= 101 109 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. 110 During 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 122 The template specified by 123 :attr:`~django.contrib.formtools.preview.FormPreview.form_template` is 124 passed ``stage_field`` in the context. This special field needs to 125 be 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 137 Note that the value of ``stage_field`` is set to ``1`` to indicate 138 that this is the first time the data is being submitted and that the 139 view should display a preview. 140 141 The template specified by 142 :attr:`~django.contrib.formtoolspreview.FormPreview.preview_template` 143 is used to preview the user's form selection and provide a way to 144 submit the form again. ``stage_field`` will be passed to the template 145 and its value should be set to ``2``, telling the 146 :class:`~django.contrib.formtools.preview.FormPreview` view that the 147 form is being posted a second and final time and that the 148 :meth:`~django.contrib.formtools.preview.FormPreview.done()` method 149 should be called. Two items related to the security hash of the form 150 are 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 158 The ``<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 170 The above template will hide the form fields from the user although 171 this isn't required. 172 173 The values of ``stage_field`` and ``hash_field`` are guaranteed not to 174 conflict with any of the field names of your 175 :class:`~django.forms.Form`. 176 109 177 110 178 Advanced ``FormPreview`` methods 111 179 ================================ 112 180 181 :meth:`~done()` is the only method you are required to implement on 182 your subclass of 183 :class:`~django.contrib.formtools.preview.FormPreview`. Aside from 184 the methods provided by the 185 :class:`~django.views.generic.edit.FormView`, you can override a 186 number 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 113 234 .. versionadded:: 1.2 114 235 115 .. method:: FormPreview.process_preview 236 .. method:: FormPreview.process_preview(request, form, context) 116 237 117 238 Given a validated form, performs any extra processing before displaying the 118 239 preview page, and saves any extra data in context. 119 240 120 241 By default, this method is empty. It is called after the form is validated, 121 242 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.