Fixed #9200 -- Added new form wizard to formtools based on class based 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
This commit is contained in:
parent
1a951fa8d4
commit
632dfa2338
|
@ -1,13 +1,19 @@
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django import forms, http
|
from django import http
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.formtools import preview, wizard, utils
|
from django.contrib.formtools import preview, wizard, utils
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import get_warnings_state, restore_warnings_state
|
from django.test.utils import get_warnings_state, restore_warnings_state
|
||||||
from django.utils import unittest
|
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!"
|
success_string = "Done was called!"
|
||||||
|
|
||||||
|
@ -24,12 +30,6 @@ class TestFormPreview(preview.FormPreview):
|
||||||
return http.HttpResponse(success_string)
|
return http.HttpResponse(success_string)
|
||||||
|
|
||||||
|
|
||||||
class TestForm(forms.Form):
|
|
||||||
field1 = forms.CharField()
|
|
||||||
field1_ = forms.CharField()
|
|
||||||
bool1 = forms.BooleanField(required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PreviewTests(TestCase):
|
class PreviewTests(TestCase):
|
||||||
urls = 'django.contrib.formtools.tests.urls'
|
urls = 'django.contrib.formtools.tests.urls'
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class PreviewTests(TestCase):
|
||||||
is created to manage the stage.
|
is created to manage the stage.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test1/')
|
response = self.client.get('/preview/')
|
||||||
stage = self.input % 1
|
stage = self.input % 1
|
||||||
self.assertContains(response, stage, 1)
|
self.assertContains(response, stage, 1)
|
||||||
self.assertEqual(response.context['custom_context'], True)
|
self.assertEqual(response.context['custom_context'], True)
|
||||||
|
@ -81,7 +81,7 @@ class PreviewTests(TestCase):
|
||||||
# Pass strings for form submittal and add stage variable to
|
# Pass strings for form submittal and add stage variable to
|
||||||
# show we previously saw first stage of the form.
|
# show we previously saw first stage of the form.
|
||||||
self.test_data.update({'stage': 1})
|
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.
|
# Check to confirm stage is set to 2 in output form.
|
||||||
stage = self.input % 2
|
stage = self.input % 2
|
||||||
self.assertContains(response, stage, 1)
|
self.assertContains(response, stage, 1)
|
||||||
|
@ -99,11 +99,11 @@ class PreviewTests(TestCase):
|
||||||
# Pass strings for form submittal and add stage variable to
|
# Pass strings for form submittal and add stage variable to
|
||||||
# show we previously saw first stage of the form.
|
# show we previously saw first stage of the form.
|
||||||
self.test_data.update({'stage':2})
|
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)
|
self.assertNotEqual(response.content, success_string)
|
||||||
hash = self.preview.security_hash(None, TestForm(self.test_data))
|
hash = self.preview.security_hash(None, TestForm(self.test_data))
|
||||||
self.test_data.update({'hash': hash})
|
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)
|
self.assertEqual(response.content, success_string)
|
||||||
|
|
||||||
def test_bool_submit(self):
|
def test_bool_submit(self):
|
||||||
|
@ -122,7 +122,7 @@ class PreviewTests(TestCase):
|
||||||
self.test_data.update({'stage':2})
|
self.test_data.update({'stage':2})
|
||||||
hash = self.preview.security_hash(None, TestForm(self.test_data))
|
hash = self.preview.security_hash(None, TestForm(self.test_data))
|
||||||
self.test_data.update({'hash':hash, 'bool1':u'False'})
|
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)
|
self.assertEqual(response.content, success_string)
|
||||||
|
|
||||||
def test_form_submit_good_hash(self):
|
def test_form_submit_good_hash(self):
|
||||||
|
@ -133,11 +133,11 @@ class PreviewTests(TestCase):
|
||||||
# Pass strings for form submittal and add stage variable to
|
# Pass strings for form submittal and add stage variable to
|
||||||
# show we previously saw first stage of the form.
|
# show we previously saw first stage of the form.
|
||||||
self.test_data.update({'stage':2})
|
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)
|
self.assertNotEqual(response.content, success_string)
|
||||||
hash = utils.form_hmac(TestForm(self.test_data))
|
hash = utils.form_hmac(TestForm(self.test_data))
|
||||||
self.test_data.update({'hash': hash})
|
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)
|
self.assertEqual(response.content, success_string)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,12 +149,12 @@ class PreviewTests(TestCase):
|
||||||
# Pass strings for form submittal and add stage variable to
|
# Pass strings for form submittal and add stage variable to
|
||||||
# show we previously saw first stage of the form.
|
# show we previously saw first stage of the form.
|
||||||
self.test_data.update({'stage':2})
|
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.assertEqual(response.status_code, 200)
|
||||||
self.assertNotEqual(response.content, success_string)
|
self.assertNotEqual(response.content, success_string)
|
||||||
hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
|
hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
|
||||||
self.test_data.update({'hash': hash})
|
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)
|
self.assertNotEqual(response.content, success_string)
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,38 +220,14 @@ class FormHmacTests(unittest.TestCase):
|
||||||
self.assertEqual(hash1, hash2)
|
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
|
# FormWizard tests
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class TestWizardClass(wizard.FormWizard):
|
||||||
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):
|
|
||||||
|
|
||||||
def get_template(self, step):
|
def get_template(self, step):
|
||||||
return 'formwizard/wizard.html'
|
return 'forms/wizard.html'
|
||||||
|
|
||||||
def done(self, request, cleaned_data):
|
def done(self, request, cleaned_data):
|
||||||
return http.HttpResponse(success_string)
|
return http.HttpResponse(success_string)
|
||||||
|
@ -269,6 +245,20 @@ class DummyRequest(http.HttpRequest):
|
||||||
|
|
||||||
class WizardTests(TestCase):
|
class WizardTests(TestCase):
|
||||||
urls = 'django.contrib.formtools.tests.urls'
|
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):
|
def setUp(self):
|
||||||
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
|
self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
|
||||||
|
@ -290,21 +280,21 @@ class WizardTests(TestCase):
|
||||||
"""
|
"""
|
||||||
step should be zero for the first form
|
step should be zero for the first form
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/wizard/')
|
response = self.client.get('/wizard1/')
|
||||||
self.assertEqual(0, response.context['step0'])
|
self.assertEqual(0, response.context['step0'])
|
||||||
|
|
||||||
def test_step_increments(self):
|
def test_step_increments(self):
|
||||||
"""
|
"""
|
||||||
step should be incremented when we go to the next page
|
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'])
|
self.assertEqual(1, response.context['step0'])
|
||||||
|
|
||||||
def test_bad_hash(self):
|
def test_bad_hash(self):
|
||||||
"""
|
"""
|
||||||
Form should not advance if the hash is missing or bad
|
Form should not advance if the hash is missing or bad
|
||||||
"""
|
"""
|
||||||
response = self.client.post('/wizard/',
|
response = self.client.post('/wizard1/',
|
||||||
{"0-field":"test",
|
{"0-field":"test",
|
||||||
"1-field":"test2",
|
"1-field":"test2",
|
||||||
"wizard_step": "1"})
|
"wizard_step": "1"})
|
||||||
|
@ -319,7 +309,7 @@ class WizardTests(TestCase):
|
||||||
"1-field": "test2",
|
"1-field": "test2",
|
||||||
"hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c",
|
"hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c",
|
||||||
"wizard_step": "1"}
|
"wizard_step": "1"}
|
||||||
response = self.client.post('/wizard/', data)
|
response = self.client.post('/wizard1/', data)
|
||||||
self.assertEqual(2, response.context['step0'])
|
self.assertEqual(2, response.context['step0'])
|
||||||
|
|
||||||
def test_11726(self):
|
def test_11726(self):
|
||||||
|
@ -330,7 +320,7 @@ class WizardTests(TestCase):
|
||||||
reached = [False]
|
reached = [False]
|
||||||
that = self
|
that = self
|
||||||
|
|
||||||
class WizardWithProcessStep(WizardClass):
|
class WizardWithProcessStep(TestWizardClass):
|
||||||
def process_step(self, request, form, step):
|
def process_step(self, request, form, step):
|
||||||
if step == 0:
|
if step == 0:
|
||||||
if self.num_steps() < 2:
|
if self.num_steps() < 2:
|
||||||
|
@ -362,7 +352,7 @@ class WizardTests(TestCase):
|
||||||
reached = [False]
|
reached = [False]
|
||||||
that = self
|
that = self
|
||||||
|
|
||||||
class WizardWithProcessStep(WizardClass):
|
class WizardWithProcessStep(TestWizardClass):
|
||||||
def process_step(self, request, form, step):
|
def process_step(self, request, form, step):
|
||||||
that.assertTrue(hasattr(form, 'cleaned_data'))
|
that.assertTrue(hasattr(form, 'cleaned_data'))
|
||||||
reached[0] = True
|
reached[0] = True
|
||||||
|
@ -386,7 +376,7 @@ class WizardTests(TestCase):
|
||||||
reached = [False]
|
reached = [False]
|
||||||
that = self
|
that = self
|
||||||
|
|
||||||
class Wizard(WizardClass):
|
class Wizard(TestWizardClass):
|
||||||
def done(self, request, form_list):
|
def done(self, request, form_list):
|
||||||
reached[0] = True
|
reached[0] = True
|
||||||
that.assertTrue(len(form_list) == 2)
|
that.assertTrue(len(form_list) == 2)
|
||||||
|
@ -409,7 +399,7 @@ class WizardTests(TestCase):
|
||||||
reached = [False]
|
reached = [False]
|
||||||
that = self
|
that = self
|
||||||
|
|
||||||
class WizardWithProcessStep(WizardClass):
|
class WizardWithProcessStep(TestWizardClass):
|
||||||
def process_step(self, request, form, step):
|
def process_step(self, request, form, step):
|
||||||
if step == 0:
|
if step == 0:
|
||||||
self.form_list[1] = WizardPageTwoAlternativeForm
|
self.form_list[1] = WizardPageTwoAlternativeForm
|
||||||
|
@ -426,3 +416,39 @@ class WizardTests(TestCase):
|
||||||
"wizard_step": "1"}
|
"wizard_step": "1"}
|
||||||
wizard(DummyRequest(POST=data))
|
wizard(DummyRequest(POST=data))
|
||||||
self.assertTrue(reached[0])
|
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)
|
||||||
|
|
|
@ -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()
|
|
@ -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>
|
|
|
@ -3,11 +3,14 @@ This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
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('',
|
urlpatterns = patterns('',
|
||||||
(r'^test1/', TestFormPreview(TestForm)),
|
url(r'^preview/', TestFormPreview(TestForm)),
|
||||||
(r'^wizard/$', WizardClass([WizardPageOneForm,
|
url(r'^wizard1/$', TestWizardClass(
|
||||||
WizardPageTwoForm,
|
[WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm])),
|
||||||
WizardPageThreeForm])),
|
url(r'^wizard2/$', ContactWizard([Page1, Page2, Page3])),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from django.contrib.formtools.wizard.legacy import FormWizard
|
|
@ -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)
|
|
@ -3,15 +3,7 @@ FormWizard class -- implements a multi-page form, validating between each
|
||||||
step and storing the form's state as HTML hidden fields so that no state is
|
step and storing the form's state as HTML hidden fields so that no state is
|
||||||
stored on the server side.
|
stored on the server side.
|
||||||
"""
|
"""
|
||||||
|
from django.forms import HiddenInput
|
||||||
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.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
from django.template.context import RequestContext
|
from django.template.context import RequestContext
|
||||||
|
@ -20,6 +12,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
|
||||||
|
from django.contrib.formtools.utils import form_hmac
|
||||||
|
|
||||||
class FormWizard(object):
|
class FormWizard(object):
|
||||||
# The HTML (and POST data) field name for the "step" variable.
|
# The HTML (and POST data) field name for the "step" variable.
|
||||||
|
@ -42,6 +35,12 @@ class FormWizard(object):
|
||||||
# A zero-based counter keeping track of which step we're in.
|
# A zero-based counter keeping track of which step we're in.
|
||||||
self.step = 0
|
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):
|
def __repr__(self):
|
||||||
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
|
return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
|
||||||
|
|
||||||
|
@ -71,7 +70,7 @@ class FormWizard(object):
|
||||||
"""
|
"""
|
||||||
if 'extra_context' in kwargs:
|
if 'extra_context' in kwargs:
|
||||||
self.extra_context.update(kwargs['extra_context'])
|
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)
|
self.parse_params(request, *args, **kwargs)
|
||||||
|
|
||||||
# Validate and process all the previous forms before instantiating the
|
# Validate and process all the previous forms before instantiating the
|
||||||
|
@ -132,7 +131,7 @@ class FormWizard(object):
|
||||||
old_data = request.POST
|
old_data = request.POST
|
||||||
prev_fields = []
|
prev_fields = []
|
||||||
if old_data:
|
if old_data:
|
||||||
hidden = forms.HiddenInput()
|
hidden = HiddenInput()
|
||||||
# Collect all data from previous steps and render it as HTML hidden fields.
|
# Collect all data from previous steps and render it as HTML hidden fields.
|
||||||
for i in range(step):
|
for i in range(step):
|
||||||
old_form = self.get_form(i, old_data)
|
old_form = self.get_form(i, old_data)
|
||||||
|
@ -177,7 +176,7 @@ class FormWizard(object):
|
||||||
"""
|
"""
|
||||||
return form_hmac(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
|
Given the request object and whatever *args and **kwargs were passed to
|
||||||
__call__(), returns the current step (which is zero-based).
|
__call__(), returns the current step (which is zero-based).
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
class MissingStorageModule(ImproperlyConfigured):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MissingStorageClass(ImproperlyConfigured):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoFileStorageConfigured(ImproperlyConfigured):
|
||||||
|
pass
|
|
@ -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)
|
|
@ -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" %}" />
|
|
@ -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 *
|
|
@ -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":{}}')
|
|
@ -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))
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from django.contrib.formtools.wizard.tests.namedwizardtests.tests import *
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
|
@ -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'),
|
||||||
|
)
|
|
@ -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
|
|
@ -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, {})
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from django.contrib.formtools.wizard.tests.wizardtests.tests import *
|
|
@ -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'
|
||||||
|
|
|
@ -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',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)])),
|
||||||
|
)
|
|
@ -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.
|
||||||
|
"""
|
||||||
|
return self.initial_dict.get(step, {})
|
||||||
|
|
||||||
|
def get_form_instance(self, step):
|
||||||
|
"""
|
||||||
|
Returns a object which will be passed to the form for `step`
|
||||||
|
as `instance`. If no instance object was provied while initializing
|
||||||
|
the form wizard, None be returned.
|
||||||
|
"""
|
||||||
|
return self.instance_dict.get(step, None)
|
||||||
|
|
||||||
|
def get_form(self, step=None, data=None, files=None):
|
||||||
|
"""
|
||||||
|
Constructs the form for a given `step`. If no `step` is defined, the
|
||||||
|
current step will be determined automatically.
|
||||||
|
|
||||||
|
The form will be initialized using the `data` argument to prefill the
|
||||||
|
new form. If needed, instance or queryset (for `ModelForm` or
|
||||||
|
`ModelFormSet`) will be added too.
|
||||||
|
"""
|
||||||
|
if step is None:
|
||||||
|
step = self.steps.current
|
||||||
|
# prepare the kwargs for the form instance.
|
||||||
|
kwargs = {
|
||||||
|
'data': data,
|
||||||
|
'files': files,
|
||||||
|
'prefix': self.get_form_prefix(step, self.form_list[step]),
|
||||||
|
'initial': self.get_form_initial(step),
|
||||||
|
}
|
||||||
|
if issubclass(self.form_list[step], forms.ModelForm):
|
||||||
|
# If the form is based on ModelForm, add instance if available.
|
||||||
|
kwargs.update({'instance': self.get_form_instance(step)})
|
||||||
|
elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
|
||||||
|
# If the form is based on ModelFormSet, add queryset if available.
|
||||||
|
kwargs.update({'queryset': self.get_form_instance(step)})
|
||||||
|
return self.form_list[step](**kwargs)
|
||||||
|
|
||||||
|
def process_step(self, form):
|
||||||
|
"""
|
||||||
|
This method is used to postprocess the form data. By default, it
|
||||||
|
returns the raw `form.data` dictionary.
|
||||||
|
"""
|
||||||
|
return self.get_form_step_data(form)
|
||||||
|
|
||||||
|
def process_step_files(self, form):
|
||||||
|
"""
|
||||||
|
This method is used to postprocess the form files. By default, it
|
||||||
|
returns the raw `form.files` dictionary.
|
||||||
|
"""
|
||||||
|
return self.get_form_step_files(form)
|
||||||
|
|
||||||
|
def render_revalidation_failure(self, step, form, **kwargs):
|
||||||
|
"""
|
||||||
|
Gets called when a form doesn't validate when rendering the done
|
||||||
|
view. By default, it changed the current step to failing forms step
|
||||||
|
and renders the form.
|
||||||
|
"""
|
||||||
|
self.storage.current_step = step
|
||||||
|
return self.render(form, **kwargs)
|
||||||
|
|
||||||
|
def get_form_step_data(self, form):
|
||||||
|
"""
|
||||||
|
Is used to return the raw form data. You may use this method to
|
||||||
|
manipulate the data.
|
||||||
|
"""
|
||||||
|
return form.data
|
||||||
|
|
||||||
|
def get_form_step_files(self, form):
|
||||||
|
"""
|
||||||
|
Is used to return the raw form files. You may use this method to
|
||||||
|
manipulate the data.
|
||||||
|
"""
|
||||||
|
return form.files
|
||||||
|
|
||||||
|
def get_all_cleaned_data(self):
|
||||||
|
"""
|
||||||
|
Returns a merged dictionary of all step cleaned_data dictionaries.
|
||||||
|
If a step contains a `FormSet`, the key will be prefixed with formset
|
||||||
|
and contain a list of the formset' cleaned_data dictionaries.
|
||||||
|
"""
|
||||||
|
cleaned_data = {}
|
||||||
|
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 form_obj.is_valid():
|
||||||
|
if isinstance(form_obj.cleaned_data, (tuple, list)):
|
||||||
|
cleaned_data.update({
|
||||||
|
'formset-%s' % form_key: form_obj.cleaned_data
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
cleaned_data.update(form_obj.cleaned_data)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def get_cleaned_data_for_step(self, step):
|
||||||
|
"""
|
||||||
|
Returns the cleaned data for a given `step`. Before returning the
|
||||||
|
cleaned data, the stored values are being revalidated through the
|
||||||
|
form. If the data doesn't validate, None will be returned.
|
||||||
|
"""
|
||||||
|
if step in self.form_list:
|
||||||
|
form_obj = self.get_form(step=step,
|
||||||
|
data=self.storage.get_step_data(step),
|
||||||
|
files=self.storage.get_step_files(step))
|
||||||
|
if form_obj.is_valid():
|
||||||
|
return form_obj.cleaned_data
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_next_step(self, step=None):
|
||||||
|
"""
|
||||||
|
Returns the next step after the given `step`. If no more steps are
|
||||||
|
available, None will be returned. If the `step` argument is None, the
|
||||||
|
current step will be determined automatically.
|
||||||
|
"""
|
||||||
|
if step is None:
|
||||||
|
step = self.steps.current
|
||||||
|
form_list = self.get_form_list()
|
||||||
|
key = form_list.keyOrder.index(step) + 1
|
||||||
|
if len(form_list.keyOrder) > key:
|
||||||
|
return form_list.keyOrder[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_prev_step(self, step=None):
|
||||||
|
"""
|
||||||
|
Returns the previous step before the given `step`. If there are no
|
||||||
|
steps available, None will be returned. If the `step` argument is
|
||||||
|
None, the current step will be determined automatically.
|
||||||
|
"""
|
||||||
|
if step is None:
|
||||||
|
step = self.steps.current
|
||||||
|
form_list = self.get_form_list()
|
||||||
|
key = form_list.keyOrder.index(step) - 1
|
||||||
|
if key >= 0:
|
||||||
|
return form_list.keyOrder[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_step_index(self, step=None):
|
||||||
|
"""
|
||||||
|
Returns the index for the given `step` name. If no step is given,
|
||||||
|
the current step will be used to get the index.
|
||||||
|
"""
|
||||||
|
if step is None:
|
||||||
|
step = self.steps.current
|
||||||
|
return self.get_form_list().keyOrder.index(step)
|
||||||
|
|
||||||
|
def get_context_data(self, form, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the template context for a step. You can overwrite this method
|
||||||
|
to add more data for all or some steps. This method returns a
|
||||||
|
dictionary containing the rendered form step. Available template
|
||||||
|
context variables are:
|
||||||
|
|
||||||
|
* all extra data stored in the storage backend
|
||||||
|
* `form` - form instance of the current step
|
||||||
|
* `wizard` - the wizard instance itself
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class MyWizard(FormWizard):
|
||||||
|
def get_context_data(self, form, **kwargs):
|
||||||
|
context = super(MyWizard, self).get_context_data(form, **kwargs)
|
||||||
|
if self.steps.current == 'my_step_name':
|
||||||
|
context.update({'another_var': True})
|
||||||
|
return context
|
||||||
|
"""
|
||||||
|
context = super(WizardView, self).get_context_data(*args, **kwargs)
|
||||||
|
context.update(self.storage.extra_data)
|
||||||
|
context['wizard'] = {
|
||||||
|
'form': form,
|
||||||
|
'steps': self.steps,
|
||||||
|
'managenent_form': ManagementForm(prefix=self.prefix, initial={
|
||||||
|
'current_step': self.steps.current,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
def render(self, form=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a ``HttpResponse`` containing a all needed context data.
|
||||||
|
"""
|
||||||
|
form = form or self.get_form()
|
||||||
|
context = self.get_context_data(form, **kwargs)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def done(self, form_list, **kwargs):
|
||||||
|
"""
|
||||||
|
This method muss be overrided by a subclass to process to form data
|
||||||
|
after processing all steps.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Your %s class has not defined a done() "
|
||||||
|
"method, which is required." % self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionWizardView(WizardView):
|
||||||
|
"""
|
||||||
|
A WizardView with pre-configured SessionStorage backend.
|
||||||
|
"""
|
||||||
|
storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
|
||||||
|
|
||||||
|
|
||||||
|
class CookieWizardView(WizardView):
|
||||||
|
"""
|
||||||
|
A WizardView with pre-configured CookieStorage backend.
|
||||||
|
"""
|
||||||
|
storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
|
||||||
|
|
||||||
|
|
||||||
|
class NamedUrlWizardView(WizardView):
|
||||||
|
"""
|
||||||
|
A WizardView with URL named steps support.
|
||||||
|
"""
|
||||||
|
url_name = None
|
||||||
|
done_step_name = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_initkwargs(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
We require a url_name to reverse URLs later. Additionally users can
|
||||||
|
pass a done_step_name to change the URL name of the "done" view.
|
||||||
|
"""
|
||||||
|
extra_kwargs = {
|
||||||
|
'done_step_name': 'done'
|
||||||
|
}
|
||||||
|
assert 'url_name' in kwargs, 'URL name is needed to resolve correct wizard URLs'
|
||||||
|
extra_kwargs['url_name'] = kwargs.pop('url_name')
|
||||||
|
|
||||||
|
if 'done_step_name' in kwargs:
|
||||||
|
extra_kwargs['done_step_name'] = kwargs.pop('done_step_name')
|
||||||
|
|
||||||
|
initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs)
|
||||||
|
initkwargs.update(extra_kwargs)
|
||||||
|
|
||||||
|
assert initkwargs['done_step_name'] not in initkwargs['form_list'], \
|
||||||
|
'step name "%s" is reserved for "done" view' % initkwargs['done_step_name']
|
||||||
|
|
||||||
|
return initkwargs
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
This renders the form or, if needed, does the http redirects.
|
||||||
|
"""
|
||||||
|
step_url = kwargs.get('step', None)
|
||||||
|
if step_url is None:
|
||||||
|
if 'reset' in self.request.GET:
|
||||||
|
self.storage.reset()
|
||||||
|
self.storage.current_step = self.steps.first
|
||||||
|
if self.request.GET:
|
||||||
|
query_string = "?%s" % self.request.GET.urlencode()
|
||||||
|
else:
|
||||||
|
query_string = ""
|
||||||
|
next_step_url = reverse(self.url_name, kwargs={
|
||||||
|
'step': self.steps.current,
|
||||||
|
}) + query_string
|
||||||
|
return redirect(next_step_url)
|
||||||
|
|
||||||
|
# is the current step the "done" name/view?
|
||||||
|
elif step_url == self.done_step_name:
|
||||||
|
last_step = self.steps.last
|
||||||
|
return self.render_done(self.get_form(step=last_step,
|
||||||
|
data=self.storage.get_step_data(last_step),
|
||||||
|
files=self.storage.get_step_files(last_step)
|
||||||
|
), **kwargs)
|
||||||
|
|
||||||
|
# is the url step name not equal to the step in the storage?
|
||||||
|
# if yes, change the step in the storage (if name exists)
|
||||||
|
elif step_url == self.steps.current:
|
||||||
|
# URL step name and storage step name are equal, render!
|
||||||
|
return self.render(self.get_form(
|
||||||
|
data=self.storage.current_step_data,
|
||||||
|
files=self.storage.current_step_data,
|
||||||
|
), **kwargs)
|
||||||
|
|
||||||
|
elif step_url in self.get_form_list():
|
||||||
|
self.storage.current_step = step_url
|
||||||
|
return self.render(self.get_form(
|
||||||
|
data=self.storage.current_step_data,
|
||||||
|
files=self.storage.current_step_data,
|
||||||
|
), **kwargs)
|
||||||
|
|
||||||
|
# invalid step name, reset to first and redirect.
|
||||||
|
else:
|
||||||
|
self.storage.current_step = self.steps.first
|
||||||
|
return redirect(self.url_name, step=self.steps.first)
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Do a redirect if user presses the prev. step button. The rest of this
|
||||||
|
is super'd from FormWizard.
|
||||||
|
"""
|
||||||
|
prev_step = self.request.POST.get('wizard_prev_step', None)
|
||||||
|
if prev_step and prev_step in self.get_form_list():
|
||||||
|
self.storage.current_step = prev_step
|
||||||
|
return redirect(self.url_name, step=prev_step)
|
||||||
|
return super(NamedUrlWizardView, self).post(*args, **kwargs)
|
||||||
|
|
||||||
|
def render_next_step(self, form, **kwargs):
|
||||||
|
"""
|
||||||
|
When using the NamedUrlFormWizard, we have to redirect to update the
|
||||||
|
browser's URL to match the shown step.
|
||||||
|
"""
|
||||||
|
next_step = self.get_next_step()
|
||||||
|
self.storage.current_step = next_step
|
||||||
|
return redirect(self.url_name, step=next_step)
|
||||||
|
|
||||||
|
def render_revalidation_failure(self, failed_step, form, **kwargs):
|
||||||
|
"""
|
||||||
|
When a step fails, we have to redirect the user to the first failing
|
||||||
|
step.
|
||||||
|
"""
|
||||||
|
self.storage.current_step = failed_step
|
||||||
|
return redirect(self.url_name, step=failed_step)
|
||||||
|
|
||||||
|
def render_done(self, form, **kwargs):
|
||||||
|
"""
|
||||||
|
When rendering the done view, we have to redirect first (if the URL
|
||||||
|
name doesn't fit).
|
||||||
|
"""
|
||||||
|
if kwargs.get('step', None) != self.done_step_name:
|
||||||
|
return redirect(self.url_name, step=self.done_step_name)
|
||||||
|
return super(NamedUrlWizardView, self).render_done(form, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NamedUrlSessionWizardView(NamedUrlWizardView):
|
||||||
|
"""
|
||||||
|
A NamedUrlWizardView with pre-configured SessionStorage backend.
|
||||||
|
"""
|
||||||
|
storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
|
||||||
|
|
||||||
|
|
||||||
|
class NamedUrlCookieWizardView(NamedUrlWizardView):
|
||||||
|
"""
|
||||||
|
A NamedUrlFormWizard with pre-configured CookieStorageBackend.
|
||||||
|
"""
|
||||||
|
storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
|
||||||
|
|
|
@ -265,3 +265,24 @@ class SimpleLazyObject(LazyObject):
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self._wrapped = self._setupfunc()
|
self._wrapped = self._setupfunc()
|
||||||
|
|
||||||
|
|
||||||
|
class lazy_property(property):
|
||||||
|
"""
|
||||||
|
A property that works with subclasses by wrapping the decorated
|
||||||
|
functions of the base class.
|
||||||
|
"""
|
||||||
|
def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
|
||||||
|
if fget is not None:
|
||||||
|
@wraps(fget)
|
||||||
|
def fget(instance, instance_type=None, name=fget.__name__):
|
||||||
|
return getattr(instance, name)()
|
||||||
|
if fset is not None:
|
||||||
|
@wraps(fset)
|
||||||
|
def fset(instance, value, name=fset.__name__):
|
||||||
|
return getattr(instance, name)(value)
|
||||||
|
if fdel is not None:
|
||||||
|
@wraps(fdel)
|
||||||
|
def fdel(instance, name=fdel.__name__):
|
||||||
|
return getattr(instance, name)()
|
||||||
|
return property(fget, fset, fdel, doc)
|
||||||
|
|
|
@ -203,6 +203,10 @@ their deprecation, as per the :ref:`Django deprecation policy
|
||||||
settings have been superseded by :setting:`IGNORABLE_404_URLS` in
|
settings have been superseded by :setting:`IGNORABLE_404_URLS` in
|
||||||
the 1.4 release. They will be removed.
|
the 1.4 release. They will be removed.
|
||||||
|
|
||||||
|
* The :doc:`form wizard </ref/contrib/formtools/form-wizard>` has been
|
||||||
|
refactored to use class based views with pluggable backends in 1.4.
|
||||||
|
The previous implementation will be deprecated.
|
||||||
|
|
||||||
* 2.0
|
* 2.0
|
||||||
* ``django.views.defaults.shortcut()``. This function has been moved
|
* ``django.views.defaults.shortcut()``. This function has been moved
|
||||||
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
|
||||||
|
|
|
@ -2,23 +2,22 @@
|
||||||
Form wizard
|
Form wizard
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. module:: django.contrib.formtools.wizard
|
.. module:: django.contrib.formtools.wizard.views
|
||||||
:synopsis: Splits forms across multiple Web pages.
|
:synopsis: Splits forms across multiple Web pages.
|
||||||
|
|
||||||
Django comes with an optional "form wizard" application that splits
|
Django comes with an optional "form wizard" application that splits
|
||||||
:doc:`forms </topics/forms/index>` across multiple Web pages. It maintains
|
:doc:`forms </topics/forms/index>` across multiple Web pages. It maintains
|
||||||
state in hashed HTML :samp:`<input type="hidden">` fields so that the full
|
state in one of the backends so that the full server-side processing can be
|
||||||
server-side processing can be delayed until the submission of the final form.
|
delayed until the submission of the final form.
|
||||||
|
|
||||||
You might want to use this if you have a lengthy form that would be too
|
You might want to use this if you have a lengthy form that would be too
|
||||||
unwieldy for display on a single page. The first page might ask the user for
|
unwieldy for display on a single page. The first page might ask the user for
|
||||||
core information, the second page might ask for less important information,
|
core information, the second page might ask for less important information,
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
The term "wizard," in this context, is `explained on Wikipedia`_.
|
The term "wizard", in this context, is `explained on Wikipedia`_.
|
||||||
|
|
||||||
.. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29
|
.. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29
|
||||||
.. _forms: ../forms/
|
|
||||||
|
|
||||||
How it works
|
How it works
|
||||||
============
|
============
|
||||||
|
@ -28,10 +27,8 @@ Here's the basic workflow for how a user would use a wizard:
|
||||||
1. The user visits the first page of the wizard, fills in the form and
|
1. The user visits the first page of the wizard, fills in the form and
|
||||||
submits it.
|
submits it.
|
||||||
2. The server validates the data. If it's invalid, the form is displayed
|
2. The server validates the data. If it's invalid, the form is displayed
|
||||||
again, with error messages. If it's valid, the server calculates a
|
again, with error messages. If it's valid, the server saves the current
|
||||||
secure hash of the data and presents the user with the next form,
|
state of the wizard in the backend and redirects to the next step.
|
||||||
saving the validated data and hash in :samp:`<input type="hidden">`
|
|
||||||
fields.
|
|
||||||
3. Step 1 and 2 repeat, for every subsequent form in the wizard.
|
3. Step 1 and 2 repeat, for every subsequent form in the wizard.
|
||||||
4. Once the user has submitted all the forms and all the data has been
|
4. Once the user has submitted all the forms and all the data has been
|
||||||
validated, the wizard processes the data -- saving it to the database,
|
validated, the wizard processes the data -- saving it to the database,
|
||||||
|
@ -40,30 +37,33 @@ Here's the basic workflow for how a user would use a wizard:
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
|
||||||
This application handles as much machinery for you as possible. Generally, you
|
This application handles as much machinery for you as possible. Generally,
|
||||||
just have to do these things:
|
you just have to do these things:
|
||||||
|
|
||||||
1. Define a number of :class:`~django.forms.Form` classes -- one per wizard
|
1. Define a number of :class:`~django.forms.Form` classes -- one per
|
||||||
page.
|
wizard page.
|
||||||
|
|
||||||
2. Create a :class:`FormWizard` class that specifies what to do once all of
|
2. Create a :class:`WizardView` subclass that specifies what to do once
|
||||||
your forms have been submitted and validated. This also lets you
|
all of your forms have been submitted and validated. This also lets
|
||||||
override some of the wizard's behavior.
|
you override some of the wizard's behavior.
|
||||||
|
|
||||||
3. Create some templates that render the forms. You can define a single,
|
3. Create some templates that render the forms. You can define a single,
|
||||||
generic template to handle every one of the forms, or you can define a
|
generic template to handle every one of the forms, or you can define a
|
||||||
specific template for each form.
|
specific template for each form.
|
||||||
|
|
||||||
4. Point your URLconf at your :class:`FormWizard` class.
|
4. Add ``django.contrib.formtools.wizard`` to your
|
||||||
|
:setting:`INSTALLED_APPS` list in your settings file.
|
||||||
|
|
||||||
|
5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method.
|
||||||
|
|
||||||
Defining ``Form`` classes
|
Defining ``Form`` classes
|
||||||
=========================
|
-------------------------
|
||||||
|
|
||||||
The first step in creating a form wizard is to create the
|
The first step in creating a form wizard is to create the
|
||||||
:class:`~django.forms.Form` classes. These should be standard
|
:class:`~django.forms.Form` classes. These should be standard
|
||||||
:class:`django.forms.Form` classes, covered in the :doc:`forms documentation
|
:class:`django.forms.Form` classes, covered in the :doc:`forms documentation
|
||||||
</topics/forms/index>`. These classes can live anywhere in your codebase, but
|
</topics/forms/index>`. These classes can live anywhere in your codebase,
|
||||||
convention is to put them in a file called :file:`forms.py` in your
|
but convention is to put them in a file called :file:`forms.py` in your
|
||||||
application.
|
application.
|
||||||
|
|
||||||
For example, let's write a "contact form" wizard, where the first page's form
|
For example, let's write a "contact form" wizard, where the first page's form
|
||||||
|
@ -79,37 +79,49 @@ the message itself. Here's what the :file:`forms.py` might look like::
|
||||||
class ContactForm2(forms.Form):
|
class ContactForm2(forms.Form):
|
||||||
message = forms.CharField(widget=forms.Textarea)
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
|
|
||||||
**Important limitation:** Because the wizard uses HTML hidden fields to store
|
|
||||||
data between pages, you may not include a :class:`~django.forms.FileField`
|
|
||||||
in any form except the last one.
|
|
||||||
|
|
||||||
Creating a ``FormWizard`` class
|
.. note::
|
||||||
===============================
|
|
||||||
|
In order to use :class:`~django.forms.FileField` in any form, see the
|
||||||
|
section :ref:`Handling files <wizard-files>` below to learn more about
|
||||||
|
what to do.
|
||||||
|
|
||||||
|
Creating a ``WizardView`` class
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
The next step is to create a
|
The next step is to create a
|
||||||
:class:`django.contrib.formtools.wizard.FormWizard` subclass. As with your
|
:class:`django.contrib.formtools.wizard.view.WizardView` subclass. You can
|
||||||
:class:`~django.forms.Form` classes, this :class:`FormWizard` class can live
|
also use the :class:`SessionWizardView` or :class:`CookieWizardView` class
|
||||||
anywhere in your codebase, but convention is to put it in :file:`forms.py`.
|
which preselects the wizard storage backend.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To use the :class:`SessionWizardView` follow the instructions
|
||||||
|
in the :doc:`sessions documentation </topics/http/sessions>` on
|
||||||
|
how to enable sessions.
|
||||||
|
|
||||||
|
We will use the :class:`SessionWizardView` in all examples but is is completly
|
||||||
|
fine to use the :class:`CookieWizardView` instead. As with your
|
||||||
|
:class:`~django.forms.Form` classes, this :class:`WizardView` class can live
|
||||||
|
anywhere in your codebase, but convention is to put it in :file:`views.py`.
|
||||||
|
|
||||||
The only requirement on this subclass is that it implement a
|
The only requirement on this subclass is that it implement a
|
||||||
:meth:`~FormWizard.done()` method.
|
:meth:`~WizardView.done()` method.
|
||||||
|
|
||||||
.. method:: FormWizard.done
|
.. method:: WizardView.done(form_list)
|
||||||
|
|
||||||
This method specifies what should happen when the data for *every* form is
|
This method specifies what should happen when the data for *every* form is
|
||||||
submitted and validated. This method is passed two arguments:
|
submitted and validated. This method is passed a list of validated
|
||||||
|
:class:`~django.forms.Form` instances.
|
||||||
|
|
||||||
* ``request`` -- an :class:`~django.http.HttpRequest` object
|
In this simplistic example, rather than performing any database operation,
|
||||||
* ``form_list`` -- a list of :class:`~django.forms.Form` classes
|
the method simply renders a template of the validated data::
|
||||||
|
|
||||||
In this simplistic example, rather than perform any database operation, the
|
|
||||||
method simply renders a template of the validated data::
|
|
||||||
|
|
||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
from django.contrib.formtools.wizard import FormWizard
|
from django.contrib.formtools.wizard.views import SessionWizardView
|
||||||
|
|
||||||
class ContactWizard(FormWizard):
|
class ContactWizard(SessionWizardView):
|
||||||
def done(self, request, form_list):
|
def done(self, form_list, **kwargs):
|
||||||
return render_to_response('done.html', {
|
return render_to_response('done.html', {
|
||||||
'form_data': [form.cleaned_data for form in form_list],
|
'form_data': [form.cleaned_data for form in form_list],
|
||||||
})
|
})
|
||||||
|
@ -119,45 +131,48 @@ good Web citizen and redirect after processing the data. Here's another
|
||||||
example::
|
example::
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.contrib.formtools.wizard import FormWizard
|
from django.contrib.formtools.wizard.views import SessionWizardView
|
||||||
|
|
||||||
class ContactWizard(FormWizard):
|
class ContactWizard(SessionWizardView):
|
||||||
def done(self, request, form_list):
|
def done(self, form_list, **kwargs):
|
||||||
do_something_with_the_form_data(form_list)
|
do_something_with_the_form_data(form_list)
|
||||||
return HttpResponseRedirect('/page-to-redirect-to-when-done/')
|
return HttpResponseRedirect('/page-to-redirect-to-when-done/')
|
||||||
|
|
||||||
See the section `Advanced FormWizard methods`_ below to learn about more
|
See the section :ref:`Advanced WizardView methods <wizardview-advanced-methods>`
|
||||||
:class:`FormWizard` hooks.
|
below to learn about more :class:`WizardView` hooks.
|
||||||
|
|
||||||
Creating templates for the forms
|
Creating templates for the forms
|
||||||
================================
|
--------------------------------
|
||||||
|
|
||||||
Next, you'll need to create a template that renders the wizard's forms. By
|
Next, you'll need to create a template that renders the wizard's forms. By
|
||||||
default, every form uses a template called :file:`forms/wizard.html`. (You can
|
default, every form uses a template called
|
||||||
change this template name by overriding :meth:`~FormWizard.get_template()`,
|
:file:`formtools/wizard/wizard_form.html`. You can change this template name
|
||||||
which is documented below. This hook also allows you to use a different
|
by overriding either the :attr:`~WizardView.template_name` attribute or the
|
||||||
template for each form.)
|
:meth:`~WizardView.get_template_names()` method, which is documented below.
|
||||||
|
This hook also allows you to use a different template for each form.
|
||||||
|
|
||||||
This template expects the following context:
|
This template expects a ``wizard`` object that has various items attached to
|
||||||
|
it:
|
||||||
|
|
||||||
|
* ``form`` -- The :class:`~django.forms.Form` instance for the current
|
||||||
|
step (either empty or with errors).
|
||||||
|
|
||||||
|
* ``steps`` -- A helper object to access the various steps related data:
|
||||||
|
|
||||||
* ``step_field`` -- The name of the hidden field containing the step.
|
|
||||||
* ``step0`` -- The current step (zero-based).
|
* ``step0`` -- The current step (zero-based).
|
||||||
* ``step`` -- The current step (one-based).
|
* ``step1`` -- The current step (one-based).
|
||||||
* ``step_count`` -- The total number of steps.
|
* ``count`` -- The total number of steps.
|
||||||
* ``form`` -- The :class:`~django.forms.Form` instance for the current step
|
* ``first`` -- The first step.
|
||||||
(either empty or with errors).
|
* ``last`` -- The last step.
|
||||||
* ``previous_fields`` -- A string representing every previous data field,
|
* ``current`` -- The current (or first) step.
|
||||||
plus hashes for completed forms, all in the form of hidden fields. Note
|
* ``next`` -- The next step.
|
||||||
that you'll need to run this through the :tfilter:`safe` template filter,
|
* ``prev`` -- The previous step.
|
||||||
to prevent auto-escaping, because it's raw HTML.
|
* ``index`` -- The index of the current step.
|
||||||
|
* ``all`` -- A list of all steps of the wizard.
|
||||||
|
|
||||||
You can supply extra context to this template in two ways:
|
You can supply additional context variables by using the
|
||||||
|
:meth:`~FormWizard.get_context_data` method of your :class:`FormWizard`
|
||||||
* Set the :attr:`~FormWizard.extra_context` attribute on your
|
subclass.
|
||||||
:class:`FormWizard` subclass to a dictionary.
|
|
||||||
|
|
||||||
* Pass a dictionary as a parameter named ``extra_context`` to your wizard's
|
|
||||||
URL pattern in your URLconf. See :ref:`hooking-wizard-into-urlconf`.
|
|
||||||
|
|
||||||
Here's a full example template:
|
Here's a full example template:
|
||||||
|
|
||||||
|
@ -166,170 +181,401 @@ Here's a full example template:
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Step {{ step }} of {{ step_count }}</p>
|
<p>Step {{ wizard.steps.current }} of {{ wizard.steps.count }}</p>
|
||||||
<form action="." method="post">{% csrf_token %}
|
<form action="." method="post">{% csrf_token %}
|
||||||
<table>
|
<table>
|
||||||
|
{{ wizard.management_form }}
|
||||||
|
{% if wizard.form.forms %}
|
||||||
|
{{ wizard.form.management_form }}
|
||||||
|
{% for form in wizard.form.forms %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{{ wizard.form }}
|
||||||
|
{% 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 %}
|
||||||
</table>
|
</table>
|
||||||
<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
|
|
||||||
{{ previous_fields|safe }}
|
|
||||||
<input type="submit">
|
<input type="submit">
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
Note that ``previous_fields``, ``step_field`` and ``step0`` are all required
|
.. note::
|
||||||
for the wizard to work properly.
|
|
||||||
|
|
||||||
.. _hooking-wizard-into-urlconf:
|
Note that ``{{ wizard.management_form }}`` **must be used** for
|
||||||
|
the wizard to work properly.
|
||||||
|
|
||||||
|
.. _wizard-urlconf:
|
||||||
|
|
||||||
Hooking the wizard into a URLconf
|
Hooking the wizard into a URLconf
|
||||||
=================================
|
---------------------------------
|
||||||
|
|
||||||
Finally, we need to specify which forms to use in the wizard, and then
|
Finally, we need to specify which forms to use in the wizard, and then
|
||||||
deploy the new :class:`FormWizard` object a URL in ``urls.py``. The
|
deploy the new :class:`WizardView` object a URL in the ``urls.py``. The
|
||||||
wizard takes a list of your :class:`~django.forms.Form` objects as
|
wizard's :meth:`as_view` method takes a list of your
|
||||||
arguments when you instantiate the Wizard::
|
:class:`~django.forms.Form` classes as an argument during instantiation::
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import patterns
|
||||||
from testapp.forms import ContactForm1, ContactForm2, ContactWizard
|
|
||||||
|
from myapp.forms import ContactForm1, ContactForm2
|
||||||
|
from myapp.views import ContactWizard
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^contact/$', ContactWizard([ContactForm1, ContactForm2])),
|
(r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
|
||||||
)
|
)
|
||||||
|
|
||||||
Advanced ``FormWizard`` methods
|
.. _wizardview-advanced-methods:
|
||||||
|
|
||||||
|
Advanced ``WizardView`` methods
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
.. class:: FormWizard
|
.. class:: WizardView
|
||||||
|
|
||||||
Aside from the :meth:`~done()` method, :class:`FormWizard` offers a few
|
Aside from the :meth:`~done()` method, :class:`WizardView` offers a few
|
||||||
advanced method hooks that let you customize how your wizard works.
|
advanced method hooks that let you customize how your wizard works.
|
||||||
|
|
||||||
Some of these methods take an argument ``step``, which is a zero-based
|
Some of these methods take an argument ``step``, which is a zero-based
|
||||||
counter representing the current step of the wizard. (E.g., the first form
|
counter as string representing the current step of the wizard. (E.g., the
|
||||||
is ``0`` and the second form is ``1``.)
|
first form is ``'0'`` and the second form is ``'1'``)
|
||||||
|
|
||||||
.. method:: FormWizard.prefix_for_step
|
.. method:: WizardView.get_form_prefix(step)
|
||||||
|
|
||||||
Given the step, returns a form prefix to use. By default, this simply uses
|
Given the step, returns a form prefix to use. By default, this simply uses
|
||||||
the step itself. For more, see the :ref:`form prefix documentation
|
the step itself. For more, see the :ref:`form prefix documentation
|
||||||
<form-prefix>`.
|
<form-prefix>`.
|
||||||
|
|
||||||
Default implementation::
|
.. method:: WizardView.process_step(form)
|
||||||
|
|
||||||
def prefix_for_step(self, step):
|
|
||||||
return str(step)
|
|
||||||
|
|
||||||
.. method:: FormWizard.render_hash_failure
|
|
||||||
|
|
||||||
Renders a template if the hash check fails. It's rare that you'd need to
|
|
||||||
override this.
|
|
||||||
|
|
||||||
Default implementation::
|
|
||||||
|
|
||||||
def render_hash_failure(self, request, step):
|
|
||||||
return self.render(self.get_form(step), request, step,
|
|
||||||
context={'wizard_error':
|
|
||||||
'We apologize, but your form has expired. Please'
|
|
||||||
' continue filling out the form from this page.'})
|
|
||||||
|
|
||||||
.. method:: FormWizard.security_hash
|
|
||||||
|
|
||||||
Calculates the security hash for the given request object and
|
|
||||||
:class:`~django.forms.Form` instance.
|
|
||||||
|
|
||||||
By default, this generates a SHA1 HMAC using your form data and your
|
|
||||||
:setting:`SECRET_KEY` setting. It's rare that somebody would need to
|
|
||||||
override this.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
def security_hash(self, request, form):
|
|
||||||
return my_hash_function(request, form)
|
|
||||||
|
|
||||||
.. method:: FormWizard.parse_params
|
|
||||||
|
|
||||||
A hook for saving state from the request object and ``args`` / ``kwargs``
|
|
||||||
that were captured from the URL by your URLconf.
|
|
||||||
|
|
||||||
By default, this does nothing.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
def parse_params(self, request, *args, **kwargs):
|
|
||||||
self.my_state = args[0]
|
|
||||||
|
|
||||||
.. method:: FormWizard.get_template
|
|
||||||
|
|
||||||
Returns the name of the template that should be used for the given step.
|
|
||||||
|
|
||||||
By default, this returns :file:`'forms/wizard.html'`, regardless of step.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
def get_template(self, step):
|
|
||||||
return 'myapp/wizard_%s.html' % step
|
|
||||||
|
|
||||||
If :meth:`~FormWizard.get_template` returns a list of strings, then the
|
|
||||||
wizard will use the template system's
|
|
||||||
:func:`~django.template.loader.select_template` function.
|
|
||||||
This means the system will use the first template that exists on the
|
|
||||||
filesystem. For example::
|
|
||||||
|
|
||||||
def get_template(self, step):
|
|
||||||
return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
|
|
||||||
|
|
||||||
.. method:: FormWizard.render_template
|
|
||||||
|
|
||||||
Renders the template for the given step, returning an
|
|
||||||
:class:`~django.http.HttpResponse` object.
|
|
||||||
|
|
||||||
Override this method if you want to add a custom context, return a
|
|
||||||
different MIME type, etc. If you only need to override the template name,
|
|
||||||
use :meth:`~FormWizard.get_template` instead.
|
|
||||||
|
|
||||||
The template will be rendered with the context documented in the
|
|
||||||
"Creating templates for the forms" section above.
|
|
||||||
|
|
||||||
.. method:: FormWizard.process_step
|
|
||||||
|
|
||||||
Hook for modifying the wizard's internal state, given a fully validated
|
Hook for modifying the wizard's internal state, given a fully validated
|
||||||
:class:`~django.forms.Form` object. The Form is guaranteed to have clean,
|
:class:`~django.forms.Form` object. The Form is guaranteed to have clean,
|
||||||
valid data.
|
valid data.
|
||||||
|
|
||||||
This method should *not* modify any of that data. Rather, it might want to
|
|
||||||
set ``self.extra_context`` or dynamically alter ``self.form_list``, based
|
|
||||||
on previously submitted forms.
|
|
||||||
|
|
||||||
Note that this method is called every time a page is rendered for *all*
|
Note that this method is called every time a page is rendered for *all*
|
||||||
submitted steps.
|
submitted steps.
|
||||||
|
|
||||||
The function signature::
|
The default implementation::
|
||||||
|
|
||||||
def process_step(self, request, form, step):
|
def process_step(self, form):
|
||||||
# ...
|
return self.get_form_step_data(form)
|
||||||
|
|
||||||
|
.. method:: WizardView.get_form_initial(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 should be returned.
|
||||||
|
|
||||||
|
The default implementation::
|
||||||
|
|
||||||
|
def get_form_initial(self, step):
|
||||||
|
return self.initial_dict.get(step, {})
|
||||||
|
|
||||||
|
.. method:: WizardView.get_form_instance(step)
|
||||||
|
|
||||||
|
Returns a object which will be passed to the form for ``step`` as
|
||||||
|
``instance``. If no instance object was provied while initializing
|
||||||
|
the form wizard, None be returned.
|
||||||
|
|
||||||
|
The default implementation::
|
||||||
|
|
||||||
|
def get_form_instance(self, step):
|
||||||
|
return self.instance_dict.get(step, None)
|
||||||
|
|
||||||
|
.. method:: WizardView.get_context_data(form, **kwargs)
|
||||||
|
|
||||||
|
Returns the template context for a step. You can overwrite this method
|
||||||
|
to add more data for all or some steps. This method returns a dictionary
|
||||||
|
containing the rendered form step.
|
||||||
|
|
||||||
|
The default template context variables are:
|
||||||
|
|
||||||
|
* Any extra data the storage backend has stored
|
||||||
|
* ``form`` -- form instance of the current step
|
||||||
|
* ``wizard`` -- the wizard instance itself
|
||||||
|
|
||||||
|
Example to add extra variables for a specific step::
|
||||||
|
|
||||||
|
def get_context_data(self, form, **kwargs):
|
||||||
|
context = super(MyWizard, self).get_context_data(form, **kwargs)
|
||||||
|
if self.steps.current == 'my_step_name':
|
||||||
|
context.update({'another_var': True})
|
||||||
|
return context
|
||||||
|
|
||||||
|
.. method:: WizardView.get_wizard_name()
|
||||||
|
|
||||||
|
This method can be used to change the wizard's internal name.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def get_wizard_name(self):
|
||||||
|
return normalize_name(self.__class__.__name__)
|
||||||
|
|
||||||
|
.. method:: WizardView.get_prefix()
|
||||||
|
|
||||||
|
This method returns a prefix for the storage backends. These backends use
|
||||||
|
the prefix to fetch the correct data for the wizard. (Multiple wizards
|
||||||
|
could save their data in one session)
|
||||||
|
|
||||||
|
You can change this method to make the wizard data prefix more unique to,
|
||||||
|
e.g. have multiple instances of one wizard in one session.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def get_prefix(self):
|
||||||
|
return self.wizard_name
|
||||||
|
|
||||||
|
.. method:: WizardView.get_form(step=None, data=None, files=None)
|
||||||
|
|
||||||
|
This method constructs the form for a given ``step``. If no ``step`` is
|
||||||
|
defined, the current step will be determined automatically.
|
||||||
|
The method gets three arguments:
|
||||||
|
|
||||||
|
* ``step`` -- The step for which the form instance should be generated.
|
||||||
|
* ``data`` -- Gets passed to the form's data argument
|
||||||
|
* ``files`` -- Gets passed to the form's files argument
|
||||||
|
|
||||||
|
You can override this method to add extra arguments to the form instance.
|
||||||
|
|
||||||
|
Example code to add a user attribute to the form on step 2::
|
||||||
|
|
||||||
|
def get_form(self, step=None, data=None, files=None):
|
||||||
|
form = super(MyWizard, self).get_form(step, data, files)
|
||||||
|
if step == '1':
|
||||||
|
form.user = self.request.user
|
||||||
|
return form
|
||||||
|
|
||||||
|
.. method:: WizardView.process_step(form)
|
||||||
|
|
||||||
|
This method gives you a way to post-process the form data before the data
|
||||||
|
gets stored within the storage backend. By default it just passed the
|
||||||
|
form.data dictionary. You should not manipulate the data here but you can
|
||||||
|
use the data to do some extra work if needed (e.g. set storage extra data).
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def process_step(self, form):
|
||||||
|
return self.get_form_step_data(form)
|
||||||
|
|
||||||
|
.. method:: WizardView.process_step_files(form)
|
||||||
|
|
||||||
|
This method gives you a way to post-process the form files before the
|
||||||
|
files gets stored within the storage backend. By default it just passed
|
||||||
|
the ``form.files`` dictionary. You should not manipulate the data here
|
||||||
|
but you can use the data to do some extra work if needed (e.g. set storage
|
||||||
|
extra data).
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def process_step_files(self, form):
|
||||||
|
return self.get_form_step_files(form)
|
||||||
|
|
||||||
|
.. method:: WizardView.render_revalidation_failure(step, form, **kwargs)
|
||||||
|
|
||||||
|
When the wizard thinks, all steps passed it revalidates all forms with the
|
||||||
|
data from the backend storage.
|
||||||
|
|
||||||
|
If any of the forms don't validate correctly, this method gets called.
|
||||||
|
This method expects two arguments, ``step`` and ``form``.
|
||||||
|
|
||||||
|
The default implementation resets the current step to the first failing
|
||||||
|
form and redirects the user to the invalid form.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def render_revalidation_failure(self, step, form, **kwargs):
|
||||||
|
self.storage.current_step = step
|
||||||
|
return self.render(form, **kwargs)
|
||||||
|
|
||||||
|
.. method:: WizardView.get_form_step_data(form)
|
||||||
|
|
||||||
|
This method fetches the form data from and returns the dictionary. You
|
||||||
|
can use this method to manipulate the values before the data gets stored
|
||||||
|
in the storage backend.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def get_form_step_data(self, form):
|
||||||
|
return form.data
|
||||||
|
|
||||||
|
.. method:: WizardView.get_form_step_files(form)
|
||||||
|
|
||||||
|
This method returns the form files. You can use this method to manipulate
|
||||||
|
the files before the data gets stored in the storage backend.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def get_form_step_files(self, form):
|
||||||
|
return form.files
|
||||||
|
|
||||||
|
.. method:: WizardView.render(form, **kwargs)
|
||||||
|
|
||||||
|
This method gets called after the get or post request was handled. You can
|
||||||
|
hook in this method to, e.g. change the type of http response.
|
||||||
|
|
||||||
|
Default implementation::
|
||||||
|
|
||||||
|
def render(self, form=None, **kwargs):
|
||||||
|
form = form or self.get_form()
|
||||||
|
context = self.get_context_data(form, **kwargs)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
Providing initial data for the forms
|
Providing initial data for the forms
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
.. attribute:: FormWizard.initial
|
.. attribute:: WizardView.initial_dict
|
||||||
|
|
||||||
Initial data for a wizard's :class:`~django.forms.Form` objects can be
|
Initial data for a wizard's :class:`~django.forms.Form` objects can be
|
||||||
provided using the optional :attr:`~FormWizard.initial` keyword argument.
|
provided using the optional :attr:`~Wizard.initial_dict` keyword argument.
|
||||||
This argument should be a dictionary mapping a step to a dictionary
|
This argument should be a dictionary mapping the steps to dictionaries
|
||||||
containing the initial data for that step. The dictionary of initial data
|
containing the initial data for each step. The dictionary of initial data
|
||||||
will be passed along to the constructor of the step's
|
will be passed along to the constructor of the step's
|
||||||
:class:`~django.forms.Form`::
|
:class:`~django.forms.Form`::
|
||||||
|
|
||||||
>>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard
|
>>> from myapp.forms import ContactForm1, ContactForm2
|
||||||
|
>>> from myapp.views import ContactWizard
|
||||||
>>> initial = {
|
>>> initial = {
|
||||||
... 0: {'subject': 'Hello', 'sender': 'user@example.com'},
|
... '0': {'subject': 'Hello', 'sender': 'user@example.com'},
|
||||||
... 1: {'message': 'Hi there!'}
|
... '1': {'message': 'Hi there!'}
|
||||||
... }
|
... }
|
||||||
>>> wiz = ContactWizard([ContactForm1, ContactForm2], initial=initial)
|
>>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)
|
||||||
>>> form1 = wiz.get_form(0)
|
>>> form1 = wiz.get_form('0')
|
||||||
>>> form2 = wiz.get_form(1)
|
>>> form2 = wiz.get_form('1')
|
||||||
>>> form1.initial
|
>>> form1.initial
|
||||||
{'sender': 'user@example.com', 'subject': 'Hello'}
|
{'sender': 'user@example.com', 'subject': 'Hello'}
|
||||||
>>> form2.initial
|
>>> form2.initial
|
||||||
{'message': 'Hi there!'}
|
{'message': 'Hi there!'}
|
||||||
|
|
||||||
|
The ``initial_dict`` can also take a list of dictionaries for a specific
|
||||||
|
step if the step is a ``FormSet``.
|
||||||
|
|
||||||
|
.. _wizard-files:
|
||||||
|
|
||||||
|
Handling files
|
||||||
|
==============
|
||||||
|
|
||||||
|
To handle :class:`~django.forms.FileField` within any step form of the wizard,
|
||||||
|
you have to add a :attr:`file_storage` to your :class:`WizardView` subclass.
|
||||||
|
|
||||||
|
This storage will temporarilyy store the uploaded files for the wizard. The
|
||||||
|
:attr:`file_storage` attribute should be a
|
||||||
|
:class:`~django.core.files.storage.Storage` subclass.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Please remember to take care of removing old files as the
|
||||||
|
:class:`WizardView` won't remove any files, whether the wizard gets
|
||||||
|
finished corretly or not.
|
||||||
|
|
||||||
|
Conditionally view/skip specific steps
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. attribute:: WizardView.condition_dict
|
||||||
|
|
||||||
|
The :meth:`~WizardView.as_view` accepts a ``condition_dict`` argument. You can pass a
|
||||||
|
dictionary of boolean values or callables. The key should match the steps
|
||||||
|
name (e.g. '0', '1').
|
||||||
|
|
||||||
|
If the value of a specific step is callable it will be called with the
|
||||||
|
:class:`WizardView` instance as the only argument. If the return value is true,
|
||||||
|
the step's form will be used.
|
||||||
|
|
||||||
|
This example provides a contact form including a condition. The condition is
|
||||||
|
used to show a message from only if a checkbox in the first step was checked.
|
||||||
|
|
||||||
|
The steps are defined in a ``forms.py``::
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
class ContactForm1(forms.Form):
|
||||||
|
subject = forms.CharField(max_length=100)
|
||||||
|
sender = forms.EmailField()
|
||||||
|
leave_message = forms.BooleanField(required=False)
|
||||||
|
|
||||||
|
class ContactForm2(forms.Form):
|
||||||
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
|
|
||||||
|
We define our wizard in a ``views.py``::
|
||||||
|
|
||||||
|
from django.shortcuts import render_to_response
|
||||||
|
from django.contrib.formtools.wizard.views import SessionWizardView
|
||||||
|
|
||||||
|
def show_message_form_condition(wizard):
|
||||||
|
# try to get the cleaned data of step 1
|
||||||
|
cleaned_data = wizard.get_cleaned_data_for_step('0') or {}
|
||||||
|
# check if the field ``leave_message`` was checked.
|
||||||
|
return cleaned_data.get('leave_message', True)
|
||||||
|
|
||||||
|
class ContactWizard(SessionWizardView):
|
||||||
|
|
||||||
|
def done(self, form_list, **kwargs):
|
||||||
|
return render_to_response('done.html', {
|
||||||
|
'form_data': [form.cleaned_data for form in form_list],
|
||||||
|
})
|
||||||
|
|
||||||
|
We need to add the ``ContactWizard`` to our ``urls.py`` file::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import pattern
|
||||||
|
|
||||||
|
from myapp.forms import ContactForm1, ContactForm2
|
||||||
|
from myapp.views import ContactWizard, show_message_form_condition
|
||||||
|
|
||||||
|
contact_forms = [ContactForm1, ContactForm2]
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^contact/$', ContactWizard.as_view(contact_forms,
|
||||||
|
condition_dict={'1': show_message_form_condition}
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
As you can see, we defined a ``show_message_form_condition`` next to our
|
||||||
|
:class:`WizardView` subclass and added a ``condition_dict`` argument to the
|
||||||
|
:meth:`~WizardView.as_view` method. The key refers to the second wizard step
|
||||||
|
(because of the zero based step index).
|
||||||
|
|
||||||
|
How to work with ModelForm and ModelFormSet
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
The WizardView supports :class:`~django.forms.ModelForm` and
|
||||||
|
:class:`~django.forms.ModelFormSet`. Additionally to the ``initial_dict``,
|
||||||
|
the :meth:`~WizardView.as_view` method takes a ``instance_dict`` argument
|
||||||
|
with a list of instances for the ``ModelForm`` and ``ModelFormSet``.
|
||||||
|
|
||||||
|
Usage of NamedUrlWizardView
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. class:: NamedUrlWizardView
|
||||||
|
|
||||||
|
There is a :class:`WizardView` subclass which adds named-urls support to the wizard.
|
||||||
|
By doing this, you can have single urls for every step.
|
||||||
|
|
||||||
|
To use the named urls, you have to change the ``urls.py``.
|
||||||
|
|
||||||
|
Below you will see an example of a contact wizard with two steps, step 1 with
|
||||||
|
"contactdata" as its name and step 2 with "leavemessage" as its name.
|
||||||
|
|
||||||
|
Additionally you have to pass two more arguments to the
|
||||||
|
:meth:`~WizardView.as_view` method:
|
||||||
|
|
||||||
|
* ``url_name`` -- the name of the url (as provided in the urls.py)
|
||||||
|
* ``done_step_name`` -- the name in the url for the done step
|
||||||
|
|
||||||
|
Example code for the changed ``urls.py`` file::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import url, patterns
|
||||||
|
|
||||||
|
from myapp.forms import ContactForm1, ContactForm2
|
||||||
|
from myapp.views import ContactWizard
|
||||||
|
|
||||||
|
named_contact_forms = (
|
||||||
|
('contactdata', ContactForm1),
|
||||||
|
('leavemessage', ContactForm2),
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_wizard = ContactWizard.as_view(named_contact_forms,
|
||||||
|
url_name='contact_step', done_step_name='finished')
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'),
|
||||||
|
url(r'^contact/$', contact_wizard, name='contact'),
|
||||||
|
)
|
||||||
|
|
|
@ -55,6 +55,22 @@ signing in Web applications.
|
||||||
|
|
||||||
See :doc:`cryptographic signing </topics/signing>` docs for more information.
|
See :doc:`cryptographic signing </topics/signing>` docs for more information.
|
||||||
|
|
||||||
|
New form wizard
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The previously shipped ``FormWizard`` of the formtools contrib app has been
|
||||||
|
replaced with a new implementation that is based on the class based views
|
||||||
|
introduced in Django 1.3. It features a pluggable storage API and doesn't
|
||||||
|
require the wizard to pass around hidden fields for every previous step.
|
||||||
|
|
||||||
|
Django 1.4 ships with a session based storage backend and a cookie based
|
||||||
|
storage backend. The latter uses the tools for
|
||||||
|
:doc:`cryptographic signing </topics/signing>` also introduced in
|
||||||
|
Django 1.4 to store the wizard state in the user's cookies.
|
||||||
|
|
||||||
|
See the :doc:`form wizard </ref/contrib/formtools/form-wizard>` docs for
|
||||||
|
more information.
|
||||||
|
|
||||||
Simple clickjacking protection
|
Simple clickjacking protection
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
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("")
|
|
|
@ -1,59 +0,0 @@
|
||||||
import re
|
|
||||||
from django import forms
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
class FormWizardWithNullBooleanField(TestCase):
|
|
||||||
urls = 'regressiontests.formwizard.urls'
|
|
||||||
|
|
||||||
input_re = re.compile('name="([^"]+)" value="([^"]+)"')
|
|
||||||
|
|
||||||
wizard_url = '/wiz/'
|
|
||||||
wizard_step_data = (
|
|
||||||
{
|
|
||||||
'0-name': 'Pony',
|
|
||||||
'0-thirsty': '2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'1-address1': '123 Main St',
|
|
||||||
'1-address2': 'Djangoland',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'2-random_crap': 'blah blah',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def grabFieldData(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 checkWizardStep(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.grabFieldData(response)
|
|
||||||
data.update(self.wizard_step_data[step_no - 1])
|
|
||||||
|
|
||||||
return self.client.post(self.wizard_url, data)
|
|
||||||
|
|
||||||
def testWizard(self):
|
|
||||||
response = self.client.get(self.wizard_url)
|
|
||||||
for step_no in range(1, len(self.wizard_step_data) + 1):
|
|
||||||
response = self.checkWizardStep(response, step_no)
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
from forms import ContactWizard, Page1, Page2, Page3
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
url(r'^wiz/$', ContactWizard([Page1, Page2, Page3])),
|
|
||||||
)
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy, lazy_property
|
||||||
|
|
||||||
|
|
||||||
class FunctionalTestCase(unittest.TestCase):
|
class FunctionalTestCase(unittest.TestCase):
|
||||||
|
@ -20,3 +20,20 @@ class FunctionalTestCase(unittest.TestCase):
|
||||||
|
|
||||||
t = lazy(lambda: Klazz(), Klazz)()
|
t = lazy(lambda: Klazz(), Klazz)()
|
||||||
self.assertTrue('base_method' in dir(t))
|
self.assertTrue('base_method' in dir(t))
|
||||||
|
|
||||||
|
def test_lazy_property(self):
|
||||||
|
|
||||||
|
class A(object):
|
||||||
|
|
||||||
|
def _get_do(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
def _set_do(self, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
do = lazy_property(_get_do, _set_do)
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def _get_do(self):
|
||||||
|
return "DO IT"
|
||||||
|
|
||||||
|
self.assertRaises(NotImplementedError, lambda: A().do)
|
||||||
|
self.assertEqual(B().do, 'DO IT')
|
||||||
|
|
Loading…
Reference in New Issue