Index: django/contrib/formtools/wizard.py
===================================================================
--- django/contrib/formtools/wizard.py	(revision 0)
+++ django/contrib/formtools/wizard.py	(revision 0)
@@ -0,0 +1,211 @@
+"""
+USAGE:
+    views:
+        class MyWizard(Wizard):
+            def done(self, request, form_list):
+                return render_to_response('done.html', {'form_data' : [ form.cleaned_data for form in form_list ] })
+    urls: 
+        (r'^$', MyWizard([MyForm, MyForm, MyForm])),
+ 
+    form template:
+        <form action="." method="POST">
+        FORM({{ step }}): {{ form }}
+        
+        step_info : <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
+ 
+        previous_fields: {{ previous_fields }}
+ 
+        <input type="submit">
+        </form>
+
+    done.html:
+        {% for data in form_data %}
+            {% for item in data.items %}
+                {{ item.0 }} : {{ item.1 }}<br />
+            {% endfor %}
+        {% endfor %}
+        
+"""
+from django.conf import settings
+from django.http import Http404
+from django.shortcuts import render_to_response
+from django.template.context import RequestContext
+ 
+from django import newforms as forms
+import cPickle as pickle
+import md5 
+ 
+class Wizard(object):
+    PREFIX="%d"
+    STEP_FIELD="wizard_step"
+ 
+    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
+    def __init__(self, form_list, initial=None):
+        " Pass list of Form classes (not instances !) "
+        self.form_list = form_list[:]
+        self.initial = initial or {}
+ 
+    def __repr__(self):
+        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
+    
+    def get_form(self, step, data=None):
+        " Shortcut to return form instance. "
+        return self.form_list[step](data, prefix=self.PREFIX % step, initial=self.initial.get(step, None)) 
+
+    def __call__(self, request, *args, **kwargs):
+        """
+        Main function that does all the hard work:
+            - initializes the wizard object (via parse_params())
+            - veryfies (using security_hash()) that noone has tempered with the data since we last saw them
+                calls failed_hash() if it is so
+                calls process_step() for every previously submitted form
+            - validates current form and
+                returns it again if errors were found
+                returns done() if it was the last form
+                returns next form otherwise
+        """
+        # add extra_context, we don't care if somebody overrides it, as long as it remains a dict 
+        self.extra_context = getattr(self, 'extra_context', {})
+        if 'extra_context' in kwargs:
+            self.extra_context.update(kwargs['extra_context'])
+ 
+        self.parse_params(request, *args, **kwargs)
+ 
+        # we only accept POST method for form delivery  no POST, no data
+        if not request.POST:
+            self.step = 0
+            return self.render(self.get_form(0), request)
+ 
+        # verify old steps' hashes
+        for i in range(self.step):
+            form = self.get_form(i, request.POST)
+            # somebody is trying to corrupt our data
+            if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
+                # revert to the corrupted step
+                return self.failed_hash(request, i)
+            self.process_step(request, form, i)
+ 
+        # process current step
+        form = self.get_form(self.step, request.POST)
+        if form.is_valid():
+            self.process_step(request, form, self.step)
+            self.step += 1
+            # this was the last step
+            if self.step == len(self.form_list):
+                form_list = [self.get_form(i, request.POST) for i in range(len(self.form_list))]
+                # validate all the forms befora passing it to done()
+                for f in form_list: f.is_valid()
+                return self.done(request, form_list)
+            form = self.get_form(self.step)
+        return self.render(form, request)
+
+    def render(self, form, request):
+        """
+        Prepare the form and call the render_template() method to do tha actual rendering.
+        """
+        if self.step >= len(self.form_list):
+            raise Http404
+ 
+        old_data = request.POST
+        prev_fields = ''
+        if old_data:
+            # old data
+            prev_fields = '\n'.join(
+                    bf.as_hidden() for i in range(self.step) for bf in self.get_form(i, old_data)
+                )
+            # hashes for old forms
+            hidden = forms.widgets.HiddenInput()
+            prev_fields += '\n'.join(
+                    hidden.render("hash_%d" % i, old_data.get("hash_%d" % i, self.security_hash(request, self.get_form(i, old_data)))) 
+                        for i in range(self.step) 
+                )
+        return self.render_template(request, form, prev_fields)
+
+    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
+ 
+    def failed_hash(self, request, i):
+        """
+        One of the hashes verifying old data doesn't match.
+        """
+        self.step = i
+        return self.render(self.get_form(self.step), request)
+ 
+    def security_hash(self, request, form):
+        """
+        Calculates the security hash for the given Form instance.
+ 
+        This creates a list of the form field names/values in a deterministic
+        order, pickles the result with the SECRET_KEY setting and takes an md5
+        hash of that.
+ 
+        Subclasses may want to take into account request-specific information
+        such as the IP address.
+        """
+        data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
+        # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
+        # Python 2.3, but Django requires 2.3 anyway, so that's OK.
+        pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
+        return md5.new(pickled).hexdigest()
+ 
+    def parse_params(self, request, *args, **kwargs):
+        """
+        Set self.step, process any additional info from parameters and/or form data
+        """
+        if request.POST:
+            self.step = int(request.POST.get(self.STEP_FIELD, 0))
+        else:
+            self.step = 0
+
+    def get_template(self):
+        """
+        Return name of the template to be rendered, use self.step to get the step number.
+        """
+        return "wizard.html"
+ 
+    def render_template(self, request, form, previous_fields):
+        """
+        Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
+ 
+        If you only wish to override the template name, use get_template
+ 
+        Some additional items are added to the context: 
+            'step_field' is the name of the hidden field containing step
+            'step' holds the current step
+            'form' containing the current form to be processed (either empty or with errors)
+            'previous_data' contains all the addtitional information, including
+                hashes for finished forms and old data in form of hidden fields
+            any additional data stored in self.extra_context
+        """
+        return render_to_response(self.get_template(), dict(
+                    step_field=self.STEP_FIELD,
+                    step=self.step,
+                    current_step=self.step + 1,
+                    step_count=self.step_count(),
+                    form=form,
+                    previous_fields=previous_fields,
+                    ** self.extra_context
+                ), context_instance=RequestContext(request))
+ 
+    def process_step(self, request, form, step):
+        """
+        This should not modify any data, it is only a hook to modify wizard's internal state
+        (such as dynamically generating form_list based on previously submited forms).
+        It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
+ 
+        Note that this method is called every time a page is rendered for ALL submitted steps.
+ 
+        Only valid data enter here.
+        """
+        pass
+
+    def step_count(self):
+        """Return number of steps"""
+        return len(self.form_list)
+ 
+    # METHODS SUBCLASSES MUST OVERRIDE ########################################
+ 
+    def done(self, request, form_list):
+        """
+        this method must be overriden, it is responsible for the end processing - it will be called with instances of all form_list with their data
+        """
+        raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
Index: tests/regressiontests/formtools/views.py
===================================================================
--- tests/regressiontests/formtools/views.py	(revision 0)
+++ tests/regressiontests/formtools/views.py	(revision 0)
@@ -0,0 +1,43 @@
+from django.contrib.formtools.wizard import Wizard
+from django import newforms as forms
+from django.shortcuts import render_to_response 
+
+class NameForm(forms.Form):
+    first_name = forms.CharField()
+    last_name = forms.CharField()
+
+class DetailsForm(forms.Form):
+    phone = forms.CharField()
+    adress = forms.CharField(widget=forms.Textarea)
+
+class ConfirmForm(forms.Form):
+    confirm = forms.BooleanField()
+
+class SureForm(forms.Form):
+    sure = forms.BooleanField()
+
+class MyWizard(Wizard):
+
+    def done(self, request, form_list):
+        form_data = [f.cleaned_data for f in form_list] 
+        return render_to_response('done.html', {'form_data': form_data}) 
+
+
+RegisterWizard = MyWizard([NameForm, DetailsForm, ConfirmForm])
+
+def wizard(request):
+    if request.POST and 'extra_form' in request.POST:
+        RegisterWizard.form_list.append(SureForm)
+
+    response = RegisterWizard(request)
+
+    hashes = [RegisterWizard.security_hash(
+        request,
+        RegisterWizard.get_form(i, request.POST)
+    ) for i in range(RegisterWizard.step)]
+
+    response['WizardHashes'] = hashes 
+
+    return response 
+
+
Index: tests/regressiontests/formtools/__init__.py
===================================================================
Index: tests/regressiontests/formtools/tests.py
===================================================================
--- tests/regressiontests/formtools/tests.py	(revision 0)
+++ tests/regressiontests/formtools/tests.py	(revision 0)
@@ -0,0 +1,98 @@
+import unittest
+from django.test.client import Client
+
+first_post = {
+    '0-first_name': 'John',
+    '0-last_name': 'Doe',
+    'wizard_step': '0'
+}
+
+second_post = {
+    '0-first_name': 'John',
+    '0-last_name': 'Doe',
+    '1-phone': '99240889',
+    '1-adress': 'memory lane',
+    'wizard_step': '1'
+}
+
+tampered_second_post =  {
+    '0-first_name': 'John',
+    '0-last_name': 'Dere',
+    '1-phone': '99240889',
+    '1-adress': 'memory lane',
+    'wizard_step': '1'
+}
+
+
+third_post = {
+    '0-first_name': 'John',
+    '0-last_name': 'Doe',
+    '1-phone': '99240889',
+    '1-adress': 'memory lane',
+    '2-confirm': '1',
+    'wizard_step': '2' 
+}
+
+fourth_post = {
+    '0-first_name': 'John',
+    '0-last_name': 'Doe',
+    '1-phone': '99240889',
+    '1-adress': 'memory lane',
+    '2-confirm': '1',
+    '3-sure': '1',
+    'wizard_step': '2'
+}
+
+
+class WizardTest(unittest.TestCase):
+    def setUp(self):
+        # Every test needs a client.
+        self.client = Client()
+
+    def test_details(self):
+        # Issue a GET request.
+        response_1 = self.client.get('/formtools/wizard/')
+        
+        # Check that the respose is 200 OK.
+        self.failUnlessEqual(response_1.status_code, 200)
+
+        # Issue a POST request.
+        response_2 = self.client.post('/formtools/wizard/', first_post)
+
+        hashes_1 = response_2['WizardHashes']
+
+        # should fail, returns the first form in content
+        postdata = dict(tampered_second_post, hash_0=hashes_1[0])
+        response_3 = self.client.post('/formtools/wizard/', postdata)
+        self.failUnlessEqual('<form action="." method="POST"><h1>FORM( 0 ): <tr><th><label for="id_0-first_name">First name:</label></th><td><input type="text" name="0-first_name" id="id_0-first_name" /></td></tr><tr><th><label for="id_0-last_name">Last name:</label></th><td><input type="text" name="0-last_name" id="id_0-last_name" /></td></tr></h1><p><strong>step_info :</strong><input type="hidden" name="wizard_step" value="0" /></p><p><input type="submit"></p></form>    \n', response_3.content)
+
+        # should not fail, returns the third form in content
+        postdata = dict(second_post, hash_0=hashes_1[0])
+        response_3 = self.client.post('/formtools/wizard/', postdata)
+        self.failUnlessEqual('<form action="." method="POST"><h1>FORM( 2 ): <tr><th><label for="id_2-confirm">Confirm:</label></th><td><input type="checkbox" name="2-confirm" id="id_2-confirm" /></td></tr></h1><p><strong>step_info :</strong><input type="hidden" name="wizard_step" value="2" /></p><p><input type="hidden" name="0-first_name" value="John" id="id_0-first_name" /><input type="hidden" name="0-last_name" value="Doe" id="id_0-last_name" /><input type="hidden" name="1-phone" value="99240889" id="id_1-phone" /><input type="hidden" name="1-adress" value="memory lane" id="id_1-adress" /><input type="hidden" name="hash_0" value="82a79154a40ed63c309f8bb349cb09e9" /><input type="hidden" name="hash_1" value="9ce63eda4282aa24049870edca937b02" /><input type="submit"></p></form>    \n', response_3.content)
+
+        hashes_2 = response_3['WizardHashes']
+
+        # make sure hashes are the same
+        self.failUnlessEqual(hashes_1[0], hashes_2[0])
+
+        postdata = dict(third_post, hash_0=hashes_2[0], hash_1=hashes_2[1])
+        response_4 = self.client.post('/formtools/wizard/', postdata) 
+        
+        hashes_3 = response_4['WizardHashes']
+        
+        # make sure hashes are the same
+        self.failUnlessEqual(hashes_1[0], hashes_3[0]) 
+        self.failUnlessEqual(hashes_2[1], hashes_3[1])
+
+        # with extra dynamically added fourth form
+        postdata = dict(fourth_post, hash_0=hashes_3[0], hash_1=hashes_3[1], hash_2=hashes_3[2], extra_form='1')
+        response_5 = self.client.post('/formtools/wizard/', postdata)
+
+        hashes_4 = response_5['WizardHashes']
+
+        self.failUnlessEqual(hashes_1[0], hashes_4[0])
+        self.failUnlessEqual(hashes_2[0], hashes_4[0])
+        
+        self.failUnlessEqual(hashes_3[1], hashes_4[1])
+        self.failUnlessEqual(hashes_3[2], hashes_4[2])
Index: tests/regressiontests/formtools/models.py
===================================================================
Index: tests/regressiontests/formtools/urls.py
===================================================================
--- tests/regressiontests/formtools/urls.py	(revision 0)
+++ tests/regressiontests/formtools/urls.py	(revision 0)
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+import views
+
+urlpatterns = patterns('',
+    (r'^wizard/$', views.wizard),
+)
Index: tests/regressiontests/formtools/templates/wizard.html
===================================================================
--- tests/regressiontests/formtools/templates/wizard.html	(revision 0)
+++ tests/regressiontests/formtools/templates/wizard.html	(revision 0)
@@ -0,0 +1,7 @@
+{% spaceless %}
+<form action="." method="POST">
+    <h1>FORM( {{ step }} ): {{ form }}</h1>
+    <p><strong>step_info :</strong> <input type="hidden" name="{{ step_field }}" value="{{ step }}" /></p>
+    <p> {{ previous_fields }} <input type="submit"> </p>
+</form>
+{% endspaceless %}    
Index: tests/regressiontests/formtools/templates/done.html
===================================================================
--- tests/regressiontests/formtools/templates/done.html	(revision 0)
+++ tests/regressiontests/formtools/templates/done.html	(revision 0)
@@ -0,0 +1,8 @@
+{% spaceless %}
+<h1>Result:</h1>
+{% for data in form_data %}
+    {% for item in data.items %}
+        <p><strong>{{ item.0 }}</strong> : {{ item.1 }}</p>
+    {% endfor %}
+{% endfor %}
+{% endspaceless %}
Index: tests/urls.py
===================================================================
--- tests/urls.py	(revision 6199)
+++ tests/urls.py	(working copy)
@@ -11,4 +11,7 @@
 
     # test urlconf for {% url %} template tag
     (r'^url_tag/', include('regressiontests.templates.urls')),
+
+    # test for form wizard
+    (r'^formtools/', include('regressiontests.formtools.urls')),
 )
Index: docs/form_wizard.txt
===================================================================
--- docs/form_wizard.txt	(revision 0)
+++ docs/form_wizard.txt	(revision 0)
@@ -0,0 +1,91 @@
+===========
+Form Wizard
+===========
+
+**New in Django devleopment version.**
+
+The form wizard allows you to divide your forms into multiple pages, 
+maintaining form data in hidden fields until submission in the final step.
+
+Usage
+=====
+
+Define the forms you will use in you wizard in a ``forms.py`` file for your 
+application::
+
+    from django import newforms as forms
+
+    class ContactForm(forms.Form):
+        subject = forms.CharField(max_length=100)
+        message = forms.CharField()
+        sender = forms.EmailField()
+        cc_myself = forms.BooleanField()
+
+    class ContactFormPage2(forms.Form):
+        code = forms.CharField()
+
+.. warning:: Do not include ``FileField`` or ``ImageField`` in any form that 
+    will not be displayed on the last page of your form wizard.
+
+In your application's ``views.py``, subclass a form wizard object.  You'll 
+need to override the ``done`` method with your own form processing code.  
+In the example below, rather than perform any database operation, the method 
+simply returns a list of the (validated) data the user entered into the form 
+to be displayed by a template ``done.html``::
+
+    from django.shortcuts import render_to_response
+    from django.contrib.formtools.wizard import Wizard
+
+    class ContactWizard(Wizard):
+        def done(self, request, form_list):
+            return render_to_response('done.html', 
+                {'form_data' : [ form.cleaned_data for form in form_list ] })
+
+Next, connect your new form wizard object to the path of your choosing in 
+``urls.py``.  The wizard takes a list of your form objects as arguments::
+
+    from django.conf.urls.defaults import *
+    from mysite.testapp.forms import ContactForm, ContactFormPage2
+    from mysite.testapp.views import ContactWizard
+
+    urlpatterns = patterns('',
+        (r'^contact/$', ContactWizard([ContactForm, ContactFormPage2])),
+    )
+
+Finally, you'll need to create a template named ``wizard.html`` in your 
+``templates`` directory.  That template will contain template code that will 
+display your form fields so that they can be processed properly by the form 
+wizard.  For example::
+
+    {% extends "base.html" %}
+    {% block content %}
+    <p>Step {{ current_step }} of {{ step_count }}</p>
+    <form action="." method="POST">
+    <table>
+    {{ form }}
+    </table>
+    <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
+    <!-- include previous fields -->
+    {{ previous_fields }}
+    <input type="submit">
+    </form>
+    {% endblock %}
+
+The ``current_step`` and ``step_count`` variables can be displayed to notify
+your users where they are in the process.  Note also the presence of the 
+template variables ``step_fields``, ``step``, ``previous_fields``, which are 
+used for hidden form fields.
+
+Advanced Usage
+==============
+
+You may override the ``process_step`` method in your wizard class to, for 
+example, dynamically generate a ``form_list`` and/or add items to 
+``self.extra_context`` based on the contents of previously submitted forms::
+
+    def process_step(self, request, form, step):
+        if step == 0:
+            # do something for the second step in your form wizard
+            # based on data submitted from step 1
+
+The method is called every time a page is rendered, for all submitted steps. 
