Skip to content

Commit

Permalink
Added support for WizardView
Browse files Browse the repository at this point in the history
  • Loading branch information
Rocky Meza committed Mar 2, 2015
1 parent a840355 commit 0749ff1
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 4 deletions.
9 changes: 8 additions & 1 deletion betterforms/multiform.py
Expand Up @@ -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 = {}
Expand Down
53 changes: 53 additions & 0 deletions docs/multiform.rst
Expand Up @@ -213,6 +213,59 @@ user/profile example, it would look something like this::
return kwargs


Working with WizardView
-----------------------

:class:`MultiForms <MultiForm>` 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
-------------

Expand Down
20 changes: 20 additions & 0 deletions tests/tests/forms.py
Expand Up @@ -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)
7 changes: 7 additions & 0 deletions tests/tests/settings.py
Expand Up @@ -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',
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions tests/tests/tests.py
Expand Up @@ -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
Expand All @@ -16,6 +17,7 @@
from .forms import (
UserProfileMultiForm, BadgeMultiForm, ErrorMultiForm,
MixedForm, NeedsFileField, ManyToManyMultiForm,
Step2Form,
)


Expand Down Expand Up @@ -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):
Expand Down
26 changes: 23 additions & 3 deletions 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'),
]

0 comments on commit 0749ff1

Please sign in to comment.