Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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 e708cf68a5fd44e24d6d6a03049807c9503285ae 1 parent fb7c393
jezdez authored
Showing with 2,594 additions and 344 deletions.
  1. +76 −50 django/contrib/formtools/tests/__init__.py
  2. +43 −0 django/contrib/formtools/tests/forms.py
  3. +13 −13 {tests/regressiontests/formwizard → django/contrib/formtools/tests}/templates/forms/wizard.html
  4. +0 −9 django/contrib/formtools/tests/templates/formwizard/wizard.html
  5. +9 −6 django/contrib/formtools/tests/urls.py
  6. +1 −0  django/contrib/formtools/wizard/__init__.py
  7. +7 −0 django/contrib/formtools/wizard/forms.py
  8. +11 −12 django/contrib/formtools/{wizard.py → wizard/legacy.py}
  9. +22 −0 django/contrib/formtools/wizard/storage/__init__.py
  10. +93 −0 django/contrib/formtools/wizard/storage/base.py
  11. +32 −0 django/contrib/formtools/wizard/storage/cookie.py
  12. +10 −0 django/contrib/formtools/wizard/storage/exceptions.py
  13. +20 −0 django/contrib/formtools/wizard/storage/session.py
  14. +17 −0 django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
  15. +6 −0 django/contrib/formtools/wizard/tests/__init__.py
  16. +43 −0 django/contrib/formtools/wizard/tests/cookiestoragetests.py
  17. +182 −0 django/contrib/formtools/wizard/tests/formtests.py
  18. +22 −0 django/contrib/formtools/wizard/tests/loadstoragetests.py
  19. +1 −0  django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
  20. +42 −0 django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
  21. +355 −0 django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
  22. +24 −0 django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
  23. +8 −0 django/contrib/formtools/wizard/tests/sessionstoragetests.py
  24. +76 −0 django/contrib/formtools/wizard/tests/storagetests.py
  25. +1 −0  django/contrib/formtools/wizard/tests/wizardtests/__init__.py
  26. +57 −0 django/contrib/formtools/wizard/tests/wizardtests/forms.py
  27. +248 −0 django/contrib/formtools/wizard/tests/wizardtests/tests.py
  28. +16 −0 django/contrib/formtools/wizard/tests/wizardtests/urls.py
  29. +684 −0 django/contrib/formtools/wizard/views.py
  30. +21 −0 django/utils/functional.py
  31. +4 −0 docs/internals/deprecation.txt
  32. +416 −170 docs/ref/contrib/formtools/form-wizard.txt
  33. +16 −0 docs/releases/1.4.txt
  34. 0  tests/regressiontests/formwizard/__init__.py
  35. +0 −18 tests/regressiontests/formwizard/forms.py
  36. 0  tests/regressiontests/formwizard/models.py
  37. +0 −59 tests/regressiontests/formwizard/tests.py
  38. +0 −6 tests/regressiontests/formwizard/urls.py
  39. +18 −1 tests/regressiontests/utils/functional.py
View
126 django/contrib/formtools/tests/__init__.py
@@ -1,13 +1,19 @@
import os
+import re
import warnings
-from django import forms, http
+from django import http
from django.conf import settings
from django.contrib.formtools import preview, wizard, utils
from django.test import TestCase
from django.test.utils import get_warnings_state, restore_warnings_state
from django.utils import unittest
+from django.contrib.formtools.wizard.tests import *
+from django.contrib.formtools.tests.forms import *
+
+warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
+ module='django.contrib.formtools.wizard')
success_string = "Done was called!"
@@ -24,12 +30,6 @@ def done(self, request, cleaned_data):
return http.HttpResponse(success_string)
-class TestForm(forms.Form):
- field1 = forms.CharField()
- field1_ = forms.CharField()
- bool1 = forms.BooleanField(required=False)
-
-
class PreviewTests(TestCase):
urls = 'django.contrib.formtools.tests.urls'
@@ -63,7 +63,7 @@ def test_form_get(self):
is created to manage the stage.
"""
- response = self.client.get('/test1/')
+ response = self.client.get('/preview/')
stage = self.input % 1
self.assertContains(response, stage, 1)
self.assertEqual(response.context['custom_context'], True)
@@ -81,7 +81,7 @@ def test_form_preview(self):
# Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form.
self.test_data.update({'stage': 1})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
# Check to confirm stage is set to 2 in output form.
stage = self.input % 2
self.assertContains(response, stage, 1)
@@ -99,11 +99,11 @@ def test_form_submit(self):
# Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form.
self.test_data.update({'stage':2})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertNotEqual(response.content, success_string)
hash = self.preview.security_hash(None, TestForm(self.test_data))
self.test_data.update({'hash': hash})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertEqual(response.content, success_string)
def test_bool_submit(self):
@@ -122,7 +122,7 @@ def test_bool_submit(self):
self.test_data.update({'stage':2})
hash = self.preview.security_hash(None, TestForm(self.test_data))
self.test_data.update({'hash':hash, 'bool1':u'False'})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertEqual(response.content, success_string)
def test_form_submit_good_hash(self):
@@ -133,11 +133,11 @@ def test_form_submit_good_hash(self):
# Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form.
self.test_data.update({'stage':2})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertNotEqual(response.content, success_string)
hash = utils.form_hmac(TestForm(self.test_data))
self.test_data.update({'hash': hash})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertEqual(response.content, success_string)
@@ -149,12 +149,12 @@ def test_form_submit_bad_hash(self):
# Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form.
self.test_data.update({'stage':2})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/preview/', self.test_data)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.content, success_string)
hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
self.test_data.update({'hash': hash})
- response = self.client.post('/test1/', self.test_data)
+ response = self.client.post('/previewpreview/', self.test_data)
self.assertNotEqual(response.content, success_string)
@@ -220,38 +220,14 @@ def test_empty_permitted(self):
self.assertEqual(hash1, hash2)
-class HashTestForm(forms.Form):
- name = forms.CharField()
- bio = forms.CharField()
-
-
-class HashTestBlankForm(forms.Form):
- name = forms.CharField(required=False)
- bio = forms.CharField(required=False)
-
#
# FormWizard tests
#
-
-class WizardPageOneForm(forms.Form):
- field = forms.CharField()
-
-
-class WizardPageTwoForm(forms.Form):
- field = forms.CharField()
-
-class WizardPageTwoAlternativeForm(forms.Form):
- field = forms.CharField()
-
-class WizardPageThreeForm(forms.Form):
- field = forms.CharField()
-
-
-class WizardClass(wizard.FormWizard):
+class TestWizardClass(wizard.FormWizard):
def get_template(self, step):
- return 'formwizard/wizard.html'
+ return 'forms/wizard.html'
def done(self, request, cleaned_data):
return http.HttpResponse(success_string)
@@ -269,6 +245,20 @@ def __init__(self, POST=None):
class WizardTests(TestCase):
urls = 'django.contrib.formtools.tests.urls'
+ input_re = re.compile('name="([^"]+)" value="([^"]+)"')
+ wizard_step_data = (
+ {
+ '0-name': 'Pony',
+ '0-thirsty': '2',
+ },
+ {
+ '1-address1': '123 Main St',
+ '1-address2': 'Djangoland',
+ },
+ {
+ '2-random_crap': 'blah blah',
+ }
+ )
def setUp(self):
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
@@ -290,21 +280,21 @@ def test_step_starts_at_zero(self):
"""
step should be zero for the first form
"""
- response = self.client.get('/wizard/')
+ response = self.client.get('/wizard1/')
self.assertEqual(0, response.context['step0'])
def test_step_increments(self):
"""
step should be incremented when we go to the next page
"""
- response = self.client.post('/wizard/', {"0-field":"test", "wizard_step":"0"})
+ response = self.client.post('/wizard1/', {"0-field":"test", "wizard_step":"0"})
self.assertEqual(1, response.context['step0'])
def test_bad_hash(self):
"""
Form should not advance if the hash is missing or bad
"""
- response = self.client.post('/wizard/',
+ response = self.client.post('/wizard1/',
{"0-field":"test",
"1-field":"test2",
"wizard_step": "1"})
@@ -319,7 +309,7 @@ def test_good_hash(self):
"1-field": "test2",
"hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c",
"wizard_step": "1"}
- response = self.client.post('/wizard/', data)
+ response = self.client.post('/wizard1/', data)
self.assertEqual(2, response.context['step0'])
def test_11726(self):
@@ -330,7 +320,7 @@ def test_11726(self):
reached = [False]
that = self
- class WizardWithProcessStep(WizardClass):
+ class WizardWithProcessStep(TestWizardClass):
def process_step(self, request, form, step):
if step == 0:
if self.num_steps() < 2:
@@ -362,7 +352,7 @@ def test_14498(self):
reached = [False]
that = self
- class WizardWithProcessStep(WizardClass):
+ class WizardWithProcessStep(TestWizardClass):
def process_step(self, request, form, step):
that.assertTrue(hasattr(form, 'cleaned_data'))
reached[0] = True
@@ -386,7 +376,7 @@ def test_14576(self):
reached = [False]
that = self
- class Wizard(WizardClass):
+ class Wizard(TestWizardClass):
def done(self, request, form_list):
reached[0] = True
that.assertTrue(len(form_list) == 2)
@@ -409,7 +399,7 @@ def test_15075(self):
reached = [False]
that = self
- class WizardWithProcessStep(WizardClass):
+ class WizardWithProcessStep(TestWizardClass):
def process_step(self, request, form, step):
if step == 0:
self.form_list[1] = WizardPageTwoAlternativeForm
@@ -426,3 +416,39 @@ def process_step(self, request, form, step):
"wizard_step": "1"}
wizard(DummyRequest(POST=data))
self.assertTrue(reached[0])
+
+ def grab_field_data(self, response):
+ """
+ Pull the appropriate field data from the context to pass to the next wizard step
+ """
+ previous_fields = response.context['previous_fields']
+ fields = {'wizard_step': response.context['step0']}
+
+ def grab(m):
+ fields[m.group(1)] = m.group(2)
+ return ''
+
+ self.input_re.sub(grab, previous_fields)
+ return fields
+
+ def check_wizard_step(self, response, step_no):
+ """
+ Helper function to test each step of the wizard
+ - Make sure the call succeeded
+ - Make sure response is the proper step number
+ - return the result from the post for the next step
+ """
+ step_count = len(self.wizard_step_data)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Step %d of %d' % (step_no, step_count))
+
+ data = self.grab_field_data(response)
+ data.update(self.wizard_step_data[step_no - 1])
+
+ return self.client.post('/wizard2/', data)
+
+ def test_9473(self):
+ response = self.client.get('/wizard2/')
+ for step_no in range(1, len(self.wizard_step_data) + 1):
+ response = self.check_wizard_step(response, step_no)
View
43 django/contrib/formtools/tests/forms.py
@@ -0,0 +1,43 @@
+from django import forms
+from django.contrib.formtools.wizard import FormWizard
+from django.http import HttpResponse
+
+class Page1(forms.Form):
+ name = forms.CharField(max_length=100)
+ thirsty = forms.NullBooleanField()
+
+class Page2(forms.Form):
+ address1 = forms.CharField(max_length=100)
+ address2 = forms.CharField(max_length=100)
+
+class Page3(forms.Form):
+ random_crap = forms.CharField(max_length=100)
+
+class ContactWizard(FormWizard):
+ def done(self, request, form_list):
+ return HttpResponse("")
+
+class TestForm(forms.Form):
+ field1 = forms.CharField()
+ field1_ = forms.CharField()
+ bool1 = forms.BooleanField(required=False)
+
+class HashTestForm(forms.Form):
+ name = forms.CharField()
+ bio = forms.CharField()
+
+class HashTestBlankForm(forms.Form):
+ name = forms.CharField(required=False)
+ bio = forms.CharField(required=False)
+
+class WizardPageOneForm(forms.Form):
+ field = forms.CharField()
+
+class WizardPageTwoForm(forms.Form):
+ field = forms.CharField()
+
+class WizardPageTwoAlternativeForm(forms.Form):
+ field = forms.CharField()
+
+class WizardPageThreeForm(forms.Form):
+ field = forms.CharField()
View
26 ...ssiontests/formwizard/templates/forms/wizard.html → ...ntrib/formtools/tests/templates/forms/wizard.html
@@ -1,13 +1,13 @@
-<html>
- <body>
- <p>Step {{ step }} of {{ step_count }}</p>
- <form action="." method="post">
- <table>
- {{ form }}
- </table>
- <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
- {{ previous_fields|safe }}
- <input type="submit">
- </form>
- </body>
-</html>
+<html>
+ <body>
+ <p>Step {{ step }} of {{ step_count }}</p>
+ <form action="." method="post">
+ <table>
+ {{ form }}
+ </table>
+ <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
+ {{ previous_fields|safe }}
+ <input type="submit">
+ </form>
+ </body>
+</html>
View
9 django/contrib/formtools/tests/templates/formwizard/wizard.html
@@ -1,9 +0,0 @@
-<p>Step {{ step }} of {{ step_count }}</p>
-<form action="." method="post">{% csrf_token %}
-<table>
-{{ form }}
-</table>
-<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
-{{ previous_fields|safe }}
-<input type="submit">
-</form>
View
15 django/contrib/formtools/tests/urls.py
@@ -3,11 +3,14 @@
"""
from django.conf.urls.defaults import *
-from django.contrib.formtools.tests import *
+from django.contrib.formtools.tests import TestFormPreview, TestWizardClass
+
+from forms import (ContactWizard, Page1, Page2, Page3, TestForm,
+ WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm)
urlpatterns = patterns('',
- (r'^test1/', TestFormPreview(TestForm)),
- (r'^wizard/$', WizardClass([WizardPageOneForm,
- WizardPageTwoForm,
- WizardPageThreeForm])),
- )
+ url(r'^preview/', TestFormPreview(TestForm)),
+ url(r'^wizard1/$', TestWizardClass(
+ [WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm])),
+ url(r'^wizard2/$', ContactWizard([Page1, Page2, Page3])),
+)
View
1  django/contrib/formtools/wizard/__init__.py
@@ -0,0 +1 @@
+from django.contrib.formtools.wizard.legacy import FormWizard
View
7 django/contrib/formtools/wizard/forms.py
@@ -0,0 +1,7 @@
+from django import forms
+
+class ManagementForm(forms.Form):
+ """
+ ``ManagementForm`` is used to keep track of the current wizard step.
+ """
+ current_step = forms.CharField(widget=forms.HiddenInput)
View
23 django/contrib/formtools/wizard.py → django/contrib/formtools/wizard/legacy.py
@@ -3,15 +3,7 @@
step and storing the form's state as HTML hidden fields so that no state is
stored on the server side.
"""
-
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
-
-from django import forms
-from django.conf import settings
-from django.contrib.formtools.utils import form_hmac
+from django.forms import HiddenInput
from django.http import Http404
from django.shortcuts import render_to_response
from django.template.context import RequestContext
@@ -20,6 +12,7 @@
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
+from django.contrib.formtools.utils import form_hmac
class FormWizard(object):
# The HTML (and POST data) field name for the "step" variable.
@@ -42,6 +35,12 @@ def __init__(self, form_list, initial=None):
# A zero-based counter keeping track of which step we're in.
self.step = 0
+ import warnings
+ warnings.warn(
+ 'Old-style form wizards have been deprecated; use the class-based '
+ 'views in django.contrib.formtools.wizard.views instead.',
+ PendingDeprecationWarning)
+
def __repr__(self):
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
@@ -71,7 +70,7 @@ def __call__(self, request, *args, **kwargs):
"""
if 'extra_context' in kwargs:
self.extra_context.update(kwargs['extra_context'])
- current_step = self.determine_step(request, *args, **kwargs)
+ current_step = self.get_current_or_first_step(request, *args, **kwargs)
self.parse_params(request, *args, **kwargs)
# Validate and process all the previous forms before instantiating the
@@ -132,7 +131,7 @@ def render(self, form, request, step, context=None):
old_data = request.POST
prev_fields = []
if old_data:
- hidden = forms.HiddenInput()
+ hidden = HiddenInput()
# Collect all data from previous steps and render it as HTML hidden fields.
for i in range(step):
old_form = self.get_form(i, old_data)
@@ -177,7 +176,7 @@ def security_hash(self, request, form):
"""
return form_hmac(form)
- def determine_step(self, request, *args, **kwargs):
+ def get_current_or_first_step(self, request, *args, **kwargs):
"""
Given the request object and whatever *args and **kwargs were passed to
__call__(), returns the current step (which is zero-based).
View
22 django/contrib/formtools/wizard/storage/__init__.py
@@ -0,0 +1,22 @@
+from django.utils.importlib import import_module
+
+from django.contrib.formtools.wizard.storage.base import BaseStorage
+from django.contrib.formtools.wizard.storage.exceptions import (
+ MissingStorageModule, MissingStorageClass, NoFileStorageConfigured)
+
+
+def get_storage(path, *args, **kwargs):
+ i = path.rfind('.')
+ module, attr = path[:i], path[i+1:]
+ try:
+ mod = import_module(module)
+ except ImportError, e:
+ raise MissingStorageModule(
+ 'Error loading storage %s: "%s"' % (module, e))
+ try:
+ storage_class = getattr(mod, attr)
+ except AttributeError:
+ raise MissingStorageClass(
+ 'Module "%s" does not define a storage named "%s"' % (module, attr))
+ return storage_class(*args, **kwargs)
+
View
93 django/contrib/formtools/wizard/storage/base.py
@@ -0,0 +1,93 @@
+from django.core.files.uploadedfile import UploadedFile
+from django.utils.functional import lazy_property
+from django.utils.encoding import smart_str
+
+from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured
+
+class BaseStorage(object):
+ step_key = 'step'
+ step_data_key = 'step_data'
+ step_files_key = 'step_files'
+ extra_data_key = 'extra_data'
+
+ def __init__(self, prefix, request=None, file_storage=None):
+ self.prefix = 'wizard_%s' % prefix
+ self.request = request
+ self.file_storage = file_storage
+
+ def init_data(self):
+ self.data = {
+ self.step_key: None,
+ self.step_data_key: {},
+ self.step_files_key: {},
+ self.extra_data_key: {},
+ }
+
+ def reset(self):
+ self.init_data()
+
+ def _get_current_step(self):
+ return self.data[self.step_key]
+
+ def _set_current_step(self, step):
+ self.data[self.step_key] = step
+
+ current_step = lazy_property(_get_current_step, _set_current_step)
+
+ def _get_extra_data(self):
+ return self.data[self.extra_data_key] or {}
+
+ def _set_extra_data(self, extra_data):
+ self.data[self.extra_data_key] = extra_data
+
+ extra_data = lazy_property(_get_extra_data, _set_extra_data)
+
+ def get_step_data(self, step):
+ return self.data[self.step_data_key].get(step, None)
+
+ def set_step_data(self, step, cleaned_data):
+ self.data[self.step_data_key][step] = cleaned_data
+
+ @property
+ def current_step_data(self):
+ return self.get_step_data(self.current_step)
+
+ def get_step_files(self, step):
+ wizard_files = self.data[self.step_files_key].get(step, {})
+
+ if wizard_files and not self.file_storage:
+ raise NoFileStorageConfigured
+
+ files = {}
+ for field, field_dict in wizard_files.iteritems():
+ field_dict = dict((smart_str(k), v)
+ for k, v in field_dict.iteritems())
+ tmp_name = field_dict.pop('tmp_name')
+ files[field] = UploadedFile(
+ file=self.file_storage.open(tmp_name), **field_dict)
+ return files or None
+
+ def set_step_files(self, step, files):
+ if files and not self.file_storage:
+ raise NoFileStorageConfigured
+
+ if step not in self.data[self.step_files_key]:
+ self.data[self.step_files_key][step] = {}
+
+ for field, field_file in (files or {}).iteritems():
+ tmp_filename = self.file_storage.save(field_file.name, field_file)
+ file_dict = {
+ 'tmp_name': tmp_filename,
+ 'name': field_file.name,
+ 'content_type': field_file.content_type,
+ 'size': field_file.size,
+ 'charset': field_file.charset
+ }
+ self.data[self.step_files_key][step][field] = file_dict
+
+ @property
+ def current_step_files(self):
+ return self.get_step_files(self.current_step)
+
+ def update_response(self, response):
+ pass
View
32 django/contrib/formtools/wizard/storage/cookie.py
@@ -0,0 +1,32 @@
+from django.core.exceptions import SuspiciousOperation
+from django.core.signing import BadSignature
+from django.utils import simplejson as json
+
+from django.contrib.formtools.wizard import storage
+
+
+class CookieStorage(storage.BaseStorage):
+ encoder = json.JSONEncoder(separators=(',', ':'))
+
+ def __init__(self, *args, **kwargs):
+ super(CookieStorage, self).__init__(*args, **kwargs)
+ self.data = self.load_data()
+ if self.data is None:
+ self.init_data()
+
+ def load_data(self):
+ try:
+ data = self.request.get_signed_cookie(self.prefix)
+ except KeyError:
+ data = None
+ except BadSignature:
+ raise SuspiciousOperation('FormWizard cookie manipulated')
+ if data is None:
+ return None
+ return json.loads(data, cls=json.JSONDecoder)
+
+ def update_response(self, response):
+ if self.data:
+ response.set_signed_cookie(self.prefix, self.encoder.encode(self.data))
+ else:
+ response.delete_cookie(self.prefix)
View
10 django/contrib/formtools/wizard/storage/exceptions.py
@@ -0,0 +1,10 @@
+from django.core.exceptions import ImproperlyConfigured
+
+class MissingStorageModule(ImproperlyConfigured):
+ pass
+
+class MissingStorageClass(ImproperlyConfigured):
+ pass
+
+class NoFileStorageConfigured(ImproperlyConfigured):
+ pass
View
20 django/contrib/formtools/wizard/storage/session.py
@@ -0,0 +1,20 @@
+from django.core.files.uploadedfile import UploadedFile
+from django.contrib.formtools.wizard import storage
+
+
+class SessionStorage(storage.BaseStorage):
+
+ def __init__(self, *args, **kwargs):
+ super(SessionStorage, self).__init__(*args, **kwargs)
+ if self.prefix not in self.request.session:
+ self.init_data()
+
+ def _get_data(self):
+ self.request.session.modified = True
+ return self.request.session[self.prefix]
+
+ def _set_data(self, value):
+ self.request.session[self.prefix] = value
+ self.request.session.modified = True
+
+ data = property(_get_data, _set_data)
View
17 django/contrib/formtools/wizard/templates/formtools/wizard/wizard_form.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+{% csrf_token %}
+{{ wizard.management_form }}
+{% if wizard.form.forms %}
+ {{ wizard.form.management_form }}
+ {% for form in wizard.form.forms %}
+ {{ form.as_p }}
+ {% endfor %}
+{% else %}
+ {{ wizard.form.as_p }}
+{% endif %}
+
+{% if wizard.steps.prev %}
+<button name="wizard_prev_step" value="{{ wizard.steps.first }}">{% trans "first step" %}</button>
+<button name="wizard_prev_step" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
+{% endif %}
+<input type="submit" name="submit" value="{% trans "submit" %}" />
View
6 django/contrib/formtools/wizard/tests/__init__.py
@@ -0,0 +1,6 @@
+from django.contrib.formtools.wizard.tests.formtests import *
+from django.contrib.formtools.wizard.tests.sessionstoragetests import *
+from django.contrib.formtools.wizard.tests.cookiestoragetests import *
+from django.contrib.formtools.wizard.tests.loadstoragetests import *
+from django.contrib.formtools.wizard.tests.wizardtests import *
+from django.contrib.formtools.wizard.tests.namedwizardtests import *
View
43 django/contrib/formtools/wizard/tests/cookiestoragetests.py
@@ -0,0 +1,43 @@
+from django.test import TestCase
+from django.core import signing
+from django.core.exceptions import SuspiciousOperation
+from django.http import HttpResponse
+
+from django.contrib.formtools.wizard.storage.cookie import CookieStorage
+from django.contrib.formtools.wizard.tests.storagetests import get_request, TestStorage
+
+class TestCookieStorage(TestStorage, TestCase):
+ def get_storage(self):
+ return CookieStorage
+
+ def test_manipulated_cookie(self):
+ request = get_request()
+ storage = self.get_storage()('wizard1', request, None)
+
+ cookie_signer = signing.get_cookie_signer(storage.prefix)
+
+ storage.request.COOKIES[storage.prefix] = cookie_signer.sign(
+ storage.encoder.encode({'key1': 'value1'}))
+
+ self.assertEqual(storage.load_data(), {'key1': 'value1'})
+
+ storage.request.COOKIES[storage.prefix] = 'i_am_manipulated'
+ self.assertRaises(SuspiciousOperation, storage.load_data)
+
+ def test_reset_cookie(self):
+ request = get_request()
+ storage = self.get_storage()('wizard1', request, None)
+
+ storage.data = {'key1': 'value1'}
+
+ response = HttpResponse()
+ storage.update_response(response)
+
+ cookie_signer = signing.get_cookie_signer(storage.prefix)
+ signed_cookie_data = cookie_signer.sign(storage.encoder.encode(storage.data))
+ self.assertEqual(response.cookies[storage.prefix].value, signed_cookie_data)
+
+ storage.init_data()
+ storage.update_response(response)
+ unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value)
+ self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}')
View
182 django/contrib/formtools/wizard/tests/formtests.py
@@ -0,0 +1,182 @@
+from django import forms, http
+from django.conf import settings
+from django.test import TestCase
+from django.template.response import TemplateResponse
+from django.utils.importlib import import_module
+
+from django.contrib.auth.models import User
+
+from django.contrib.formtools.wizard.views import (WizardView,
+ SessionWizardView,
+ CookieWizardView)
+
+
+class DummyRequest(http.HttpRequest):
+ def __init__(self, POST=None):
+ super(DummyRequest, self).__init__()
+ self.method = POST and "POST" or "GET"
+ if POST is not None:
+ self.POST.update(POST)
+ self.session = {}
+ self._dont_enforce_csrf_checks = True
+
+def get_request(*args, **kwargs):
+ request = DummyRequest(*args, **kwargs)
+ engine = import_module(settings.SESSION_ENGINE)
+ request.session = engine.SessionStore(None)
+ return request
+
+class Step1(forms.Form):
+ name = forms.CharField()
+
+class Step2(forms.Form):
+ name = forms.CharField()
+
+class Step3(forms.Form):
+ data = forms.CharField()
+
+class UserForm(forms.ModelForm):
+ class Meta:
+ model = User
+
+UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2)
+
+class TestWizard(WizardView):
+ storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
+
+ def dispatch(self, request, *args, **kwargs):
+ response = super(TestWizard, self).dispatch(request, *args, **kwargs)
+ return response, self
+
+class FormTests(TestCase):
+ def test_form_init(self):
+ testform = TestWizard.get_initkwargs([Step1, Step2])
+ self.assertEquals(testform['form_list'], {u'0': Step1, u'1': Step2})
+
+ testform = TestWizard.get_initkwargs([('start', Step1), ('step2', Step2)])
+ self.assertEquals(
+ testform['form_list'], {u'start': Step1, u'step2': Step2})
+
+ testform = TestWizard.get_initkwargs([Step1, Step2, ('finish', Step3)])
+ self.assertEquals(
+ testform['form_list'], {u'0': Step1, u'1': Step2, u'finish': Step3})
+
+ def test_first_step(self):
+ request = get_request()
+
+ testform = TestWizard.as_view([Step1, Step2])
+ response, instance = testform(request)
+ self.assertEquals(instance.steps.current, u'0')
+
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ response, instance = testform(request)
+
+ self.assertEquals(instance.steps.current, 'start')
+
+ def test_persistence(self):
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ request = get_request({'test_wizard-current_step': 'start',
+ 'name': 'data1'})
+ response, instance = testform(request)
+ self.assertEquals(instance.steps.current, 'start')
+
+ instance.storage.current_step = 'step2'
+
+ testform2 = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ request.POST = {'test_wizard-current_step': 'step2'}
+ response, instance = testform2(request)
+ self.assertEquals(instance.steps.current, 'step2')
+
+ def test_form_condition(self):
+ request = get_request()
+
+ testform = TestWizard.as_view(
+ [('start', Step1), ('step2', Step2), ('step3', Step3)],
+ condition_dict={'step2': True})
+ response, instance = testform(request)
+ self.assertEquals(instance.get_next_step(), 'step2')
+
+ testform = TestWizard.as_view(
+ [('start', Step1), ('step2', Step2), ('step3', Step3)],
+ condition_dict={'step2': False})
+ response, instance = testform(request)
+ self.assertEquals(instance.get_next_step(), 'step3')
+
+ def test_form_prefix(self):
+ request = get_request()
+
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ response, instance = testform(request)
+
+ self.assertEqual(instance.get_form_prefix(), 'start')
+ self.assertEqual(instance.get_form_prefix('another'), 'another')
+
+ def test_form_initial(self):
+ request = get_request()
+
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)],
+ initial_dict={'start': {'name': 'value1'}})
+ response, instance = testform(request)
+
+ self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'})
+ self.assertEqual(instance.get_form_initial('step2'), {})
+
+ def test_form_instance(self):
+ request = get_request()
+ the_instance = User()
+ testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)],
+ instance_dict={'start': the_instance})
+ response, instance = testform(request)
+
+ self.assertEqual(
+ instance.get_form_instance('start'),
+ the_instance)
+ self.assertEqual(
+ instance.get_form_instance('non_exist_instance'),
+ None)
+
+ def test_formset_instance(self):
+ request = get_request()
+ the_instance1, created = User.objects.get_or_create(
+ username='testuser1')
+ the_instance2, created = User.objects.get_or_create(
+ username='testuser2')
+ testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)],
+ instance_dict={'start': User.objects.filter(username='testuser1')})
+ response, instance = testform(request)
+
+ self.assertEqual(list(instance.get_form_instance('start')), [the_instance1])
+ self.assertEqual(instance.get_form_instance('non_exist_instance'), None)
+
+ self.assertEqual(instance.get_form().initial_form_count(), 1)
+
+ def test_done(self):
+ request = get_request()
+
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ response, instance = testform(request)
+
+ self.assertRaises(NotImplementedError, instance.done, None)
+
+ def test_revalidation(self):
+ request = get_request()
+
+ testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
+ response, instance = testform(request)
+ instance.render_done(None)
+ self.assertEqual(instance.storage.current_step, 'start')
+
+
+class SessionFormTests(TestCase):
+ def test_init(self):
+ request = get_request()
+ testform = SessionWizardView.as_view([('start', Step1)])
+ self.assertTrue(isinstance(testform(request), TemplateResponse))
+
+
+class CookieFormTests(TestCase):
+ def test_init(self):
+ request = get_request()
+ testform = CookieWizardView.as_view([('start', Step1)])
+ self.assertTrue(isinstance(testform(request), TemplateResponse))
+
View
22 django/contrib/formtools/wizard/tests/loadstoragetests.py
@@ -0,0 +1,22 @@
+from django.test import TestCase
+
+from django.contrib.formtools.wizard.storage import (get_storage,
+ MissingStorageModule,
+ MissingStorageClass)
+from django.contrib.formtools.wizard.storage.base import BaseStorage
+
+
+class TestLoadStorage(TestCase):
+ def test_load_storage(self):
+ self.assertEqual(
+ type(get_storage('django.contrib.formtools.wizard.storage.base.BaseStorage', 'wizard1')),
+ BaseStorage)
+
+ def test_missing_module(self):
+ self.assertRaises(MissingStorageModule, get_storage,
+ 'django.contrib.formtools.wizard.storage.idontexist.IDontExistStorage', 'wizard1')
+
+ def test_missing_class(self):
+ self.assertRaises(MissingStorageClass, get_storage,
+ 'django.contrib.formtools.wizard.storage.base.IDontExistStorage', 'wizard1')
+
View
1  django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
@@ -0,0 +1 @@
+from django.contrib.formtools.wizard.tests.namedwizardtests.tests import *
View
42 django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
@@ -0,0 +1,42 @@
+from django import forms
+from django.forms.formsets import formset_factory
+from django.http import HttpResponse
+from django.template import Template, Context
+
+from django.contrib.auth.models import User
+
+from django.contrib.formtools.wizard.views import NamedUrlWizardView
+
+class Page1(forms.Form):
+ name = forms.CharField(max_length=100)
+ user = forms.ModelChoiceField(queryset=User.objects.all())
+ thirsty = forms.NullBooleanField()
+
+class Page2(forms.Form):
+ address1 = forms.CharField(max_length=100)
+ address2 = forms.CharField(max_length=100)
+
+class Page3(forms.Form):
+ random_crap = forms.CharField(max_length=100)
+
+Page4 = formset_factory(Page3, extra=2)
+
+class ContactWizard(NamedUrlWizardView):
+ def done(self, form_list, **kwargs):
+ c = Context({
+ 'form_list': [x.cleaned_data for x in form_list],
+ 'all_cleaned_data': self.get_all_cleaned_data()
+ })
+
+ for form in self.form_list.keys():
+ c[form] = self.get_cleaned_data_for_step(form)
+
+ c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
+ return HttpResponse(Template('').render(c))
+
+class SessionContactWizard(ContactWizard):
+ storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
+
+class CookieContactWizard(ContactWizard):
+ storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
+
View
355 django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
@@ -0,0 +1,355 @@
+import os
+
+from django.core.urlresolvers import reverse
+from django.http import QueryDict
+from django.test import TestCase
+from django.conf import settings
+
+from django.contrib.auth.models import User
+
+from django.contrib.formtools import wizard
+
+from django.contrib.formtools.wizard.views import (NamedUrlSessionWizardView,
+ NamedUrlCookieWizardView)
+from django.contrib.formtools.wizard.tests.formtests import (get_request,
+ Step1,
+ Step2)
+
+class NamedWizardTests(object):
+ urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
+
+ def setUp(self):
+ self.testuser, created = User.objects.get_or_create(username='testuser1')
+ self.wizard_step_data[0]['form1-user'] = self.testuser.pk
+
+ wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
+ settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
+
+ def tearDown(self):
+ del settings.TEMPLATE_DIRS[-1]
+
+ def test_initial_call(self):
+ response = self.client.get(reverse('%s_start' % self.wizard_urlname))
+ self.assertEqual(response.status_code, 302)
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+ wizard = response.context['wizard']
+ self.assertEqual(wizard['steps'].current, 'form1')
+ self.assertEqual(wizard['steps'].step0, 0)
+ self.assertEqual(wizard['steps'].step1, 1)
+ self.assertEqual(wizard['steps'].last, 'form4')
+ self.assertEqual(wizard['steps'].prev, None)
+ self.assertEqual(wizard['steps'].next, 'form2')
+ self.assertEqual(wizard['steps'].count, 4)
+
+ def test_initial_call_with_params(self):
+ get_params = {'getvar1': 'getval1', 'getvar2': 'getval2'}
+ response = self.client.get(reverse('%s_start' % self.wizard_urlname),
+ get_params)
+ self.assertEqual(response.status_code, 302)
+
+ # Test for proper redirect GET parameters
+ location = response['Location']
+ self.assertNotEqual(location.find('?'), -1)
+ querydict = QueryDict(location[location.find('?') + 1:])
+ self.assertEqual(dict(querydict.items()), get_params)
+
+ def test_form_post_error(self):
+ response = self.client.post(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
+ self.wizard_step_1_data)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+ self.assertEqual(response.context['wizard']['form'].errors,
+ {'name': [u'This field is required.'],
+ 'user': [u'This field is required.']})
+
+ def test_form_post_success(self):
+ response = self.client.post(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ wizard = response.context['wizard']
+ self.assertEqual(wizard['steps'].current, 'form2')
+ self.assertEqual(wizard['steps'].step0, 1)
+ self.assertEqual(wizard['steps'].prev, 'form1')
+ self.assertEqual(wizard['steps'].next, 'form3')
+
+ def test_form_stepback(self):
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname, kwargs={
+ 'step': response.context['wizard']['steps'].current
+ }), {'wizard_prev_step': response.context['wizard']['steps'].prev})
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ def test_form_jump(self):
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form3'}))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form3')
+
+ def test_form_finish(self):
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[1])
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form3')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[2])
+ response = self.client.get(response['Location'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form4')
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[3])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ self.assertEqual(response.context['form_list'], [
+ {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
+ {'address1': u'123 Main St', 'address2': u'Djangoland'},
+ {'random_crap': u'blah blah'},
+ [{'random_crap': u'blah blah'}, {'random_crap': u'blah blah'}]])
+
+ def test_cleaned_data(self):
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[1])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[2])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[3])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ self.assertEqual(
+ response.context['all_cleaned_data'],
+ {'name': u'Pony', 'thirsty': True, 'user': self.testuser,
+ 'address1': u'123 Main St', 'address2': u'Djangoland',
+ 'random_crap': u'blah blah', 'formset-form4': [
+ {'random_crap': u'blah blah'},
+ {'random_crap': u'blah blah'}
+ ]})
+
+ def test_manipulated_data(self):
+ response = self.client.get(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[1])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[2])
+ loc = response['Location']
+ response = self.client.get(loc)
+ self.assertEqual(response.status_code, 200, loc)
+
+ self.client.cookies.pop('sessionid', None)
+ self.client.cookies.pop('wizard_cookie_contact_wizard', None)
+
+ response = self.client.post(
+ reverse(self.wizard_urlname,
+ kwargs={'step': response.context['wizard']['steps'].current}),
+ self.wizard_step_data[3])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ def test_form_reset(self):
+ response = self.client.post(
+ reverse(self.wizard_urlname, kwargs={'step': 'form1'}),
+ self.wizard_step_data[0])
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.get(
+ '%s?reset=1' % reverse('%s_start' % self.wizard_urlname))
+ self.assertEqual(response.status_code, 302)
+
+ response = self.client.get(response['Location'])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+class NamedSessionWizardTests(NamedWizardTests, TestCase):
+ wizard_urlname = 'nwiz_session'
+ wizard_step_1_data = {
+ 'session_contact_wizard-current_step': 'form1',
+ }
+ wizard_step_data = (
+ {
+ 'form1-name': 'Pony',
+ 'form1-thirsty': '2',
+ 'session_contact_wizard-current_step': 'form1',
+ },
+ {
+ 'form2-address1': '123 Main St',
+ 'form2-address2': 'Djangoland',
+ 'session_contact_wizard-current_step': 'form2',
+ },
+ {
+ 'form3-random_crap': 'blah blah',
+ 'session_contact_wizard-current_step': 'form3',
+ },
+ {
+ 'form4-INITIAL_FORMS': '0',
+ 'form4-TOTAL_FORMS': '2',
+ 'form4-MAX_NUM_FORMS': '0',
+ 'form4-0-random_crap': 'blah blah',
+ 'form4-1-random_crap': 'blah blah',
+ 'session_contact_wizard-current_step': 'form4',
+ }
+ )
+
+class NamedCookieWizardTests(NamedWizardTests, TestCase):
+ wizard_urlname = 'nwiz_cookie'
+ wizard_step_1_data = {
+ 'cookie_contact_wizard-current_step': 'form1',
+ }
+ wizard_step_data = (
+ {
+ 'form1-name': 'Pony',
+ 'form1-thirsty': '2',
+ 'cookie_contact_wizard-current_step': 'form1',
+ },
+ {
+ 'form2-address1': '123 Main St',
+ 'form2-address2': 'Djangoland',
+ 'cookie_contact_wizard-current_step': 'form2',
+ },
+ {
+ 'form3-random_crap': 'blah blah',
+ 'cookie_contact_wizard-current_step': 'form3',
+ },
+ {
+ 'form4-INITIAL_FORMS': '0',
+ 'form4-TOTAL_FORMS': '2',
+ 'form4-MAX_NUM_FORMS': '0',
+ 'form4-0-random_crap': 'blah blah',
+ 'form4-1-random_crap': 'blah blah',
+ 'cookie_contact_wizard-current_step': 'form4',
+ }
+ )
+
+
+class NamedFormTests(object):
+ urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
+
+ def test_revalidation(self):
+ request = get_request()
+
+ testform = self.formwizard_class.as_view(
+ [('start', Step1), ('step2', Step2)],
+ url_name=self.wizard_urlname)
+ response, instance = testform(request, step='done')
+
+ instance.render_done(None)
+ self.assertEqual(instance.storage.current_step, 'start')
+
+class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView):
+
+ def dispatch(self, request, *args, **kwargs):
+ response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs)
+ return response, self
+
+class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView):
+
+ def dispatch(self, request, *args, **kwargs):
+ response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs)
+ return response, self
+
+
+class NamedSessionFormTests(NamedFormTests, TestCase):
+ formwizard_class = TestNamedUrlSessionFormWizard
+ wizard_urlname = 'nwiz_session'
+
+
+class NamedCookieFormTests(NamedFormTests, TestCase):
+ formwizard_class = TestNamedUrlCookieFormWizard
+ wizard_urlname = 'nwiz_cookie'
View
24 django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
@@ -0,0 +1,24 @@
+from django.conf.urls.defaults import *
+from django.contrib.formtools.wizard.tests.namedwizardtests.forms import (
+ SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
+
+def get_named_session_wizard():
+ return SessionContactWizard.as_view(
+ [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
+ url_name='nwiz_session',
+ done_step_name='nwiz_session_done'
+ )
+
+def get_named_cookie_wizard():
+ return CookieContactWizard.as_view(
+ [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
+ url_name='nwiz_cookie',
+ done_step_name='nwiz_cookie_done'
+ )
+
+urlpatterns = patterns('',
+ url(r'^nwiz_session/(?P<step>.+)/$', get_named_session_wizard(), name='nwiz_session'),
+ url(r'^nwiz_session/$', get_named_session_wizard(), name='nwiz_session_start'),
+ url(r'^nwiz_cookie/(?P<step>.+)/$', get_named_cookie_wizard(), name='nwiz_cookie'),
+ url(r'^nwiz_cookie/$', get_named_cookie_wizard(), name='nwiz_cookie_start'),
+)
View
8 django/contrib/formtools/wizard/tests/sessionstoragetests.py
@@ -0,0 +1,8 @@
+from django.test import TestCase
+
+from django.contrib.formtools.wizard.tests.storagetests import TestStorage
+from django.contrib.formtools.wizard.storage.session import SessionStorage
+
+class TestSessionStorage(TestStorage, TestCase):
+ def get_storage(self):
+ return SessionStorage
View
76 django/contrib/formtools/wizard/tests/storagetests.py
@@ -0,0 +1,76 @@
+from datetime import datetime
+
+from django.http import HttpRequest
+from django.conf import settings
+from django.utils.importlib import import_module
+
+from django.contrib.auth.models import User
+
+def get_request():
+ request = HttpRequest()
+ engine = import_module(settings.SESSION_ENGINE)
+ request.session = engine.SessionStore(None)
+ return request
+
+class TestStorage(object):
+ def setUp(self):
+ self.testuser, created = User.objects.get_or_create(username='testuser1')
+
+ def test_current_step(self):
+ request = get_request()
+ storage = self.get_storage()('wizard1', request, None)
+ my_step = 2
+
+ self.assertEqual(storage.current_step, None)
+
+ storage.current_step = my_step
+ self.assertEqual(storage.current_step, my_step)
+
+ storage.reset()
+ self.assertEqual(storage.current_step, None)
+
+ storage.current_step = my_step
+ storage2 = self.get_storage()('wizard2', request, None)
+ self.assertEqual(storage2.current_step, None)
+
+ def test_step_data(self):
+ request = get_request()
+ storage = self.get_storage()('wizard1', request, None)
+ step1 = 'start'
+ step_data1 = {'field1': 'data1',
+ 'field2': 'data2',
+ 'field3': datetime.now(),
+ 'field4': self.testuser}
+
+ self.assertEqual(storage.get_step_data(step1), None)
+
+ storage.set_step_data(step1, step_data1)
+ self.assertEqual(storage.get_step_data(step1), step_data1)
+
+ storage.reset()
+ self.assertEqual(storage.get_step_data(step1), None)
+
+ storage.set_step_data(step1, step_data1)
+ storage2 = self.get_storage()('wizard2', request, None)
+ self.assertEqual(storage2.get_step_data(step1), None)
+
+ def test_extra_context(self):
+ request = get_request()
+ storage = self.get_storage()('wizard1', request, None)
+ extra_context = {'key1': 'data1',
+ 'key2': 'data2',
+ 'key3': datetime.now(),
+ 'key4': self.testuser}
+
+ self.assertEqual(storage.extra_data, {})
+
+ storage.extra_data = extra_context
+ self.assertEqual(storage.extra_data, extra_context)
+
+ storage.reset()
+ self.assertEqual(storage.extra_data, {})
+
+ storage.extra_data = extra_context
+ storage2 = self.get_storage()('wizard2', request, None)
+ self.assertEqual(storage2.extra_data, {})
+
View
1  django/contrib/formtools/wizard/tests/wizardtests/__init__.py
@@ -0,0 +1 @@
+from django.contrib.formtools.wizard.tests.wizardtests.tests import *
View
57 django/contrib/formtools/wizard/tests/wizardtests/forms.py
@@ -0,0 +1,57 @@
+import tempfile
+
+from django import forms
+from django.core.files.storage import FileSystemStorage
+from django.forms.formsets import formset_factory
+from django.http import HttpResponse
+from django.template import Template, Context
+
+from django.contrib.auth.models import User
+
+from django.contrib.formtools.wizard.views import WizardView
+
+temp_storage_location = tempfile.mkdtemp()
+temp_storage = FileSystemStorage(location=temp_storage_location)
+
+class Page1(forms.Form):
+ name = forms.CharField(max_length=100)
+ user = forms.ModelChoiceField(queryset=User.objects.all())
+ thirsty = forms.NullBooleanField()
+
+class Page2(forms.Form):
+ address1 = forms.CharField(max_length=100)
+ address2 = forms.CharField(max_length=100)
+ file1 = forms.FileField()
+
+class Page3(forms.Form):
+ random_crap = forms.CharField(max_length=100)
+
+Page4 = formset_factory(Page3, extra=2)
+
+class ContactWizard(WizardView):
+ file_storage = temp_storage
+
+ def done(self, form_list, **kwargs):
+ c = Context({
+ 'form_list': [x.cleaned_data for x in form_list],
+ 'all_cleaned_data': self.get_all_cleaned_data()
+ })
+
+ for form in self.form_list.keys():
+ c[form] = self.get_cleaned_data_for_step(form)
+
+ c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
+ return HttpResponse(Template('').render(c))
+
+ def get_context_data(self, form, **kwargs):
+ context = super(ContactWizard, self).get_context_data(form, **kwargs)
+ if self.storage.current_step == 'form2':
+ context.update({'another_var': True})
+ return context
+
+class SessionContactWizard(ContactWizard):
+ storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
+
+class CookieContactWizard(ContactWizard):
+ storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
+
View
248 django/contrib/formtools/wizard/tests/wizardtests/tests.py
@@ -0,0 +1,248 @@
+import os
+
+from django.test import TestCase
+from django.conf import settings
+from django.contrib.auth.models import User
+
+from django.contrib.formtools import wizard
+
+class WizardTests(object):
+ urls = 'django.contrib.formtools.wizard.tests.wizardtests.urls'
+
+ def setUp(self):
+ self.testuser, created = User.objects.get_or_create(username='testuser1')
+ self.wizard_step_data[0]['form1-user'] = self.testuser.pk
+
+ wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
+ settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
+
+ def tearDown(self):
+ del settings.TEMPLATE_DIRS[-1]
+
+ def test_initial_call(self):
+ response = self.client.get(self.wizard_url)
+ wizard = response.context['wizard']
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(wizard['steps'].current, 'form1')
+ self.assertEqual(wizard['steps'].step0, 0)
+ self.assertEqual(wizard['steps'].step1, 1)
+ self.assertEqual(wizard['steps'].last, 'form4')
+ self.assertEqual(wizard['steps'].prev, None)
+ self.assertEqual(wizard['steps'].next, 'form2')
+ self.assertEqual(wizard['steps'].count, 4)
+
+ def test_form_post_error(self):
+ response = self.client.post(self.wizard_url, self.wizard_step_1_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+ self.assertEqual(response.context['wizard']['form'].errors,
+ {'name': [u'This field is required.'],
+ 'user': [u'This field is required.']})
+
+ def test_form_post_success(self):
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ wizard = response.context['wizard']
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(wizard['steps'].current, 'form2')
+ self.assertEqual(wizard['steps'].step0, 1)
+ self.assertEqual(wizard['steps'].prev, 'form1')
+ self.assertEqual(wizard['steps'].next, 'form3')
+
+ def test_form_stepback(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.post(self.wizard_url, {
+ 'wizard_prev_step': response.context['wizard']['steps'].prev})
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ def test_template_context(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+ self.assertEqual(response.context.get('another_var', None), None)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+ self.assertEqual(response.context.get('another_var', None), True)
+
+ def test_form_finish(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ post_data = self.wizard_step_data[1]
+ post_data['form2-file1'] = open(__file__)
+ response = self.client.post(self.wizard_url, post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form3')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[2])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form4')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[3])
+ self.assertEqual(response.status_code, 200)
+
+ all_data = response.context['form_list']
+ self.assertEqual(all_data[1]['file1'].read(), open(__file__).read())
+ del all_data[1]['file1']
+ self.assertEqual(all_data, [
+ {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
+ {'address1': u'123 Main St', 'address2': u'Djangoland'},
+ {'random_crap': u'blah blah'},
+ [{'random_crap': u'blah blah'},
+ {'random_crap': u'blah blah'}]])
+
+ def test_cleaned_data(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+
+ post_data = self.wizard_step_data[1]
+ post_data['form2-file1'] = open(__file__)
+ response = self.client.post(self.wizard_url, post_data)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[2])
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[3])
+ self.assertEqual(response.status_code, 200)
+
+ all_data = response.context['all_cleaned_data']
+ self.assertEqual(all_data['file1'].read(), open(__file__).read())
+ del all_data['file1']
+ self.assertEqual(all_data, {
+ 'name': u'Pony', 'thirsty': True, 'user': self.testuser,
+ 'address1': u'123 Main St', 'address2': u'Djangoland',
+ 'random_crap': u'blah blah', 'formset-form4': [
+ {'random_crap': u'blah blah'},
+ {'random_crap': u'blah blah'}]})
+
+ def test_manipulated_data(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+
+ post_data = self.wizard_step_data[1]
+ post_data['form2-file1'] = open(__file__)
+ response = self.client.post(self.wizard_url, post_data)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[2])
+ self.assertEqual(response.status_code, 200)
+ self.client.cookies.pop('sessionid', None)
+ self.client.cookies.pop('wizard_cookie_contact_wizard', None)
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[3])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ def test_form_refresh(self):
+ response = self.client.get(self.wizard_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form1')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ post_data = self.wizard_step_data[1]
+ post_data['form2-file1'] = open(__file__)
+ response = self.client.post(self.wizard_url, post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form3')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[2])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form4')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[0])
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['wizard']['steps'].current, 'form2')
+
+ response = self.client.post(self.wizard_url, self.wizard_step_data[3])
+ self.assertEqual(response.status_code, 200)
+
+
+class SessionWizardTests(WizardTests, TestCase):
+ wizard_url = '/wiz_session/'
+ wizard_step_1_data = {
+ 'session_contact_wizard-current_step': 'form1',
+ }
+ wizard_step_data = (
+ {
+ 'form1-name': 'Pony',
+ 'form1-thirsty': '2',
+ 'session_contact_wizard-current_step': 'form1',
+ },
+ {
+ 'form2-address1': '123 Main St',
+ 'form2-address2': 'Djangoland',
+ 'session_contact_wizard-current_step': 'form2',
+ },
+ {
+ 'form3-random_crap': 'blah blah',
+ 'session_contact_wizard-current_step': 'form3',
+ },
+ {
+ 'form4-INITIAL_FORMS': '0',
+ 'form4-TOTAL_FORMS': '2',
+ 'form4-MAX_NUM_FORMS': '0',
+ 'form4-0-random_crap': 'blah blah',
+ 'form4-1-random_crap': 'blah blah',
+ 'session_contact_wizard-current_step': 'form4',
+ }
+ )
+
+class CookieWizardTests(WizardTests, TestCase):
+ wizard_url = '/wiz_cookie/'
+ wizard_step_1_data = {
+ 'cookie_contact_wizard-current_step': 'form1',
+ }
+ wizard_step_data = (
+ {
+ 'form1-name': 'Pony',
+ 'form1-thirsty': '2',
+ 'cookie_contact_wizard-current_step': 'form1',
+ },
+ {
+ 'form2-address1': '123 Main St',
+ 'form2-address2': 'Djangoland',
+ 'cookie_contact_wizard-current_step': 'form2',
+ },
+ {
+ 'form3-random_crap': 'blah blah',
+ 'cookie_contact_wizard-current_step': 'form3',
+ },
+ {
+ 'form4-INITIAL_FORMS': '0',
+ 'form4-TOTAL_FORMS': '2',
+ 'form4-MAX_NUM_FORMS': '0',
+ 'form4-0-random_crap': 'blah blah',
+ 'form4-1-random_crap': 'blah blah',
+ 'cookie_contact_wizard-current_step': 'form4',
+ }
+ )
+
+
View
16 django/contrib/formtools/wizard/tests/wizardtests/urls.py
@@ -0,0 +1,16 @@
+from django.conf.urls.defaults import *
+from django.contrib.formtools.wizard.tests.wizardtests.forms import (
+ SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
+
+urlpatterns = patterns('',
+ url(r'^wiz_session/$', SessionContactWizard.as_view(
+ [('form1', Page1),
+ ('form2', Page2),
+ ('form3', Page3),
+ ('form4', Page4)])),
+ url(r'^wiz_cookie/$', CookieContactWizard.as_view(
+ [('form1', Page1),
+ ('form2', Page2),
+ ('form3', Page3),
+ ('form4', Page4)])),
+)
View
684 django/contrib/formtools/wizard/views.py
@@ -0,0 +1,684 @@
+import copy
+import re
+
+from django import forms
+from django.shortcuts import redirect
+from django.core.urlresolvers import reverse
+from django.forms import formsets, ValidationError
+from django.views.generic import TemplateView
+from django.utils.datastructures import SortedDict
+from django.utils.decorators import classonlymethod
+
+from django.contrib.formtools.wizard.storage import get_storage
+from django.contrib.formtools.wizard.storage.exceptions import NoFileStorageConfigured
+from django.contrib.formtools.wizard.forms import ManagementForm
+
+
+def normalize_name(name):
+ new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
+ return new.lower().strip('_')
+
+class StepsHelper(object):
+
+ def __init__(self, wizard):
+ self._wizard = wizard
+
+ def __dir__(self):
+ return self.all
+
+ def __len__(self):
+ return self.count
+
+ def __repr__(self):
+ return '<StepsHelper for %s (steps: %s)>' % (self._wizard, self.all)
+
+ @property
+ def all(self):
+ "Returns the names of all steps/forms."
+ return self._wizard.get_form_list().keys()
+
+ @property
+ def count(self):
+ "Returns the total number of steps/forms in this the wizard."
+ return len(self.all)
+
+ @property
+ def current(self):
+ """
+ Returns the current step. If no current step is stored in the
+ storage backend, the first step will be returned.
+ """
+ return self._wizard.storage.current_step or self.first
+
+ @property
+ def first(self):
+ "Returns the name of the first step."
+ return self.all[0]
+
+ @property
+ def last(self):
+ "Returns the name of the last step."
+ return self.all[-1]
+
+ @property
+ def next(self):
+ "Returns the next step."
+ return self._wizard.get_next_step()
+
+ @property
+ def prev(self):
+ "Returns the previous step."
+ return self._wizard.get_prev_step()
+
+ @property
+ def index(self):
+ "Returns the index for the current step."
+ return self._wizard.get_step_index()
+
+ @property
+ def step0(self):
+ return int(self.index)
+
+ @property
+ def step1(self):
+ return int(self.index) + 1
+
+
+class WizardView(TemplateView):
+ """
+ The WizardView is used to create multi-page forms and handles all the
+ storage and validation stuff. The wizard is based on Django's generic
+ class based views.
+ """
+ storage_name = None
+ form_list = None
+ initial_dict = None
+ instance_dict = None
+ condition_dict = None
+ template_name = 'formtools/wizard/wizard_form.html'
+
+ def __repr__(self):
+ return '<%s: forms: %s>' % (self.__class__.__name__, self.form_list)
+
+ @classonlymethod
+ def as_view(cls, *args, **kwargs):
+ """
+ This method is used within urls.py to create unique formwizard
+ instances for every request. We need to override this method because
+ we add some kwargs which are needed to make the formwizard usable.
+ """
+ initkwargs = cls.get_initkwargs(*args, **kwargs)
+ return super(WizardView, cls).as_view(**initkwargs)
+
+ @classmethod
+ def get_initkwargs(cls, form_list,
+ initial_dict=None, instance_dict=None, condition_dict=None):
+ """
+ Creates a dict with all needed parameters for the form wizard instances.
+
+ * `form_list` - is a list of forms. The list entries can be single form
+ classes or tuples of (`step_name`, `form_class`). If you pass a list
+ of forms, the formwizard will convert the class list to
+ (`zero_based_counter`, `form_class`). This is needed to access the
+ form for a specific step.
+ * `initial_dict` - contains a dictionary of initial data dictionaries.
+ The key should be equal to the `step_name` in the `form_list` (or
+ the str of the zero based counter - if no step_names added in the
+ `form_list`)
+ * `instance_dict` - contains a dictionary of instance objects. This list
+ is only used when `ModelForm`s are used. The key should be equal to
+ the `step_name` in the `form_list`. Same rules as for `initial_dict`
+ apply.
+ * `condition_dict` - contains a dictionary of boolean values or
+ callables. If the value of for a specific `step_name` is callable it
+ will be called with the formwizard instance as the only argument.
+ If the return value is true, the step's form will be used.
+ """
+ kwargs = {
+ 'initial_dict': initial_dict or {},
+ 'instance_dict': instance_dict or {},
+ 'condition_dict': condition_dict or {},
+ }
+ init_form_list = SortedDict()
+
+ assert len(form_list) > 0, 'at least one form is needed'
+
+ # walk through the passed form list
+ for i, form in enumerate(form_list):
+ if isinstance(form, (list, tuple)):
+ # if the element is a tuple, add the tuple to the new created
+ # sorted dictionary.
+ init_form_list[unicode(form[0])] = form[1]
+ else:
+ # if not, add the form with a zero based counter as unicode
+ init_form_list[unicode(i)] = form
+
+ # walk through the ne created list of forms
+ for form in init_form_list.itervalues():
+ if issubclass(form, formsets.BaseFormSet):
+ # if the element is based on BaseFormSet (FormSet/ModelFormSet)
+ # we need to override the form variable.
+ form = form.form
+ # check if any form contains a FileField, if yes, we need a
+ # file_storage added to the formwizard (by subclassing).
+ for field in form.base_fields.itervalues():
+ if (isinstance(field, forms.FileField) and
+ not hasattr(cls, 'file_storage')):
+ raise NoFileStorageConfigured
+
+ # build the kwargs for the formwizard instances
+ kwargs['form_list'] = init_form_list
+ return kwargs
+
+ def get_wizard_name(self):
+ return normalize_name(self.__class__.__name__)
+
+ def get_prefix(self):
+ # TODO: Add some kind of unique id to prefix
+ return self.wizard_name
+
+ def get_form_list(self):
+ """
+ This method returns a form_list based on the initial form list but
+ checks if there is a condition method/value in the condition_list.
+ If an entry exists in the condition list, it will call/read the value
+ and respect the result. (True means add the form, False means ignore
+ the form)
+
+ The form_list is always generated on the fly because condition methods
+ could use data from other (maybe previous forms).
+ """
+ form_list = SortedDict()
+ for form_key, form_class in self.form_list.iteritems():
+ # try to fetch the value from condition list, by default, the form
+ # gets passed to the new list.
+ condition = self.condition_dict.get(form_key, True)
+ if callable(condition):
+ # call the value if needed, passes the current instance.
+ condition = condition(self)
+ if condition:
+ form_list[form_key] = form_class
+ return form_list
+
+ def dispatch(self, request, *args, **kwargs):
+ """
+ This method gets called by the routing engine. The first argument is
+ `request` which contains a `HttpRequest` instance.
+ The request is stored in `self.request` for later use. The storage
+ instance is stored in `self.storage`.
+
+ After processing the request using the `dispatch` method, the
+ response gets updated by the storage engine (for example add cookies).
+ """
+ # add the storage engine to the current formwizard instance
+ self.wizard_name = self.get_wizard_name()
+ self.prefix = self.get_prefix()
+ self.storage = get_storage(self.storage_name, self.prefix, request,
+ getattr(self, 'file_storage', None))
+ self.steps = StepsHelper(self)
+ response = super(WizardView, self).dispatch(request, *args, **kwargs)
+
+ # update the response (e.g. adding cookies)
+ self.storage.update_response(response)
+ return response
+
+ def get(self, request, *args, **kwargs):
+ """
+ This method handles GET requests.
+
+ If a GET request reaches this point, the wizard assumes that the user
+ just starts at the first step or wants to restart the process.
+ The data of the wizard will be resetted before rendering the first step.
+ """
+ self.storage.reset()
+
+ # reset the current step to the first step.
+ self.storage.current_step = self.steps.first
+ return self.render(self.get_form())
+
+ def post(self, *args, **kwargs):
+ """
+ This method handles POST requests.
+
+ The wizard will render either the current step (if form validation
+ wasn't successful), the next step (if the current step was stored
+ successful) or the done view (if no more steps are available)
+ """
+ # Look for a wizard_prev_step element in the posted data which
+ # contains a valid step name. If one was found, render the requested
+ # form. (This makes stepping back a lot easier).
+ wizard_prev_step = self.request.POST.get('wizard_prev_step', None)
+ if wizard_prev_step and wizard_prev_step in self.get_form_list():
+ self.storage.current_step = wizard_prev_step
+ form = self.get_form(
+ data=self.storage.get_step_data(self.steps.current),
+ files=self.storage.get_step_files(self.steps.current))
+ return self.render(form)
+
+ # Check if form was refreshed
+ management_form = ManagementForm(self.request.POST, prefix=self.prefix)
+ if not management_form.is_valid():
+ raise ValidationError(
+ 'ManagementForm data is missing or has been tampered.')
+
+ form_current_step = management_form.cleaned_data['current_step']
+ if (form_current_step != self.steps.current and
+ self.storage.current_step is not None):
+ # form refreshed, change current step
+ self.storage.current_step = form_current_step
+
+ # get the form for the current step
+ form = self.get_form(data=self.request.POST, files=self.request.FILES)
+
+ # and try to validate
+ if form.is_valid():
+ # if the form is valid, store the cleaned data and files.
+ self.storage.set_step_data(self.steps.current, self.process_step(form))
+ self.storage.set_step_files(self.steps.current, self.process_step_files(form))
+
+ # check if the current step is the last step
+ if self.steps.current == self.steps.last:
+ # no more steps, render done view
+ return self.render_done(form, **kwargs)
+ else:
+ # proceed to the next step
+ return self.render_next_step(form)
+ return self.render(form)
+
+ def render_next_step(self, form, **kwargs):
+ """
+ THis method gets called when the next step/form should be rendered.
+ `form` contains the last/current form.
+ """
+ # get the form instance based on the data from the storage backend
+ # (if available).
+ next_step = self.steps.next
+ new_form = self.get_form(next_step,
+ data=self.storage.get_step_data(next_step),
+ files=self.storage.get_step_files(next_step))
+
+ # change the stored current step
+ self.storage.current_step = next_step
+ return self.render(new_form, **kwargs)
+
+ def render_done(self, form, **kwargs):
+ """
+ This method gets called when all forms passed. The method should also
+ re-validate all steps to prevent manipulation. If any form don't
+ validate, `render_revalidation_failure` should get called.
+ If everything is fine call `done`.
+ """
+ final_form_list = []
+ # walk through the form list and try to validate the data again.
+ for form_key in self.get_form_list():
+ form_obj = self.get_form(step=form_key,
+ data=self.storage.get_step_data(form_key),
+ files=self.storage.get_step_files(form_key))
+ if not form_obj.is_valid():
+ return self.render_revalidation_failure(form_key, form_obj, **kwargs)
+ final_form_list.append(form_obj)
+
+ # render the done view and reset the wizard before returning the
+ # response. This is needed to prevent from rendering done with the
+ # same data twice.
+ done_response = self.done(final_form_list, **kwargs)
+ self.storage.reset()
+ return done_response
+
+ def get_form_prefix(self, step=None, form=None):
+ """
+ Returns the prefix which will be used when calling the actual form for
+ the given step. `step` contains the step-name, `form` the form which
+ will be called with the returned prefix.
+
+ If no step is given, the form_prefix will determine the current step
+ automatically.
+ """
+ if step is None:
+ step = self.steps.current
+ return str(step)
+
+ def get_form_initial(self, step):
+ """
+ Returns a dictionary which will be passed to the form for `step`
+ as `initial`. If no initial data was provied while initializing the
+ form wizard, a empty dictionary will be returned.
+ """