From 0749ff10dbe19f3ad4817aab2a113aa92836aa19 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Mon, 2 Mar 2015 00:42:22 -0700 Subject: [PATCH] Added support for WizardView --- betterforms/multiform.py | 9 +++- docs/multiform.rst | 53 +++++++++++++++++++ tests/tests/forms.py | 20 +++++++ tests/tests/settings.py | 7 +++ .../formtools/wizard/wizard_form.html | 0 tests/tests/tests.py | 20 +++++++ tests/tests/urls.py | 26 +++++++-- 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tests/tests/templates/formtools/wizard/wizard_form.html diff --git a/betterforms/multiform.py b/betterforms/multiform.py index ab8d5f0..480098f 100644 --- a/betterforms/multiform.py +++ b/betterforms/multiform.py @@ -25,7 +25,14 @@ class MultiForm(object): """ form_classes = {} - def __init__(self, *args, **kwargs): + def __init__(self, data=None, files=None, *args, **kwargs): + # Some things, such as the WizardView expect these to exist. + self.data, self.files = data, files + kwargs.update( + data=data, + files=files, + ) + self.initials = kwargs.pop('initial', None) if self.initials is None: self.initials = {} diff --git a/docs/multiform.rst b/docs/multiform.rst index 6cd3a71..06f89cb 100644 --- a/docs/multiform.rst +++ b/docs/multiform.rst @@ -213,6 +213,59 @@ user/profile example, it would look something like this:: return kwargs +Working with WizardView +----------------------- + +:class:`MultiForms ` also support the ``WizardView`` classes +provided by django-formtools_ (or Django before 1.8), however you must set a +``base_fields`` attribute on your form class. :: + + # forms.py + from django import forms + from betterforms.multiform import MultiForm + + class Step1Form(MultiModelForm): + # We have to set base_fields to a dictionary because the WizardView + # tries to introspect it. + base_fields = {} + + form_classes = { + 'user': UserEditForm, + 'profile': UserProfileForm, + } + +Then you can use it like normal. :: + + # views.py + try: + from django.contrib.formtools.wizard.views import SessionWizardView + except ImportError: # Django >= 1.8 + from formtools.wizard.views import SessionWizardView + + from .forms import Step1Form, Step2Form + + class MyWizardView(SessionWizardView): + def done(self, form_list, **kwargs): + step1form = form_list[0] + # You can get the data for the user form like this: + user = step1form['user'].save() + # ... + + wizard_view = MyWizardView.as_view([Step1Form, Step2Form]) + +The reason we have to set ``base_fields`` to a dictionary is that the +``WizardView`` does some introspection to determine if any of the forms accept +files and then it makes sure that the ``WizardView`` has a ``file_storage`` on +it. By setting ``base_fields`` to an empty dictionary, we can bypass this check. + +.. warning:: + + If you have have any forms that accept Files, you must configure the + ``file_storage`` attribute for your WizardView. + +.. _django-formtools: http://django-formtools.readthedocs.org/en/latest/wizard.html + + API Reference ------------- diff --git a/tests/tests/forms.py b/tests/tests/forms.py index ad669d8..820c6be 100644 --- a/tests/tests/forms.py +++ b/tests/tests/forms.py @@ -100,3 +100,23 @@ class ManyToManyMultiForm(MultiModelForm): 'badge': BadgeForm, 'author': AuthorForm, } + + +class OptionalFileForm(forms.Form): + myfile = forms.FileField(required=False) + + +class Step1Form(MultiModelForm): + # This is required because the WizardView introspects it, but we don't have + # a way of determining this dynamically, so just set it to an empty + # dictionary. + base_fields = {} + + form_classes = { + 'myfile': OptionalFileForm, + 'profile': ProfileForm, + } + + +class Step2Form(forms.Form): + confirm = forms.BooleanField(required=True) diff --git a/tests/tests/settings.py b/tests/tests/settings.py index 2d3d57b..ee324e0 100644 --- a/tests/tests/settings.py +++ b/tests/tests/settings.py @@ -5,6 +5,13 @@ PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + # We need the SessionMiddleware for the WizardView support tests in Django >= 1.7 + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', +) + INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.contenttypes', diff --git a/tests/tests/templates/formtools/wizard/wizard_form.html b/tests/tests/templates/formtools/wizard/wizard_form.html new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests/tests.py b/tests/tests/tests.py index 4d4f2f3..b60be0d 100644 --- a/tests/tests/tests.py +++ b/tests/tests/tests.py @@ -6,6 +6,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.views.generic import CreateView +from django.core import urlresolvers try: from django.utils.encoding import force_text @@ -16,6 +17,7 @@ from .forms import ( UserProfileMultiForm, BadgeMultiForm, ErrorMultiForm, MixedForm, NeedsFileField, ManyToManyMultiForm, + Step2Form, ) @@ -155,6 +157,24 @@ def test_handles_none_initial_value(self): # Used to throw an AttributeError UserProfileMultiForm(initial=None) + def test_works_with_wizard_view(self): + url = urlresolvers.reverse('test_wizard') + self.client.get(url) + + response = self.client.post(url, { + 'test_wizard_view-current_step': '0', + 'profile__0-name': 'John Doe', + }) + view = response.context['view'] + self.assertEqual(view.storage.current_step, '1') + + response = self.client.post(url, { + 'test_wizard_view-current_step': '1', + '1-confirm': True, + }) + form_list = response.context['form_list'] + self.assertEqual(form_list[0]['profile'].cleaned_data['name'], 'John Doe') + class MultiModelFormTest(TestCase): def test_save(self): diff --git a/tests/tests/urls.py b/tests/tests/urls.py index 4566873..8bd8119 100644 --- a/tests/tests/urls.py +++ b/tests/tests/urls.py @@ -1,5 +1,25 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import url +# TODO: this was removed in Django 1.8 we should get it from django-formtools +from django.contrib.formtools.wizard.views import SessionWizardView -urlpatterns = patterns('', -) +from .forms import Step1Form, Step2Form + + +class TestWizardView(SessionWizardView): + def get_context_data(self, **kwargs): + kwargs = super(TestWizardView, self).get_context_data(**kwargs) + # Django < 1.5 does not set the view object in the context + kwargs['view'] = self + return kwargs + + def done(self, form_list, **kwargs): + context = { + 'form_list': form_list, + } + return self.render_to_response(context) + + +urlpatterns = [ + url(r'^test-wizard-view/$', TestWizardView.as_view([Step1Form, Step2Form]), name='test_wizard'), +]