From 7547a2117f30383b6da5173507b2a0013ee77fc7 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 +++++++ .../formtools/wizard/wizard_form.html | 0 tests/tests/tests.py | 20 +++++++ tests/tests/urls.py | 20 +++++-- 6 files changed, 118 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/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..67488c0 100644 --- a/tests/tests/urls.py +++ b/tests/tests/urls.py @@ -1,5 +1,19 @@ -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 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'), +]