Ticket #9200: 9200-3.diff
File 9200-3.diff, 156.8 KB (added by , 13 years ago) |
---|
-
django/contrib/formtools/tests/__init__.py
diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index be0372a..7084386 100644
a b 1 1 import os 2 import re 2 3 import warnings 3 4 4 from django import forms,http5 from django import http 5 6 from django.conf import settings 6 7 from django.contrib.formtools import preview, wizard, utils 7 8 from django.test import TestCase 8 9 from django.test.utils import get_warnings_state, restore_warnings_state 9 10 from django.utils import unittest 10 11 12 from django.contrib.formtools.wizard.tests import * 13 from django.contrib.formtools.tests.forms import * 14 15 warnings.filterwarnings('ignore', category=PendingDeprecationWarning, 16 module='django.contrib.formtools.wizard') 11 17 12 18 success_string = "Done was called!" 13 19 … … class TestFormPreview(preview.FormPreview): 24 30 return http.HttpResponse(success_string) 25 31 26 32 27 class TestForm(forms.Form):28 field1 = forms.CharField()29 field1_ = forms.CharField()30 bool1 = forms.BooleanField(required=False)31 32 33 33 class PreviewTests(TestCase): 34 34 urls = 'django.contrib.formtools.tests.urls' 35 35 … … class PreviewTests(TestCase): 63 63 is created to manage the stage. 64 64 65 65 """ 66 response = self.client.get('/ test1/')66 response = self.client.get('/preview/') 67 67 stage = self.input % 1 68 68 self.assertContains(response, stage, 1) 69 69 self.assertEqual(response.context['custom_context'], True) … … class PreviewTests(TestCase): 81 81 # Pass strings for form submittal and add stage variable to 82 82 # show we previously saw first stage of the form. 83 83 self.test_data.update({'stage': 1}) 84 response = self.client.post('/ test1/', self.test_data)84 response = self.client.post('/preview/', self.test_data) 85 85 # Check to confirm stage is set to 2 in output form. 86 86 stage = self.input % 2 87 87 self.assertContains(response, stage, 1) … … class PreviewTests(TestCase): 99 99 # Pass strings for form submittal and add stage variable to 100 100 # show we previously saw first stage of the form. 101 101 self.test_data.update({'stage':2}) 102 response = self.client.post('/ test1/', self.test_data)102 response = self.client.post('/preview/', self.test_data) 103 103 self.assertNotEqual(response.content, success_string) 104 104 hash = self.preview.security_hash(None, TestForm(self.test_data)) 105 105 self.test_data.update({'hash': hash}) 106 response = self.client.post('/ test1/', self.test_data)106 response = self.client.post('/preview/', self.test_data) 107 107 self.assertEqual(response.content, success_string) 108 108 109 109 def test_bool_submit(self): … … class PreviewTests(TestCase): 122 122 self.test_data.update({'stage':2}) 123 123 hash = self.preview.security_hash(None, TestForm(self.test_data)) 124 124 self.test_data.update({'hash':hash, 'bool1':u'False'}) 125 response = self.client.post('/ test1/', self.test_data)125 response = self.client.post('/preview/', self.test_data) 126 126 self.assertEqual(response.content, success_string) 127 127 128 128 def test_form_submit_good_hash(self): … … class PreviewTests(TestCase): 133 133 # Pass strings for form submittal and add stage variable to 134 134 # show we previously saw first stage of the form. 135 135 self.test_data.update({'stage':2}) 136 response = self.client.post('/ test1/', self.test_data)136 response = self.client.post('/preview/', self.test_data) 137 137 self.assertNotEqual(response.content, success_string) 138 138 hash = utils.form_hmac(TestForm(self.test_data)) 139 139 self.test_data.update({'hash': hash}) 140 response = self.client.post('/ test1/', self.test_data)140 response = self.client.post('/preview/', self.test_data) 141 141 self.assertEqual(response.content, success_string) 142 142 143 143 … … class PreviewTests(TestCase): 149 149 # Pass strings for form submittal and add stage variable to 150 150 # show we previously saw first stage of the form. 151 151 self.test_data.update({'stage':2}) 152 response = self.client.post('/ test1/', self.test_data)152 response = self.client.post('/preview/', self.test_data) 153 153 self.assertEqual(response.status_code, 200) 154 154 self.assertNotEqual(response.content, success_string) 155 155 hash = utils.form_hmac(TestForm(self.test_data)) + "bad" 156 156 self.test_data.update({'hash': hash}) 157 response = self.client.post('/ test1/', self.test_data)157 response = self.client.post('/previewpreview/', self.test_data) 158 158 self.assertNotEqual(response.content, success_string) 159 159 160 160 … … class FormHmacTests(unittest.TestCase): 220 220 self.assertEqual(hash1, hash2) 221 221 222 222 223 class HashTestForm(forms.Form):224 name = forms.CharField()225 bio = forms.CharField()226 227 228 class HashTestBlankForm(forms.Form):229 name = forms.CharField(required=False)230 bio = forms.CharField(required=False)231 232 223 # 233 224 # FormWizard tests 234 225 # 235 226 236 237 class WizardPageOneForm(forms.Form): 238 field = forms.CharField() 239 240 241 class WizardPageTwoForm(forms.Form): 242 field = forms.CharField() 243 244 class WizardPageTwoAlternativeForm(forms.Form): 245 field = forms.CharField() 246 247 class WizardPageThreeForm(forms.Form): 248 field = forms.CharField() 249 250 251 class WizardClass(wizard.FormWizard): 227 class TestWizardClass(wizard.FormWizard): 252 228 253 229 def get_template(self, step): 254 return 'form wizard/wizard.html'230 return 'forms/wizard.html' 255 231 256 232 def done(self, request, cleaned_data): 257 233 return http.HttpResponse(success_string) … … class DummyRequest(http.HttpRequest): 269 245 270 246 class WizardTests(TestCase): 271 247 urls = 'django.contrib.formtools.tests.urls' 248 input_re = re.compile('name="([^"]+)" value="([^"]+)"') 249 wizard_step_data = ( 250 { 251 '0-name': 'Pony', 252 '0-thirsty': '2', 253 }, 254 { 255 '1-address1': '123 Main St', 256 '1-address2': 'Djangoland', 257 }, 258 { 259 '2-random_crap': 'blah blah', 260 } 261 ) 272 262 273 263 def setUp(self): 274 264 self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS … … class WizardTests(TestCase): 290 280 """ 291 281 step should be zero for the first form 292 282 """ 293 response = self.client.get('/wizard /')283 response = self.client.get('/wizard1/') 294 284 self.assertEqual(0, response.context['step0']) 295 285 296 286 def test_step_increments(self): 297 287 """ 298 288 step should be incremented when we go to the next page 299 289 """ 300 response = self.client.post('/wizard /', {"0-field":"test", "wizard_step":"0"})290 response = self.client.post('/wizard1/', {"0-field":"test", "wizard_step":"0"}) 301 291 self.assertEqual(1, response.context['step0']) 302 292 303 293 def test_bad_hash(self): 304 294 """ 305 295 Form should not advance if the hash is missing or bad 306 296 """ 307 response = self.client.post('/wizard /',297 response = self.client.post('/wizard1/', 308 298 {"0-field":"test", 309 299 "1-field":"test2", 310 300 "wizard_step": "1"}) … … class WizardTests(TestCase): 319 309 "1-field": "test2", 320 310 "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", 321 311 "wizard_step": "1"} 322 response = self.client.post('/wizard /', data)312 response = self.client.post('/wizard1/', data) 323 313 self.assertEqual(2, response.context['step0']) 324 314 325 315 def test_11726(self): … … class WizardTests(TestCase): 330 320 reached = [False] 331 321 that = self 332 322 333 class WizardWithProcessStep( WizardClass):323 class WizardWithProcessStep(TestWizardClass): 334 324 def process_step(self, request, form, step): 335 325 if step == 0: 336 326 if self.num_steps() < 2: … … class WizardTests(TestCase): 362 352 reached = [False] 363 353 that = self 364 354 365 class WizardWithProcessStep( WizardClass):355 class WizardWithProcessStep(TestWizardClass): 366 356 def process_step(self, request, form, step): 367 357 that.assertTrue(hasattr(form, 'cleaned_data')) 368 358 reached[0] = True … … class WizardTests(TestCase): 386 376 reached = [False] 387 377 that = self 388 378 389 class Wizard( WizardClass):379 class Wizard(TestWizardClass): 390 380 def done(self, request, form_list): 391 381 reached[0] = True 392 382 that.assertTrue(len(form_list) == 2) … … class WizardTests(TestCase): 409 399 reached = [False] 410 400 that = self 411 401 412 class WizardWithProcessStep( WizardClass):402 class WizardWithProcessStep(TestWizardClass): 413 403 def process_step(self, request, form, step): 414 404 if step == 0: 415 405 self.form_list[1] = WizardPageTwoAlternativeForm … … class WizardTests(TestCase): 426 416 "wizard_step": "1"} 427 417 wizard(DummyRequest(POST=data)) 428 418 self.assertTrue(reached[0]) 419 420 def grab_field_data(self, response): 421 """ 422 Pull the appropriate field data from the context to pass to the next wizard step 423 """ 424 previous_fields = response.context['previous_fields'] 425 fields = {'wizard_step': response.context['step0']} 426 427 def grab(m): 428 fields[m.group(1)] = m.group(2) 429 return '' 430 431 self.input_re.sub(grab, previous_fields) 432 return fields 433 434 def check_wizard_step(self, response, step_no): 435 """ 436 Helper function to test each step of the wizard 437 - Make sure the call succeeded 438 - Make sure response is the proper step number 439 - return the result from the post for the next step 440 """ 441 step_count = len(self.wizard_step_data) 442 443 self.assertEqual(response.status_code, 200) 444 self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) 445 446 data = self.grab_field_data(response) 447 data.update(self.wizard_step_data[step_no - 1]) 448 449 return self.client.post('/wizard2/', data) 450 451 def test_9473(self): 452 response = self.client.get('/wizard2/') 453 for step_no in range(1, len(self.wizard_step_data) + 1): 454 response = self.check_wizard_step(response, step_no) -
new file django/contrib/formtools/tests/forms.py
diff --git a/django/contrib/formtools/tests/forms.py b/django/contrib/formtools/tests/forms.py new file mode 100644 index 0000000..93bad92
- + 1 from django import forms 2 from django.contrib.formtools.wizard import FormWizard 3 from django.http import HttpResponse 4 5 class Page1(forms.Form): 6 name = forms.CharField(max_length=100) 7 thirsty = forms.NullBooleanField() 8 9 class Page2(forms.Form): 10 address1 = forms.CharField(max_length=100) 11 address2 = forms.CharField(max_length=100) 12 13 class Page3(forms.Form): 14 random_crap = forms.CharField(max_length=100) 15 16 class ContactWizard(FormWizard): 17 def done(self, request, form_list): 18 return HttpResponse("") 19 20 class TestForm(forms.Form): 21 field1 = forms.CharField() 22 field1_ = forms.CharField() 23 bool1 = forms.BooleanField(required=False) 24 25 class HashTestForm(forms.Form): 26 name = forms.CharField() 27 bio = forms.CharField() 28 29 class HashTestBlankForm(forms.Form): 30 name = forms.CharField(required=False) 31 bio = forms.CharField(required=False) 32 33 class WizardPageOneForm(forms.Form): 34 field = forms.CharField() 35 36 class WizardPageTwoForm(forms.Form): 37 field = forms.CharField() 38 39 class WizardPageTwoAlternativeForm(forms.Form): 40 field = forms.CharField() 41 42 class WizardPageThreeForm(forms.Form): 43 field = forms.CharField() -
new file django/contrib/formtools/tests/templates/forms/wizard.html
diff --git a/django/contrib/formtools/tests/templates/forms/wizard.html b/django/contrib/formtools/tests/templates/forms/wizard.html new file mode 100644 index 0000000..c911c3c
- + 1 <html> 2 <body> 3 <p>Step {{ step }} of {{ step_count }}</p> 4 <form action="." method="post"> 5 <table> 6 {{ form }} 7 </table> 8 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" /> 9 {{ previous_fields|safe }} 10 <input type="submit"> 11 </form> 12 </body> 13 </html> -
deleted file django/contrib/formtools/tests/templates/formwizard/wizard.html
diff --git a/django/contrib/formtools/tests/templates/formwizard/wizard.html b/django/contrib/formtools/tests/templates/formwizard/wizard.html deleted file mode 100644 index 42b6e78..0000000
+ - 1 <p>Step {{ step }} of {{ step_count }}</p>2 <form action="." method="post">{% csrf_token %}3 <table>4 {{ form }}5 </table>6 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />7 {{ previous_fields|safe }}8 <input type="submit">9 </form> -
django/contrib/formtools/tests/urls.py
diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py index 6fc1e4e..f058335 100644
a b This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. 3 3 """ 4 4 5 5 from django.conf.urls.defaults import * 6 from django.contrib.formtools.tests import * 6 from django.contrib.formtools.tests import TestFormPreview, TestWizardClass 7 8 from forms import (ContactWizard, Page1, Page2, Page3, TestForm, 9 WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm) 7 10 8 11 urlpatterns = patterns('', 9 (r'^test1/', TestFormPreview(TestForm)),10 (r'^wizard/$', WizardClass([WizardPageOneForm,11 WizardPageTwoForm,12 WizardPageThreeForm])),13 12 url(r'^preview/', TestFormPreview(TestForm)), 13 url(r'^wizard1/$', TestWizardClass( 14 [WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm])), 15 url(r'^wizard2/$', ContactWizard([Page1, Page2, Page3])), 16 ) -
deleted file django/contrib/formtools/wizard.py
diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py deleted file mode 100644 index c19578c..0000000
+ - 1 """2 FormWizard class -- implements a multi-page form, validating between each3 step and storing the form's state as HTML hidden fields so that no state is4 stored on the server side.5 """6 7 try:8 import cPickle as pickle9 except ImportError:10 import pickle11 12 from django import forms13 from django.conf import settings14 from django.contrib.formtools.utils import form_hmac15 from django.http import Http40416 from django.shortcuts import render_to_response17 from django.template.context import RequestContext18 from django.utils.crypto import constant_time_compare19 from django.utils.translation import ugettext_lazy as _20 from django.utils.decorators import method_decorator21 from django.views.decorators.csrf import csrf_protect22 23 24 class FormWizard(object):25 # The HTML (and POST data) field name for the "step" variable.26 step_field_name="wizard_step"27 28 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################29 30 def __init__(self, form_list, initial=None):31 """32 Start a new wizard with a list of forms.33 34 form_list should be a list of Form classes (not instances).35 """36 self.form_list = form_list[:]37 self.initial = initial or {}38 39 # Dictionary of extra template context variables.40 self.extra_context = {}41 42 # A zero-based counter keeping track of which step we're in.43 self.step = 044 45 def __repr__(self):46 return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)47 48 def get_form(self, step, data=None):49 "Helper method that returns the Form instance for the given step."50 # Sanity check.51 if step >= self.num_steps():52 raise Http404('Step %s does not exist' % step)53 return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))54 55 def num_steps(self):56 "Helper method that returns the number of steps."57 # You might think we should just set "self.num_steps = len(form_list)"58 # in __init__(), but this calculation needs to be dynamic, because some59 # hook methods might alter self.form_list.60 return len(self.form_list)61 62 def _check_security_hash(self, token, request, form):63 expected = self.security_hash(request, form)64 return constant_time_compare(token, expected)65 66 @method_decorator(csrf_protect)67 def __call__(self, request, *args, **kwargs):68 """69 Main method that does all the hard work, conforming to the Django view70 interface.71 """72 if 'extra_context' in kwargs:73 self.extra_context.update(kwargs['extra_context'])74 current_step = self.determine_step(request, *args, **kwargs)75 self.parse_params(request, *args, **kwargs)76 77 # Validate and process all the previous forms before instantiating the78 # current step's form in case self.process_step makes changes to79 # self.form_list.80 81 # If any of them fails validation, that must mean the validator relied82 # on some other input, such as an external Web site.83 84 # It is also possible that alidation might fail under certain attack85 # situations: an attacker might be able to bypass previous stages, and86 # generate correct security hashes for all the skipped stages by virtue87 # of:88 # 1) having filled out an identical form which doesn't have the89 # validation (and does something different at the end),90 # 2) or having filled out a previous version of the same form which91 # had some validation missing,92 # 3) or previously having filled out the form when they had more93 # privileges than they do now.94 #95 # Since the hashes only take into account values, and not other other96 # validation the form might do, we must re-do validation now for97 # security reasons.98 previous_form_list = []99 for i in range(current_step):100 f = self.get_form(i, request.POST)101 if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),102 request, f):103 return self.render_hash_failure(request, i)104 105 if not f.is_valid():106 return self.render_revalidation_failure(request, i, f)107 else:108 self.process_step(request, f, i)109 previous_form_list.append(f)110 111 # Process the current step. If it's valid, go to the next step or call112 # done(), depending on whether any steps remain.113 if request.method == 'POST':114 form = self.get_form(current_step, request.POST)115 else:116 form = self.get_form(current_step)117 118 if form.is_valid():119 self.process_step(request, form, current_step)120 next_step = current_step + 1121 122 if next_step == self.num_steps():123 return self.done(request, previous_form_list + [form])124 else:125 form = self.get_form(next_step)126 self.step = current_step = next_step127 128 return self.render(form, request, current_step)129 130 def render(self, form, request, step, context=None):131 "Renders the given Form object, returning an HttpResponse."132 old_data = request.POST133 prev_fields = []134 if old_data:135 hidden = forms.HiddenInput()136 # Collect all data from previous steps and render it as HTML hidden fields.137 for i in range(step):138 old_form = self.get_form(i, old_data)139 hash_name = 'hash_%s' % i140 prev_fields.extend([bf.as_hidden() for bf in old_form])141 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))142 return self.render_template(request, form, ''.join(prev_fields), step, context)143 144 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################145 146 def prefix_for_step(self, step):147 "Given the step, returns a Form prefix to use."148 return str(step)149 150 def render_hash_failure(self, request, step):151 """152 Hook for rendering a template if a hash check failed.153 154 step is the step that failed. Any previous step is guaranteed to be155 valid.156 157 This default implementation simply renders the form for the given step,158 but subclasses may want to display an error message, etc.159 """160 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.')})161 162 def render_revalidation_failure(self, request, step, form):163 """164 Hook for rendering a template if final revalidation failed.165 166 It is highly unlikely that this point would ever be reached, but See167 the comment in __call__() for an explanation.168 """169 return self.render(form, request, step)170 171 def security_hash(self, request, form):172 """173 Calculates the security hash for the given HttpRequest and Form instances.174 175 Subclasses may want to take into account request-specific information,176 such as the IP address.177 """178 return form_hmac(form)179 180 def determine_step(self, request, *args, **kwargs):181 """182 Given the request object and whatever *args and **kwargs were passed to183 __call__(), returns the current step (which is zero-based).184 185 Note that the result should not be trusted. It may even be a completely186 invalid number. It's not the job of this method to validate it.187 """188 if not request.POST:189 return 0190 try:191 step = int(request.POST.get(self.step_field_name, 0))192 except ValueError:193 return 0194 return step195 196 def parse_params(self, request, *args, **kwargs):197 """198 Hook for setting some state, given the request object and whatever199 *args and **kwargs were passed to __call__(), sets some state.200 201 This is called at the beginning of __call__().202 """203 pass204 205 def get_template(self, step):206 """207 Hook for specifying the name of the template to use for a given step.208 209 Note that this can return a tuple of template names if you'd like to210 use the template system's select_template() hook.211 """212 return 'forms/wizard.html'213 214 def render_template(self, request, form, previous_fields, step, context=None):215 """216 Renders the template for the given step, returning an HttpResponse object.217 218 Override this method if you want to add a custom context, return a219 different MIME type, etc. If you only need to override the template220 name, use get_template() instead.221 222 The template will be rendered with the following context:223 step_field -- The name of the hidden field containing the step.224 step0 -- The current step (zero-based).225 step -- The current step (one-based).226 step_count -- The total number of steps.227 form -- The Form instance for the current step (either empty228 or with errors).229 previous_fields -- A string representing every previous data field,230 plus hashes for completed forms, all in the form of231 hidden fields. Note that you'll need to run this232 through the "safe" template filter, to prevent233 auto-escaping, because it's raw HTML.234 """235 context = context or {}236 context.update(self.extra_context)237 return render_to_response(self.get_template(step), dict(context,238 step_field=self.step_field_name,239 step0=step,240 step=step + 1,241 step_count=self.num_steps(),242 form=form,243 previous_fields=previous_fields244 ), context_instance=RequestContext(request))245 246 def process_step(self, request, form, step):247 """248 Hook for modifying the FormWizard's internal state, given a fully249 validated Form object. The Form is guaranteed to have clean, valid250 data.251 252 This method should *not* modify any of that data. Rather, it might want253 to set self.extra_context or dynamically alter self.form_list, based on254 previously submitted forms.255 256 Note that this method is called every time a page is rendered for *all*257 submitted steps.258 """259 pass260 261 # METHODS SUBCLASSES MUST OVERRIDE ########################################262 263 def done(self, request, form_list):264 """265 Hook for doing something with the validated data. This is responsible266 for the final processing.267 268 form_list is a list of Form instances, each containing clean, valid269 data.270 """271 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) -
new file django/contrib/formtools/wizard/__init__.py
diff --git a/django/contrib/formtools/wizard/__init__.py b/django/contrib/formtools/wizard/__init__.py new file mode 100644 index 0000000..8e51a31
- + 1 from django.contrib.formtools.wizard.legacy import FormWizard -
new file django/contrib/formtools/wizard/forms.py
diff --git a/django/contrib/formtools/wizard/forms.py b/django/contrib/formtools/wizard/forms.py new file mode 100644 index 0000000..bf46c5c
- + 1 from django import forms 2 3 class ManagementForm(forms.Form): 4 """ 5 ``ManagementForm`` is used to keep track of the current wizard step. 6 """ 7 current_step = forms.CharField(widget=forms.HiddenInput) -
new file django/contrib/formtools/wizard/legacy.py
diff --git a/django/contrib/formtools/wizard/legacy.py b/django/contrib/formtools/wizard/legacy.py new file mode 100644 index 0000000..532635a
- + 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 from django.forms import HiddenInput 7 from django.http import Http404 8 from django.shortcuts import render_to_response 9 from django.template.context import RequestContext 10 from django.utils.crypto import constant_time_compare 11 from django.utils.translation import ugettext_lazy as _ 12 from django.utils.decorators import method_decorator 13 from django.views.decorators.csrf import csrf_protect 14 15 from django.contrib.formtools.utils import form_hmac 16 17 class FormWizard(object): 18 # The HTML (and POST data) field name for the "step" variable. 19 step_field_name="wizard_step" 20 21 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### 22 23 def __init__(self, form_list, initial=None): 24 """ 25 Start a new wizard with a list of forms. 26 27 form_list should be a list of Form classes (not instances). 28 """ 29 self.form_list = form_list[:] 30 self.initial = initial or {} 31 32 # Dictionary of extra template context variables. 33 self.extra_context = {} 34 35 # A zero-based counter keeping track of which step we're in. 36 self.step = 0 37 38 import warnings 39 warnings.warn( 40 'Old-style form wizards have been deprecated; use the class-based ' 41 'views in django.contrib.formtools.wizard.views instead.', 42 PendingDeprecationWarning) 43 44 def __repr__(self): 45 return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) 46 47 def get_form(self, step, data=None): 48 "Helper method that returns the Form instance for the given step." 49 # Sanity check. 50 if step >= self.num_steps(): 51 raise Http404('Step %s does not exist' % step) 52 return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None)) 53 54 def num_steps(self): 55 "Helper method that returns the number of steps." 56 # You might think we should just set "self.num_steps = len(form_list)" 57 # in __init__(), but this calculation needs to be dynamic, because some 58 # hook methods might alter self.form_list. 59 return len(self.form_list) 60 61 def _check_security_hash(self, token, request, form): 62 expected = self.security_hash(request, form) 63 return constant_time_compare(token, expected) 64 65 @method_decorator(csrf_protect) 66 def __call__(self, request, *args, **kwargs): 67 """ 68 Main method that does all the hard work, conforming to the Django view 69 interface. 70 """ 71 if 'extra_context' in kwargs: 72 self.extra_context.update(kwargs['extra_context']) 73 current_step = self.get_current_or_first_step(request, *args, **kwargs) 74 self.parse_params(request, *args, **kwargs) 75 76 # Validate and process all the previous forms before instantiating the 77 # current step's form in case self.process_step makes changes to 78 # self.form_list. 79 80 # If any of them fails validation, that must mean the validator relied 81 # on some other input, such as an external Web site. 82 83 # It is also possible that alidation might fail under certain attack 84 # situations: an attacker might be able to bypass previous stages, and 85 # generate correct security hashes for all the skipped stages by virtue 86 # of: 87 # 1) having filled out an identical form which doesn't have the 88 # validation (and does something different at the end), 89 # 2) or having filled out a previous version of the same form which 90 # had some validation missing, 91 # 3) or previously having filled out the form when they had more 92 # privileges than they do now. 93 # 94 # Since the hashes only take into account values, and not other other 95 # validation the form might do, we must re-do validation now for 96 # security reasons. 97 previous_form_list = [] 98 for i in range(current_step): 99 f = self.get_form(i, request.POST) 100 if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), 101 request, f): 102 return self.render_hash_failure(request, i) 103 104 if not f.is_valid(): 105 return self.render_revalidation_failure(request, i, f) 106 else: 107 self.process_step(request, f, i) 108 previous_form_list.append(f) 109 110 # Process the current step. If it's valid, go to the next step or call 111 # done(), depending on whether any steps remain. 112 if request.method == 'POST': 113 form = self.get_form(current_step, request.POST) 114 else: 115 form = self.get_form(current_step) 116 117 if form.is_valid(): 118 self.process_step(request, form, current_step) 119 next_step = current_step + 1 120 121 if next_step == self.num_steps(): 122 return self.done(request, previous_form_list + [form]) 123 else: 124 form = self.get_form(next_step) 125 self.step = current_step = next_step 126 127 return self.render(form, request, current_step) 128 129 def render(self, form, request, step, context=None): 130 "Renders the given Form object, returning an HttpResponse." 131 old_data = request.POST 132 prev_fields = [] 133 if old_data: 134 hidden = HiddenInput() 135 # Collect all data from previous steps and render it as HTML hidden fields. 136 for i in range(step): 137 old_form = self.get_form(i, old_data) 138 hash_name = 'hash_%s' % i 139 prev_fields.extend([bf.as_hidden() for bf in old_form]) 140 prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form)))) 141 return self.render_template(request, form, ''.join(prev_fields), step, context) 142 143 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## 144 145 def prefix_for_step(self, step): 146 "Given the step, returns a Form prefix to use." 147 return str(step) 148 149 def render_hash_failure(self, request, step): 150 """ 151 Hook for rendering a template if a hash check failed. 152 153 step is the step that failed. Any previous step is guaranteed to be 154 valid. 155 156 This default implementation simply renders the form for the given step, 157 but subclasses may want to display an error message, etc. 158 """ 159 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.')}) 160 161 def render_revalidation_failure(self, request, step, form): 162 """ 163 Hook for rendering a template if final revalidation failed. 164 165 It is highly unlikely that this point would ever be reached, but See 166 the comment in __call__() for an explanation. 167 """ 168 return self.render(form, request, step) 169 170 def security_hash(self, request, form): 171 """ 172 Calculates the security hash for the given HttpRequest and Form instances. 173 174 Subclasses may want to take into account request-specific information, 175 such as the IP address. 176 """ 177 return form_hmac(form) 178 179 def get_current_or_first_step(self, request, *args, **kwargs): 180 """ 181 Given the request object and whatever *args and **kwargs were passed to 182 __call__(), returns the current step (which is zero-based). 183 184 Note that the result should not be trusted. It may even be a completely 185 invalid number. It's not the job of this method to validate it. 186 """ 187 if not request.POST: 188 return 0 189 try: 190 step = int(request.POST.get(self.step_field_name, 0)) 191 except ValueError: 192 return 0 193 return step 194 195 def parse_params(self, request, *args, **kwargs): 196 """ 197 Hook for setting some state, given the request object and whatever 198 *args and **kwargs were passed to __call__(), sets some state. 199 200 This is called at the beginning of __call__(). 201 """ 202 pass 203 204 def get_template(self, step): 205 """ 206 Hook for specifying the name of the template to use for a given step. 207 208 Note that this can return a tuple of template names if you'd like to 209 use the template system's select_template() hook. 210 """ 211 return 'forms/wizard.html' 212 213 def render_template(self, request, form, previous_fields, step, context=None): 214 """ 215 Renders the template for the given step, returning an HttpResponse object. 216 217 Override this method if you want to add a custom context, return a 218 different MIME type, etc. If you only need to override the template 219 name, use get_template() instead. 220 221 The template will be rendered with the following context: 222 step_field -- The name of the hidden field containing the step. 223 step0 -- The current step (zero-based). 224 step -- The current step (one-based). 225 step_count -- The total number of steps. 226 form -- The Form instance for the current step (either empty 227 or with errors). 228 previous_fields -- A string representing every previous data field, 229 plus hashes for completed forms, all in the form of 230 hidden fields. Note that you'll need to run this 231 through the "safe" template filter, to prevent 232 auto-escaping, because it's raw HTML. 233 """ 234 context = context or {} 235 context.update(self.extra_context) 236 return render_to_response(self.get_template(step), dict(context, 237 step_field=self.step_field_name, 238 step0=step, 239 step=step + 1, 240 step_count=self.num_steps(), 241 form=form, 242 previous_fields=previous_fields 243 ), context_instance=RequestContext(request)) 244 245 def process_step(self, request, form, step): 246 """ 247 Hook for modifying the FormWizard's internal state, given a fully 248 validated Form object. The Form is guaranteed to have clean, valid 249 data. 250 251 This method should *not* modify any of that data. Rather, it might want 252 to set self.extra_context or dynamically alter self.form_list, based on 253 previously submitted forms. 254 255 Note that this method is called every time a page is rendered for *all* 256 submitted steps. 257 """ 258 pass 259 260 # METHODS SUBCLASSES MUST OVERRIDE ######################################## 261 262 def done(self, request, form_list): 263 """ 264 Hook for doing something with the validated data. This is responsible 265 for the final processing. 266 267 form_list is a list of Form instances, each containing clean, valid 268 data. 269 """ 270 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) -
new file django/contrib/formtools/wizard/storage/__init__.py
diff --git a/django/contrib/formtools/wizard/storage/__init__.py b/django/contrib/formtools/wizard/storage/__init__.py new file mode 100644 index 0000000..b88ccc7
- + 1 from django.utils.importlib import import_module 2 3 from django.contrib.formtools.wizard.storage.base import BaseStorage 4 from django.contrib.formtools.wizard.storage.exceptions import ( 5 MissingStorageModule, MissingStorageClass, NoFileStorageConfigured) 6 7 8 def get_storage(path, *args, **kwargs): 9 i = path.rfind('.') 10 module, attr = path[:i], path[i+1:] 11 try: 12 mod = import_module(module) 13 except ImportError, e: 14 raise MissingStorageModule( 15 'Error loading storage %s: "%s"' % (module, e)) 16 try: 17 storage_class = getattr(mod, attr) 18 except AttributeError: 19 raise MissingStorageClass( 20 'Module "%s" does not define a storage named "%s"' % (module, attr)) 21 return storage_class(*args, **kwargs) 22 -
new file django/contrib/formtools/wizard/storage/base.py
diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py new file mode 100644 index 0000000..475b39d
- + 1 from django.core.files.uploadedfile import UploadedFile 2 from django.utils.functional import lazy_property 3 from django.utils.encoding import smart_str 4 5 from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured 6 7 class BaseStorage(object): 8 step_key = 'step' 9 step_data_key = 'step_data' 10 step_files_key = 'step_files' 11 extra_data_key = 'extra_data' 12 13 def __init__(self, prefix, request=None, file_storage=None): 14 self.prefix = 'wizard_%s' % prefix 15 self.request = request 16 self.file_storage = file_storage 17 18 def init_data(self): 19 self.data = { 20 self.step_key: None, 21 self.step_data_key: {}, 22 self.step_files_key: {}, 23 self.extra_data_key: {}, 24 } 25 26 def reset(self): 27 self.init_data() 28 29 def _get_current_step(self): 30 return self.data[self.step_key] 31 32 def _set_current_step(self, step): 33 self.data[self.step_key] = step 34 35 current_step = lazy_property(_get_current_step, _set_current_step) 36 37 def _get_extra_data(self): 38 return self.data[self.extra_data_key] or {} 39 40 def _set_extra_data(self, extra_data): 41 self.data[self.extra_data_key] = extra_data 42 43 extra_data = lazy_property(_get_extra_data, _set_extra_data) 44 45 def get_step_data(self, step): 46 return self.data[self.step_data_key].get(step, None) 47 48 def set_step_data(self, step, cleaned_data): 49 self.data[self.step_data_key][step] = cleaned_data 50 51 @property 52 def current_step_data(self): 53 return self.get_step_data(self.current_step) 54 55 def get_step_files(self, step): 56 wizard_files = self.data[self.step_files_key].get(step, {}) 57 58 if wizard_files and not self.file_storage: 59 raise NoFileStorageConfigured 60 61 files = {} 62 for field, field_dict in wizard_files.iteritems(): 63 field_dict = dict((smart_str(k), v) 64 for k, v in field_dict.iteritems()) 65 tmp_name = field_dict.pop('tmp_name') 66 files[field] = UploadedFile( 67 file=self.file_storage.open(tmp_name), **field_dict) 68 return files or None 69 70 def set_step_files(self, step, files): 71 if files and not self.file_storage: 72 raise NoFileStorageConfigured 73 74 if step not in self.data[self.step_files_key]: 75 self.data[self.step_files_key][step] = {} 76 77 for field, field_file in (files or {}).iteritems(): 78 tmp_filename = self.file_storage.save(field_file.name, field_file) 79 file_dict = { 80 'tmp_name': tmp_filename, 81 'name': field_file.name, 82 'content_type': field_file.content_type, 83 'size': field_file.size, 84 'charset': field_file.charset 85 } 86 self.data[self.step_files_key][step][field] = file_dict 87 88 @property 89 def current_step_files(self): 90 return self.get_step_files(self.current_step) 91 92 def update_response(self, response): 93 pass -
new file django/contrib/formtools/wizard/storage/cookie.py
diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py new file mode 100644 index 0000000..af26e01
- + 1 from django.core.exceptions import SuspiciousOperation 2 from django.core.signing import BadSignature 3 from django.utils import simplejson as json 4 5 from django.contrib.formtools.wizard import storage 6 7 8 class CookieStorage(storage.BaseStorage): 9 encoder = json.JSONEncoder(separators=(',', ':')) 10 11 def __init__(self, *args, **kwargs): 12 super(CookieStorage, self).__init__(*args, **kwargs) 13 self.data = self.load_data() 14 if self.data is None: 15 self.init_data() 16 17 def load_data(self): 18 try: 19 data = self.request.get_signed_cookie(self.prefix) 20 except KeyError: 21 data = None 22 except BadSignature: 23 raise SuspiciousOperation('FormWizard cookie manipulated') 24 if data is None: 25 return None 26 return json.loads(data, cls=json.JSONDecoder) 27 28 def update_response(self, response): 29 if self.data: 30 response.set_signed_cookie(self.prefix, self.encoder.encode(self.data)) 31 else: 32 response.delete_cookie(self.prefix) -
new file django/contrib/formtools/wizard/storage/exceptions.py
diff --git a/django/contrib/formtools/wizard/storage/exceptions.py b/django/contrib/formtools/wizard/storage/exceptions.py new file mode 100644 index 0000000..eab9030
- + 1 from django.core.exceptions import ImproperlyConfigured 2 3 class MissingStorageModule(ImproperlyConfigured): 4 pass 5 6 class MissingStorageClass(ImproperlyConfigured): 7 pass 8 9 class NoFileStorageConfigured(ImproperlyConfigured): 10 pass -
new file django/contrib/formtools/wizard/storage/session.py
diff --git a/django/contrib/formtools/wizard/storage/session.py b/django/contrib/formtools/wizard/storage/session.py new file mode 100644 index 0000000..84a3848
- + 1 from django.core.files.uploadedfile import UploadedFile 2 from django.contrib.formtools.wizard import storage 3 4 5 class SessionStorage(storage.BaseStorage): 6 7 def __init__(self, *args, **kwargs): 8 super(SessionStorage, self).__init__(*args, **kwargs) 9 if self.prefix not in self.request.session: 10 self.init_data() 11 12 def _get_data(self): 13 self.request.session.modified = True 14 return self.request.session[self.prefix] 15 16 def _set_data(self, value): 17 self.request.session[self.prefix] = value 18 self.request.session.modified = True 19 20 data = property(_get_data, _set_data) -
new file django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
diff --git a/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html b/django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html new file mode 100644 index 0000000..b98e58d
- + 1 {% load i18n %} 2 {% csrf_token %} 3 {{ wizard.management_form }} 4 {% if wizard.form.forms %} 5 {{ wizard.form.management_form }} 6 {% for form in wizard.form.forms %} 7 {{ form.as_p }} 8 {% endfor %} 9 {% else %} 10 {{ wizard.form.as_p }} 11 {% endif %} 12 13 {% if wizard.steps.prev %} 14 <button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button> 15 <button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button> 16 {% endif %} 17 <input type="submit" name="submit" value="{% trans "submit" %}" /> -
new file django/contrib/formtools/wizard/tests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/__init__.py b/django/contrib/formtools/wizard/tests/__init__.py new file mode 100644 index 0000000..7c66c82
- + 1 from django.contrib.formtools.wizard.tests.formtests import * 2 from django.contrib.formtools.wizard.tests.sessionstoragetests import * 3 from django.contrib.formtools.wizard.tests.cookiestoragetests import * 4 from django.contrib.formtools.wizard.tests.loadstoragetests import * 5 from django.contrib.formtools.wizard.tests.wizardtests import * 6 from django.contrib.formtools.wizard.tests.namedwizardtests import * -
new file django/contrib/formtools/wizard/tests/cookiestoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/cookiestoragetests.py b/django/contrib/formtools/wizard/tests/cookiestoragetests.py new file mode 100644 index 0000000..74c7e82
- + 1 from django.test import TestCase 2 from django.core import signing 3 from django.core.exceptions import SuspiciousOperation 4 from django.http import HttpResponse 5 6 from django.contrib.formtools.wizard.storage.cookie import CookieStorage 7 from django.contrib.formtools.wizard.tests.storagetests import get_request, TestStorage 8 9 class TestCookieStorage(TestStorage, TestCase): 10 def get_storage(self): 11 return CookieStorage 12 13 def test_manipulated_cookie(self): 14 request = get_request() 15 storage = self.get_storage()('wizard1', request, None) 16 17 cookie_signer = signing.get_cookie_signer(storage.prefix) 18 19 storage.request.COOKIES[storage.prefix] = cookie_signer.sign( 20 storage.encoder.encode({'key1': 'value1'})) 21 22 self.assertEqual(storage.load_data(), {'key1': 'value1'}) 23 24 storage.request.COOKIES[storage.prefix] = 'i_am_manipulated' 25 self.assertRaises(SuspiciousOperation, storage.load_data) 26 27 def test_reset_cookie(self): 28 request = get_request() 29 storage = self.get_storage()('wizard1', request, None) 30 31 storage.data = {'key1': 'value1'} 32 33 response = HttpResponse() 34 storage.update_response(response) 35 36 cookie_signer = signing.get_cookie_signer(storage.prefix) 37 signed_cookie_data = cookie_signer.sign(storage.encoder.encode(storage.data)) 38 self.assertEqual(response.cookies[storage.prefix].value, signed_cookie_data) 39 40 storage.init_data() 41 storage.update_response(response) 42 unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) 43 self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}') -
new file django/contrib/formtools/wizard/tests/formtests.py
diff --git a/django/contrib/formtools/wizard/tests/formtests.py b/django/contrib/formtools/wizard/tests/formtests.py new file mode 100644 index 0000000..111981f
- + 1 from django import forms, http 2 from django.conf import settings 3 from django.test import TestCase 4 from django.template.response import TemplateResponse 5 from django.utils.importlib import import_module 6 7 from django.contrib.auth.models import User 8 9 from django.contrib.formtools.wizard.views import (WizardView, 10 SessionWizardView, 11 CookieWizardView) 12 13 14 class DummyRequest(http.HttpRequest): 15 def __init__(self, POST=None): 16 super(DummyRequest, self).__init__() 17 self.method = POST and "POST" or "GET" 18 if POST is not None: 19 self.POST.update(POST) 20 self.session = {} 21 self._dont_enforce_csrf_checks = True 22 23 def get_request(*args, **kwargs): 24 request = DummyRequest(*args, **kwargs) 25 engine = import_module(settings.SESSION_ENGINE) 26 request.session = engine.SessionStore(None) 27 return request 28 29 class Step1(forms.Form): 30 name = forms.CharField() 31 32 class Step2(forms.Form): 33 name = forms.CharField() 34 35 class Step3(forms.Form): 36 data = forms.CharField() 37 38 class UserForm(forms.ModelForm): 39 class Meta: 40 model = User 41 42 UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2) 43 44 class TestWizard(WizardView): 45 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 46 47 def dispatch(self, request, *args, **kwargs): 48 response = super(TestWizard, self).dispatch(request, *args, **kwargs) 49 return response, self 50 51 class FormTests(TestCase): 52 def test_form_init(self): 53 testform = TestWizard.get_initkwargs([Step1, Step2]) 54 self.assertEquals(testform['form_list'], {u'0': Step1, u'1': Step2}) 55 56 testform = TestWizard.get_initkwargs([('start', Step1), ('step2', Step2)]) 57 self.assertEquals( 58 testform['form_list'], {u'start': Step1, u'step2': Step2}) 59 60 testform = TestWizard.get_initkwargs([Step1, Step2, ('finish', Step3)]) 61 self.assertEquals( 62 testform['form_list'], {u'0': Step1, u'1': Step2, u'finish': Step3}) 63 64 def test_first_step(self): 65 request = get_request() 66 67 testform = TestWizard.as_view([Step1, Step2]) 68 response, instance = testform(request) 69 self.assertEquals(instance.steps.current, u'0') 70 71 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 72 response, instance = testform(request) 73 74 self.assertEquals(instance.steps.current, 'start') 75 76 def test_persistence(self): 77 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 78 request = get_request({'test_wizard-current_step': 'start', 79 'name': 'data1'}) 80 response, instance = testform(request) 81 self.assertEquals(instance.steps.current, 'start') 82 83 instance.storage.current_step = 'step2' 84 85 testform2 = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 86 request.POST = {'test_wizard-current_step': 'step2'} 87 response, instance = testform2(request) 88 self.assertEquals(instance.steps.current, 'step2') 89 90 def test_form_condition(self): 91 request = get_request() 92 93 testform = TestWizard.as_view( 94 [('start', Step1), ('step2', Step2), ('step3', Step3)], 95 condition_dict={'step2': True}) 96 response, instance = testform(request) 97 self.assertEquals(instance.get_next_step(), 'step2') 98 99 testform = TestWizard.as_view( 100 [('start', Step1), ('step2', Step2), ('step3', Step3)], 101 condition_dict={'step2': False}) 102 response, instance = testform(request) 103 self.assertEquals(instance.get_next_step(), 'step3') 104 105 def test_form_prefix(self): 106 request = get_request() 107 108 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 109 response, instance = testform(request) 110 111 self.assertEqual(instance.get_form_prefix(), 'start') 112 self.assertEqual(instance.get_form_prefix('another'), 'another') 113 114 def test_form_initial(self): 115 request = get_request() 116 117 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)], 118 initial_dict={'start': {'name': 'value1'}}) 119 response, instance = testform(request) 120 121 self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'}) 122 self.assertEqual(instance.get_form_initial('step2'), {}) 123 124 def test_form_instance(self): 125 request = get_request() 126 the_instance = User() 127 testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)], 128 instance_dict={'start': the_instance}) 129 response, instance = testform(request) 130 131 self.assertEqual( 132 instance.get_form_instance('start'), 133 the_instance) 134 self.assertEqual( 135 instance.get_form_instance('non_exist_instance'), 136 None) 137 138 def test_formset_instance(self): 139 request = get_request() 140 the_instance1, created = User.objects.get_or_create( 141 username='testuser1') 142 the_instance2, created = User.objects.get_or_create( 143 username='testuser2') 144 testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)], 145 instance_dict={'start': User.objects.filter(username='testuser1')}) 146 response, instance = testform(request) 147 148 self.assertEqual(list(instance.get_form_instance('start')), [the_instance1]) 149 self.assertEqual(instance.get_form_instance('non_exist_instance'), None) 150 151 self.assertEqual(instance.get_form().initial_form_count(), 1) 152 153 def test_done(self): 154 request = get_request() 155 156 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 157 response, instance = testform(request) 158 159 self.assertRaises(NotImplementedError, instance.done, None) 160 161 def test_revalidation(self): 162 request = get_request() 163 164 testform = TestWizard.as_view([('start', Step1), ('step2', Step2)]) 165 response, instance = testform(request) 166 instance.render_done(None) 167 self.assertEqual(instance.storage.current_step, 'start') 168 169 170 class SessionFormTests(TestCase): 171 def test_init(self): 172 request = get_request() 173 testform = SessionWizardView.as_view([('start', Step1)]) 174 self.assertTrue(isinstance(testform(request), TemplateResponse)) 175 176 177 class CookieFormTests(TestCase): 178 def test_init(self): 179 request = get_request() 180 testform = CookieWizardView.as_view([('start', Step1)]) 181 self.assertTrue(isinstance(testform(request), TemplateResponse)) 182 -
new file django/contrib/formtools/wizard/tests/loadstoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/loadstoragetests.py b/django/contrib/formtools/wizard/tests/loadstoragetests.py new file mode 100644 index 0000000..267dee0
- + 1 from django.test import TestCase 2 3 from django.contrib.formtools.wizard.storage import (get_storage, 4 MissingStorageModule, 5 MissingStorageClass) 6 from django.contrib.formtools.wizard.storage.base import BaseStorage 7 8 9 class TestLoadStorage(TestCase): 10 def test_load_storage(self): 11 self.assertEqual( 12 type(get_storage('django.contrib.formtools.wizard.storage.base.BaseStorage', 'wizard1')), 13 BaseStorage) 14 15 def test_missing_module(self): 16 self.assertRaises(MissingStorageModule, get_storage, 17 'django.contrib.formtools.wizard.storage.idontexist.IDontExistStorage', 'wizard1') 18 19 def test_missing_class(self): 20 self.assertRaises(MissingStorageClass, get_storage, 21 'django.contrib.formtools.wizard.storage.base.IDontExistStorage', 'wizard1') 22 -
new file django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py b/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py new file mode 100644 index 0000000..4387356
- + 1 from django.contrib.formtools.wizard.tests.namedwizardtests.tests import * 2 No newline at end of file -
new file django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py b/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py new file mode 100644 index 0000000..ae98126
- + 1 from django import forms 2 from django.forms.formsets import formset_factory 3 from django.http import HttpResponse 4 from django.template import Template, Context 5 6 from django.contrib.auth.models import User 7 8 from django.contrib.formtools.wizard.views import NamedUrlWizardView 9 10 class Page1(forms.Form): 11 name = forms.CharField(max_length=100) 12 user = forms.ModelChoiceField(queryset=User.objects.all()) 13 thirsty = forms.NullBooleanField() 14 15 class Page2(forms.Form): 16 address1 = forms.CharField(max_length=100) 17 address2 = forms.CharField(max_length=100) 18 19 class Page3(forms.Form): 20 random_crap = forms.CharField(max_length=100) 21 22 Page4 = formset_factory(Page3, extra=2) 23 24 class ContactWizard(NamedUrlWizardView): 25 def done(self, form_list, **kwargs): 26 c = Context({ 27 'form_list': [x.cleaned_data for x in form_list], 28 'all_cleaned_data': self.get_all_cleaned_data() 29 }) 30 31 for form in self.form_list.keys(): 32 c[form] = self.get_cleaned_data_for_step(form) 33 34 c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail') 35 return HttpResponse(Template('').render(c)) 36 37 class SessionContactWizard(ContactWizard): 38 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 39 40 class CookieContactWizard(ContactWizard): 41 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 42 -
new file django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py b/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py new file mode 100644 index 0000000..cc442d7
- + 1 import os 2 3 from django.core.urlresolvers import reverse 4 from django.http import QueryDict 5 from django.test import TestCase 6 from django.conf import settings 7 8 from django.contrib.auth.models import User 9 10 from django.contrib.formtools import wizard 11 12 from django.contrib.formtools.wizard.views import (NamedUrlSessionWizardView, 13 NamedUrlCookieWizardView) 14 from django.contrib.formtools.wizard.tests.formtests import (get_request, 15 Step1, 16 Step2) 17 18 class NamedWizardTests(object): 19 urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls' 20 21 def setUp(self): 22 self.testuser, created = User.objects.get_or_create(username='testuser1') 23 self.wizard_step_data[0]['form1-user'] = self.testuser.pk 24 25 wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')] 26 settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs 27 28 def tearDown(self): 29 del settings.TEMPLATE_DIRS[-1] 30 31 def test_initial_call(self): 32 response = self.client.get(reverse('%s_start' % self.wizard_urlname)) 33 self.assertEqual(response.status_code, 302) 34 response = self.client.get(response['Location']) 35 self.assertEqual(response.status_code, 200) 36 wizard = response.context['wizard'] 37 self.assertEqual(wizard['steps'].current, 'form1') 38 self.assertEqual(wizard['steps'].step0, 0) 39 self.assertEqual(wizard['steps'].step1, 1) 40 self.assertEqual(wizard['steps'].last, 'form4') 41 self.assertEqual(wizard['steps'].prev, None) 42 self.assertEqual(wizard['steps'].next, 'form2') 43 self.assertEqual(wizard['steps'].count, 4) 44 45 def test_initial_call_with_params(self): 46 get_params = {'getvar1': 'getval1', 'getvar2': 'getval2'} 47 response = self.client.get(reverse('%s_start' % self.wizard_urlname), 48 get_params) 49 self.assertEqual(response.status_code, 302) 50 51 # Test for proper redirect GET parameters 52 location = response['Location'] 53 self.assertNotEqual(location.find('?'), -1) 54 querydict = QueryDict(location[location.find('?') + 1:]) 55 self.assertEqual(dict(querydict.items()), get_params) 56 57 def test_form_post_error(self): 58 response = self.client.post( 59 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 60 self.wizard_step_1_data) 61 62 self.assertEqual(response.status_code, 200) 63 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 64 self.assertEqual(response.context['wizard']['form'].errors, 65 {'name': [u'This field is required.'], 66 'user': [u'This field is required.']}) 67 68 def test_form_post_success(self): 69 response = self.client.post( 70 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 71 self.wizard_step_data[0]) 72 response = self.client.get(response['Location']) 73 74 self.assertEqual(response.status_code, 200) 75 wizard = response.context['wizard'] 76 self.assertEqual(wizard['steps'].current, 'form2') 77 self.assertEqual(wizard['steps'].step0, 1) 78 self.assertEqual(wizard['steps'].prev, 'form1') 79 self.assertEqual(wizard['steps'].next, 'form3') 80 81 def test_form_stepback(self): 82 response = self.client.get( 83 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 84 85 self.assertEqual(response.status_code, 200) 86 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 87 88 response = self.client.post( 89 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 90 self.wizard_step_data[0]) 91 response = self.client.get(response['Location']) 92 93 self.assertEqual(response.status_code, 200) 94 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 95 96 response = self.client.post( 97 reverse(self.wizard_urlname, kwargs={ 98 'step': response.context['wizard']['steps'].current 99 }), {'wizard_prev_step': response.context['wizard']['steps'].prev}) 100 response = self.client.get(response['Location']) 101 102 self.assertEqual(response.status_code, 200) 103 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 104 105 def test_form_jump(self): 106 response = self.client.get( 107 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 108 109 self.assertEqual(response.status_code, 200) 110 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 111 112 response = self.client.get( 113 reverse(self.wizard_urlname, kwargs={'step': 'form3'})) 114 self.assertEqual(response.status_code, 200) 115 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 116 117 def test_form_finish(self): 118 response = self.client.get( 119 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 120 121 self.assertEqual(response.status_code, 200) 122 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 123 124 response = self.client.post( 125 reverse(self.wizard_urlname, 126 kwargs={'step': response.context['wizard']['steps'].current}), 127 self.wizard_step_data[0]) 128 response = self.client.get(response['Location']) 129 130 self.assertEqual(response.status_code, 200) 131 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 132 133 response = self.client.post( 134 reverse(self.wizard_urlname, 135 kwargs={'step': response.context['wizard']['steps'].current}), 136 self.wizard_step_data[1]) 137 response = self.client.get(response['Location']) 138 139 self.assertEqual(response.status_code, 200) 140 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 141 142 response = self.client.post( 143 reverse(self.wizard_urlname, 144 kwargs={'step': response.context['wizard']['steps'].current}), 145 self.wizard_step_data[2]) 146 response = self.client.get(response['Location']) 147 148 self.assertEqual(response.status_code, 200) 149 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 150 151 response = self.client.post( 152 reverse(self.wizard_urlname, 153 kwargs={'step': response.context['wizard']['steps'].current}), 154 self.wizard_step_data[3]) 155 response = self.client.get(response['Location']) 156 self.assertEqual(response.status_code, 200) 157 158 self.assertEqual(response.context['form_list'], [ 159 {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, 160 {'address1': u'123 Main St', 'address2': u'Djangoland'}, 161 {'random_crap': u'blah blah'}, 162 [{'random_crap': u'blah blah'}, {'random_crap': u'blah blah'}]]) 163 164 def test_cleaned_data(self): 165 response = self.client.get( 166 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 167 self.assertEqual(response.status_code, 200) 168 169 response = self.client.post( 170 reverse(self.wizard_urlname, 171 kwargs={'step': response.context['wizard']['steps'].current}), 172 self.wizard_step_data[0]) 173 response = self.client.get(response['Location']) 174 self.assertEqual(response.status_code, 200) 175 176 response = self.client.post( 177 reverse(self.wizard_urlname, 178 kwargs={'step': response.context['wizard']['steps'].current}), 179 self.wizard_step_data[1]) 180 response = self.client.get(response['Location']) 181 self.assertEqual(response.status_code, 200) 182 183 response = self.client.post( 184 reverse(self.wizard_urlname, 185 kwargs={'step': response.context['wizard']['steps'].current}), 186 self.wizard_step_data[2]) 187 response = self.client.get(response['Location']) 188 self.assertEqual(response.status_code, 200) 189 190 response = self.client.post( 191 reverse(self.wizard_urlname, 192 kwargs={'step': response.context['wizard']['steps'].current}), 193 self.wizard_step_data[3]) 194 response = self.client.get(response['Location']) 195 self.assertEqual(response.status_code, 200) 196 197 self.assertEqual( 198 response.context['all_cleaned_data'], 199 {'name': u'Pony', 'thirsty': True, 'user': self.testuser, 200 'address1': u'123 Main St', 'address2': u'Djangoland', 201 'random_crap': u'blah blah', 'formset-form4': [ 202 {'random_crap': u'blah blah'}, 203 {'random_crap': u'blah blah'} 204 ]}) 205 206 def test_manipulated_data(self): 207 response = self.client.get( 208 reverse(self.wizard_urlname, kwargs={'step': 'form1'})) 209 self.assertEqual(response.status_code, 200) 210 211 response = self.client.post( 212 reverse(self.wizard_urlname, 213 kwargs={'step': response.context['wizard']['steps'].current}), 214 self.wizard_step_data[0]) 215 response = self.client.get(response['Location']) 216 self.assertEqual(response.status_code, 200) 217 218 response = self.client.post( 219 reverse(self.wizard_urlname, 220 kwargs={'step': response.context['wizard']['steps'].current}), 221 self.wizard_step_data[1]) 222 response = self.client.get(response['Location']) 223 self.assertEqual(response.status_code, 200) 224 225 response = self.client.post( 226 reverse(self.wizard_urlname, 227 kwargs={'step': response.context['wizard']['steps'].current}), 228 self.wizard_step_data[2]) 229 loc = response['Location'] 230 response = self.client.get(loc) 231 self.assertEqual(response.status_code, 200, loc) 232 233 self.client.cookies.pop('sessionid', None) 234 self.client.cookies.pop('wizard_cookie_contact_wizard', None) 235 236 response = self.client.post( 237 reverse(self.wizard_urlname, 238 kwargs={'step': response.context['wizard']['steps'].current}), 239 self.wizard_step_data[3]) 240 241 self.assertEqual(response.status_code, 200) 242 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 243 244 def test_form_reset(self): 245 response = self.client.post( 246 reverse(self.wizard_urlname, kwargs={'step': 'form1'}), 247 self.wizard_step_data[0]) 248 response = self.client.get(response['Location']) 249 self.assertEqual(response.status_code, 200) 250 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 251 252 response = self.client.get( 253 '%s?reset=1' % reverse('%s_start' % self.wizard_urlname)) 254 self.assertEqual(response.status_code, 302) 255 256 response = self.client.get(response['Location']) 257 self.assertEqual(response.status_code, 200) 258 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 259 260 class NamedSessionWizardTests(NamedWizardTests, TestCase): 261 wizard_urlname = 'nwiz_session' 262 wizard_step_1_data = { 263 'session_contact_wizard-current_step': 'form1', 264 } 265 wizard_step_data = ( 266 { 267 'form1-name': 'Pony', 268 'form1-thirsty': '2', 269 'session_contact_wizard-current_step': 'form1', 270 }, 271 { 272 'form2-address1': '123 Main St', 273 'form2-address2': 'Djangoland', 274 'session_contact_wizard-current_step': 'form2', 275 }, 276 { 277 'form3-random_crap': 'blah blah', 278 'session_contact_wizard-current_step': 'form3', 279 }, 280 { 281 'form4-INITIAL_FORMS': '0', 282 'form4-TOTAL_FORMS': '2', 283 'form4-MAX_NUM_FORMS': '0', 284 'form4-0-random_crap': 'blah blah', 285 'form4-1-random_crap': 'blah blah', 286 'session_contact_wizard-current_step': 'form4', 287 } 288 ) 289 290 class NamedCookieWizardTests(NamedWizardTests, TestCase): 291 wizard_urlname = 'nwiz_cookie' 292 wizard_step_1_data = { 293 'cookie_contact_wizard-current_step': 'form1', 294 } 295 wizard_step_data = ( 296 { 297 'form1-name': 'Pony', 298 'form1-thirsty': '2', 299 'cookie_contact_wizard-current_step': 'form1', 300 }, 301 { 302 'form2-address1': '123 Main St', 303 'form2-address2': 'Djangoland', 304 'cookie_contact_wizard-current_step': 'form2', 305 }, 306 { 307 'form3-random_crap': 'blah blah', 308 'cookie_contact_wizard-current_step': 'form3', 309 }, 310 { 311 'form4-INITIAL_FORMS': '0', 312 'form4-TOTAL_FORMS': '2', 313 'form4-MAX_NUM_FORMS': '0', 314 'form4-0-random_crap': 'blah blah', 315 'form4-1-random_crap': 'blah blah', 316 'cookie_contact_wizard-current_step': 'form4', 317 } 318 ) 319 320 321 class NamedFormTests(object): 322 urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls' 323 324 def test_revalidation(self): 325 request = get_request() 326 327 testform = self.formwizard_class.as_view( 328 [('start', Step1), ('step2', Step2)], 329 url_name=self.wizard_urlname) 330 response, instance = testform(request, step='done') 331 332 instance.render_done(None) 333 self.assertEqual(instance.storage.current_step, 'start') 334 335 class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView): 336 337 def dispatch(self, request, *args, **kwargs): 338 response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs) 339 return response, self 340 341 class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView): 342 343 def dispatch(self, request, *args, **kwargs): 344 response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs) 345 return response, self 346 347 348 class NamedSessionFormTests(NamedFormTests, TestCase): 349 formwizard_class = TestNamedUrlSessionFormWizard 350 wizard_urlname = 'nwiz_session' 351 352 353 class NamedCookieFormTests(NamedFormTests, TestCase): 354 formwizard_class = TestNamedUrlCookieFormWizard 355 wizard_urlname = 'nwiz_cookie' -
new file django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py b/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py new file mode 100644 index 0000000..a97ca98
- + 1 from django.conf.urls.defaults import * 2 from django.contrib.formtools.wizard.tests.namedwizardtests.forms import ( 3 SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4) 4 5 def get_named_session_wizard(): 6 return SessionContactWizard.as_view( 7 [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)], 8 url_name='nwiz_session', 9 done_step_name='nwiz_session_done' 10 ) 11 12 def get_named_cookie_wizard(): 13 return CookieContactWizard.as_view( 14 [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)], 15 url_name='nwiz_cookie', 16 done_step_name='nwiz_cookie_done' 17 ) 18 19 urlpatterns = patterns('', 20 url(r'^nwiz_session/(?P<step>.+)/$', get_named_session_wizard(), name='nwiz_session'), 21 url(r'^nwiz_session/$', get_named_session_wizard(), name='nwiz_session_start'), 22 url(r'^nwiz_cookie/(?P<step>.+)/$', get_named_cookie_wizard(), name='nwiz_cookie'), 23 url(r'^nwiz_cookie/$', get_named_cookie_wizard(), name='nwiz_cookie_start'), 24 ) -
new file django/contrib/formtools/wizard/tests/sessionstoragetests.py
diff --git a/django/contrib/formtools/wizard/tests/sessionstoragetests.py b/django/contrib/formtools/wizard/tests/sessionstoragetests.py new file mode 100644 index 0000000..c643921
- + 1 from django.test import TestCase 2 3 from django.contrib.formtools.wizard.tests.storagetests import TestStorage 4 from django.contrib.formtools.wizard.storage.session import SessionStorage 5 6 class TestSessionStorage(TestStorage, TestCase): 7 def get_storage(self): 8 return SessionStorage -
new file django/contrib/formtools/wizard/tests/storagetests.py
diff --git a/django/contrib/formtools/wizard/tests/storagetests.py b/django/contrib/formtools/wizard/tests/storagetests.py new file mode 100644 index 0000000..fec4fae
- + 1 from datetime import datetime 2 3 from django.http import HttpRequest 4 from django.conf import settings 5 from django.utils.importlib import import_module 6 7 from django.contrib.auth.models import User 8 9 def get_request(): 10 request = HttpRequest() 11 engine = import_module(settings.SESSION_ENGINE) 12 request.session = engine.SessionStore(None) 13 return request 14 15 class TestStorage(object): 16 def setUp(self): 17 self.testuser, created = User.objects.get_or_create(username='testuser1') 18 19 def test_current_step(self): 20 request = get_request() 21 storage = self.get_storage()('wizard1', request, None) 22 my_step = 2 23 24 self.assertEqual(storage.current_step, None) 25 26 storage.current_step = my_step 27 self.assertEqual(storage.current_step, my_step) 28 29 storage.reset() 30 self.assertEqual(storage.current_step, None) 31 32 storage.current_step = my_step 33 storage2 = self.get_storage()('wizard2', request, None) 34 self.assertEqual(storage2.current_step, None) 35 36 def test_step_data(self): 37 request = get_request() 38 storage = self.get_storage()('wizard1', request, None) 39 step1 = 'start' 40 step_data1 = {'field1': 'data1', 41 'field2': 'data2', 42 'field3': datetime.now(), 43 'field4': self.testuser} 44 45 self.assertEqual(storage.get_step_data(step1), None) 46 47 storage.set_step_data(step1, step_data1) 48 self.assertEqual(storage.get_step_data(step1), step_data1) 49 50 storage.reset() 51 self.assertEqual(storage.get_step_data(step1), None) 52 53 storage.set_step_data(step1, step_data1) 54 storage2 = self.get_storage()('wizard2', request, None) 55 self.assertEqual(storage2.get_step_data(step1), None) 56 57 def test_extra_context(self): 58 request = get_request() 59 storage = self.get_storage()('wizard1', request, None) 60 extra_context = {'key1': 'data1', 61 'key2': 'data2', 62 'key3': datetime.now(), 63 'key4': self.testuser} 64 65 self.assertEqual(storage.extra_data, {}) 66 67 storage.extra_data = extra_context 68 self.assertEqual(storage.extra_data, extra_context) 69 70 storage.reset() 71 self.assertEqual(storage.extra_data, {}) 72 73 storage.extra_data = extra_context 74 storage2 = self.get_storage()('wizard2', request, None) 75 self.assertEqual(storage2.extra_data, {}) 76 -
new file django/contrib/formtools/wizard/tests/wizardtests/__init__.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/__init__.py b/django/contrib/formtools/wizard/tests/wizardtests/__init__.py new file mode 100644 index 0000000..9173cd8
- + 1 from django.contrib.formtools.wizard.tests.wizardtests.tests import * 2 No newline at end of file -
new file django/contrib/formtools/wizard/tests/wizardtests/forms.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/forms.py b/django/contrib/formtools/wizard/tests/wizardtests/forms.py new file mode 100644 index 0000000..726d74a
- + 1 import tempfile 2 3 from django import forms 4 from django.core.files.storage import FileSystemStorage 5 from django.forms.formsets import formset_factory 6 from django.http import HttpResponse 7 from django.template import Template, Context 8 9 from django.contrib.auth.models import User 10 11 from django.contrib.formtools.wizard.views import WizardView 12 13 temp_storage_location = tempfile.mkdtemp() 14 temp_storage = FileSystemStorage(location=temp_storage_location) 15 16 class Page1(forms.Form): 17 name = forms.CharField(max_length=100) 18 user = forms.ModelChoiceField(queryset=User.objects.all()) 19 thirsty = forms.NullBooleanField() 20 21 class Page2(forms.Form): 22 address1 = forms.CharField(max_length=100) 23 address2 = forms.CharField(max_length=100) 24 file1 = forms.FileField() 25 26 class Page3(forms.Form): 27 random_crap = forms.CharField(max_length=100) 28 29 Page4 = formset_factory(Page3, extra=2) 30 31 class ContactWizard(WizardView): 32 file_storage = temp_storage 33 34 def done(self, form_list, **kwargs): 35 c = Context({ 36 'form_list': [x.cleaned_data for x in form_list], 37 'all_cleaned_data': self.get_all_cleaned_data() 38 }) 39 40 for form in self.form_list.keys(): 41 c[form] = self.get_cleaned_data_for_step(form) 42 43 c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail') 44 return HttpResponse(Template('').render(c)) 45 46 def get_context_data(self, form, **kwargs): 47 context = super(ContactWizard, self).get_context_data(form, **kwargs) 48 if self.storage.current_step == 'form2': 49 context.update({'another_var': True}) 50 return context 51 52 class SessionContactWizard(ContactWizard): 53 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 54 55 class CookieContactWizard(ContactWizard): 56 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 57 -
new file django/contrib/formtools/wizard/tests/wizardtests/tests.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/tests.py b/django/contrib/formtools/wizard/tests/wizardtests/tests.py new file mode 100644 index 0000000..f64b2ba
- + 1 import os 2 3 from django.test import TestCase 4 from django.conf import settings 5 from django.contrib.auth.models import User 6 7 from django.contrib.formtools import wizard 8 9 class WizardTests(object): 10 urls = 'django.contrib.formtools.wizard.tests.wizardtests.urls' 11 12 def setUp(self): 13 self.testuser, created = User.objects.get_or_create(username='testuser1') 14 self.wizard_step_data[0]['form1-user'] = self.testuser.pk 15 16 wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')] 17 settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs 18 19 def tearDown(self): 20 del settings.TEMPLATE_DIRS[-1] 21 22 def test_initial_call(self): 23 response = self.client.get(self.wizard_url) 24 wizard = response.context['wizard'] 25 self.assertEqual(response.status_code, 200) 26 self.assertEqual(wizard['steps'].current, 'form1') 27 self.assertEqual(wizard['steps'].step0, 0) 28 self.assertEqual(wizard['steps'].step1, 1) 29 self.assertEqual(wizard['steps'].last, 'form4') 30 self.assertEqual(wizard['steps'].prev, None) 31 self.assertEqual(wizard['steps'].next, 'form2') 32 self.assertEqual(wizard['steps'].count, 4) 33 34 def test_form_post_error(self): 35 response = self.client.post(self.wizard_url, self.wizard_step_1_data) 36 self.assertEqual(response.status_code, 200) 37 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 38 self.assertEqual(response.context['wizard']['form'].errors, 39 {'name': [u'This field is required.'], 40 'user': [u'This field is required.']}) 41 42 def test_form_post_success(self): 43 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 44 wizard = response.context['wizard'] 45 self.assertEqual(response.status_code, 200) 46 self.assertEqual(wizard['steps'].current, 'form2') 47 self.assertEqual(wizard['steps'].step0, 1) 48 self.assertEqual(wizard['steps'].prev, 'form1') 49 self.assertEqual(wizard['steps'].next, 'form3') 50 51 def test_form_stepback(self): 52 response = self.client.get(self.wizard_url) 53 self.assertEqual(response.status_code, 200) 54 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 55 56 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 57 self.assertEqual(response.status_code, 200) 58 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 59 60 response = self.client.post(self.wizard_url, { 61 'wizard_prev_step': response.context['wizard']['steps'].prev}) 62 self.assertEqual(response.status_code, 200) 63 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 64 65 def test_template_context(self): 66 response = self.client.get(self.wizard_url) 67 self.assertEqual(response.status_code, 200) 68 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 69 self.assertEqual(response.context.get('another_var', None), None) 70 71 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 72 self.assertEqual(response.status_code, 200) 73 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 74 self.assertEqual(response.context.get('another_var', None), True) 75 76 def test_form_finish(self): 77 response = self.client.get(self.wizard_url) 78 self.assertEqual(response.status_code, 200) 79 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 80 81 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 82 self.assertEqual(response.status_code, 200) 83 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 84 85 post_data = self.wizard_step_data[1] 86 post_data['form2-file1'] = open(__file__) 87 response = self.client.post(self.wizard_url, post_data) 88 self.assertEqual(response.status_code, 200) 89 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 90 91 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 92 self.assertEqual(response.status_code, 200) 93 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 94 95 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 96 self.assertEqual(response.status_code, 200) 97 98 all_data = response.context['form_list'] 99 self.assertEqual(all_data[1]['file1'].read(), open(__file__).read()) 100 del all_data[1]['file1'] 101 self.assertEqual(all_data, [ 102 {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, 103 {'address1': u'123 Main St', 'address2': u'Djangoland'}, 104 {'random_crap': u'blah blah'}, 105 [{'random_crap': u'blah blah'}, 106 {'random_crap': u'blah blah'}]]) 107 108 def test_cleaned_data(self): 109 response = self.client.get(self.wizard_url) 110 self.assertEqual(response.status_code, 200) 111 112 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 113 self.assertEqual(response.status_code, 200) 114 115 post_data = self.wizard_step_data[1] 116 post_data['form2-file1'] = open(__file__) 117 response = self.client.post(self.wizard_url, post_data) 118 self.assertEqual(response.status_code, 200) 119 120 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 121 self.assertEqual(response.status_code, 200) 122 123 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 124 self.assertEqual(response.status_code, 200) 125 126 all_data = response.context['all_cleaned_data'] 127 self.assertEqual(all_data['file1'].read(), open(__file__).read()) 128 del all_data['file1'] 129 self.assertEqual(all_data, { 130 'name': u'Pony', 'thirsty': True, 'user': self.testuser, 131 'address1': u'123 Main St', 'address2': u'Djangoland', 132 'random_crap': u'blah blah', 'formset-form4': [ 133 {'random_crap': u'blah blah'}, 134 {'random_crap': u'blah blah'}]}) 135 136 def test_manipulated_data(self): 137 response = self.client.get(self.wizard_url) 138 self.assertEqual(response.status_code, 200) 139 140 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 141 self.assertEqual(response.status_code, 200) 142 143 post_data = self.wizard_step_data[1] 144 post_data['form2-file1'] = open(__file__) 145 response = self.client.post(self.wizard_url, post_data) 146 self.assertEqual(response.status_code, 200) 147 148 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 149 self.assertEqual(response.status_code, 200) 150 self.client.cookies.pop('sessionid', None) 151 self.client.cookies.pop('wizard_cookie_contact_wizard', None) 152 153 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 154 self.assertEqual(response.status_code, 200) 155 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 156 157 def test_form_refresh(self): 158 response = self.client.get(self.wizard_url) 159 self.assertEqual(response.status_code, 200) 160 self.assertEqual(response.context['wizard']['steps'].current, 'form1') 161 162 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 163 self.assertEqual(response.status_code, 200) 164 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 165 166 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 167 self.assertEqual(response.status_code, 200) 168 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 169 170 post_data = self.wizard_step_data[1] 171 post_data['form2-file1'] = open(__file__) 172 response = self.client.post(self.wizard_url, post_data) 173 self.assertEqual(response.status_code, 200) 174 self.assertEqual(response.context['wizard']['steps'].current, 'form3') 175 176 response = self.client.post(self.wizard_url, self.wizard_step_data[2]) 177 self.assertEqual(response.status_code, 200) 178 self.assertEqual(response.context['wizard']['steps'].current, 'form4') 179 180 response = self.client.post(self.wizard_url, self.wizard_step_data[0]) 181 self.assertEqual(response.status_code, 200) 182 self.assertEqual(response.context['wizard']['steps'].current, 'form2') 183 184 response = self.client.post(self.wizard_url, self.wizard_step_data[3]) 185 self.assertEqual(response.status_code, 200) 186 187 188 class SessionWizardTests(WizardTests, TestCase): 189 wizard_url = '/wiz_session/' 190 wizard_step_1_data = { 191 'session_contact_wizard-current_step': 'form1', 192 } 193 wizard_step_data = ( 194 { 195 'form1-name': 'Pony', 196 'form1-thirsty': '2', 197 'session_contact_wizard-current_step': 'form1', 198 }, 199 { 200 'form2-address1': '123 Main St', 201 'form2-address2': 'Djangoland', 202 'session_contact_wizard-current_step': 'form2', 203 }, 204 { 205 'form3-random_crap': 'blah blah', 206 'session_contact_wizard-current_step': 'form3', 207 }, 208 { 209 'form4-INITIAL_FORMS': '0', 210 'form4-TOTAL_FORMS': '2', 211 'form4-MAX_NUM_FORMS': '0', 212 'form4-0-random_crap': 'blah blah', 213 'form4-1-random_crap': 'blah blah', 214 'session_contact_wizard-current_step': 'form4', 215 } 216 ) 217 218 class CookieWizardTests(WizardTests, TestCase): 219 wizard_url = '/wiz_cookie/' 220 wizard_step_1_data = { 221 'cookie_contact_wizard-current_step': 'form1', 222 } 223 wizard_step_data = ( 224 { 225 'form1-name': 'Pony', 226 'form1-thirsty': '2', 227 'cookie_contact_wizard-current_step': 'form1', 228 }, 229 { 230 'form2-address1': '123 Main St', 231 'form2-address2': 'Djangoland', 232 'cookie_contact_wizard-current_step': 'form2', 233 }, 234 { 235 'form3-random_crap': 'blah blah', 236 'cookie_contact_wizard-current_step': 'form3', 237 }, 238 { 239 'form4-INITIAL_FORMS': '0', 240 'form4-TOTAL_FORMS': '2', 241 'form4-MAX_NUM_FORMS': '0', 242 'form4-0-random_crap': 'blah blah', 243 'form4-1-random_crap': 'blah blah', 244 'cookie_contact_wizard-current_step': 'form4', 245 } 246 ) 247 248 -
new file django/contrib/formtools/wizard/tests/wizardtests/urls.py
diff --git a/django/contrib/formtools/wizard/tests/wizardtests/urls.py b/django/contrib/formtools/wizard/tests/wizardtests/urls.py new file mode 100644 index 0000000..e305397
- + 1 from django.conf.urls.defaults import * 2 from django.contrib.formtools.wizard.tests.wizardtests.forms import ( 3 SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4) 4 5 urlpatterns = patterns('', 6 url(r'^wiz_session/$', SessionContactWizard.as_view( 7 [('form1', Page1), 8 ('form2', Page2), 9 ('form3', Page3), 10 ('form4', Page4)])), 11 url(r'^wiz_cookie/$', CookieContactWizard.as_view( 12 [('form1', Page1), 13 ('form2', Page2), 14 ('form3', Page3), 15 ('form4', Page4)])), 16 ) -
new file django/contrib/formtools/wizard/views.py
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py new file mode 100644 index 0000000..c0f8b3b
- + 1 import copy 2 import re 3 4 from django import forms 5 from django.shortcuts import redirect 6 from django.core.urlresolvers import reverse 7 from django.forms import formsets, ValidationError 8 from django.views.generic import TemplateView 9 from django.utils.datastructures import SortedDict 10 from django.utils.decorators import classonlymethod 11 12 from django.contrib.formtools.wizard.storage import get_storage 13 from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured 14 from django.contrib.formtools.wizard.forms import ManagementForm 15 16 17 def normalize_name(name): 18 new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name) 19 return new.lower().strip('_') 20 21 class StepsHelper(object): 22 23 def __init__(self, wizard): 24 self._wizard = wizard 25 26 def __dir__(self): 27 return self.all 28 29 def __len__(self): 30 return self.count 31 32 def __repr__(self): 33 return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all) 34 35 @property 36 def all(self): 37 "Returns the names of all steps/forms." 38 return self._wizard.get_form_list().keys() 39 40 @property 41 def count(self): 42 "Returns the total number of steps/forms in this the wizard." 43 return len(self.all) 44 45 @property 46 def current(self): 47 """ 48 Returns the current step. If no current step is stored in the 49 storage backend, the first step will be returned. 50 """ 51 return self._wizard.storage.current_step or self.first 52 53 @property 54 def first(self): 55 "Returns the name of the first step." 56 return self.all[0] 57 58 @property 59 def last(self): 60 "Returns the name of the last step." 61 return self.all[-1] 62 63 @property 64 def next(self): 65 "Returns the next step." 66 return self._wizard.get_next_step() 67 68 @property 69 def prev(self): 70 "Returns the previous step." 71 return self._wizard.get_prev_step() 72 73 @property 74 def index(self): 75 "Returns the index for the current step." 76 return self._wizard.get_step_index() 77 78 @property 79 def step0(self): 80 return int(self.index) 81 82 @property 83 def step1(self): 84 return int(self.index) + 1 85 86 87 class WizardView(TemplateView): 88 """ 89 The WizardView is used to create multi-page forms and handles all the 90 storage and validation stuff. The wizard is based on Django's generic 91 class based views. 92 """ 93 storage_name = None 94 form_list = None 95 initial_dict = None 96 instance_dict = None 97 condition_dict = None 98 template_name = 'formtools/wizard/wizard_form.html' 99 100 def __repr__(self): 101 return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list) 102 103 @classonlymethod 104 def as_view(cls, *args, **kwargs): 105 """ 106 This method is used within urls.py to create unique formwizard 107 instances for every request. We need to override this method because 108 we add some kwargs which are needed to make the formwizard usable. 109 """ 110 initkwargs = cls.get_initkwargs(*args, **kwargs) 111 return super(WizardView, cls).as_view(**initkwargs) 112 113 @classmethod 114 def get_initkwargs(cls, form_list, 115 initial_dict=None, instance_dict=None, condition_dict=None): 116 """ 117 Creates a dict with all needed parameters for the form wizard instances. 118 119 * `form_list` - is a list of forms. The list entries can be single form 120 classes or tuples of (`step_name`, `form_class`). If you pass a list 121 of forms, the formwizard will convert the class list to 122 (`zero_based_counter`, `form_class`). This is needed to access the 123 form for a specific step. 124 * `initial_dict` - contains a dictionary of initial data dictionaries. 125 The key should be equal to the `step_name` in the `form_list` (or 126 the str of the zero based counter - if no step_names added in the 127 `form_list`) 128 * `instance_dict` - contains a dictionary of instance objects. This list 129 is only used when `ModelForm`s are used. The key should be equal to 130 the `step_name` in the `form_list`. Same rules as for `initial_dict` 131 apply. 132 * `condition_dict` - contains a dictionary of boolean values or 133 callables. If the value of for a specific `step_name` is callable it 134 will be called with the formwizard instance as the only argument. 135 If the return value is true, the step's form will be used. 136 """ 137 kwargs = { 138 'initial_dict': initial_dict or {}, 139 'instance_dict': instance_dict or {}, 140 'condition_dict': condition_dict or {}, 141 } 142 init_form_list = SortedDict() 143 144 assert len(form_list) > 0, 'at least one form is needed' 145 146 # walk through the passed form list 147 for i, form in enumerate(form_list): 148 if isinstance(form, (list, tuple)): 149 # if the element is a tuple, add the tuple to the new created 150 # sorted dictionary. 151 init_form_list[unicode(form[0])] = form[1] 152 else: 153 # if not, add the form with a zero based counter as unicode 154 init_form_list[unicode(i)] = form 155 156 # walk through the ne created list of forms 157 for form in init_form_list.itervalues(): 158 if issubclass(form, formsets.BaseFormSet): 159 # if the element is based on BaseFormSet (FormSet/ModelFormSet) 160 # we need to override the form variable. 161 form = form.form 162 # check if any form contains a FileField, if yes, we need a 163 # file_storage added to the formwizard (by subclassing). 164 for field in form.base_fields.itervalues(): 165 if (isinstance(field, forms.FileField) and 166 not hasattr(cls, 'file_storage')): 167 raise NoFileStorageConfigured 168 169 # build the kwargs for the formwizard instances 170 kwargs['form_list'] = init_form_list 171 return kwargs 172 173 def get_wizard_name(self): 174 return normalize_name(self.__class__.__name__) 175 176 def get_prefix(self): 177 # TODO: Add some kind of unique id to prefix 178 return self.wizard_name 179 180 def get_form_list(self): 181 """ 182 This method returns a form_list based on the initial form list but 183 checks if there is a condition method/value in the condition_list. 184 If an entry exists in the condition list, it will call/read the value 185 and respect the result. (True means add the form, False means ignore 186 the form) 187 188 The form_list is always generated on the fly because condition methods 189 could use data from other (maybe previous forms). 190 """ 191 form_list = SortedDict() 192 for form_key, form_class in self.form_list.iteritems(): 193 # try to fetch the value from condition list, by default, the form 194 # gets passed to the new list. 195 condition = self.condition_dict.get(form_key, True) 196 if callable(condition): 197 # call the value if needed, passes the current instance. 198 condition = condition(self) 199 if condition: 200 form_list[form_key] = form_class 201 return form_list 202 203 def dispatch(self, request, *args, **kwargs): 204 """ 205 This method gets called by the routing engine. The first argument is 206 `request` which contains a `HttpRequest` instance. 207 The request is stored in `self.request` for later use. The storage 208 instance is stored in `self.storage`. 209 210 After processing the request using the `dispatch` method, the 211 response gets updated by the storage engine (for example add cookies). 212 """ 213 # add the storage engine to the current formwizard instance 214 self.wizard_name = self.get_wizard_name() 215 self.prefix = self.get_prefix() 216 self.storage = get_storage(self.storage_name, self.prefix, request, 217 getattr(self, 'file_storage', None)) 218 self.steps = StepsHelper(self) 219 response = super(WizardView, self).dispatch(request, *args, **kwargs) 220 221 # update the response (e.g. adding cookies) 222 self.storage.update_response(response) 223 return response 224 225 def get(self, request, *args, **kwargs): 226 """ 227 This method handles GET requests. 228 229 If a GET request reaches this point, the wizard assumes that the user 230 just starts at the first step or wants to restart the process. 231 The data of the wizard will be resetted before rendering the first step. 232 """ 233 self.storage.reset() 234 235 # reset the current step to the first step. 236 self.storage.current_step = self.steps.first 237 return self.render(self.get_form()) 238 239 def post(self, *args, **kwargs): 240 """ 241 This method handles POST requests. 242 243 The wizard will render either the current step (if form validation 244 wasn't successful), the next step (if the current step was stored 245 successful) or the done view (if no more steps are available) 246 """ 247 # Look for a wizard_prev_step element in the posted data which 248 # contains a valid step name. If one was found, render the requested 249 # form. (This makes stepping back a lot easier). 250 wizard_prev_step = self.request.POST.get('wizard_prev_step', None) 251 if wizard_prev_step and wizard_prev_step in self.get_form_list(): 252 self.storage.current_step = wizard_prev_step 253 form = self.get_form( 254 data=self.storage.get_step_data(self.steps.current), 255 files=self.storage.get_step_files(self.steps.current)) 256 return self.render(form) 257 258 # Check if form was refreshed 259 management_form = ManagementForm(self.request.POST, prefix=self.prefix) 260 if not management_form.is_valid(): 261 raise ValidationError( 262 'ManagementForm data is missing or has been tampered.') 263 264 form_current_step = management_form.cleaned_data['current_step'] 265 if (form_current_step != self.steps.current and 266 self.storage.current_step is not None): 267 # form refreshed, change current step 268 self.storage.current_step = form_current_step 269 270 # get the form for the current step 271 form = self.get_form(data=self.request.POST, files=self.request.FILES) 272 273 # and try to validate 274 if form.is_valid(): 275 # if the form is valid, store the cleaned data and files. 276 self.storage.set_step_data(self.steps.current, self.process_step(form)) 277 self.storage.set_step_files(self.steps.current, self.process_step_files(form)) 278 279 # check if the current step is the last step 280 if self.steps.current == self.steps.last: 281 # no more steps, render done view 282 return self.render_done(form, **kwargs) 283 else: 284 # proceed to the next step 285 return self.render_next_step(form) 286 return self.render(form) 287 288 def render_next_step(self, form, **kwargs): 289 """ 290 THis method gets called when the next step/form should be rendered. 291 `form` contains the last/current form. 292 """ 293 # get the form instance based on the data from the storage backend 294 # (if available). 295 next_step = self.steps.next 296 new_form = self.get_form(next_step, 297 data=self.storage.get_step_data(next_step), 298 files=self.storage.get_step_files(next_step)) 299 300 # change the stored current step 301 self.storage.current_step = next_step 302 return self.render(new_form, **kwargs) 303 304 def render_done(self, form, **kwargs): 305 """ 306 This method gets called when all forms passed. The method should also 307 re-validate all steps to prevent manipulation. If any form don't 308 validate, `render_revalidation_failure` should get called. 309 If everything is fine call `done`. 310 """ 311 final_form_list = [] 312 # walk through the form list and try to validate the data again. 313 for form_key in self.get_form_list(): 314 form_obj = self.get_form(step=form_key, 315 data=self.storage.get_step_data(form_key), 316 files=self.storage.get_step_files(form_key)) 317 if not form_obj.is_valid(): 318 return self.render_revalidation_failure(form_key, form_obj, **kwargs) 319 final_form_list.append(form_obj) 320 321 # render the done view and reset the wizard before returning the 322 # response. This is needed to prevent from rendering done with the 323 # same data twice. 324 done_response = self.done(final_form_list, **kwargs) 325 self.storage.reset() 326 return done_response 327 328 def get_form_prefix(self, step=None, form=None): 329 """ 330 Returns the prefix which will be used when calling the actual form for 331 the given step. `step` contains the step-name, `form` the form which 332 will be called with the returned prefix. 333 334 If no step is given, the form_prefix will determine the current step 335 automatically. 336 """ 337 if step is None: 338 step = self.steps.current 339 return str(step) 340 341 def get_form_initial(self, step): 342 """ 343 Returns a dictionary which will be passed to the form for `step` 344 as `initial`. If no initial data was provied while initializing the 345 form wizard, a empty dictionary will be returned. 346 """ 347 return self.initial_dict.get(step, {}) 348 349 def get_form_instance(self, step): 350 """ 351 Returns a object which will be passed to the form for `step` 352 as `instance`. If no instance object was provied while initializing 353 the form wizard, None be returned. 354 """ 355 return self.instance_dict.get(step, None) 356 357 def get_form(self, step=None, data=None, files=None): 358 """ 359 Constructs the form for a given `step`. If no `step` is defined, the 360 current step will be determined automatically. 361 362 The form will be initialized using the `data` argument to prefill the 363 new form. If needed, instance or queryset (for `ModelForm` or 364 `ModelFormSet`) will be added too. 365 """ 366 if step is None: 367 step = self.steps.current 368 # prepare the kwargs for the form instance. 369 kwargs = { 370 'data': data, 371 'files': files, 372 'prefix': self.get_form_prefix(step, self.form_list[step]), 373 'initial': self.get_form_initial(step), 374 } 375 if issubclass(self.form_list[step], forms.ModelForm): 376 # If the form is based on ModelForm, add instance if available. 377 kwargs.update({'instance': self.get_form_instance(step)}) 378 elif issubclass(self.form_list[step], forms.models.BaseModelFormSet): 379 # If the form is based on ModelFormSet, add queryset if available. 380 kwargs.update({'queryset': self.get_form_instance(step)}) 381 return self.form_list[step](**kwargs) 382 383 def process_step(self, form): 384 """ 385 This method is used to postprocess the form data. By default, it 386 returns the raw `form.data` dictionary. 387 """ 388 return self.get_form_step_data(form) 389 390 def process_step_files(self, form): 391 """ 392 This method is used to postprocess the form files. By default, it 393 returns the raw `form.files` dictionary. 394 """ 395 return self.get_form_step_files(form) 396 397 def render_revalidation_failure(self, step, form, **kwargs): 398 """ 399 Gets called when a form doesn't validate when rendering the done 400 view. By default, it changed the current step to failing forms step 401 and renders the form. 402 """ 403 self.storage.current_step = step 404 return self.render(form, **kwargs) 405 406 def get_form_step_data(self, form): 407 """ 408 Is used to return the raw form data. You may use this method to 409 manipulate the data. 410 """ 411 return form.data 412 413 def get_form_step_files(self, form): 414 """ 415 Is used to return the raw form files. You may use this method to 416 manipulate the data. 417 """ 418 return form.files 419 420 def get_all_cleaned_data(self): 421 """ 422 Returns a merged dictionary of all step cleaned_data dictionaries. 423 If a step contains a `FormSet`, the key will be prefixed with formset 424 and contain a list of the formset' cleaned_data dictionaries. 425 """ 426 cleaned_data = {} 427 for form_key in self.get_form_list(): 428 form_obj = self.get_form( 429 step=form_key, 430 data=self.storage.get_step_data(form_key), 431 files=self.storage.get_step_files(form_key) 432 ) 433 if form_obj.is_valid(): 434 if isinstance(form_obj.cleaned_data, (tuple, list)): 435 cleaned_data.update({ 436 'formset-%s' % form_key: form_obj.cleaned_data 437 }) 438 else: 439 cleaned_data.update(form_obj.cleaned_data) 440 return cleaned_data 441 442 def get_cleaned_data_for_step(self, step): 443 """ 444 Returns the cleaned data for a given `step`. Before returning the 445 cleaned data, the stored values are being revalidated through the 446 form. If the data doesn't validate, None will be returned. 447 """ 448 if step in self.form_list: 449 form_obj = self.get_form(step=step, 450 data=self.storage.get_step_data(step), 451 files=self.storage.get_step_files(step)) 452 if form_obj.is_valid(): 453 return form_obj.cleaned_data 454 return None 455 456 def get_next_step(self, step=None): 457 """ 458 Returns the next step after the given `step`. If no more steps are 459 available, None will be returned. If the `step` argument is None, the 460 current step will be determined automatically. 461 """ 462 if step is None: 463 step = self.steps.current 464 form_list = self.get_form_list() 465 key = form_list.keyOrder.index(step) + 1 466 if len(form_list.keyOrder) > key: 467 return form_list.keyOrder[key] 468 return None 469 470 def get_prev_step(self, step=None): 471 """ 472 Returns the previous step before the given `step`. If there are no 473 steps available, None will be returned. If the `step` argument is 474 None, the current step will be determined automatically. 475 """ 476 if step is None: 477 step = self.steps.current 478 form_list = self.get_form_list() 479 key = form_list.keyOrder.index(step) - 1 480 if key >= 0: 481 return form_list.keyOrder[key] 482 return None 483 484 def get_step_index(self, step=None): 485 """ 486 Returns the index for the given `step` name. If no step is given, 487 the current step will be used to get the index. 488 """ 489 if step is None: 490 step = self.steps.current 491 return self.get_form_list().keyOrder.index(step) 492 493 def get_context_data(self, form, *args, **kwargs): 494 """ 495 Returns the template context for a step. You can overwrite this method 496 to add more data for all or some steps. This method returns a 497 dictionary containing the rendered form step. Available template 498 context variables are: 499 500 * all extra data stored in the storage backend 501 * `form` - form instance of the current step 502 * `wizard` - the wizard instance itself 503 504 Example: 505 506 .. code-block:: python 507 508 class MyWizard(FormWizard): 509 def get_context_data(self, form, **kwargs): 510 context = super(MyWizard, self).get_context_data(form, **kwargs) 511 if self.steps.current == 'my_step_name': 512 context.update({'another_var': True}) 513 return context 514 """ 515 context = super(WizardView, self).get_context_data(*args, **kwargs) 516 context.update(self.storage.extra_data) 517 context['wizard'] = { 518 'form': form, 519 'steps': self.steps, 520 'managenent_form': ManagementForm(prefix=self.prefix, initial={ 521 'current_step': self.steps.current, 522 }), 523 } 524 return context 525 526 def render(self, form=None, **kwargs): 527 """ 528 Returns a ``HttpResponse`` containing a all needed context data. 529 """ 530 form = form or self.get_form() 531 context = self.get_context_data(form, **kwargs) 532 return self.render_to_response(context) 533 534 def done(self, form_list, **kwargs): 535 """ 536 This method muss be overrided by a subclass to process to form data 537 after processing all steps. 538 """ 539 raise NotImplementedError("Your %s class has not defined a done() " 540 "method, which is required." % self.__class__.__name__) 541 542 543 class SessionWizardView(WizardView): 544 """ 545 A WizardView with pre-configured SessionStorage backend. 546 """ 547 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 548 549 550 class CookieWizardView(WizardView): 551 """ 552 A WizardView with pre-configured CookieStorage backend. 553 """ 554 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 555 556 557 class NamedUrlWizardView(WizardView): 558 """ 559 A WizardView with URL named steps support. 560 """ 561 url_name = None 562 done_step_name = None 563 564 @classmethod 565 def get_initkwargs(cls, *args, **kwargs): 566 """ 567 We require a url_name to reverse URLs later. Additionally users can 568 pass a done_step_name to change the URL name of the "done" view. 569 """ 570 extra_kwargs = { 571 'done_step_name': 'done' 572 } 573 assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs' 574 extra_kwargs['url_name'] = kwargs.pop('url_name') 575 576 if 'done_step_name' in kwargs: 577 extra_kwargs['done_step_name'] = kwargs.pop('done_step_name') 578 579 initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs) 580 initkwargs.update(extra_kwargs) 581 582 assert initkwargs['done_step_name'] not in initkwargs['form_list'], \ 583 'step name "%s" is reserved for "done" view' % initkwargs['done_step_name'] 584 585 return initkwargs 586 587 def get(self, *args, **kwargs): 588 """ 589 This renders the form or, if needed, does the http redirects. 590 """ 591 step_url = kwargs.get('step', None) 592 if step_url is None: 593 if 'reset' in self.request.GET: 594 self.storage.reset() 595 self.storage.current_step = self.steps.first 596 if self.request.GET: 597 query_string = "?%s" % self.request.GET.urlencode() 598 else: 599 query_string = "" 600 next_step_url = reverse(self.url_name, kwargs={ 601 'step': self.steps.current, 602 }) + query_string 603 return redirect(next_step_url) 604 605 # is the current step the "done" name/view? 606 elif step_url == self.done_step_name: 607 last_step = self.steps.last 608 return self.render_done(self.get_form(step=last_step, 609 data=self.storage.get_step_data(last_step), 610 files=self.storage.get_step_files(last_step) 611 ), **kwargs) 612 613 # is the url step name not equal to the step in the storage? 614 # if yes, change the step in the storage (if name exists) 615 elif step_url == self.steps.current: 616 # URL step name and storage step name are equal, render! 617 return self.render(self.get_form( 618 data=self.storage.current_step_data, 619 files=self.storage.current_step_data, 620 ), **kwargs) 621 622 elif step_url in self.get_form_list(): 623 self.storage.current_step = step_url 624 return self.render(self.get_form( 625 data=self.storage.current_step_data, 626 files=self.storage.current_step_data, 627 ), **kwargs) 628 629 # invalid step name, reset to first and redirect. 630 else: 631 self.storage.current_step = self.steps.first 632 return redirect(self.url_name, step=self.steps.first) 633 634 def post(self, *args, **kwargs): 635 """ 636 Do a redirect if user presses the prev. step button. The rest of this 637 is super'd from FormWizard. 638 """ 639 prev_step = self.request.POST.get('wizard_prev_step', None) 640 if prev_step and prev_step in self.get_form_list(): 641 self.storage.current_step = prev_step 642 return redirect(self.url_name, step=prev_step) 643 return super(NamedUrlWizardView, self).post(*args, **kwargs) 644 645 def render_next_step(self, form, **kwargs): 646 """ 647 When using the NamedUrlFormWizard, we have to redirect to update the 648 browser's URL to match the shown step. 649 """ 650 next_step = self.get_next_step() 651 self.storage.current_step = next_step 652 return redirect(self.url_name, step=next_step) 653 654 def render_revalidation_failure(self, failed_step, form, **kwargs): 655 """ 656 When a step fails, we have to redirect the user to the first failing 657 step. 658 """ 659 self.storage.current_step = failed_step 660 return redirect(self.url_name, step=failed_step) 661 662 def render_done(self, form, **kwargs): 663 """ 664 When rendering the done view, we have to redirect first (if the URL 665 name doesn't fit). 666 """ 667 if kwargs.get('step', None) != self.done_step_name: 668 return redirect(self.url_name, step=self.done_step_name) 669 return super(NamedUrlWizardView, self).render_done(form, **kwargs) 670 671 672 class NamedUrlSessionWizardView(NamedUrlWizardView): 673 """ 674 A NamedUrlWizardView with pre-configured SessionStorage backend. 675 """ 676 storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage' 677 678 679 class NamedUrlCookieWizardView(NamedUrlWizardView): 680 """ 681 A NamedUrlFormWizard with pre-configured CookieStorageBackend. 682 """ 683 storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage' 684 -
django/utils/functional.py
diff --git a/django/utils/functional.py b/django/utils/functional.py index b0233de..76f3639 100644
a b class SimpleLazyObject(LazyObject): 265 265 266 266 def _setup(self): 267 267 self._wrapped = self._setupfunc() 268 269 270 class lazy_property(property): 271 """ 272 A property that works with subclasses by wrapping the decorated 273 functions of the base class. 274 """ 275 def __new__(cls, fget=None, fset=None, fdel=None, doc=None): 276 if fget is not None: 277 @wraps(fget) 278 def fget(instance, instance_type=None, name=fget.__name__): 279 return getattr(instance, name)() 280 if fset is not None: 281 @wraps(fset) 282 def fset(instance, value, name=fset.__name__): 283 return getattr(instance, name)(value) 284 if fdel is not None: 285 @wraps(fdel) 286 def fdel(instance, name=fdel.__name__): 287 return getattr(instance, name)() 288 return property(fget, fset, fdel, doc) -
docs/internals/deprecation.txt
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 01e4500..4d81cb7 100644
a b their deprecation, as per the :ref:`Django deprecation policy 203 203 settings have been superseded by :setting:`IGNORABLE_404_URLS` in 204 204 the 1.4 release. They will be removed. 205 205 206 * The :doc:`form wizard </ref/contrib/formtools/form-wizard>` has been 207 refactored to use class based views with pluggable backends in 1.4. 208 The previous implementation will be deprecated. 209 206 210 * 2.0 207 211 * ``django.views.defaults.shortcut()``. This function has been moved 208 212 to ``django.contrib.contenttypes.views.shortcut()`` as part of the -
docs/ref/contrib/formtools/form-wizard.txt
diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index cbacd59..2434c8b 100644
a b 2 2 Form wizard 3 3 =========== 4 4 5 .. module:: django.contrib.formtools.wizard 5 .. module:: django.contrib.formtools.wizard.views 6 6 :synopsis: Splits forms across multiple Web pages. 7 7 8 8 Django comes with an optional "form wizard" application that splits 9 9 :doc:`forms </topics/forms/index>` across multiple Web pages. It maintains 10 state in hashed HTML :samp:`<input type="hidden">` fields so that the full11 server-side processing can bedelayed until the submission of the final form.10 state in one of the backends so that the full server-side processing can be 11 delayed until the submission of the final form. 12 12 13 13 You might want to use this if you have a lengthy form that would be too 14 14 unwieldy for display on a single page. The first page might ask the user for 15 15 core information, the second page might ask for less important information, 16 16 etc. 17 17 18 The term "wizard ,"in this context, is `explained on Wikipedia`_.18 The term "wizard", in this context, is `explained on Wikipedia`_. 19 19 20 20 .. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29 21 .. _forms: ../forms/22 21 23 22 How it works 24 23 ============ … … Here's the basic workflow for how a user would use a wizard: 28 27 1. The user visits the first page of the wizard, fills in the form and 29 28 submits it. 30 29 2. The server validates the data. If it's invalid, the form is displayed 31 again, with error messages. If it's valid, the server calculates a 32 secure hash of the data and presents the user with the next form, 33 saving the validated data and hash in :samp:`<input type="hidden">` 34 fields. 30 again, with error messages. If it's valid, the server saves the current 31 state of the wizard in the backend and redirects to the next step. 35 32 3. Step 1 and 2 repeat, for every subsequent form in the wizard. 36 33 4. Once the user has submitted all the forms and all the data has been 37 34 validated, the wizard processes the data -- saving it to the database, … … Here's the basic workflow for how a user would use a wizard: 40 37 Usage 41 38 ===== 42 39 43 This application handles as much machinery for you as possible. Generally, you44 just have to do these things:40 This application handles as much machinery for you as possible. Generally, 41 you just have to do these things: 45 42 46 1. Define a number of :class:`~django.forms.Form` classes -- one per wizard47 page.43 1. Define a number of :class:`~django.forms.Form` classes -- one per 44 wizard page. 48 45 49 2. Create a :class:` FormWizard` class that specifies what to do once all of50 your forms have been submitted and validated. This also lets you51 override some of the wizard's behavior.46 2. Create a :class:`WizardView` subclass that specifies what to do once 47 all of your forms have been submitted and validated. This also lets 48 you override some of the wizard's behavior. 52 49 53 50 3. Create some templates that render the forms. You can define a single, 54 51 generic template to handle every one of the forms, or you can define a 55 52 specific template for each form. 56 53 57 4. Point your URLconf at your :class:`FormWizard` class. 54 4. Add ``django.contrib.formtools.wizard`` to your 55 :setting:`INSTALLED_APPS` list in your settings file. 56 57 5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method. 58 58 59 59 Defining ``Form`` classes 60 ========================= 60 ------------------------- 61 61 62 62 The first step in creating a form wizard is to create the 63 63 :class:`~django.forms.Form` classes. These should be standard 64 64 :class:`django.forms.Form` classes, covered in the :doc:`forms documentation 65 </topics/forms/index>`. These classes can live anywhere in your codebase, but66 convention is to put them in a file called :file:`forms.py` in your65 </topics/forms/index>`. These classes can live anywhere in your codebase, 66 but convention is to put them in a file called :file:`forms.py` in your 67 67 application. 68 68 69 69 For example, let's write a "contact form" wizard, where the first page's form … … the message itself. Here's what the :file:`forms.py` might look like:: 79 79 class ContactForm2(forms.Form): 80 80 message = forms.CharField(widget=forms.Textarea) 81 81 82 **Important limitation:** Because the wizard uses HTML hidden fields to store83 data between pages, you may not include a :class:`~django.forms.FileField`84 in any form except the last one.85 86 Creating a ``FormWizard`` class87 ===============================88 82 89 The next step is to create a 90 :class:`django.contrib.formtools.wizard.FormWizard` subclass. As with your 91 :class:`~django.forms.Form` classes, this :class:`FormWizard` class can live 92 anywhere in your codebase, but convention is to put it in :file:`forms.py`. 83 .. note:: 93 84 94 The only requirement on this subclass is that it implement a 95 :meth:`~FormWizard.done()` method. 85 In order to use :class:`~django.forms.FileField` in any form, see the 86 section :ref:`Handling files <wizard-files>` below to learn more about 87 what to do. 96 88 97 .. method:: FormWizard.done 89 Creating a ``WizardView`` class 90 ------------------------------- 98 91 99 This method specifies what should happen when the data for *every* form is 100 submitted and validated. This method is passed two arguments: 92 The next step is to create a 93 :class:`django.contrib.formtools.wizard.view.WizardView` subclass. You can 94 also use the :class:`SessionWizardView` or :class:`CookieWizardView` class 95 which preselects the wizard storage backend. 101 96 102 * ``request`` -- an :class:`~django.http.HttpRequest` object 103 * ``form_list`` -- a list of :class:`~django.forms.Form` classes 97 .. note:: 104 98 105 In this simplistic example, rather than perform any database operation, the 106 method simply renders a template of the validated data:: 99 To use the :class:`SessionWizardView` follow the instructions 100 in the :doc:`sessions documentation </topics/http/sessions>` on 101 how to enable sessions. 107 102 108 from django.shortcuts import render_to_response 109 from django.contrib.formtools.wizard import FormWizard 103 We will use the :class:`SessionWizardView` in all examples but is is completly 104 fine to use the :class:`CookieWizardView` instead. As with your 105 :class:`~django.forms.Form` classes, this :class:`WizardView` class can live 106 anywhere in your codebase, but convention is to put it in :file:`views.py`. 110 107 111 class ContactWizard(FormWizard): 112 def done(self, request, form_list): 113 return render_to_response('done.html', { 114 'form_data': [form.cleaned_data for form in form_list], 115 }) 108 The only requirement on this subclass is that it implement a 109 :meth:`~WizardView.done()` method. 116 110 117 Note that this method will be called via ``POST``, so it really ought to be a 118 good Web citizen and redirect after processing the data. Here's another 119 example:: 111 .. method:: WizardView.done(form_list) 120 112 121 from django.http import HttpResponseRedirect 122 from django.contrib.formtools.wizard import FormWizard 113 This method specifies what should happen when the data for *every* form is 114 submitted and validated. This method is passed a list of validated 115 :class:`~django.forms.Form` instances. 123 116 124 class ContactWizard(FormWizard): 125 def done(self, request, form_list): 126 do_something_with_the_form_data(form_list) 127 return HttpResponseRedirect('/page-to-redirect-to-when-done/') 117 In this simplistic example, rather than performing any database operation, 118 the method simply renders a template of the validated data:: 128 119 129 See the section `Advanced FormWizard methods`_ below to learn about more130 :class:`FormWizard` hooks. 120 from django.shortcuts import render_to_response 121 from django.contrib.formtools.wizard.views import SessionWizardView 131 122 132 Creating templates for the forms 133 ================================ 123 class ContactWizard(SessionWizardView): 124 def done(self, form_list, **kwargs): 125 return render_to_response('done.html', { 126 'form_data': [form.cleaned_data for form in form_list], 127 }) 134 128 135 Next, you'll need to create a template that renders the wizard's forms. By 136 default, every form uses a template called :file:`forms/wizard.html`. (You can 137 change this template name by overriding :meth:`~FormWizard.get_template()`, 138 which is documented below. This hook also allows you to use a different 139 template for each form.) 129 Note that this method will be called via ``POST``, so it really ought to be a 130 good Web citizen and redirect after processing the data. Here's another 131 example:: 140 132 141 This template expects the following context: 133 from django.http import HttpResponseRedirect 134 from django.contrib.formtools.wizard.views import SessionWizardView 142 135 143 * ``step_field`` -- The name of the hidden field containing the step. 144 * ``step0`` -- The current step (zero-based). 145 * ``step`` -- The current step (one-based). 146 * ``step_count`` -- The total number of steps. 147 * ``form`` -- The :class:`~django.forms.Form` instance for the current step 148 (either empty or with errors). 149 * ``previous_fields`` -- A string representing every previous data field, 150 plus hashes for completed forms, all in the form of hidden fields. Note 151 that you'll need to run this through the :tfilter:`safe` template filter, 152 to prevent auto-escaping, because it's raw HTML. 136 class ContactWizard(SessionWizardView): 137 def done(self, form_list, **kwargs): 138 do_something_with_the_form_data(form_list) 139 return HttpResponseRedirect('/page-to-redirect-to-when-done/') 153 140 154 You can supply extra context to this template in two ways: 141 See the section :ref:`Advanced WizardView methods <wizardview-advanced-methods>` 142 below to learn about more :class:`WizardView` hooks. 155 143 156 * Set the :attr:`~FormWizard.extra_context` attribute on your 157 :class:`FormWizard` subclass to a dictionary. 144 Creating templates for the forms 145 -------------------------------- 158 146 159 * Pass a dictionary as a parameter named ``extra_context`` to your wizard's 160 URL pattern in your URLconf. See :ref:`hooking-wizard-into-urlconf`. 147 Next, you'll need to create a template that renders the wizard's forms. By 148 default, every form uses a template called 149 :file:`formtools/wizard/wizard_form.html`. You can change this template name 150 by overriding either the :attr:`~WizardView.template_name` attribute or the 151 :meth:`~WizardView.get_template_names()` method, which is documented below. 152 This hook also allows you to use a different template for each form. 153 154 This template expects a ``wizard`` object that has various items attached to 155 it: 156 157 * ``form`` -- The :class:`~django.forms.Form` instance for the current 158 step (either empty or with errors). 159 160 * ``steps`` -- A helper object to access the various steps related data: 161 162 * ``step0`` -- The current step (zero-based). 163 * ``step1`` -- The current step (one-based). 164 * ``count`` -- The total number of steps. 165 * ``first`` -- The first step. 166 * ``last`` -- The last step. 167 * ``current`` -- The current (or first) step. 168 * ``next`` -- The next step. 169 * ``prev`` -- The previous step. 170 * ``index`` -- The index of the current step. 171 * ``all`` -- A list of all steps of the wizard. 172 173 You can supply additional context variables by using the 174 :meth:`~FormWizard.get_context_data` method of your :class:`FormWizard` 175 subclass. 161 176 162 177 Here's a full example template: 163 178 … … Here's a full example template: 166 181 {% extends "base.html" %} 167 182 168 183 {% block content %} 169 <p>Step {{ step }} of {{ step_count }}</p>184 <p>Step {{ wizard.steps.current }} of {{ wizard.steps.count }}</p> 170 185 <form action="." method="post">{% csrf_token %} 171 186 <table> 172 {{ form }} 187 {{ wizard.management_form }} 188 {% if wizard.form.forms %} 189 {{ wizard.form.management_form }} 190 {% for form in wizard.form.forms %} 191 {{ form }} 192 {% endfor %} 193 {% else %} 194 {{ wizard.form }} 195 {% endif %} 196 {% if wizard.steps.prev %} 197 <button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button> 198 <button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button> 199 {% endif %} 173 200 </table> 174 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />175 {{ previous_fields|safe }}176 201 <input type="submit"> 177 202 </form> 178 203 {% endblock %} 179 204 180 Note that ``previous_fields``, ``step_field`` and ``step0`` are all required 181 for the wizard to work properly. 205 .. note:: 182 206 183 .. _hooking-wizard-into-urlconf: 207 Note that ``{{ wizard.management_form }}`` **must be used** for 208 the wizard to work properly. 209 210 .. _wizard-urlconf: 184 211 185 212 Hooking the wizard into a URLconf 186 ================================= 213 --------------------------------- 187 214 188 215 Finally, we need to specify which forms to use in the wizard, and then 189 deploy the new :class:`FormWizard` object a URL in ``urls.py``. The 190 wizard takes a list of your :class:`~django.forms.Form` objects as 191 arguments when you instantiate the Wizard:: 216 deploy the new :class:`WizardView` object a URL in the ``urls.py``. The 217 wizard's :meth:`as_view` method takes a list of your 218 :class:`~django.forms.Form` classes as an argument during instantiation:: 219 220 from django.conf.urls.defaults import patterns 192 221 193 from django.conf.urls.defaults import *194 from testapp.forms import ContactForm1, ContactForm2,ContactWizard222 from myapp.forms import ContactForm1, ContactForm2 223 from myapp.views import ContactWizard 195 224 196 225 urlpatterns = patterns('', 197 (r'^contact/$', ContactWizard ([ContactForm1, ContactForm2])),226 (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])), 198 227 ) 199 228 200 Advanced ``FormWizard`` methods 229 .. _wizardview-advanced-methods: 230 231 Advanced ``WizardView`` methods 201 232 =============================== 202 233 203 .. class:: FormWizard234 .. class:: WizardView 204 235 205 Aside from the :meth:`~done()` method, :class:` FormWizard` offers a few236 Aside from the :meth:`~done()` method, :class:`WizardView` offers a few 206 237 advanced method hooks that let you customize how your wizard works. 207 238 208 239 Some of these methods take an argument ``step``, which is a zero-based 209 counter representing the current step of the wizard. (E.g., the first form210 is ``0`` and the second form is ``1``.)240 counter as string representing the current step of the wizard. (E.g., the 241 first form is ``'0'`` and the second form is ``'1'``) 211 242 212 .. method:: FormWizard.prefix_for_step243 .. method:: WizardView.get_form_prefix(step) 213 244 214 Given the step, returns a form prefix to use. 245 Given the step, returns a form prefix to use. By default, this simply uses 215 246 the step itself. For more, see the :ref:`form prefix documentation 216 247 <form-prefix>`. 217 248 249 .. method:: WizardView.process_step(form) 250 251 Hook for modifying the wizard's internal state, given a fully validated 252 :class:`~django.forms.Form` object. The Form is guaranteed to have clean, 253 valid data. 254 255 Note that this method is called every time a page is rendered for *all* 256 submitted steps. 257 258 The default implementation:: 259 260 def process_step(self, form): 261 return self.get_form_step_data(form) 262 263 .. method:: WizardView.get_form_initial(step) 264 265 Returns a dictionary which will be passed to the form for ``step`` as 266 ``initial``. If no initial data was provied while initializing the 267 form wizard, a empty dictionary should be returned. 268 269 The default implementation:: 270 271 def get_form_initial(self, step): 272 return self.initial_dict.get(step, {}) 273 274 .. method:: WizardView.get_form_instance(step) 275 276 Returns a object which will be passed to the form for ``step`` as 277 ``instance``. If no instance object was provied while initializing 278 the form wizard, None be returned. 279 280 The default implementation:: 281 282 def get_form_instance(self, step): 283 return self.instance_dict.get(step, None) 284 285 .. method:: WizardView.get_context_data(form, **kwargs) 286 287 Returns the template context for a step. You can overwrite this method 288 to add more data for all or some steps. This method returns a dictionary 289 containing the rendered form step. 290 291 The default template context variables are: 292 293 * Any extra data the storage backend has stored 294 * ``form`` -- form instance of the current step 295 * ``wizard`` -- the wizard instance itself 296 297 Example to add extra variables for a specific step:: 298 299 def get_context_data(self, form, **kwargs): 300 context = super(MyWizard, self).get_context_data(form, **kwargs) 301 if self.steps.current == 'my_step_name': 302 context.update({'another_var': True}) 303 return context 304 305 .. method:: WizardView.get_wizard_name() 306 307 This method can be used to change the wizard's internal name. 308 218 309 Default implementation:: 219 310 220 def prefix_for_step(self, step): 221 return str(step) 311 def get_wizard_name(self): 312 return normalize_name(self.__class__.__name__) 313 314 .. method:: WizardView.get_prefix() 222 315 223 .. method:: FormWizard.render_hash_failure 316 This method returns a prefix for the storage backends. These backends use 317 the prefix to fetch the correct data for the wizard. (Multiple wizards 318 could save their data in one session) 224 319 225 Renders a template if the hash check fails. It's rare that you'd need to226 override this.320 You can change this method to make the wizard data prefix more unique to, 321 e.g. have multiple instances of one wizard in one session. 227 322 228 323 Default implementation:: 229 324 230 def render_hash_failure(self, request, step): 231 return self.render(self.get_form(step), request, step, 232 context={'wizard_error': 233 'We apologize, but your form has expired. Please' 234 ' continue filling out the form from this page.'}) 325 def get_prefix(self): 326 return self.wizard_name 235 327 236 .. method:: FormWizard.security_hash328 .. method:: WizardView.get_form(step=None, data=None, files=None) 237 329 238 Calculates the security hash for the given request object and 239 :class:`~django.forms.Form` instance. 330 This method constructs the form for a given ``step``. If no ``step`` is 331 defined, the current step will be determined automatically. 332 The method gets three arguments: 240 333 241 By default, this generates a SHA1 HMAC using your form data and your242 :setting:`SECRET_KEY` setting. It's rare that somebody would need to243 override this.334 * ``step`` -- The step for which the form instance should be generated. 335 * ``data`` -- Gets passed to the form's data argument 336 * ``files`` -- Gets passed to the form's files argument 244 337 245 Example::338 You can override this method to add extra arguments to the form instance. 246 339 247 def security_hash(self, request, form): 248 return my_hash_function(request, form) 340 Example code to add a user attribute to the form on step 2:: 249 341 250 .. method:: FormWizard.parse_params 342 def get_form(self, step=None, data=None, files=None): 343 form = super(MyWizard, self).get_form(step, data, files) 344 if step == '1': 345 form.user = self.request.user 346 return form 251 347 252 A hook for saving state from the request object and ``args`` / ``kwargs`` 253 that were captured from the URL by your URLconf. 348 .. method:: WizardView.process_step(form) 254 349 255 By default, this does nothing. 350 This method gives you a way to post-process the form data before the data 351 gets stored within the storage backend. By default it just passed the 352 form.data dictionary. You should not manipulate the data here but you can 353 use the data to do some extra work if needed (e.g. set storage extra data). 256 354 257 Example::355 Default implementation:: 258 356 259 def p arse_params(self, request, *args, **kwargs):260 self.my_state = args[0]357 def process_step(self, form): 358 return self.get_form_step_data(form) 261 359 262 .. method:: FormWizard.get_template360 .. method:: WizardView.process_step_files(form) 263 361 264 Returns the name of the template that should be used for the given step. 362 This method gives you a way to post-process the form files before the 363 files gets stored within the storage backend. By default it just passed 364 the ``form.files`` dictionary. You should not manipulate the data here 365 but you can use the data to do some extra work if needed (e.g. set storage 366 extra data). 265 367 266 By default, this returns :file:`'forms/wizard.html'`, regardless of step.368 Default implementation:: 267 369 268 Example:: 370 def process_step_files(self, form): 371 return self.get_form_step_files(form) 269 372 270 def get_template(self, step): 271 return 'myapp/wizard_%s.html' % step 373 .. method:: WizardView.render_revalidation_failure(step, form, **kwargs) 272 374 273 If :meth:`~FormWizard.get_template` returns a list of strings, then the 274 wizard will use the template system's 275 :func:`~django.template.loader.select_template` function. 276 This means the system will use the first template that exists on the 277 filesystem. For example:: 375 When the wizard thinks, all steps passed it revalidates all forms with the 376 data from the backend storage. 278 377 279 def get_template(self, step):280 return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']378 If any of the forms don't validate correctly, this method gets called. 379 This method expects two arguments, ``step`` and ``form``. 281 380 282 .. method:: FormWizard.render_template 381 The default implementation resets the current step to the first failing 382 form and redirects the user to the invalid form. 283 383 284 Renders the template for the given step, returning an 285 :class:`~django.http.HttpResponse` object. 384 Default implementation:: 286 385 287 Override this method if you want to add a custom context, return a288 different MIME type, etc. If you only need to override the template name,289 use :meth:`~FormWizard.get_template` instead.386 def render_revalidation_failure(self, step, form, **kwargs): 387 self.storage.current_step = step 388 return self.render(form, **kwargs) 290 389 291 The template will be rendered with the context documented in the 292 "Creating templates for the forms" section above. 390 .. method:: WizardView.get_form_step_data(form) 293 391 294 .. method:: FormWizard.process_step 392 This method fetches the form data from and returns the dictionary. You 393 can use this method to manipulate the values before the data gets stored 394 in the storage backend. 295 395 296 Hook for modifying the wizard's internal state, given a fully validated 297 :class:`~django.forms.Form` object. The Form is guaranteed to have clean, 298 valid data. 396 Default implementation:: 299 397 300 This method should *not* modify any of that data. Rather, it might want to 301 set ``self.extra_context`` or dynamically alter ``self.form_list``, based 302 on previously submitted forms. 398 def get_form_step_data(self, form): 399 return form.data 303 400 304 Note that this method is called every time a page is rendered for *all* 305 submitted steps. 401 .. method:: WizardView.get_form_step_files(form) 402 403 This method returns the form files. You can use this method to manipulate 404 the files before the data gets stored in the storage backend. 405 406 Default implementation:: 407 408 def get_form_step_files(self, form): 409 return form.files 306 410 307 The function signature:: 411 .. method:: WizardView.render(form, **kwargs) 308 412 309 def process_step(self, request, form, step): 310 # ... 413 This method gets called after the get or post request was handled. You can 414 hook in this method to, e.g. change the type of http response. 415 416 Default implementation:: 417 418 def render(self, form=None, **kwargs): 419 form = form or self.get_form() 420 context = self.get_context_data(form, **kwargs) 421 return self.render_to_response(context) 311 422 312 423 Providing initial data for the forms 313 424 ==================================== 314 425 315 .. attribute:: FormWizard.initial426 .. attribute:: WizardView.initial_dict 316 427 317 428 Initial data for a wizard's :class:`~django.forms.Form` objects can be 318 provided using the optional :attr:`~ FormWizard.initial` keyword argument.319 This argument should be a dictionary mapping a step to a dictionary320 containing the initial data for thatstep. The dictionary of initial data429 provided using the optional :attr:`~Wizard.initial_dict` keyword argument. 430 This argument should be a dictionary mapping the steps to dictionaries 431 containing the initial data for each step. The dictionary of initial data 321 432 will be passed along to the constructor of the step's 322 433 :class:`~django.forms.Form`:: 323 434 324 >>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard 435 >>> from myapp.forms import ContactForm1, ContactForm2 436 >>> from myapp.views import ContactWizard 325 437 >>> initial = { 326 ... 0: {'subject': 'Hello', 'sender': 'user@example.com'},327 ... 1: {'message': 'Hi there!'}438 ... '0': {'subject': 'Hello', 'sender': 'user@example.com'}, 439 ... '1': {'message': 'Hi there!'} 328 440 ... } 329 >>> wiz = ContactWizard ([ContactForm1, ContactForm2], initial=initial)330 >>> form1 = wiz.get_form( 0)331 >>> form2 = wiz.get_form( 1)441 >>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial) 442 >>> form1 = wiz.get_form('0') 443 >>> form2 = wiz.get_form('1') 332 444 >>> form1.initial 333 445 {'sender': 'user@example.com', 'subject': 'Hello'} 334 446 >>> form2.initial 335 447 {'message': 'Hi there!'} 448 449 The ``initial_dict`` can also take a list of dictionaries for a specific 450 step if the step is a ``FormSet``. 451 452 .. _wizard-files: 453 454 Handling files 455 ============== 456 457 To handle :class:`~django.forms.FileField` within any step form of the wizard, 458 you have to add a :attr:`file_storage` to your :class:`WizardView` subclass. 459 460 This storage will temporarilyy store the uploaded files for the wizard. The 461 :attr:`file_storage` attribute should be a 462 :class:`~django.core.files.storage.Storage` subclass. 463 464 .. warning:: 465 466 Please remember to take care of removing old files as the 467 :class:`WizardView` won't remove any files, whether the wizard gets 468 finished corretly or not. 469 470 Conditionally view/skip specific steps 471 ====================================== 472 473 .. attribute:: WizardView.condition_dict 474 475 The :meth:`~WizardView.as_view` accepts a ``condition_dict`` argument. You can pass a 476 dictionary of boolean values or callables. The key should match the steps 477 name (e.g. '0', '1'). 478 479 If the value of a specific step is callable it will be called with the 480 :class:`WizardView` instance as the only argument. If the return value is true, 481 the step's form will be used. 482 483 This example provides a contact form including a condition. The condition is 484 used to show a message from only if a checkbox in the first step was checked. 485 486 The steps are defined in a ``forms.py``:: 487 488 from django import forms 489 490 class ContactForm1(forms.Form): 491 subject = forms.CharField(max_length=100) 492 sender = forms.EmailField() 493 leave_message = forms.BooleanField(required=False) 494 495 class ContactForm2(forms.Form): 496 message = forms.CharField(widget=forms.Textarea) 497 498 We define our wizard in a ``views.py``:: 499 500 from django.shortcuts import render_to_response 501 from django.contrib.formtools.wizard.views import SessionWizardView 502 503 def show_message_form_condition(wizard): 504 # try to get the cleaned data of step 1 505 cleaned_data = wizard.get_cleaned_data_for_step('0') or {} 506 # check if the field ``leave_message`` was checked. 507 return cleaned_data.get('leave_message', True) 508 509 class ContactWizard(SessionWizardView): 510 511 def done(self, form_list, **kwargs): 512 return render_to_response('done.html', { 513 'form_data': [form.cleaned_data for form in form_list], 514 }) 515 516 We need to add the ``ContactWizard`` to our ``urls.py`` file:: 517 518 from django.conf.urls.defaults import pattern 519 520 from myapp.forms import ContactForm1, ContactForm2 521 from myapp.views import ContactWizard, show_message_form_condition 522 523 contact_forms = [ContactForm1, ContactForm2] 524 525 urlpatterns = patterns('', 526 (r'^contact/$', ContactWizard.as_view(contact_forms, 527 condition_dict={'1': show_message_form_condition} 528 )), 529 ) 530 531 As you can see, we defined a ``show_message_form_condition`` next to our 532 :class:`WizardView` subclass and added a ``condition_dict`` argument to the 533 :meth:`~WizardView.as_view` method. The key refers to the second wizard step 534 (because of the zero based step index). 535 536 How to work with ModelForm and ModelFormSet 537 =========================================== 538 539 The WizardView supports :class:`~django.forms.ModelForm` and 540 :class:`~django.forms.ModelFormSet`. Additionally to the ``initial_dict``, 541 the :meth:`~WizardView.as_view` method takes a ``instance_dict`` argument 542 with a list of instances for the ``ModelForm`` and ``ModelFormSet``. 543 544 Usage of NamedUrlWizardView 545 =========================== 546 547 .. class:: NamedUrlWizardView 548 549 There is a :class:`WizardView` subclass which adds named-urls support to the wizard. 550 By doing this, you can have single urls for every step. 551 552 To use the named urls, you have to change the ``urls.py``. 553 554 Below you will see an example of a contact wizard with two steps, step 1 with 555 "contactdata" as its name and step 2 with "leavemessage" as its name. 556 557 Additionally you have to pass two more arguments to the 558 :meth:`~WizardView.as_view` method: 559 560 * ``url_name`` -- the name of the url (as provided in the urls.py) 561 * ``done_step_name`` -- the name in the url for the done step 562 563 Example code for the changed ``urls.py`` file:: 564 565 from django.conf.urls.defaults import url, patterns 566 567 from myapp.forms import ContactForm1, ContactForm2 568 from myapp.views import ContactWizard 569 570 named_contact_forms = ( 571 ('contactdata', ContactForm1), 572 ('leavemessage', ContactForm2), 573 ) 574 575 contact_wizard = ContactWizard.as_view(named_contact_forms, 576 url_name='contact_step', done_step_name='finished') 577 578 urlpatterns = patterns('', 579 url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'), 580 url(r'^contact/$', contact_wizard, name='contact'), 581 ) -
docs/releases/1.4.txt
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 7fdf0d7..2a2090a 100644
a b signing in Web applications. 55 55 56 56 See :doc:`cryptographic signing </topics/signing>` docs for more information. 57 57 58 New form wizard 59 ~~~~~~~~~~~~~~~ 60 61 The previously shipped ``FormWizard`` of the formtools contrib app has been 62 replaced with a new implementation that is based on the class based views 63 introduced in Django 1.3. It features a pluggable storage API and doesn't 64 require the wizard to pass around hidden fields for every previous step. 65 66 Django 1.4 ships with a session based storage backend and a cookie based 67 storage backend. The latter uses the tools for 68 :doc:`cryptographic signing </topics/signing>` also introduced in 69 Django 1.4 to store the wizard state in the user's cookies. 70 71 See the :doc:`form wizard </ref/contrib/formtools/form-wizard>` docs for 72 more information. 73 58 74 Simple clickjacking protection 59 75 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 76 -
deleted file tests/regressiontests/formwizard/forms.py
diff --git a/tests/regressiontests/formwizard/__init__.py b/tests/regressiontests/formwizard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/regressiontests/formwizard/forms.py b/tests/regressiontests/formwizard/forms.py deleted file mode 100644 index f458eda..0000000
+ - 1 from django import forms2 from django.contrib.formtools.wizard import FormWizard3 from django.http import HttpResponse4 5 class Page1(forms.Form):6 name = forms.CharField(max_length=100)7 thirsty = forms.NullBooleanField()8 9 class Page2(forms.Form):10 address1 = forms.CharField(max_length=100)11 address2 = forms.CharField(max_length=100)12 13 class Page3(forms.Form):14 random_crap = forms.CharField(max_length=100)15 16 class ContactWizard(FormWizard):17 def done(self, request, form_list):18 return HttpResponse("") -
deleted file tests/regressiontests/formwizard/templates/forms/wizard.html
diff --git a/tests/regressiontests/formwizard/models.py b/tests/regressiontests/formwizard/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/regressiontests/formwizard/templates/forms/wizard.html b/tests/regressiontests/formwizard/templates/forms/wizard.html deleted file mode 100644 index a31378f..0000000
+ - 1 <html>2 <body>3 <p>Step {{ step }} of {{ step_count }}</p>4 <form action="." method="post">5 <table>6 {{ form }}7 </table>8 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />9 {{ previous_fields|safe }}10 <input type="submit">11 </form>12 </body>13 </html>14 No newline at end of file -
deleted file tests/regressiontests/formwizard/tests.py
diff --git a/tests/regressiontests/formwizard/tests.py b/tests/regressiontests/formwizard/tests.py deleted file mode 100644 index 0c94d2e..0000000
+ - 1 import re2 from django import forms3 from django.test import TestCase4 5 class FormWizardWithNullBooleanField(TestCase):6 urls = 'regressiontests.formwizard.urls'7 8 input_re = re.compile('name="([^"]+)" value="([^"]+)"')9 10 wizard_url = '/wiz/'11 wizard_step_data = (12 {13 '0-name': 'Pony',14 '0-thirsty': '2',15 },16 {17 '1-address1': '123 Main St',18 '1-address2': 'Djangoland',19 },20 {21 '2-random_crap': 'blah blah',22 }23 )24 25 def grabFieldData(self, response):26 """27 Pull the appropriate field data from the context to pass to the next wizard step28 """29 previous_fields = response.context['previous_fields']30 fields = {'wizard_step': response.context['step0']}31 32 def grab(m):33 fields[m.group(1)] = m.group(2)34 return ''35 36 self.input_re.sub(grab, previous_fields)37 return fields38 39 def checkWizardStep(self, response, step_no):40 """41 Helper function to test each step of the wizard42 - Make sure the call succeeded43 - Make sure response is the proper step number44 - return the result from the post for the next step45 """46 step_count = len(self.wizard_step_data)47 48 self.assertEqual(response.status_code, 200)49 self.assertContains(response, 'Step %d of %d' % (step_no, step_count))50 51 data = self.grabFieldData(response)52 data.update(self.wizard_step_data[step_no - 1])53 54 return self.client.post(self.wizard_url, data)55 56 def testWizard(self):57 response = self.client.get(self.wizard_url)58 for step_no in range(1, len(self.wizard_step_data) + 1):59 response = self.checkWizardStep(response, step_no) -
deleted file tests/regressiontests/formwizard/urls.py
diff --git a/tests/regressiontests/formwizard/urls.py b/tests/regressiontests/formwizard/urls.py deleted file mode 100644 index d964bc6..0000000
+ - 1 from django.conf.urls.defaults import *2 from forms import ContactWizard, Page1, Page2, Page33 4 urlpatterns = patterns('',5 url(r'^wiz/$', ContactWizard([Page1, Page2, Page3])),6 ) -
tests/regressiontests/utils/functional.py
diff --git a/tests/regressiontests/utils/functional.py b/tests/regressiontests/utils/functional.py index 2784ddd..90a6f08 100644
a b 1 1 from django.utils import unittest 2 from django.utils.functional import lazy 2 from django.utils.functional import lazy, lazy_property 3 3 4 4 5 5 class FunctionalTestCase(unittest.TestCase): … … class FunctionalTestCase(unittest.TestCase): 20 20 21 21 t = lazy(lambda: Klazz(), Klazz)() 22 22 self.assertTrue('base_method' in dir(t)) 23 24 def test_lazy_property(self): 25 26 class A(object): 27 28 def _get_do(self): 29 raise NotImplementedError 30 def _set_do(self, value): 31 raise NotImplementedError 32 do = lazy_property(_get_do, _set_do) 33 34 class B(A): 35 def _get_do(self): 36 return "DO IT" 37 38 self.assertRaises(NotImplementedError, lambda: A().do) 39 self.assertEqual(B().do, 'DO IT')