Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #9200 -- Added new form wizard to formtools based on class base…

…d views. Many thanks to Stephan Jäkel, ddurham and ElliottM for their work.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16307 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 632dfa233839f8c76bd6b065b8420fe0ab96175a 1 parent 1a951fa
Jannis Leidel authored June 01, 2011

Showing 39 changed files with 2,594 additions and 344 deletions. Show diff stats Hide diff stats

  1. 126  django/contrib/formtools/tests/__init__.py
  2. 43  django/contrib/formtools/tests/forms.py
  3. 26  {tests/regressiontests/formwizard → django/contrib/formtools/tests}/templates/forms/wizard.html
  4. 9  django/contrib/formtools/tests/templates/formwizard/wizard.html
  5. 15  django/contrib/formtools/tests/urls.py
  6. 1  django/contrib/formtools/wizard/__init__.py
  7. 7  django/contrib/formtools/wizard/forms.py
  8. 23  django/contrib/formtools/{wizard.py → wizard/legacy.py}
  9. 22  django/contrib/formtools/wizard/storage/__init__.py
  10. 93  django/contrib/formtools/wizard/storage/base.py
  11. 32  django/contrib/formtools/wizard/storage/cookie.py
  12. 10  django/contrib/formtools/wizard/storage/exceptions.py
  13. 20  django/contrib/formtools/wizard/storage/session.py
  14. 17  django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
  15. 6  django/contrib/formtools/wizard/tests/__init__.py
  16. 43  django/contrib/formtools/wizard/tests/cookiestoragetests.py
  17. 182  django/contrib/formtools/wizard/tests/formtests.py
  18. 22  django/contrib/formtools/wizard/tests/loadstoragetests.py
  19. 1  django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
  20. 42  django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
  21. 355  django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
  22. 24  django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
  23. 8  django/contrib/formtools/wizard/tests/sessionstoragetests.py
  24. 76  django/contrib/formtools/wizard/tests/storagetests.py
  25. 1  django/contrib/formtools/wizard/tests/wizardtests/__init__.py
  26. 57  django/contrib/formtools/wizard/tests/wizardtests/forms.py
  27. 248  django/contrib/formtools/wizard/tests/wizardtests/tests.py
  28. 16  django/contrib/formtools/wizard/tests/wizardtests/urls.py
  29. 684  django/contrib/formtools/wizard/views.py
  30. 21  django/utils/functional.py
  31. 4  docs/internals/deprecation.txt
  32. 586  docs/ref/contrib/formtools/form-wizard.txt
  33. 16  docs/releases/1.4.txt
  34. 0  tests/regressiontests/formwizard/__init__.py
  35. 18  tests/regressiontests/formwizard/forms.py
  36. 0  tests/regressiontests/formwizard/models.py
  37. 59  tests/regressiontests/formwizard/tests.py
  38. 6  tests/regressiontests/formwizard/urls.py
  39. 19  tests/regressiontests/utils/functional.py
126  django/contrib/formtools/tests/__init__.py
... ...
@@ -1,13 +1,19 @@
1 1
 import os
  2
+import re
2 3
 import warnings
3 4
 
4  
-from django import forms, http
  5
+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
 
@@ -24,12 +30,6 @@ def done(self, request, cleaned_data):
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
 
@@ -63,7 +63,7 @@ def test_form_get(self):
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)
@@ -81,7 +81,7 @@ def test_form_preview(self):
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)
@@ -99,11 +99,11 @@ def test_form_submit(self):
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):
@@ -122,7 +122,7 @@ def test_bool_submit(self):
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):
@@ -133,11 +133,11 @@ def test_form_submit_good_hash(self):
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
 
@@ -149,12 +149,12 @@ def test_form_submit_bad_hash(self):
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
 
@@ -220,38 +220,14 @@ def test_empty_permitted(self):
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 'formwizard/wizard.html'
  230
+        return 'forms/wizard.html'
255 231
 
256 232
     def done(self, request, cleaned_data):
257 233
         return http.HttpResponse(success_string)
@@ -269,6 +245,20 @@ def __init__(self, POST=None):
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
@@ -290,21 +280,21 @@ def test_step_starts_at_zero(self):
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"})
@@ -319,7 +309,7 @@ def test_good_hash(self):
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):
@@ -330,7 +320,7 @@ def test_11726(self):
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:
@@ -362,7 +352,7 @@ def test_14498(self):
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
@@ -386,7 +376,7 @@ def test_14576(self):
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)
@@ -409,7 +399,7 @@ def test_15075(self):
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
@@ -426,3 +416,39 @@ def process_step(self, request, form, step):
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)
43  django/contrib/formtools/tests/forms.py
... ...
@@ -0,0 +1,43 @@
  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()
26  ...ssiontests/formwizard/templates/forms/wizard.html → ...ntrib/formtools/tests/templates/forms/wizard.html
... ...
@@ -1,13 +1,13 @@
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>
  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>
9  django/contrib/formtools/tests/templates/formwizard/wizard.html
... ...
@@ -1,9 +0,0 @@
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>
15  django/contrib/formtools/tests/urls.py
@@ -3,11 +3,14 @@
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
+)
1  django/contrib/formtools/wizard/__init__.py
... ...
@@ -0,0 +1 @@
  1
+from django.contrib.formtools.wizard.legacy import FormWizard
7  django/contrib/formtools/wizard/forms.py
... ...
@@ -0,0 +1,7 @@
  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)
23  django/contrib/formtools/wizard.py → django/contrib/formtools/wizard/legacy.py
@@ -3,15 +3,7 @@
3 3
 step and storing the form's state as HTML hidden fields so that no state is
4 4
 stored on the server side.
5 5
 """
6  
-
7  
-try:
8  
-    import cPickle as pickle
9  
-except ImportError:
10  
-    import pickle
11  
-
12  
-from django import forms
13  
-from django.conf import settings
14  
-from django.contrib.formtools.utils import form_hmac
  6
+from django.forms import HiddenInput
15 7
 from django.http import Http404
16 8
 from django.shortcuts import render_to_response
17 9
 from django.template.context import RequestContext
@@ -20,6 +12,7 @@
20 12
 from django.utils.decorators import method_decorator
21 13
 from django.views.decorators.csrf import csrf_protect
22 14
 
  15
+from django.contrib.formtools.utils import form_hmac
23 16
 
24 17
 class FormWizard(object):
25 18
     # The HTML (and POST data) field name for the "step" variable.
@@ -42,6 +35,12 @@ def __init__(self, form_list, initial=None):
42 35
         # A zero-based counter keeping track of which step we're in.
43 36
         self.step = 0
44 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
+
45 44
     def __repr__(self):
46 45
         return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
47 46
 
@@ -71,7 +70,7 @@ def __call__(self, request, *args, **kwargs):
71 70
         """
72 71
         if 'extra_context' in kwargs:
73 72
             self.extra_context.update(kwargs['extra_context'])
74  
-        current_step = self.determine_step(request, *args, **kwargs)
  73
+        current_step = self.get_current_or_first_step(request, *args, **kwargs)
75 74
         self.parse_params(request, *args, **kwargs)
76 75
 
77 76
         # Validate and process all the previous forms before instantiating the
@@ -132,7 +131,7 @@ def render(self, form, request, step, context=None):
132 131
         old_data = request.POST
133 132
         prev_fields = []
134 133
         if old_data:
135  
-            hidden = forms.HiddenInput()
  134
+            hidden = HiddenInput()
136 135
             # Collect all data from previous steps and render it as HTML hidden fields.
137 136
             for i in range(step):
138 137
                 old_form = self.get_form(i, old_data)
@@ -177,7 +176,7 @@ def security_hash(self, request, form):
177 176
         """
178 177
         return form_hmac(form)
179 178
 
180  
-    def determine_step(self, request, *args, **kwargs):
  179
+    def get_current_or_first_step(self, request, *args, **kwargs):
181 180
         """
182 181
         Given the request object and whatever *args and **kwargs were passed to
183 182
         __call__(), returns the current step (which is zero-based).
22  django/contrib/formtools/wizard/storage/__init__.py
... ...
@@ -0,0 +1,22 @@
  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
+
93  django/contrib/formtools/wizard/storage/base.py
... ...
@@ -0,0 +1,93 @@
  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
32  django/contrib/formtools/wizard/storage/cookie.py
... ...
@@ -0,0 +1,32 @@
10  django/contrib/formtools/wizard/storage/exceptions.py
... ...
@@ -0,0 +1,10 @@
  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
20  django/contrib/formtools/wizard/storage/session.py
... ...
@@ -0,0 +1,20 @@
  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)
17  django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
... ...
@@ -0,0 +1,17 @@
  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" %}" />
6  django/contrib/formtools/wizard/tests/__init__.py
... ...
@@ -0,0 +1,6 @@
  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 *
43  django/contrib/formtools/wizard/tests/cookiestoragetests.py
... ...
@@ -0,0 +1,43 @@
  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":{}}')
182  django/contrib/formtools/wizard/tests/formtests.py
... ...
@@ -0,0 +1,182 @@
  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
+
22  django/contrib/formtools/wizard/tests/loadstoragetests.py
... ...
@@ -0,0 +1,22 @@
  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
+
1  django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
... ...
@@ -0,0 +1 @@
  1
+from django.contrib.formtools.wizard.tests.namedwizardtests.tests import *
42  django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
... ...
@@ -0,0 +1,42 @@
  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
+
355  django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
... ...
@@ -0,0 +1,355 @@
  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
+