`.
Here's a full example template:
@@ -166,170 +181,426 @@ Here's a full example template:
{% extends "base.html" %}
{% block content %}
- Step {{ step }} of {{ step_count }}
+ Step {{ wizard.steps.current }} of {{ wizard.steps.count }}
{% endblock %}
-Note that ``previous_fields``, ``step_field`` and ``step0`` are all required
-for the wizard to work properly.
+.. note::
+
+ Note that ``management_form`` **must be used** for the wizard to work properly.
-.. _hooking-wizard-into-urlconf:
+.. _wizard-urlconf:
Hooking the wizard into a URLconf
-=================================
+---------------------------------
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
-wizard takes a list of your :class:`~django.forms.Form` objects as
-arguments when you instantiate the Wizard::
+deploy the new :class:`WizardView` object a URL in the ``urls.py``. The
+wizard's :meth:`as_view` method takes a list of your
+:class:`~django.forms.Form` classes as an argument during instantiation::
- from django.conf.urls.defaults import *
- from testapp.forms import ContactForm1, ContactForm2, ContactWizard
+ from django.conf.urls.defaults import patterns
+
+ from myapp.forms import ContactForm1, ContactForm2
+ from myapp.views import ContactWizard
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.
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
- is ``0`` and the second form is ``1``.)
+ counter as string representing the current step of the wizard. (E.g., the
+ 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
`.
+.. method:: WizardView.process_step(form)
+
+ Hook for modifying the wizard's internal state, given a fully validated
+ :class:`~django.forms.Form` object. The Form is guaranteed to have clean,
+ valid data.
+
+ Note that this method is called every time a page is rendered for *all*
+ submitted steps.
+
+ The default implementation::
+
+ 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 data that :attr:`WizardView.get_extra_data` returns.
+ * ``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 prefix_for_step(self, step):
- return str(step)
+ def get_wizard_name(self):
+ return normalize_name(self.__class__.__name__)
+
+.. method:: WizardView.get_prefix()
-.. method:: FormWizard.render_hash_failure
+ 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)
- Renders a template if the hash check fails. It's rare that you'd need to
- override this.
+ 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 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.'})
+ def get_prefix(self):
+ return self.wizard_name
-.. method:: FormWizard.security_hash
+.. method:: WizardView.get_form(step=None, data=None, files=None)
- Calculates the security hash for the given request object and
- :class:`~django.forms.Form` instance.
+ 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:
- 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.
+ * ``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
- Example::
+ You can override this method to add extra arguments to the form instance.
- def security_hash(self, request, form):
- return my_hash_function(request, form)
+ Example code to add a user attribute to the form on step 2::
-.. method:: FormWizard.parse_params
+ 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
- A hook for saving state from the request object and ``args`` / ``kwargs``
- that were captured from the URL by your URLconf.
+.. method:: WizardView.process_step(form)
- By default, this does nothing.
+ 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 extra_data).
- Example::
+ Default implementation::
- def parse_params(self, request, *args, **kwargs):
- self.my_state = args[0]
+ def process_step(self, form):
+ return self.get_form_step_data(form)
-.. method:: FormWizard.get_template
+.. method:: WizardView.process_step_files(form)
- Returns the name of the template that should be used for the given step.
+ 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 extra
+ data).
- By default, this returns :file:`'forms/wizard.html'`, regardless of step.
+ Default implementation::
- Example::
+ def process_step_files(self, form):
+ return self.get_form_step_files(form)
- def get_template(self, step):
- return 'myapp/wizard_%s.html' % step
+.. method:: WizardView.render_revalidation_failure(step, form, **kwargs)
- 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::
+ When the wizard thinks, all steps passed it revalidates all forms with the
+ data from the backend storage.
- def get_template(self, step):
- return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
+ If any of the forms don't validate correctly, this method gets called.
+ This method expects two arguments, ``step`` and ``form``.
-.. method:: FormWizard.render_template
+ The default implementation resets the current step to the first failing
+ form and redirects the user to the invalid form.
- Renders the template for the given step, returning an
- :class:`~django.http.HttpResponse` object.
+ Default implementation::
- 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.
+ def render_revalidation_failure(self, step, form, **kwargs):
+ self.storage.current_step = step
+ return self.render(form, **kwargs)
- The template will be rendered with the context documented in the
- "Creating templates for the forms" section above.
+.. method:: WizardView.get_form_step_data(form)
-.. method:: FormWizard.process_step
+ 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.
- Hook for modifying the wizard's internal state, given a fully validated
- :class:`~django.forms.Form` object. The Form is guaranteed to have clean,
- valid data.
+ Default implementation::
- 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.
+ def get_form_step_data(self, form):
+ return form.data
- Note that this method is called every time a page is rendered for *all*
- submitted steps.
+.. 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.get_extra_data
+
+ This method returns the content of the stored extra data. You can override
+ this method to change the extra data which gets passed to the template.
+ The default implementation passes the extra data dictionary from the
+ storage backend.
+
+ Default implementation::
+
+ def get_extra_data(self):
+ return self.storage.extra_data
+
+.. method:: WizardView.update_extra_data(data)
+
+ This method expects one argument ``data``. The method will fetch the
+ current extra data, update the dictionary with the passed data and store
+ the content back to storage backend.
+
+ You could change this method to protect the extra_data dictionary from
+ external changes by just doing nothing.
- The function signature::
+ Example code::
- def process_step(self, request, form, step):
- # ...
+ def update_extra_data(self, data):
+ pass
+
+.. 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
====================================
-.. attribute:: FormWizard.initial
+.. attribute:: WizardView.initial_dict
Initial data for a wizard's :class:`~django.forms.Form` objects can be
- provided using the optional :attr:`~FormWizard.initial` keyword argument.
- This argument should be a dictionary mapping a step to a dictionary
- containing the initial data for that step. The dictionary of initial data
+ provided using the optional :attr:`~Wizard.initial_dict` keyword argument.
+ This argument should be a dictionary mapping the steps to dictionaries
+ containing the initial data for each step. The dictionary of initial data
will be passed along to the constructor of the step's
:class:`~django.forms.Form`::
- >>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard
+ >>> from myapp.forms import ContactForm1, ContactForm2
+ >>> from myapp.views import ContactWizard
>>> initial = {
- ... 0: {'subject': 'Hello', 'sender': 'user@example.com'},
- ... 1: {'message': 'Hi there!'}
+ ... '0': {'subject': 'Hello', 'sender': 'user@example.com'},
+ ... '1': {'message': 'Hi there!'}
... }
- >>> wiz = ContactWizard([ContactForm1, ContactForm2], initial=initial)
- >>> form1 = wiz.get_form(0)
- >>> form2 = wiz.get_form(1)
+ >>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)
+ >>> form1 = wiz.get_form('0')
+ >>> form2 = wiz.get_form('1')
>>> form1.initial
{'sender': 'user@example.com', 'subject': 'Hello'}
>>> form2.initial
{'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.+)/$', contact_wizard, name='contact_step'),
+ url(r'^contact/$', contact_wizard, name='contact'),
+ )
diff --git a/tests/regressiontests/formwizard/tests.py b/tests/regressiontests/formwizard/tests.py
index 0c94d2e..944569d 100644
--- a/tests/regressiontests/formwizard/tests.py
+++ b/tests/regressiontests/formwizard/tests.py
@@ -1,7 +1,12 @@
import re
+import warnings
from django import forms
from django.test import TestCase
+warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
+ module='django.contrib.formtools.wizard')
+
+
class FormWizardWithNullBooleanField(TestCase):
urls = 'regressiontests.formwizard.urls'
diff --git a/tests/regressiontests/utils/functional.py b/tests/regressiontests/utils/functional.py
index 2784ddd..90a6f08 100644
--- a/tests/regressiontests/utils/functional.py
+++ b/tests/regressiontests/utils/functional.py
@@ -1,5 +1,5 @@
from django.utils import unittest
-from django.utils.functional import lazy
+from django.utils.functional import lazy, lazy_property
class FunctionalTestCase(unittest.TestCase):
@@ -20,3 +20,20 @@ class FunctionalTestCase(unittest.TestCase):
t = lazy(lambda: Klazz(), Klazz)()
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')