Code

Ticket #9200: ticket9200-1.diff

File ticket9200-1.diff, 138.4 KB (added by jezdez, 3 years ago)

Updated patch (including changes for secure cookie from #12417)

Line 
1diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
2index 88aa5a3..c98cab7 100644
3--- a/django/conf/global_settings.py
4+++ b/django/conf/global_settings.py
5@@ -476,6 +476,12 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
6 # The number of days a password reset link is valid for
7 PASSWORD_RESET_TIMEOUT_DAYS = 3
8 
9+###########
10+# SIGNING #
11+###########
12+
13+SIGNING_BACKEND = 'django.core.signing.TimestampSigner'
14+
15 ########
16 # CSRF #
17 ########
18diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
19index be0372a..911bc65 100644
20--- a/django/contrib/formtools/tests/__init__.py
21+++ b/django/contrib/formtools/tests/__init__.py
22@@ -8,6 +8,8 @@ from django.test import TestCase
23 from django.test.utils import get_warnings_state, restore_warnings_state
24 from django.utils import unittest
25 
26+from django.contrib.formtools.wizard.tests import *
27+
28 
29 success_string = "Done was called!"
30 
31diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py
32deleted file mode 100644
33index c19578c..0000000
34--- a/django/contrib/formtools/wizard.py
35+++ /dev/null
36@@ -1,271 +0,0 @@
37-"""
38-FormWizard class -- implements a multi-page form, validating between each
39-step and storing the form's state as HTML hidden fields so that no state is
40-stored on the server side.
41-"""
42-
43-try:
44-    import cPickle as pickle
45-except ImportError:
46-    import pickle
47-
48-from django import forms
49-from django.conf import settings
50-from django.contrib.formtools.utils import form_hmac
51-from django.http import Http404
52-from django.shortcuts import render_to_response
53-from django.template.context import RequestContext
54-from django.utils.crypto import constant_time_compare
55-from django.utils.translation import ugettext_lazy as _
56-from django.utils.decorators import method_decorator
57-from django.views.decorators.csrf import csrf_protect
58-
59-
60-class FormWizard(object):
61-    # The HTML (and POST data) field name for the "step" variable.
62-    step_field_name="wizard_step"
63-
64-    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
65-
66-    def __init__(self, form_list, initial=None):
67-        """
68-        Start a new wizard with a list of forms.
69-
70-        form_list should be a list of Form classes (not instances).
71-        """
72-        self.form_list = form_list[:]
73-        self.initial = initial or {}
74-
75-        # Dictionary of extra template context variables.
76-        self.extra_context = {}
77-
78-        # A zero-based counter keeping track of which step we're in.
79-        self.step = 0
80-
81-    def __repr__(self):
82-        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
83-
84-    def get_form(self, step, data=None):
85-        "Helper method that returns the Form instance for the given step."
86-        # Sanity check.
87-        if step >= self.num_steps():
88-            raise Http404('Step %s does not exist' % step)
89-        return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
90-
91-    def num_steps(self):
92-        "Helper method that returns the number of steps."
93-        # You might think we should just set "self.num_steps = len(form_list)"
94-        # in __init__(), but this calculation needs to be dynamic, because some
95-        # hook methods might alter self.form_list.
96-        return len(self.form_list)
97-
98-    def _check_security_hash(self, token, request, form):
99-        expected = self.security_hash(request, form)
100-        return constant_time_compare(token, expected)
101-
102-    @method_decorator(csrf_protect)
103-    def __call__(self, request, *args, **kwargs):
104-        """
105-        Main method that does all the hard work, conforming to the Django view
106-        interface.
107-        """
108-        if 'extra_context' in kwargs:
109-            self.extra_context.update(kwargs['extra_context'])
110-        current_step = self.determine_step(request, *args, **kwargs)
111-        self.parse_params(request, *args, **kwargs)
112-
113-        # Validate and process all the previous forms before instantiating the
114-        # current step's form in case self.process_step makes changes to
115-        # self.form_list.
116-
117-        # If any of them fails validation, that must mean the validator relied
118-        # on some other input, such as an external Web site.
119-
120-        # It is also possible that alidation might fail under certain attack
121-        # situations: an attacker might be able to bypass previous stages, and
122-        # generate correct security hashes for all the skipped stages by virtue
123-        # of:
124-        #  1) having filled out an identical form which doesn't have the
125-        #     validation (and does something different at the end),
126-        #  2) or having filled out a previous version of the same form which
127-        #     had some validation missing,
128-        #  3) or previously having filled out the form when they had more
129-        #     privileges than they do now.
130-        #
131-        # Since the hashes only take into account values, and not other other
132-        # validation the form might do, we must re-do validation now for
133-        # security reasons.
134-        previous_form_list = []
135-        for i in range(current_step):
136-            f = self.get_form(i, request.POST)
137-            if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),
138-                                             request, f):
139-                return self.render_hash_failure(request, i)
140-
141-            if not f.is_valid():
142-                return self.render_revalidation_failure(request, i, f)
143-            else:
144-                self.process_step(request, f, i)
145-                previous_form_list.append(f)
146-
147-        # Process the current step. If it's valid, go to the next step or call
148-        # done(), depending on whether any steps remain.
149-        if request.method == 'POST':
150-            form = self.get_form(current_step, request.POST)
151-        else:
152-            form = self.get_form(current_step)
153-
154-        if form.is_valid():
155-            self.process_step(request, form, current_step)
156-            next_step = current_step + 1
157-
158-            if next_step == self.num_steps():
159-                return self.done(request, previous_form_list + [form])
160-            else:
161-                form = self.get_form(next_step)
162-                self.step = current_step = next_step
163-
164-        return self.render(form, request, current_step)
165-
166-    def render(self, form, request, step, context=None):
167-        "Renders the given Form object, returning an HttpResponse."
168-        old_data = request.POST
169-        prev_fields = []
170-        if old_data:
171-            hidden = forms.HiddenInput()
172-            # Collect all data from previous steps and render it as HTML hidden fields.
173-            for i in range(step):
174-                old_form = self.get_form(i, old_data)
175-                hash_name = 'hash_%s' % i
176-                prev_fields.extend([bf.as_hidden() for bf in old_form])
177-                prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
178-        return self.render_template(request, form, ''.join(prev_fields), step, context)
179-
180-    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
181-
182-    def prefix_for_step(self, step):
183-        "Given the step, returns a Form prefix to use."
184-        return str(step)
185-
186-    def render_hash_failure(self, request, step):
187-        """
188-        Hook for rendering a template if a hash check failed.
189-
190-        step is the step that failed. Any previous step is guaranteed to be
191-        valid.
192-
193-        This default implementation simply renders the form for the given step,
194-        but subclasses may want to display an error message, etc.
195-        """
196-        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.')})
197-
198-    def render_revalidation_failure(self, request, step, form):
199-        """
200-        Hook for rendering a template if final revalidation failed.
201-
202-        It is highly unlikely that this point would ever be reached, but See
203-        the comment in __call__() for an explanation.
204-        """
205-        return self.render(form, request, step)
206-
207-    def security_hash(self, request, form):
208-        """
209-        Calculates the security hash for the given HttpRequest and Form instances.
210-
211-        Subclasses may want to take into account request-specific information,
212-        such as the IP address.
213-        """
214-        return form_hmac(form)
215-
216-    def determine_step(self, request, *args, **kwargs):
217-        """
218-        Given the request object and whatever *args and **kwargs were passed to
219-        __call__(), returns the current step (which is zero-based).
220-
221-        Note that the result should not be trusted. It may even be a completely
222-        invalid number. It's not the job of this method to validate it.
223-        """
224-        if not request.POST:
225-            return 0
226-        try:
227-            step = int(request.POST.get(self.step_field_name, 0))
228-        except ValueError:
229-            return 0
230-        return step
231-
232-    def parse_params(self, request, *args, **kwargs):
233-        """
234-        Hook for setting some state, given the request object and whatever
235-        *args and **kwargs were passed to __call__(), sets some state.
236-
237-        This is called at the beginning of __call__().
238-        """
239-        pass
240-
241-    def get_template(self, step):
242-        """
243-        Hook for specifying the name of the template to use for a given step.
244-
245-        Note that this can return a tuple of template names if you'd like to
246-        use the template system's select_template() hook.
247-        """
248-        return 'forms/wizard.html'
249-
250-    def render_template(self, request, form, previous_fields, step, context=None):
251-        """
252-        Renders the template for the given step, returning an HttpResponse object.
253-
254-        Override this method if you want to add a custom context, return a
255-        different MIME type, etc. If you only need to override the template
256-        name, use get_template() instead.
257-
258-        The template will be rendered with the following context:
259-            step_field -- The name of the hidden field containing the step.
260-            step0      -- The current step (zero-based).
261-            step       -- The current step (one-based).
262-            step_count -- The total number of steps.
263-            form       -- The Form instance for the current step (either empty
264-                          or with errors).
265-            previous_fields -- A string representing every previous data field,
266-                          plus hashes for completed forms, all in the form of
267-                          hidden fields. Note that you'll need to run this
268-                          through the "safe" template filter, to prevent
269-                          auto-escaping, because it's raw HTML.
270-        """
271-        context = context or {}
272-        context.update(self.extra_context)
273-        return render_to_response(self.get_template(step), dict(context,
274-            step_field=self.step_field_name,
275-            step0=step,
276-            step=step + 1,
277-            step_count=self.num_steps(),
278-            form=form,
279-            previous_fields=previous_fields
280-        ), context_instance=RequestContext(request))
281-
282-    def process_step(self, request, form, step):
283-        """
284-        Hook for modifying the FormWizard's internal state, given a fully
285-        validated Form object. The Form is guaranteed to have clean, valid
286-        data.
287-
288-        This method should *not* modify any of that data. Rather, it might want
289-        to set self.extra_context or dynamically alter self.form_list, based on
290-        previously submitted forms.
291-
292-        Note that this method is called every time a page is rendered for *all*
293-        submitted steps.
294-        """
295-        pass
296-
297-    # METHODS SUBCLASSES MUST OVERRIDE ########################################
298-
299-    def done(self, request, form_list):
300-        """
301-        Hook for doing something with the validated data. This is responsible
302-        for the final processing.
303-
304-        form_list is a list of Form instances, each containing clean, valid
305-        data.
306-        """
307-        raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
308diff --git a/django/contrib/formtools/wizard/__init__.py b/django/contrib/formtools/wizard/__init__.py
309new file mode 100644
310index 0000000..c19578c
311--- /dev/null
312+++ b/django/contrib/formtools/wizard/__init__.py
313@@ -0,0 +1,271 @@
314+"""
315+FormWizard class -- implements a multi-page form, validating between each
316+step and storing the form's state as HTML hidden fields so that no state is
317+stored on the server side.
318+"""
319+
320+try:
321+    import cPickle as pickle
322+except ImportError:
323+    import pickle
324+
325+from django import forms
326+from django.conf import settings
327+from django.contrib.formtools.utils import form_hmac
328+from django.http import Http404
329+from django.shortcuts import render_to_response
330+from django.template.context import RequestContext
331+from django.utils.crypto import constant_time_compare
332+from django.utils.translation import ugettext_lazy as _
333+from django.utils.decorators import method_decorator
334+from django.views.decorators.csrf import csrf_protect
335+
336+
337+class FormWizard(object):
338+    # The HTML (and POST data) field name for the "step" variable.
339+    step_field_name="wizard_step"
340+
341+    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
342+
343+    def __init__(self, form_list, initial=None):
344+        """
345+        Start a new wizard with a list of forms.
346+
347+        form_list should be a list of Form classes (not instances).
348+        """
349+        self.form_list = form_list[:]
350+        self.initial = initial or {}
351+
352+        # Dictionary of extra template context variables.
353+        self.extra_context = {}
354+
355+        # A zero-based counter keeping track of which step we're in.
356+        self.step = 0
357+
358+    def __repr__(self):
359+        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
360+
361+    def get_form(self, step, data=None):
362+        "Helper method that returns the Form instance for the given step."
363+        # Sanity check.
364+        if step >= self.num_steps():
365+            raise Http404('Step %s does not exist' % step)
366+        return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
367+
368+    def num_steps(self):
369+        "Helper method that returns the number of steps."
370+        # You might think we should just set "self.num_steps = len(form_list)"
371+        # in __init__(), but this calculation needs to be dynamic, because some
372+        # hook methods might alter self.form_list.
373+        return len(self.form_list)
374+
375+    def _check_security_hash(self, token, request, form):
376+        expected = self.security_hash(request, form)
377+        return constant_time_compare(token, expected)
378+
379+    @method_decorator(csrf_protect)
380+    def __call__(self, request, *args, **kwargs):
381+        """
382+        Main method that does all the hard work, conforming to the Django view
383+        interface.
384+        """
385+        if 'extra_context' in kwargs:
386+            self.extra_context.update(kwargs['extra_context'])
387+        current_step = self.determine_step(request, *args, **kwargs)
388+        self.parse_params(request, *args, **kwargs)
389+
390+        # Validate and process all the previous forms before instantiating the
391+        # current step's form in case self.process_step makes changes to
392+        # self.form_list.
393+
394+        # If any of them fails validation, that must mean the validator relied
395+        # on some other input, such as an external Web site.
396+
397+        # It is also possible that alidation might fail under certain attack
398+        # situations: an attacker might be able to bypass previous stages, and
399+        # generate correct security hashes for all the skipped stages by virtue
400+        # of:
401+        #  1) having filled out an identical form which doesn't have the
402+        #     validation (and does something different at the end),
403+        #  2) or having filled out a previous version of the same form which
404+        #     had some validation missing,
405+        #  3) or previously having filled out the form when they had more
406+        #     privileges than they do now.
407+        #
408+        # Since the hashes only take into account values, and not other other
409+        # validation the form might do, we must re-do validation now for
410+        # security reasons.
411+        previous_form_list = []
412+        for i in range(current_step):
413+            f = self.get_form(i, request.POST)
414+            if not self._check_security_hash(request.POST.get("hash_%d" % i, ''),
415+                                             request, f):
416+                return self.render_hash_failure(request, i)
417+
418+            if not f.is_valid():
419+                return self.render_revalidation_failure(request, i, f)
420+            else:
421+                self.process_step(request, f, i)
422+                previous_form_list.append(f)
423+
424+        # Process the current step. If it's valid, go to the next step or call
425+        # done(), depending on whether any steps remain.
426+        if request.method == 'POST':
427+            form = self.get_form(current_step, request.POST)
428+        else:
429+            form = self.get_form(current_step)
430+
431+        if form.is_valid():
432+            self.process_step(request, form, current_step)
433+            next_step = current_step + 1
434+
435+            if next_step == self.num_steps():
436+                return self.done(request, previous_form_list + [form])
437+            else:
438+                form = self.get_form(next_step)
439+                self.step = current_step = next_step
440+
441+        return self.render(form, request, current_step)
442+
443+    def render(self, form, request, step, context=None):
444+        "Renders the given Form object, returning an HttpResponse."
445+        old_data = request.POST
446+        prev_fields = []
447+        if old_data:
448+            hidden = forms.HiddenInput()
449+            # Collect all data from previous steps and render it as HTML hidden fields.
450+            for i in range(step):
451+                old_form = self.get_form(i, old_data)
452+                hash_name = 'hash_%s' % i
453+                prev_fields.extend([bf.as_hidden() for bf in old_form])
454+                prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
455+        return self.render_template(request, form, ''.join(prev_fields), step, context)
456+
457+    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
458+
459+    def prefix_for_step(self, step):
460+        "Given the step, returns a Form prefix to use."
461+        return str(step)
462+
463+    def render_hash_failure(self, request, step):
464+        """
465+        Hook for rendering a template if a hash check failed.
466+
467+        step is the step that failed. Any previous step is guaranteed to be
468+        valid.
469+
470+        This default implementation simply renders the form for the given step,
471+        but subclasses may want to display an error message, etc.
472+        """
473+        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.')})
474+
475+    def render_revalidation_failure(self, request, step, form):
476+        """
477+        Hook for rendering a template if final revalidation failed.
478+
479+        It is highly unlikely that this point would ever be reached, but See
480+        the comment in __call__() for an explanation.
481+        """
482+        return self.render(form, request, step)
483+
484+    def security_hash(self, request, form):
485+        """
486+        Calculates the security hash for the given HttpRequest and Form instances.
487+
488+        Subclasses may want to take into account request-specific information,
489+        such as the IP address.
490+        """
491+        return form_hmac(form)
492+
493+    def determine_step(self, request, *args, **kwargs):
494+        """
495+        Given the request object and whatever *args and **kwargs were passed to
496+        __call__(), returns the current step (which is zero-based).
497+
498+        Note that the result should not be trusted. It may even be a completely
499+        invalid number. It's not the job of this method to validate it.
500+        """
501+        if not request.POST:
502+            return 0
503+        try:
504+            step = int(request.POST.get(self.step_field_name, 0))
505+        except ValueError:
506+            return 0
507+        return step
508+
509+    def parse_params(self, request, *args, **kwargs):
510+        """
511+        Hook for setting some state, given the request object and whatever
512+        *args and **kwargs were passed to __call__(), sets some state.
513+
514+        This is called at the beginning of __call__().
515+        """
516+        pass
517+
518+    def get_template(self, step):
519+        """
520+        Hook for specifying the name of the template to use for a given step.
521+
522+        Note that this can return a tuple of template names if you'd like to
523+        use the template system's select_template() hook.
524+        """
525+        return 'forms/wizard.html'
526+
527+    def render_template(self, request, form, previous_fields, step, context=None):
528+        """
529+        Renders the template for the given step, returning an HttpResponse object.
530+
531+        Override this method if you want to add a custom context, return a
532+        different MIME type, etc. If you only need to override the template
533+        name, use get_template() instead.
534+
535+        The template will be rendered with the following context:
536+            step_field -- The name of the hidden field containing the step.
537+            step0      -- The current step (zero-based).
538+            step       -- The current step (one-based).
539+            step_count -- The total number of steps.
540+            form       -- The Form instance for the current step (either empty
541+                          or with errors).
542+            previous_fields -- A string representing every previous data field,
543+                          plus hashes for completed forms, all in the form of
544+                          hidden fields. Note that you'll need to run this
545+                          through the "safe" template filter, to prevent
546+                          auto-escaping, because it's raw HTML.
547+        """
548+        context = context or {}
549+        context.update(self.extra_context)
550+        return render_to_response(self.get_template(step), dict(context,
551+            step_field=self.step_field_name,
552+            step0=step,
553+            step=step + 1,
554+            step_count=self.num_steps(),
555+            form=form,
556+            previous_fields=previous_fields
557+        ), context_instance=RequestContext(request))
558+
559+    def process_step(self, request, form, step):
560+        """
561+        Hook for modifying the FormWizard's internal state, given a fully
562+        validated Form object. The Form is guaranteed to have clean, valid
563+        data.
564+
565+        This method should *not* modify any of that data. Rather, it might want
566+        to set self.extra_context or dynamically alter self.form_list, based on
567+        previously submitted forms.
568+
569+        Note that this method is called every time a page is rendered for *all*
570+        submitted steps.
571+        """
572+        pass
573+
574+    # METHODS SUBCLASSES MUST OVERRIDE ########################################
575+
576+    def done(self, request, form_list):
577+        """
578+        Hook for doing something with the validated data. This is responsible
579+        for the final processing.
580+
581+        form_list is a list of Form instances, each containing clean, valid
582+        data.
583+        """
584+        raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
585diff --git a/django/contrib/formtools/wizard/storage/__init__.py b/django/contrib/formtools/wizard/storage/__init__.py
586new file mode 100644
587index 0000000..7f03028
588--- /dev/null
589+++ b/django/contrib/formtools/wizard/storage/__init__.py
590@@ -0,0 +1,29 @@
591+from django.core.exceptions import ImproperlyConfigured
592+from django.utils.importlib import import_module
593+
594+from django.contrib.formtools.wizard.storage.base import BaseStorage
595+
596+class MissingStorageModule(ImproperlyConfigured):
597+    pass
598+
599+class MissingStorageClass(ImproperlyConfigured):
600+    pass
601+
602+class NoFileStorageConfigured(ImproperlyConfigured):
603+    pass
604+
605+def get_storage(path, *args, **kwargs):
606+    i = path.rfind('.')
607+    module, attr = path[:i], path[i+1:]
608+    try:
609+        mod = import_module(module)
610+    except ImportError, e:
611+        raise MissingStorageModule(
612+            'Error loading storage %s: "%s"' % (module, e))
613+    try:
614+        storage_class = getattr(mod, attr)
615+    except AttributeError:
616+        raise MissingStorageClass(
617+            'Module "%s" does not define a storage named "%s"' % (module, attr))
618+    return storage_class(*args, **kwargs)
619+
620diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py
621new file mode 100644
622index 0000000..0e9c677
623--- /dev/null
624+++ b/django/contrib/formtools/wizard/storage/base.py
625@@ -0,0 +1,37 @@
626+class BaseStorage(object):
627+    def __init__(self, prefix):
628+        self.prefix = 'wizard_%s' % prefix
629+
630+    def get_current_step(self):
631+        raise NotImplementedError
632+
633+    def set_current_step(self, step):
634+        raise NotImplementedError
635+
636+    def get_step_data(self, step):
637+        raise NotImplementedError
638+
639+    def get_current_step_data(self):
640+        raise NotImplementedError
641+
642+    def set_step_data(self, step, cleaned_data):
643+        raise NotImplementedError
644+
645+    def get_step_files(self, step):
646+        raise NotImplementedError
647+
648+    def set_step_files(self, step, files):
649+        raise NotImplementedError
650+
651+    def get_extra_context_data(self):
652+        raise NotImplementedError
653+
654+    def set_extra_context_data(self, extra_context):
655+        raise NotImplementedError
656+
657+    def reset(self):
658+        raise NotImplementedError
659+
660+    def update_response(self, response):
661+        raise NotImplementedError
662+
663diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py
664new file mode 100644
665index 0000000..f11cd15
666--- /dev/null
667+++ b/django/contrib/formtools/wizard/storage/cookie.py
668@@ -0,0 +1,123 @@
669+from django.core.exceptions import SuspiciousOperation
670+from django.core.signing import BadSignature
671+from django.core.files.uploadedfile import UploadedFile
672+from django.utils import simplejson as json
673+
674+from django.contrib.formtools.wizard.storage import (BaseStorage,
675+                                                     NoFileStorageConfigured)
676+
677+class CookieStorage(BaseStorage):
678+    step_cookie_key = 'step'
679+    step_data_cookie_key = 'step_data'
680+    step_files_cookie_key = 'step_files'
681+    extra_context_cookie_key = 'extra_context'
682+
683+    def __init__(self, prefix, request, file_storage, *args, **kwargs):
684+        super(CookieStorage, self).__init__(prefix)
685+        self.file_storage = file_storage
686+        self.request = request
687+        self.cookie_data = self.load_cookie_data()
688+        if self.cookie_data is None:
689+            self.init_storage()
690+
691+    def init_storage(self):
692+        self.cookie_data = {
693+            self.step_cookie_key: None,
694+            self.step_data_cookie_key: {},
695+            self.step_files_cookie_key: {},
696+            self.extra_context_cookie_key: {},
697+        }
698+        return True
699+
700+    def get_current_step(self):
701+        return self.cookie_data[self.step_cookie_key]
702+
703+    def set_current_step(self, step):
704+        self.cookie_data[self.step_cookie_key] = step
705+        return True
706+
707+    def get_step_data(self, step):
708+        return self.cookie_data[self.step_data_cookie_key].get(step, None)
709+
710+    def get_current_step_data(self):
711+        return self.get_step_data(self.get_current_step())
712+
713+    def set_step_data(self, step, cleaned_data):
714+        self.cookie_data[self.step_data_cookie_key][step] = cleaned_data
715+        return True
716+
717+    def set_step_files(self, step, files):
718+        if files and not self.file_storage:
719+            raise NoFileStorageConfigured
720+
721+        if step not in self.cookie_data[self.step_files_cookie_key]:
722+            self.cookie_data[self.step_files_cookie_key][step] = {}
723+
724+        for field, field_file in (files or {}).items():
725+            tmp_filename = self.file_storage.save(field_file.name, field_file)
726+            file_dict = {
727+                'tmp_name': tmp_filename,
728+                'name': field_file.name,
729+                'content_type': field_file.content_type,
730+                'size': field_file.size,
731+                'charset': field_file.charset
732+            }
733+            self.cookie_data[self.step_files_cookie_key][step][field] = file_dict
734+
735+        return True
736+
737+    def get_current_step_files(self):
738+        return self.get_step_files(self.get_current_step())
739+
740+    def get_step_files(self, step):
741+        session_files = self.cookie_data[self.step_files_cookie_key].get(step, {})
742+
743+        if session_files and not self.file_storage:
744+            raise NoFileStorageConfigured
745+
746+        files = {}
747+        for field, field_dict in session_files.items():
748+            files[field] = UploadedFile(
749+                file=self.file_storage.open(field_dict['tmp_name']),
750+                name=field_dict['name'],
751+                content_type=field_dict['content_type'],
752+                size=field_dict['size'],
753+                charset=field_dict['charset'],
754+            )
755+        return files or None
756+
757+    def get_extra_context_data(self):
758+        return self.cookie_data[self.extra_context_cookie_key] or {}
759+
760+    def set_extra_context_data(self, extra_context):
761+        self.cookie_data[self.extra_context_cookie_key] = extra_context
762+        return True
763+
764+    def reset(self):
765+        return self.init_storage()
766+
767+    def update_response(self, response):
768+        if len(self.cookie_data) > 0:
769+            response.set_signed_cookie(self.prefix,
770+                self.create_cookie_data(self.cookie_data))
771+        else:
772+            response.delete_cookie(self.prefix)
773+        return response
774+
775+    def load_cookie_data(self):
776+        try:
777+            data = self.request.get_signed_cookie(self.prefix)
778+        except KeyError:
779+            data = None
780+        except BadSignature:
781+            raise SuspiciousOperation('FormWizard cookie manipulated')
782+
783+        if data is None:
784+            return None
785+
786+        return json.loads(data, cls=json.JSONDecoder)
787+
788+    def create_cookie_data(self, data):
789+        encoder = json.JSONEncoder(separators=(',', ':'))
790+        return encoder.encode(data)
791+
792diff --git a/django/contrib/formtools/wizard/storage/session.py b/django/contrib/formtools/wizard/storage/session.py
793new file mode 100644
794index 0000000..35468e7
795--- /dev/null
796+++ b/django/contrib/formtools/wizard/storage/session.py
797@@ -0,0 +1,106 @@
798+from django.core.files.uploadedfile import UploadedFile
799+
800+from django.contrib.formtools.wizard.storage import (BaseStorage,
801+                                                     NoFileStorageConfigured)
802+
803+class SessionStorage(BaseStorage):
804+    step_session_key = 'step'
805+    step_data_session_key = 'step_data'
806+    step_files_session_key = 'step_files'
807+    extra_context_session_key = 'extra_context'
808+
809+    def __init__(self, prefix, request, file_storage=None, *args, **kwargs):
810+        super(SessionStorage, self).__init__(prefix)
811+        self.request = request
812+        self.file_storage = file_storage
813+        if self.prefix not in self.request.session:
814+            self.init_storage()
815+
816+    def init_storage(self):
817+        self.request.session[self.prefix] = {
818+            self.step_session_key: None,
819+            self.step_data_session_key: {},
820+            self.step_files_session_key: {},
821+            self.extra_context_session_key: {},
822+        }
823+        self.request.session.modified = True
824+        return True
825+
826+    def get_current_step(self):
827+        return self.request.session[self.prefix][self.step_session_key]
828+
829+    def set_current_step(self, step):
830+        self.request.session[self.prefix][self.step_session_key] = step
831+        self.request.session.modified = True
832+        return True
833+
834+    def get_step_data(self, step):
835+        return self.request.session[self.prefix][self.step_data_session_key].get(step, None)
836+
837+    def get_current_step_data(self):
838+        return self.get_step_data(self.get_current_step())
839+
840+    def set_step_data(self, step, cleaned_data):
841+        self.request.session[self.prefix][self.step_data_session_key][step] = cleaned_data
842+        self.request.session.modified = True
843+        return True
844+
845+    def set_step_files(self, step, files):
846+        if files and not self.file_storage:
847+            raise NoFileStorageConfigured
848+
849+        if step not in self.request.session[self.prefix][self.step_files_session_key]:
850+            self.request.session[self.prefix][self.step_files_session_key][step] = {}
851+
852+        for field, field_file in (files or {}).items():
853+            tmp_filename = self.file_storage.save(field_file.name, field_file)
854+            file_dict = {
855+                'tmp_name': tmp_filename,
856+                'name': field_file.name,
857+                'content_type': field_file.content_type,
858+                'size': field_file.size,
859+                'charset': field_file.charset
860+            }
861+            self.request.session[self.prefix][self.step_files_session_key][step][field] = file_dict
862+
863+        self.request.session.modified = True
864+        return True
865+
866+    def get_current_step_files(self):
867+        return self.get_step_files(self.get_current_step())
868+
869+    def get_step_files(self, step):
870+        session_files = self.request.session[self.prefix][self.step_files_session_key].get(step, {})
871+
872+        if session_files and not self.file_storage:
873+            raise NoFileStorageConfigured
874+
875+        files = {}
876+        for field, field_dict in session_files.items():
877+            files[field] = UploadedFile(
878+                file=self.file_storage.open(field_dict['tmp_name']),
879+                name=field_dict['name'],
880+                content_type=field_dict['content_type'],
881+                size=field_dict['size'],
882+                charset=field_dict['charset'],
883+            )
884+        return files or None
885+
886+    def get_extra_context_data(self):
887+        return self.request.session[self.prefix][self.extra_context_session_key] or {}
888+
889+    def set_extra_context_data(self, extra_context):
890+        self.request.session[self.prefix][self.extra_context_session_key] = extra_context
891+        self.request.session.modified = True
892+        return True
893+
894+    def reset(self):
895+        if self.file_storage:
896+            for step_fields in self.request.session[self.prefix][self.step_files_session_key].values():
897+                for file_dict in step_fields.values():
898+                    self.file_storage.delete(file_dict['tmp_name'])
899+        return self.init_storage()
900+
901+    def update_response(self, response):
902+        return response
903+
904diff --git a/django/contrib/formtools/wizard/templates/formtools/wizard/wizard.html b/django/contrib/formtools/wizard/templates/formtools/wizard/wizard.html
905new file mode 100644
906index 0000000..6981312
907--- /dev/null
908+++ b/django/contrib/formtools/wizard/templates/formtools/wizard/wizard.html
909@@ -0,0 +1,16 @@
910+{% load i18n %}
911+{% csrf_token %}
912+{% if form.forms %}
913+    {{ form.management_form }}
914+    {% for fs in form.forms %}
915+        {{ fs.as_p }}
916+    {% endfor %}
917+{% else %}
918+    {{ form.as_p }}
919+{% endif %}
920+
921+{% if form_prev_step %}
922+<button name="form_prev_step" value="{{ form_first_step }}">{% trans "first step" %}</button>
923+<button name="form_prev_step" value="{{ form_prev_step }}">{% trans "prev step" %}</button>
924+{% endif %}
925+<input type="submit" name="submit" value="{% trans "submit" %}" />
926diff --git a/django/contrib/formtools/wizard/tests/__init__.py b/django/contrib/formtools/wizard/tests/__init__.py
927new file mode 100644
928index 0000000..22fd8bc
929--- /dev/null
930+++ b/django/contrib/formtools/wizard/tests/__init__.py
931@@ -0,0 +1,7 @@
932+from django.contrib.formtools.wizard.tests.formtests import *
933+from django.contrib.formtools.wizard.tests.basestoragetests import *
934+from django.contrib.formtools.wizard.tests.sessionstoragetests import *
935+from django.contrib.formtools.wizard.tests.cookiestoragetests import *
936+from django.contrib.formtools.wizard.tests.loadstoragetests import *
937+from django.contrib.formtools.wizard.tests.wizardtests import *
938+from django.contrib.formtools.wizard.tests.namedwizardtests import *
939diff --git a/django/contrib/formtools/wizard/tests/basestoragetests.py b/django/contrib/formtools/wizard/tests/basestoragetests.py
940new file mode 100644
941index 0000000..4e46dba
942--- /dev/null
943+++ b/django/contrib/formtools/wizard/tests/basestoragetests.py
944@@ -0,0 +1,39 @@
945+from django.test import TestCase
946+from django.contrib.formtools.wizard.storage.base import BaseStorage
947+
948+class TestBaseStorage(TestCase):
949+    def setUp(self):
950+        self.storage = BaseStorage('wizard1')
951+
952+    def test_get_current_step(self):
953+        self.assertRaises(NotImplementedError,
954+                          self.storage.get_current_step)
955+
956+    def test_set_current_step(self):
957+        self.assertRaises(NotImplementedError,
958+                          self.storage.set_current_step, None)
959+
960+    def test_get_step_data(self):
961+        self.assertRaises(NotImplementedError,
962+                          self.storage.get_step_data, None)
963+
964+    def test_set_step_data(self):
965+        self.assertRaises(NotImplementedError,
966+                          self.storage.set_step_data, None, None)
967+
968+    def test_get_extra_context_data(self):
969+        self.assertRaises(NotImplementedError,
970+                          self.storage.get_extra_context_data)
971+
972+    def test_set_extra_context_data(self):
973+        self.assertRaises(NotImplementedError,
974+                          self.storage.set_extra_context_data, None)
975+
976+    def test_reset(self):
977+        self.assertRaises(NotImplementedError,
978+                          self.storage.reset)
979+
980+    def test_update_response(self):
981+        self.assertRaises(NotImplementedError,
982+                          self.storage.update_response, None)
983+
984diff --git a/django/contrib/formtools/wizard/tests/cookiestoragetests.py b/django/contrib/formtools/wizard/tests/cookiestoragetests.py
985new file mode 100644
986index 0000000..945df5c
987--- /dev/null
988+++ b/django/contrib/formtools/wizard/tests/cookiestoragetests.py
989@@ -0,0 +1,49 @@
990+from django.test import TestCase
991+from django.core import signing
992+from django.core.exceptions import SuspiciousOperation
993+from django.http import HttpResponse
994+
995+from django.contrib.formtools.wizard.storage.cookie import CookieStorage
996+from django.contrib.formtools.wizard.tests.storagetests import *
997+
998+class TestCookieStorage(TestStorage, TestCase):
999+    def get_storage(self):
1000+        return CookieStorage
1001+
1002+    def test_manipulated_cookie(self):
1003+        request = get_request()
1004+        storage = self.get_storage()('wizard1', request, None)
1005+
1006+        cookie_signer = signing.get_cookie_signer()
1007+
1008+        storage.request.COOKIES[storage.prefix] = cookie_signer.sign(
1009+            storage.create_cookie_data({'key1': 'value1'}),
1010+            salt=storage.prefix)
1011+
1012+        self.assertEqual(storage.load_cookie_data(), {'key1': 'value1'})
1013+
1014+        storage.request.COOKIES[storage.prefix] = 'i_am_manipulated'
1015+        self.assertRaises(SuspiciousOperation, storage.load_cookie_data)
1016+
1017+        #raise SuspiciousOperation('FormWizard cookie manipulated')
1018+
1019+    def test_delete_cookie(self):
1020+        request = get_request()
1021+        storage = self.get_storage()('wizard1', request, None)
1022+
1023+        storage.cookie_data = {'key1': 'value1'}
1024+
1025+        response = HttpResponse()
1026+        storage.update_response(response)
1027+
1028+        cookie_signer = signing.get_cookie_signer()
1029+        signed_cookie_data = cookie_signer.sign(
1030+            storage.create_cookie_data(storage.cookie_data),
1031+            salt=storage.prefix)
1032+
1033+        self.assertEqual(response.cookies[storage.prefix].value,
1034+            signed_cookie_data)
1035+
1036+        storage.cookie_data = {}
1037+        storage.update_response(response)
1038+        self.assertEqual(response.cookies[storage.prefix].value, '')
1039diff --git a/django/contrib/formtools/wizard/tests/formtests.py b/django/contrib/formtools/wizard/tests/formtests.py
1040new file mode 100644
1041index 0000000..b600eb3
1042--- /dev/null
1043+++ b/django/contrib/formtools/wizard/tests/formtests.py
1044@@ -0,0 +1,204 @@
1045+from django import forms, http
1046+from django.conf import settings
1047+from django.test import TestCase
1048+from django.template.response import TemplateResponse
1049+from django.utils.importlib import import_module
1050+
1051+from django.contrib.auth.models import User
1052+
1053+from django.contrib.formtools.wizard.views import (WizardView,
1054+                                                   SessionWizardView,
1055+                                                   CookieWizardView)
1056+
1057+
1058+class DummyRequest(http.HttpRequest):
1059+    def __init__(self, POST=None):
1060+        super(DummyRequest, self).__init__()
1061+        self.method = POST and "POST" or "GET"
1062+        if POST is not None:
1063+            self.POST.update(POST)
1064+        self.session = {}
1065+        self._dont_enforce_csrf_checks = True
1066+
1067+def get_request(*args, **kwargs):
1068+    request = DummyRequest(*args, **kwargs)
1069+    engine = import_module(settings.SESSION_ENGINE)
1070+    request.session = engine.SessionStore(None)
1071+    return request
1072+
1073+class Step1(forms.Form):
1074+    name = forms.CharField()
1075+
1076+class Step2(forms.Form):
1077+    name = forms.CharField()
1078+
1079+class Step3(forms.Form):
1080+    data = forms.CharField()
1081+
1082+class UserForm(forms.ModelForm):
1083+    class Meta:
1084+        model = User
1085+
1086+UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2)
1087+
1088+class TestWizard(WizardView):
1089+    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
1090+
1091+    def dispatch(self, request, *args, **kwargs):
1092+        response = super(TestWizard, self).dispatch(request, *args, **kwargs)
1093+        return response, self
1094+
1095+class FormTests(TestCase):
1096+    def test_form_init(self):
1097+        testform = TestWizard.get_initkwargs([Step1, Step2])
1098+        self.assertEquals(testform['form_list'], {u'0': Step1, u'1': Step2})
1099+
1100+        testform = TestWizard.get_initkwargs([('start', Step1), ('step2', Step2)])
1101+        self.assertEquals(
1102+            testform['form_list'], {u'start': Step1, u'step2': Step2})
1103+
1104+        testform = TestWizard.get_initkwargs([Step1, Step2, ('finish', Step3)])
1105+        self.assertEquals(
1106+            testform['form_list'], {u'0': Step1, u'1': Step2, u'finish': Step3})
1107+
1108+    def test_first_step(self):
1109+        request = get_request()
1110+
1111+        testform = TestWizard.as_view([Step1, Step2])
1112+        response, instance = testform(request)
1113+        self.assertEquals(instance.determine_step(), u'0')
1114+
1115+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1116+        response, instance = testform(request)
1117+
1118+        self.assertEquals(instance.determine_step(), 'start')
1119+
1120+    def test_persistence(self):
1121+        request = get_request({'name': 'data1'})
1122+
1123+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1124+        response, instance = testform(request)
1125+        self.assertEquals(instance.determine_step(), 'start')
1126+        instance.storage.set_current_step('step2')
1127+
1128+        testform2 = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1129+        response, instance = testform2(request)
1130+        self.assertEquals(instance.determine_step(), 'step2')
1131+
1132+    def test_form_condition(self):
1133+        request = get_request()
1134+
1135+        testform = TestWizard.as_view(
1136+            [('start', Step1), ('step2', Step2), ('step3', Step3)],
1137+            condition_list={'step2': True})
1138+        response, instance = testform(request)
1139+        self.assertEquals(instance.get_next_step(), 'step2')
1140+
1141+        testform = TestWizard.as_view(
1142+            [('start', Step1), ('step2', Step2), ('step3', Step3)],
1143+            condition_list={'step2': False})
1144+        response, instance = testform(request)
1145+        self.assertEquals(instance.get_next_step(), 'step3')
1146+
1147+    def test_add_extra_context(self):
1148+        request = get_request()
1149+
1150+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1151+        response, instance = testform(
1152+            request, extra_context={'key1': 'value1'})
1153+        self.assertEqual(instance.get_extra_context(), {'key1': 'value1'})
1154+
1155+        request.method = 'POST'
1156+        response, instance = testform(
1157+            request, extra_context={'key1': 'value1'})
1158+        self.assertEqual(instance.get_extra_context(), {'key1': 'value1'})
1159+
1160+    def test_form_prefix(self):
1161+        request = get_request()
1162+
1163+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1164+        response, instance = testform(request)
1165+
1166+        self.assertEqual(instance.get_form_prefix(), 'start')
1167+        self.assertEqual(instance.get_form_prefix('another'), 'another')
1168+
1169+    def test_form_initial(self):
1170+        request = get_request()
1171+
1172+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)],
1173+            initial_list={'start': {'name': 'value1'}})
1174+        response, instance = testform(request)
1175+
1176+        self.assertEqual(instance.get_form_initial('start'), {'name': 'value1'})
1177+        self.assertEqual(instance.get_form_initial('step2'), {})
1178+
1179+    def test_form_instance(self):
1180+        request = get_request()
1181+        the_instance = User()
1182+        testform = TestWizard.as_view([('start', UserForm), ('step2', Step2)],
1183+            instance_list={'start': the_instance})
1184+        response, instance = testform(request)
1185+
1186+        self.assertEqual(
1187+            instance.get_form_instance('start'),
1188+            the_instance)
1189+        self.assertEqual(
1190+            instance.get_form_instance('non_exist_instance'),
1191+            None)
1192+
1193+    def test_formset_instance(self):
1194+        request = get_request()
1195+        the_instance1, created = User.objects.get_or_create(
1196+            username='testuser1')
1197+        the_instance2, created = User.objects.get_or_create(
1198+            username='testuser2')
1199+        testform = TestWizard.as_view([('start', UserFormSet), ('step2', Step2)],
1200+            instance_list={'start': User.objects.filter(username='testuser1')})
1201+        response, instance = testform(request)
1202+
1203+        self.assertEqual(list(instance.get_form_instance('start')), [the_instance1])
1204+        self.assertEqual(instance.get_form_instance('non_exist_instance'), None)
1205+
1206+        self.assertEqual(instance.get_form().initial_form_count(), 1)
1207+
1208+    def test_done(self):
1209+        request = get_request()
1210+
1211+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1212+        response, instance = testform(request)
1213+
1214+        self.assertRaises(NotImplementedError, instance.done, None)
1215+
1216+    def test_revalidation(self):
1217+        request = get_request()
1218+
1219+        testform = TestWizard.as_view([('start', Step1), ('step2', Step2)])
1220+        response, instance = testform(request)
1221+        instance.render_done(None)
1222+        self.assertEqual(instance.storage.get_current_step(), 'start')
1223+
1224+    def test_form_refresh(self):
1225+        testform = TestWizard.as_view([('start', Step1), ('step2', UserFormSet)])
1226+        request = get_request({'start-name': 'foo'})
1227+        request.method = 'POST'
1228+
1229+        response, instance = testform(request)
1230+        self.assertEqual(instance.storage.get_current_step(), 'step2')
1231+        # refresh form
1232+        response, instance = testform(request)
1233+        self.assertEqual(instance.storage.get_current_step(), 'step2')
1234+
1235+
1236+class SessionFormTests(TestCase):
1237+    def test_init(self):
1238+        request = get_request()
1239+        testform = SessionWizardView.as_view([('start', Step1)])
1240+        self.assertTrue(isinstance(testform(request), TemplateResponse))
1241+
1242+
1243+class CookieFormTests(TestCase):
1244+    def test_init(self):
1245+        request = get_request()
1246+        testform = CookieWizardView.as_view([('start', Step1)])
1247+        self.assertTrue(isinstance(testform(request), TemplateResponse))
1248+
1249diff --git a/django/contrib/formtools/wizard/tests/loadstoragetests.py b/django/contrib/formtools/wizard/tests/loadstoragetests.py
1250new file mode 100644
1251index 0000000..267dee0
1252--- /dev/null
1253+++ b/django/contrib/formtools/wizard/tests/loadstoragetests.py
1254@@ -0,0 +1,22 @@
1255+from django.test import TestCase
1256+
1257+from django.contrib.formtools.wizard.storage import (get_storage,
1258+                                                     MissingStorageModule,
1259+                                                     MissingStorageClass)
1260+from django.contrib.formtools.wizard.storage.base import BaseStorage
1261+
1262+
1263+class TestLoadStorage(TestCase):
1264+    def test_load_storage(self):
1265+        self.assertEqual(
1266+            type(get_storage('django.contrib.formtools.wizard.storage.base.BaseStorage', 'wizard1')),
1267+            BaseStorage)
1268+
1269+    def test_missing_module(self):
1270+        self.assertRaises(MissingStorageModule, get_storage,
1271+            'django.contrib.formtools.wizard.storage.idontexist.IDontExistStorage', 'wizard1')
1272+
1273+    def test_missing_class(self):
1274+        self.assertRaises(MissingStorageClass, get_storage,
1275+            'django.contrib.formtools.wizard.storage.base.IDontExistStorage', 'wizard1')
1276+
1277diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py b/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
1278new file mode 100644
1279index 0000000..4387356
1280--- /dev/null
1281+++ b/django/contrib/formtools/wizard/tests/namedwizardtests/__init__.py
1282@@ -0,0 +1 @@
1283+from django.contrib.formtools.wizard.tests.namedwizardtests.tests import *
1284\ No newline at end of file
1285diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py b/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
1286new file mode 100644
1287index 0000000..ae98126
1288--- /dev/null
1289+++ b/django/contrib/formtools/wizard/tests/namedwizardtests/forms.py
1290@@ -0,0 +1,42 @@
1291+from django import forms
1292+from django.forms.formsets import formset_factory
1293+from django.http import HttpResponse
1294+from django.template import Template, Context
1295+
1296+from django.contrib.auth.models import User
1297+
1298+from django.contrib.formtools.wizard.views import NamedUrlWizardView
1299+
1300+class Page1(forms.Form):
1301+    name = forms.CharField(max_length=100)
1302+    user = forms.ModelChoiceField(queryset=User.objects.all())
1303+    thirsty = forms.NullBooleanField()
1304+
1305+class Page2(forms.Form):
1306+    address1 = forms.CharField(max_length=100)
1307+    address2 = forms.CharField(max_length=100)
1308+
1309+class Page3(forms.Form):
1310+    random_crap = forms.CharField(max_length=100)
1311+
1312+Page4 = formset_factory(Page3, extra=2)
1313+
1314+class ContactWizard(NamedUrlWizardView):
1315+    def done(self, form_list, **kwargs):
1316+        c = Context({
1317+            'form_list': [x.cleaned_data for x in form_list],
1318+            'all_cleaned_data': self.get_all_cleaned_data()
1319+        })
1320+
1321+        for form in self.form_list.keys():
1322+            c[form] = self.get_cleaned_data_for_step(form)
1323+
1324+        c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
1325+        return HttpResponse(Template('').render(c))
1326+
1327+class SessionContactWizard(ContactWizard):
1328+    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
1329+
1330+class CookieContactWizard(ContactWizard):
1331+    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
1332+
1333diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/models.py b/django/contrib/formtools/wizard/tests/namedwizardtests/models.py
1334new file mode 100644
1335index 0000000..e69de29
1336diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py b/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
1337new file mode 100644
1338index 0000000..de83764
1339--- /dev/null
1340+++ b/django/contrib/formtools/wizard/tests/namedwizardtests/tests.py
1341@@ -0,0 +1,334 @@
1342+import os
1343+
1344+from django.core.urlresolvers import reverse
1345+from django.http import QueryDict
1346+from django.test import TestCase
1347+from django.conf import settings
1348+
1349+from django.contrib.auth.models import User
1350+
1351+from django.contrib.formtools import wizard
1352+
1353+from django.contrib.formtools.wizard.views import (NamedUrlSessionWizardView,
1354+                                                   NamedUrlCookieWizardView)
1355+from django.contrib.formtools.wizard.tests.formtests import (get_request,
1356+                                                             Step1,
1357+                                                             Step2)
1358+
1359+class NamedWizardTests(object):
1360+    urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
1361+
1362+    wizard_step_data = (
1363+        {
1364+            'form1-name': 'Pony',
1365+            'form1-thirsty': '2',
1366+        },
1367+        {
1368+            'form2-address1': '123 Main St',
1369+            'form2-address2': 'Djangoland',
1370+        },
1371+        {
1372+            'form3-random_crap': 'blah blah',
1373+        },
1374+        {
1375+            'form4-INITIAL_FORMS': '0',
1376+            'form4-TOTAL_FORMS': '2',
1377+            'form4-MAX_NUM_FORMS': '0',
1378+            'form4-0-random_crap': 'blah blah',
1379+            'form4-1-random_crap': 'blah blah',
1380+        }
1381+    )
1382+
1383+    def setUp(self):
1384+        self.testuser, created = User.objects.get_or_create(username='testuser1')
1385+        self.wizard_step_data[0]['form1-user'] = self.testuser.pk
1386+
1387+        wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
1388+        settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
1389+
1390+    def tearDown(self):
1391+        del settings.TEMPLATE_DIRS[-1]
1392+
1393+    def test_initial_call(self):
1394+        response = self.client.get(reverse('%s_start' % self.wizard_urlname))
1395+        self.assertEqual(response.status_code, 302)
1396+        response = self.client.get(response['Location'])
1397+        self.assertEqual(response.status_code, 200)
1398+        self.assertEqual(response.context['form_step'], 'form1')
1399+        self.assertEqual(response.context['form_step0'], 0)
1400+        self.assertEqual(response.context['form_step1'], 1)
1401+        self.assertEqual(response.context['form_last_step'], 'form4')
1402+        self.assertEqual(response.context['form_prev_step'], None)
1403+        self.assertEqual(response.context['form_next_step'], 'form2')
1404+        self.assertEqual(response.context['form_step_count'], 4)
1405+
1406+    def test_initial_call_with_params(self):
1407+        get_params = {'getvar1': 'getval1', 'getvar2': 'getval2'}
1408+        response = self.client.get(reverse('%s_start' % self.wizard_urlname),
1409+                                   get_params)
1410+        self.assertEqual(response.status_code, 302)
1411+
1412+        # Test for proper redirect GET parameters
1413+        location = response['Location']
1414+        self.assertNotEqual(location.find('?'), -1)
1415+        querydict = QueryDict(location[location.find('?') + 1:])
1416+        self.assertEqual(dict(querydict.items()), get_params)
1417+
1418+    def test_form_post_error(self):
1419+        response = self.client.post(
1420+            reverse(self.wizard_urlname, kwargs={'step':'form1'}))
1421+
1422+        self.assertEqual(response.status_code, 200)
1423+        self.assertEqual(response.context['form_step'], 'form1')
1424+        self.assertEqual(response.context['form'].errors,
1425+                         {'name': [u'This field is required.'],
1426+                          'user': [u'This field is required.']})
1427+
1428+    def test_form_post_success(self):
1429+        response = self.client.post(
1430+            reverse(self.wizard_urlname, kwargs={'step':'form1'}),
1431+            self.wizard_step_data[0])
1432+        response = self.client.get(response['Location'])
1433+
1434+        self.assertEqual(response.status_code, 200)
1435+        self.assertEqual(response.context['form_step'], 'form2')
1436+        self.assertEqual(response.context['form_step0'], 1)
1437+        self.assertEqual(response.context['form_prev_step'], 'form1')
1438+        self.assertEqual(response.context['form_next_step'], 'form3')
1439+
1440+    def test_form_stepback(self):
1441+        response = self.client.get(
1442+            reverse(self.wizard_urlname, kwargs={'step':'form1'}))
1443+
1444+        self.assertEqual(response.status_code, 200)
1445+        self.assertEqual(response.context['form_step'], 'form1')
1446+
1447+        response = self.client.post(
1448+            reverse(self.wizard_urlname, kwargs={'step':'form1'}),
1449+            self.wizard_step_data[0])
1450+        response = self.client.get(response['Location'])
1451+
1452+        self.assertEqual(response.status_code, 200)
1453+        self.assertEqual(response.context['form_step'], 'form2')
1454+
1455+        response = self.client.post(
1456+            reverse(self.wizard_urlname,
1457+                    kwargs={'step': response.context['form_step']}),
1458+            {'form_prev_step': response.context['form_prev_step']})
1459+        response = self.client.get(response['Location'])
1460+
1461+        self.assertEqual(response.status_code, 200)
1462+        self.assertEqual(response.context['form_step'], 'form1')
1463+
1464+    def test_form_jump(self):
1465+        response = self.client.get(
1466+            reverse(self.wizard_urlname, kwargs={'step':'form1'}))
1467+
1468+        self.assertEqual(response.status_code, 200)
1469+        self.assertEqual(response.context['form_step'], 'form1')
1470+
1471+        response = self.client.get(
1472+            reverse(self.wizard_urlname, kwargs={'step':'form3'}))
1473+        self.assertEqual(response.status_code, 200)
1474+        self.assertEqual(response.context['form_step'], 'form3')
1475+
1476+    def test_form_finish(self):
1477+        response = self.client.get(
1478+            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
1479+
1480+        self.assertEqual(response.status_code, 200)
1481+        self.assertEqual(response.context['form_step'], 'form1')
1482+
1483+        response = self.client.post(
1484+            reverse(self.wizard_urlname,
1485+                    kwargs={'step': response.context['form_step']}),
1486+            self.wizard_step_data[0])
1487+        response = self.client.get(response['Location'])
1488+
1489+        self.assertEqual(response.status_code, 200)
1490+        self.assertEqual(response.context['form_step'], 'form2')
1491+
1492+        response = self.client.post(
1493+            reverse(self.wizard_urlname,
1494+                    kwargs={'step': response.context['form_step']}),
1495+            self.wizard_step_data[1])
1496+        response = self.client.get(response['Location'])
1497+
1498+        self.assertEqual(response.status_code, 200)
1499+        self.assertEqual(response.context['form_step'], 'form3')
1500+
1501+        response = self.client.post(
1502+            reverse(self.wizard_urlname,
1503+                    kwargs={'step': response.context['form_step']}),
1504+            self.wizard_step_data[2])
1505+        response = self.client.get(response['Location'])
1506+
1507+        self.assertEqual(response.status_code, 200)
1508+        self.assertEqual(response.context['form_step'], 'form4')
1509+
1510+        response = self.client.post(
1511+            reverse(self.wizard_urlname,
1512+                    kwargs={'step': response.context['form_step']}),
1513+            self.wizard_step_data[3])
1514+        response = self.client.get(response['Location'])
1515+        self.assertEqual(response.status_code, 200)
1516+
1517+        self.assertEqual(response.context['form_list'], [
1518+            {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
1519+            {'address1': u'123 Main St', 'address2': u'Djangoland'},
1520+            {'random_crap': u'blah blah'},
1521+            [{'random_crap': u'blah blah'}, {'random_crap': u'blah blah'}]])
1522+
1523+    def test_cleaned_data(self):
1524+        response = self.client.get(
1525+            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
1526+        self.assertEqual(response.status_code, 200)
1527+
1528+        response = self.client.post(
1529+            reverse(self.wizard_urlname,
1530+                    kwargs={'step': response.context['form_step']}),
1531+            self.wizard_step_data[0])
1532+        response = self.client.get(response['Location'])
1533+        self.assertEqual(response.status_code, 200)
1534+
1535+        response = self.client.post(
1536+            reverse(self.wizard_urlname,
1537+                    kwargs={'step': response.context['form_step']}),
1538+            self.wizard_step_data[1])
1539+        response = self.client.get(response['Location'])
1540+        self.assertEqual(response.status_code, 200)
1541+
1542+        response = self.client.post(
1543+            reverse(self.wizard_urlname,
1544+                    kwargs={'step': response.context['form_step']}),
1545+            self.wizard_step_data[2])
1546+        response = self.client.get(response['Location'])
1547+        self.assertEqual(response.status_code, 200)
1548+
1549+        response = self.client.post(
1550+            reverse(self.wizard_urlname,
1551+                    kwargs={'step': response.context['form_step']}),
1552+            self.wizard_step_data[3])
1553+        response = self.client.get(response['Location'])
1554+        self.assertEqual(response.status_code, 200)
1555+
1556+        self.assertEqual(
1557+            response.context['all_cleaned_data'],
1558+            {'name': u'Pony', 'thirsty': True, 'user': self.testuser,
1559+             'address1': u'123 Main St', 'address2': u'Djangoland',
1560+             'random_crap': u'blah blah', 'formset-form4': [
1561+                 {'random_crap': u'blah blah'},
1562+                 {'random_crap': u'blah blah'}
1563+             ]})
1564+
1565+    def test_manipulated_data(self):
1566+        response = self.client.get(
1567+            reverse(self.wizard_urlname, kwargs={'step': 'form1'}))
1568+        self.assertEqual(response.status_code, 200)
1569+
1570+        response = self.client.post(
1571+            reverse(self.wizard_urlname,
1572+                    kwargs={'step': response.context['form_step']}),
1573+            self.wizard_step_data[0])
1574+        response = self.client.get(response['Location'])
1575+        self.assertEqual(response.status_code, 200)
1576+
1577+        response = self.client.post(
1578+            reverse(self.wizard_urlname,
1579+                    kwargs={'step': response.context['form_step']}),
1580+            self.wizard_step_data[1])
1581+        response = self.client.get(response['Location'])
1582+        self.assertEqual(response.status_code, 200)
1583+
1584+        response = self.client.post(
1585+            reverse(self.wizard_urlname,
1586+                    kwargs={'step': response.context['form_step']}),
1587+            self.wizard_step_data[2])
1588+        response = self.client.get(response['Location'])
1589+        self.assertEqual(response.status_code, 200)
1590+
1591+        self.client.cookies.pop('sessionid', None)
1592+        self.client.cookies.pop('wizard_cookie_contact_wizard', None)
1593+
1594+        response = self.client.post(
1595+            reverse(self.wizard_urlname,
1596+                    kwargs={'step': response.context['form_step']}),
1597+            self.wizard_step_data[3])
1598+        self.assertEqual(response.status_code, 200)
1599+        self.assertEqual(response.context.get('form_step', None), 'form1')
1600+
1601+    def test_form_reset(self):
1602+        response = self.client.post(
1603+            reverse(self.wizard_urlname, kwargs={'step':'form1'}),
1604+            self.wizard_step_data[0])
1605+        response = self.client.get(response['Location'])
1606+        self.assertEqual(response.status_code, 200)
1607+        self.assertEqual(response.context['form_step'], 'form2')
1608+
1609+        response = self.client.get(
1610+            '%s?reset=1' % reverse('%s_start' % self.wizard_urlname))
1611+        self.assertEqual(response.status_code, 302)
1612+
1613+        response = self.client.get(response['Location'])
1614+        self.assertEqual(response.status_code, 200)
1615+        self.assertEqual(response.context['form_step'], 'form1')
1616+
1617+class NamedSessionWizardTests(NamedWizardTests, TestCase):
1618+    wizard_urlname = 'nwiz_session'
1619+
1620+class NamedCookieWizardTests(NamedWizardTests, TestCase):
1621+    wizard_urlname = 'nwiz_cookie'
1622+
1623+class NamedFormTests(object):
1624+    urls = 'django.contrib.formtools.wizard.tests.namedwizardtests.urls'
1625+
1626+    def test_add_extra_context(self):
1627+        request = get_request()
1628+
1629+        testform = self.formwizard_class.as_view(
1630+            [('start', Step1), ('step2', Step2)],
1631+            url_name=self.wizard_urlname)
1632+
1633+        response, instance = testform(request,
1634+                                      step='form1',
1635+                                      extra_context={'key1': 'value1'})
1636+        self.assertEqual(instance.get_extra_context(), {'key1': 'value1'})
1637+
1638+        instance.reset_wizard()
1639+
1640+        response, instance = testform(request,
1641+                                      extra_context={'key2': 'value2'})
1642+        self.assertEqual(instance.get_extra_context(), {'key2': 'value2'})
1643+
1644+    def test_revalidation(self):
1645+        request = get_request()
1646+
1647+        testform = self.formwizard_class.as_view(
1648+            [('start', Step1), ('step2', Step2)],
1649+            url_name=self.wizard_urlname)
1650+        response, instance = testform(request, step='done')
1651+
1652+        instance.render_done(None)
1653+        self.assertEqual(instance.storage.get_current_step(), 'start')
1654+
1655+class TestNamedUrlSessionFormWizard(NamedUrlSessionWizardView):
1656+
1657+    def dispatch(self, request, *args, **kwargs):
1658+        response = super(TestNamedUrlSessionFormWizard, self).dispatch(request, *args, **kwargs)
1659+        return response, self
1660+
1661+class TestNamedUrlCookieFormWizard(NamedUrlCookieWizardView):
1662+
1663+    def dispatch(self, request, *args, **kwargs):
1664+        response = super(TestNamedUrlCookieFormWizard, self).dispatch(request, *args, **kwargs)
1665+        return response, self
1666+
1667+
1668+class NamedSessionFormTests(NamedFormTests, TestCase):
1669+    formwizard_class = TestNamedUrlSessionFormWizard
1670+    wizard_urlname = 'nwiz_session'
1671+
1672+class NamedCookieFormTests(NamedFormTests, TestCase):
1673+    formwizard_class = TestNamedUrlCookieFormWizard
1674+    wizard_urlname = 'nwiz_cookie'
1675+
1676diff --git a/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py b/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
1677new file mode 100644
1678index 0000000..a97ca98
1679--- /dev/null
1680+++ b/django/contrib/formtools/wizard/tests/namedwizardtests/urls.py
1681@@ -0,0 +1,24 @@
1682+from django.conf.urls.defaults import *
1683+from django.contrib.formtools.wizard.tests.namedwizardtests.forms import (
1684+    SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
1685+
1686+def get_named_session_wizard():
1687+    return SessionContactWizard.as_view(
1688+        [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
1689+        url_name='nwiz_session',
1690+        done_step_name='nwiz_session_done'
1691+    )
1692+
1693+def get_named_cookie_wizard():
1694+    return CookieContactWizard.as_view(
1695+        [('form1', Page1), ('form2', Page2), ('form3', Page3), ('form4', Page4)],
1696+        url_name='nwiz_cookie',
1697+        done_step_name='nwiz_cookie_done'
1698+    )
1699+
1700+urlpatterns = patterns('',
1701+    url(r'^nwiz_session/(?P<step>.+)/$', get_named_session_wizard(), name='nwiz_session'),
1702+    url(r'^nwiz_session/$', get_named_session_wizard(), name='nwiz_session_start'),
1703+    url(r'^nwiz_cookie/(?P<step>.+)/$', get_named_cookie_wizard(), name='nwiz_cookie'),
1704+    url(r'^nwiz_cookie/$', get_named_cookie_wizard(), name='nwiz_cookie_start'),
1705+)
1706diff --git a/django/contrib/formtools/wizard/tests/sessionstoragetests.py b/django/contrib/formtools/wizard/tests/sessionstoragetests.py
1707new file mode 100644
1708index 0000000..b89e9c2
1709--- /dev/null
1710+++ b/django/contrib/formtools/wizard/tests/sessionstoragetests.py
1711@@ -0,0 +1,9 @@
1712+from django.test import TestCase
1713+
1714+from django.contrib.formtools.wizard.tests.storagetests import *
1715+from django.contrib.formtools.wizard.storage.session import SessionStorage
1716+
1717+class TestSessionStorage(TestStorage, TestCase):
1718+    def get_storage(self):
1719+        return SessionStorage
1720+
1721diff --git a/django/contrib/formtools/wizard/tests/storagetests.py b/django/contrib/formtools/wizard/tests/storagetests.py
1722new file mode 100644
1723index 0000000..897d062
1724--- /dev/null
1725+++ b/django/contrib/formtools/wizard/tests/storagetests.py
1726@@ -0,0 +1,76 @@
1727+from datetime import datetime
1728+
1729+from django.http import HttpRequest
1730+from django.conf import settings
1731+from django.utils.importlib import import_module
1732+
1733+from django.contrib.auth.models import User
1734+
1735+def get_request():
1736+    request = HttpRequest()
1737+    engine = import_module(settings.SESSION_ENGINE)
1738+    request.session = engine.SessionStore(None)
1739+    return request
1740+
1741+class TestStorage(object):
1742+    def setUp(self):
1743+        self.testuser, created = User.objects.get_or_create(username='testuser1')
1744+
1745+    def test_current_step(self):
1746+        request = get_request()
1747+        storage = self.get_storage()('wizard1', request, None)
1748+        my_step = 2
1749+
1750+        self.assertEqual(storage.get_current_step(), None)
1751+
1752+        storage.set_current_step(my_step)
1753+        self.assertEqual(storage.get_current_step(), my_step)
1754+
1755+        storage.reset()
1756+        self.assertEqual(storage.get_current_step(), None)
1757+
1758+        storage.set_current_step(my_step)
1759+        storage2 = self.get_storage()('wizard2', request, None)
1760+        self.assertEqual(storage2.get_current_step(), None)
1761+
1762+    def test_step_data(self):
1763+        request = get_request()
1764+        storage = self.get_storage()('wizard1', request, None)
1765+        step1 = 'start'
1766+        step_data1 = {'field1': 'data1',
1767+                      'field2': 'data2',
1768+                      'field3': datetime.now(),
1769+                      'field4': self.testuser}
1770+
1771+        self.assertEqual(storage.get_step_data(step1), None)
1772+
1773+        storage.set_step_data(step1, step_data1)
1774+        self.assertEqual(storage.get_step_data(step1), step_data1)
1775+
1776+        storage.reset()
1777+        self.assertEqual(storage.get_step_data(step1), None)
1778+
1779+        storage.set_step_data(step1, step_data1)
1780+        storage2 = self.get_storage()('wizard2', request, None)
1781+        self.assertEqual(storage2.get_step_data(step1), None)
1782+
1783+    def test_extra_context(self):
1784+        request = get_request()
1785+        storage = self.get_storage()('wizard1', request, None)
1786+        extra_context = {'key1': 'data1',
1787+                         'key2': 'data2',
1788+                         'key3': datetime.now(),
1789+                         'key4': self.testuser}
1790+
1791+        self.assertEqual(storage.get_extra_context_data(), {})
1792+
1793+        storage.set_extra_context_data(extra_context)
1794+        self.assertEqual(storage.get_extra_context_data(), extra_context)
1795+
1796+        storage.reset()
1797+        self.assertEqual(storage.get_extra_context_data(), {})
1798+
1799+        storage.set_extra_context_data(extra_context)
1800+        storage2 = self.get_storage()('wizard2', request, None)
1801+        self.assertEqual(storage2.get_extra_context_data(), {})
1802+
1803diff --git a/django/contrib/formtools/wizard/tests/wizardtests/__init__.py b/django/contrib/formtools/wizard/tests/wizardtests/__init__.py
1804new file mode 100644
1805index 0000000..9173cd8
1806--- /dev/null
1807+++ b/django/contrib/formtools/wizard/tests/wizardtests/__init__.py
1808@@ -0,0 +1 @@
1809+from django.contrib.formtools.wizard.tests.wizardtests.tests import *
1810\ No newline at end of file
1811diff --git a/django/contrib/formtools/wizard/tests/wizardtests/forms.py b/django/contrib/formtools/wizard/tests/wizardtests/forms.py
1812new file mode 100644
1813index 0000000..971ff4d
1814--- /dev/null
1815+++ b/django/contrib/formtools/wizard/tests/wizardtests/forms.py
1816@@ -0,0 +1,57 @@
1817+import tempfile
1818+
1819+from django import forms
1820+from django.core.files.storage import FileSystemStorage
1821+from django.forms.formsets import formset_factory
1822+from django.http import HttpResponse
1823+from django.template import Template, Context
1824+
1825+from django.contrib.auth.models import User
1826+
1827+from django.contrib.formtools.wizard.views import WizardView
1828+
1829+temp_storage_location = tempfile.mkdtemp()
1830+temp_storage = FileSystemStorage(location=temp_storage_location)
1831+
1832+class Page1(forms.Form):
1833+    name = forms.CharField(max_length=100)
1834+    user = forms.ModelChoiceField(queryset=User.objects.all())
1835+    thirsty = forms.NullBooleanField()
1836+
1837+class Page2(forms.Form):
1838+    address1 = forms.CharField(max_length=100)
1839+    address2 = forms.CharField(max_length=100)
1840+    file1 = forms.FileField()
1841+
1842+class Page3(forms.Form):
1843+    random_crap = forms.CharField(max_length=100)
1844+
1845+Page4 = formset_factory(Page3, extra=2)
1846+
1847+class ContactWizard(WizardView):
1848+    file_storage = temp_storage
1849+
1850+    def done(self, form_list, **kwargs):
1851+        c = Context({
1852+            'form_list': [x.cleaned_data for x in form_list],
1853+            'all_cleaned_data': self.get_all_cleaned_data()
1854+        })
1855+
1856+        for form in self.form_list.keys():
1857+            c[form] = self.get_cleaned_data_for_step(form)
1858+
1859+        c['this_will_fail'] = self.get_cleaned_data_for_step('this_will_fail')
1860+        return HttpResponse(Template('').render(c))
1861+
1862+    def get_context_data(self, form, **kwargs):
1863+        context = super(ContactWizard, self).get_context_data(form, **kwargs)
1864+        if self.storage.get_current_step() == 'form2':
1865+            context.update({'another_var': True})
1866+        return context
1867+
1868+class SessionContactWizard(ContactWizard):
1869+    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
1870+
1871+class CookieContactWizard(ContactWizard):
1872+    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
1873+
1874diff --git a/django/contrib/formtools/wizard/tests/wizardtests/models.py b/django/contrib/formtools/wizard/tests/wizardtests/models.py
1875new file mode 100644
1876index 0000000..e69de29
1877diff --git a/django/contrib/formtools/wizard/tests/wizardtests/tests.py b/django/contrib/formtools/wizard/tests/wizardtests/tests.py
1878new file mode 100644
1879index 0000000..2dc8fa0
1880--- /dev/null
1881+++ b/django/contrib/formtools/wizard/tests/wizardtests/tests.py
1882@@ -0,0 +1,186 @@
1883+import os
1884+
1885+from django.test import TestCase
1886+from django.conf import settings
1887+from django.contrib.auth.models import User
1888+
1889+from django.contrib.formtools import wizard
1890+
1891+class WizardTests(object):
1892+    urls = 'django.contrib.formtools.wizard.tests.wizardtests.urls'
1893+
1894+    wizard_step_data = (
1895+        {
1896+            'form1-name': 'Pony',
1897+            'form1-thirsty': '2',
1898+        },
1899+        {
1900+            'form2-address1': '123 Main St',
1901+            'form2-address2': 'Djangoland',
1902+        },
1903+        {
1904+            'form3-random_crap': 'blah blah',
1905+        },
1906+        {
1907+            'form4-INITIAL_FORMS': '0',
1908+            'form4-TOTAL_FORMS': '2',
1909+            'form4-MAX_NUM_FORMS': '0',
1910+            'form4-0-random_crap': 'blah blah',
1911+            'form4-1-random_crap': 'blah blah',
1912+        }
1913+    )
1914+
1915+    def setUp(self):
1916+        self.testuser, created = User.objects.get_or_create(username='testuser1')
1917+        self.wizard_step_data[0]['form1-user'] = self.testuser.pk
1918+
1919+        wizard_template_dirs = [os.path.join(os.path.dirname(wizard.__file__), 'templates')]
1920+        settings.TEMPLATE_DIRS = list(settings.TEMPLATE_DIRS) + wizard_template_dirs
1921+
1922+    def tearDown(self):
1923+        del settings.TEMPLATE_DIRS[-1]
1924+
1925+    def test_initial_call(self):
1926+        response = self.client.get(self.wizard_url)
1927+
1928+        self.assertEqual(response.status_code, 200)
1929+        self.assertEqual(response.context['form_step'], 'form1')
1930+        self.assertEqual(response.context['form_step0'], 0)
1931+        self.assertEqual(response.context['form_step1'], 1)
1932+        self.assertEqual(response.context['form_last_step'], 'form4')
1933+        self.assertEqual(response.context['form_prev_step'], None)
1934+        self.assertEqual(response.context['form_next_step'], 'form2')
1935+        self.assertEqual(response.context['form_step_count'], 4)
1936+
1937+    def test_form_post_error(self):
1938+        response = self.client.post(self.wizard_url)
1939+
1940+        self.assertEqual(response.status_code, 200)
1941+        self.assertEqual(response.context['form_step'], 'form1')
1942+        self.assertEqual(response.context['form'].errors,
1943+                         {'name': [u'This field is required.'],
1944+                          'user': [u'This field is required.']})
1945+
1946+    def test_form_post_success(self):
1947+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
1948+
1949+        self.assertEqual(response.status_code, 200)
1950+        self.assertEqual(response.context['form_step'], 'form2')
1951+        self.assertEqual(response.context['form_step0'], 1)
1952+        self.assertEqual(response.context['form_prev_step'], 'form1')
1953+        self.assertEqual(response.context['form_next_step'], 'form3')
1954+
1955+    def test_form_stepback(self):
1956+        response = self.client.get(self.wizard_url)
1957+
1958+        self.assertEqual(response.status_code, 200)
1959+        self.assertEqual(response.context['form_step'], 'form1')
1960+
1961+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
1962+
1963+        self.assertEqual(response.status_code, 200)
1964+        self.assertEqual(response.context['form_step'], 'form2')
1965+
1966+        response = self.client.post(
1967+            self.wizard_url,
1968+            {'form_prev_step': response.context['form_prev_step']})
1969+
1970+        self.assertEqual(response.status_code, 200)
1971+        self.assertEqual(response.context['form_step'], 'form1')
1972+
1973+    def test_template_context(self):
1974+        response = self.client.get(self.wizard_url)
1975+
1976+        self.assertEqual(response.status_code, 200)
1977+        self.assertEqual(response.context['form_step'], 'form1')
1978+        self.assertEqual(response.context.get('another_var', None), None)
1979+
1980+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
1981+
1982+        self.assertEqual(response.status_code, 200)
1983+        self.assertEqual(response.context['form_step'], 'form2')
1984+        self.assertEqual(response.context.get('another_var', None), True)
1985+
1986+    def test_form_finish(self):
1987+        response = self.client.get(self.wizard_url)
1988+
1989+        self.assertEqual(response.status_code, 200)
1990+        self.assertEqual(response.context['form_step'], 'form1')
1991+
1992+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
1993+
1994+        self.assertEqual(response.status_code, 200)
1995+        self.assertEqual(response.context['form_step'], 'form2')
1996+
1997+        post_data = self.wizard_step_data[1]
1998+        post_data['form2-file1'] = open(__file__)
1999+        response = self.client.post(self.wizard_url, post_data)
2000+
2001+        self.assertEqual(response.status_code, 200)
2002+        self.assertEqual(response.context['form_step'], 'form3')
2003+
2004+        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
2005+
2006+        self.assertEqual(response.status_code, 200)
2007+        self.assertEqual(response.context['form_step'], 'form4')
2008+
2009+        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
2010+        self.assertEqual(response.status_code, 200)
2011+
2012+        all_data = response.context['form_list']
2013+        self.assertEqual(all_data[1]['file1'].read(), open(__file__).read())
2014+        del all_data[1]['file1']
2015+        self.assertEqual(all_data, [
2016+            {'name': u'Pony', 'thirsty': True, 'user': self.testuser},
2017+            {'address1': u'123 Main St', 'address2': u'Djangoland'},
2018+            {'random_crap': u'blah blah'},
2019+            [{'random_crap': u'blah blah'},
2020+             {'random_crap': u'blah blah'}]])
2021+
2022+    def test_cleaned_data(self):
2023+        response = self.client.get(self.wizard_url)
2024+        self.assertEqual(response.status_code, 200)
2025+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
2026+        self.assertEqual(response.status_code, 200)
2027+        post_data = self.wizard_step_data[1]
2028+        post_data['form2-file1'] = open(__file__)
2029+        response = self.client.post(self.wizard_url, post_data)
2030+        self.assertEqual(response.status_code, 200)
2031+        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
2032+        self.assertEqual(response.status_code, 200)
2033+        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
2034+        self.assertEqual(response.status_code, 200)
2035+
2036+        all_data = response.context['all_cleaned_data']
2037+        self.assertEqual(all_data['file1'].read(), open(__file__).read())
2038+        del all_data['file1']
2039+        self.assertEqual(all_data, {
2040+            'name': u'Pony', 'thirsty': True, 'user': self.testuser,
2041+            'address1': u'123 Main St', 'address2': u'Djangoland',
2042+            'random_crap': u'blah blah', 'formset-form4': [
2043+                {'random_crap': u'blah blah'},
2044+                {'random_crap': u'blah blah'}]})
2045+
2046+    def test_manipulated_data(self):
2047+        response = self.client.get(self.wizard_url)
2048+        self.assertEqual(response.status_code, 200)
2049+        response = self.client.post(self.wizard_url, self.wizard_step_data[0])
2050+        self.assertEqual(response.status_code, 200)
2051+        post_data = self.wizard_step_data[1]
2052+        post_data['form2-file1'] = open(__file__)
2053+        response = self.client.post(self.wizard_url, post_data)
2054+        self.assertEqual(response.status_code, 200)
2055+        response = self.client.post(self.wizard_url, self.wizard_step_data[2])
2056+        self.assertEqual(response.status_code, 200)
2057+        self.client.cookies.pop('sessionid', None)
2058+        self.client.cookies.pop('wizard_cookie_contact_wizard', None)
2059+        response = self.client.post(self.wizard_url, self.wizard_step_data[3])
2060+        self.assertEqual(response.status_code, 200)
2061+        self.assertEqual(response.context.get('form_step', None), 'form1')
2062+
2063+class SessionWizardTests(WizardTests, TestCase):
2064+    wizard_url = '/wiz_session/'
2065+
2066+class CookieWizardTests(WizardTests, TestCase):
2067+    wizard_url = '/wiz_cookie/'
2068+
2069diff --git a/django/contrib/formtools/wizard/tests/wizardtests/urls.py b/django/contrib/formtools/wizard/tests/wizardtests/urls.py
2070new file mode 100644
2071index 0000000..e305397
2072--- /dev/null
2073+++ b/django/contrib/formtools/wizard/tests/wizardtests/urls.py
2074@@ -0,0 +1,16 @@
2075+from django.conf.urls.defaults import *
2076+from django.contrib.formtools.wizard.tests.wizardtests.forms import (
2077+    SessionContactWizard, CookieContactWizard, Page1, Page2, Page3, Page4)
2078+
2079+urlpatterns = patterns('',
2080+    url(r'^wiz_session/$', SessionContactWizard.as_view(
2081+        [('form1', Page1),
2082+         ('form2', Page2),
2083+         ('form3', Page3),
2084+         ('form4', Page4)])),
2085+    url(r'^wiz_cookie/$', CookieContactWizard.as_view(
2086+        [('form1', Page1),
2087+         ('form2', Page2),
2088+         ('form3', Page3),
2089+         ('form4', Page4)])),
2090+)
2091diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
2092new file mode 100644
2093index 0000000..f00a428
2094--- /dev/null
2095+++ b/django/contrib/formtools/wizard/views.py
2096@@ -0,0 +1,700 @@
2097+import re
2098+
2099+from django import forms
2100+from django.core.urlresolvers import reverse
2101+from django.forms import formsets
2102+from django.http import HttpResponseRedirect
2103+from django.views.generic import TemplateView
2104+from django.utils.datastructures import SortedDict
2105+from django.utils.decorators import classonlymethod
2106+
2107+from django.contrib.formtools.wizard.storage import get_storage, NoFileStorageConfigured
2108+
2109+def normalize_name(name):
2110+    new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', name)
2111+    return new.lower().strip('_')
2112+
2113+class WizardView(TemplateView):
2114+    """
2115+    The WizardView is used to create multi-page forms and handles all the
2116+    storage and validation stuff. The wizard is based on Django's generic
2117+    class based views.
2118+    """
2119+    storage_name = None
2120+    form_list = None
2121+    initial_list = None
2122+    instance_list = None
2123+    condition_list = None
2124+    template_name = 'formtools/wizard/wizard.html'
2125+
2126+    @classonlymethod
2127+    def as_view(cls, *args, **kwargs):
2128+        """
2129+        This method is used within urls.py to create unique formwizard
2130+        instances for every request. We need to override this method because
2131+        we add some kwargs which are needed to make the formwizard usable.
2132+        """
2133+        initkwargs = cls.get_initkwargs(*args, **kwargs)
2134+        return super(WizardView, cls).as_view(**initkwargs)
2135+
2136+    @classmethod
2137+    def get_initkwargs(cls, form_list,
2138+            initial_list=None, instance_list=None, condition_list=None):
2139+        """
2140+        Creates a dict with all needed parameters for the form wizard instances.
2141+
2142+        * `form_list` - is a list of forms. The list entries can be single form
2143+          classes or tuples of (`step_name`, `form_class`). If you pass a list
2144+          of forms, the formwizard will convert the class list to
2145+          (`zero_based_counter`, `form_class`). This is needed to access the
2146+          form for a specific step.
2147+        * `initial_list` - contains a dictionary of initial data dictionaries.
2148+          The key should be equal to the `step_name` in the `form_list` (or
2149+          the str of the zero based counter - if no step_names added in the
2150+          `form_list`)
2151+        * `instance_list` - contains a dictionary of instance objects. This list
2152+          is only used when `ModelForm`s are used. The key should be equal to
2153+          the `step_name` in the `form_list`. Same rules as for `initial_list`
2154+          apply.
2155+        * `condition_list` - contains a dictionary of boolean values or
2156+          callables. If the value of for a specific `step_name` is callable it
2157+          will be called with the formwizard instance as the only argument.
2158+          If the return value is true, the step's form will be used.
2159+        """
2160+        kwargs = {
2161+            'initial_list': initial_list or {},
2162+            'instance_list': instance_list or {},
2163+            'condition_list': condition_list or {},
2164+        }
2165+        init_form_list = SortedDict()
2166+
2167+        assert len(form_list) > 0, 'at least one form is needed'
2168+
2169+        # walk through the passed form list
2170+        for i, form in enumerate(form_list):
2171+            if isinstance(form, (list, tuple)):
2172+                # if the element is a tuple, add the tuple to the new created
2173+                # sorted dictionary.
2174+                init_form_list[unicode(form[0])] = form[1]
2175+            else:
2176+                # if not, add the form with a zero based counter as unicode
2177+                init_form_list[unicode(i)] = form
2178+
2179+        # walk through the ne created list of forms
2180+        for form in init_form_list.values():
2181+            if issubclass(form, formsets.BaseFormSet):
2182+                # if the element is based on BaseFormSet (FormSet/ModelFormSet)
2183+                # we need to override the form variable.
2184+                form = form.form
2185+            # check if any form contains a FileField, if yes, we need a
2186+            # file_storage added to the formwizard (by subclassing).
2187+            for field in form.base_fields.values():
2188+                if (isinstance(field, forms.FileField) and
2189+                        not hasattr(cls, 'file_storage')):
2190+                    raise NoFileStorageConfigured
2191+
2192+        # build the kwargs for the formwizard instances
2193+        kwargs['form_list'] = init_form_list
2194+        return kwargs
2195+
2196+    def __repr__(self):
2197+        return '<%s: form_list: %s, initial_list: %s>' % (
2198+            self.__class__.__name__, self.form_list, self.initial_list)
2199+
2200+    def dispatch(self, request, *args, **kwargs):
2201+        """
2202+        This method gets called by the routing engine. The first argument is
2203+        `request` which contains a `HttpRequest` instance.
2204+        The request is stored in `self.request` for later use. The storage
2205+        instance is stored in `self.storage`.
2206+
2207+        After processing the request using the `dispatch` method, the
2208+        response gets updated by the storage engine (for example add cookies).
2209+        """
2210+        # add the storage engine to the current formwizard instance
2211+        self.storage = get_storage(
2212+            self.storage_name, normalize_name(self.__class__.__name__),
2213+            request, getattr(self, 'file_storage', None))
2214+        response = super(WizardView, self).dispatch(request, *args, **kwargs)
2215+
2216+        # update the response (e.g. adding cookies)
2217+        self.storage.update_response(response)
2218+        return response
2219+
2220+    def get_form_list(self):
2221+        """
2222+        This method returns a form_list based on the initial form list but
2223+        checks if there is a condition method/value in the condition_list.
2224+        If an entry exists in the condition list, it will call/read the value
2225+        and respect the result. (True means add the form, False means ignore
2226+        the form)
2227+
2228+        The form_list is always generated on the fly because condition methods
2229+        could use data from other (maybe previous forms).
2230+        """
2231+        form_list = SortedDict()
2232+        for form_key, form_class in self.form_list.items():
2233+            # try to fetch the value from condition list, by default, the form
2234+            # gets passed to the new list.
2235+            condition = self.condition_list.get(form_key, True)
2236+            if callable(condition):
2237+                # call the value if needed, passes the current instance.
2238+                condition = condition(self)
2239+            if condition:
2240+                form_list[form_key] = form_class
2241+        return form_list
2242+
2243+    def get(self, request, *args, **kwargs):
2244+        """
2245+        This method handles GET requests.
2246+
2247+        If a GET request reaches this point, the wizard assumes that the user
2248+        just starts at the first step or wants to restart the process.
2249+        The data of the wizard will be resetted before rendering the first step.
2250+        """
2251+        self.reset_wizard()
2252+
2253+        # if there is an extra_context item in the kwars, pass the data to the
2254+        # storage engine.
2255+        self.update_extra_context(kwargs.get('extra_context', {}))
2256+
2257+        # reset the current step to the first step.
2258+        self.storage.set_current_step(self.get_first_step())
2259+        return self.render(self.get_form())
2260+
2261+    def post(self, *args, **kwargs):
2262+        """
2263+        This method handles POST requests.
2264+
2265+        The wizard will render either the current step (if form validation
2266+        wasn't successful), the next step (if the current step was stored
2267+        successful) or the done view (if no more steps are available)
2268+        """
2269+        # if there is an extra_context item in the kwargs,
2270+        # pass the data to the storage engine.
2271+        self.update_extra_context(kwargs.get('extra_context', {}))
2272+
2273+        # Look for a form_prev_step element in the posted data which contains
2274+        # a valid step name. If one was found, render the requested form.
2275+        # (This makes stepping back a lot easier).
2276+        form_prev_step = self.request.POST.get('form_prev_step', None)
2277+        if form_prev_step and form_prev_step in self.get_form_list():
2278+            self.storage.set_current_step(form_prev_step)
2279+            current_step = self.determine_step()
2280+            form = self.get_form(data=self.storage.get_step_data(current_step),
2281+                files=self.storage.get_step_files(current_step))
2282+        else:
2283+            # TODO: refactor the form-was-refreshed code
2284+            # Check if form was refreshed
2285+            current_step = self.determine_step()
2286+            prev_step = self.get_prev_step(step=current_step)
2287+            for value in self.request.POST:
2288+                if (prev_step and not value.startswith(current_step) and
2289+                        value.startswith(prev_step)):
2290+                    # form refreshed, change current step
2291+                    self.storage.set_current_step(prev_step)
2292+                    break
2293+
2294+            # get the form for the current step
2295+            form = self.get_form(data=self.request.POST,
2296+                                 files=self.request.FILES)
2297+
2298+            # and try to validate
2299+            if form.is_valid():
2300+                # if the form is valid, store the cleaned data and files.
2301+                current_step = self.determine_step()
2302+                self.storage.set_step_data(current_step, self.process_step(form))
2303+                self.storage.set_step_files(current_step, self.process_step_files(form))
2304+
2305+                # check if the current step is the last step
2306+                if current_step == self.get_last_step():
2307+                    # no more steps, render done view
2308+                    return self.render_done(form, **kwargs)
2309+                else:
2310+                    # proceed to the next step
2311+                    return self.render_next_step(form)
2312+        return self.render(form)
2313+
2314+    def render_next_step(self, form, **kwargs):
2315+        """
2316+        THis method gets called when the next step/form should be rendered.
2317+        `form` contains the last/current form.
2318+        """
2319+        next_step = self.get_next_step()
2320+        # get the form instance based on the data from the storage backend
2321+        # (if available).
2322+        new_form = self.get_form(next_step,
2323+                                 data=self.storage.get_step_data(next_step),
2324+                                 files=self.storage.get_step_files(next_step))
2325+
2326+        # change the stored current step
2327+        self.storage.set_current_step(next_step)
2328+        return self.render(new_form, **kwargs)
2329+
2330+    def render_done(self, form, **kwargs):
2331+        """
2332+        This method gets called when all forms passed. The method should also
2333+        re-validate all steps to prevent manipulation. If any form don't
2334+        validate, `render_revalidation_failure` should get called.
2335+        If everything is fine call `done`.
2336+        """
2337+        final_form_list = []
2338+        # walk through the form list and try to validate the data again.
2339+        for form_key in self.get_form_list():
2340+            form_obj = self.get_form(
2341+                step=form_key,
2342+                data=self.storage.get_step_data(form_key),
2343+                files=self.storage.get_step_files(form_key)
2344+            )
2345+            if not form_obj.is_valid():
2346+                return self.render_revalidation_failure(form_key,
2347+                                                        form_obj,
2348+                                                        **kwargs)
2349+            final_form_list.append(form_obj)
2350+
2351+        # render the done view and reset the wizard before returning the
2352+        # response. This is needed to prevent from rendering done with the
2353+        # same data twice.
2354+        done_response = self.done(final_form_list, **kwargs)
2355+        self.reset_wizard()
2356+        return done_response
2357+
2358+    def get_form_prefix(self, step=None, form=None):
2359+        """
2360+        Returns the prefix which will be used when calling the actual form for
2361+        the given step. `step` contains the step-name, `form` the form which
2362+        will be called with the returned prefix.
2363+
2364+        If no step is given, the form_prefix will determine the current step
2365+        automatically.
2366+        """
2367+        if step is None:
2368+            step = self.determine_step()
2369+        return str(step)
2370+
2371+    def get_form_initial(self, step):
2372+        """
2373+        Returns a dictionary which will be passed to the form for `step`
2374+        as `initial`. If no initial data was provied while initializing the
2375+        form wizard, a empty dictionary will be returned.
2376+        """
2377+        return self.initial_list.get(step, {})
2378+
2379+    def get_form_instance(self, step):
2380+        """
2381+        Returns a object which will be passed to the form for `step`
2382+        as `instance`. If no instance object was provied while initializing
2383+        the form wizard, None be returned.
2384+        """
2385+        return self.instance_list.get(step, None)
2386+
2387+    def get_form(self, step=None, data=None, files=None):
2388+        """
2389+        Constructs the form for a given `step`. If no `step` is defined, the
2390+        current step will be determined automatically.
2391+
2392+        The form will be initialized using the `data` argument to prefill the
2393+        new form. If needed, instance or queryset (for `ModelForm` or
2394+        `ModelFormSet`) will be added too.
2395+        """
2396+        if step is None:
2397+            step = self.determine_step()
2398+
2399+        # prepare the kwargs for the form instance.
2400+        kwargs = {
2401+            'data': data,
2402+            'files': files,
2403+            'prefix': self.get_form_prefix(step, self.form_list[step]),
2404+            'initial': self.get_form_initial(step),
2405+        }
2406+        if issubclass(self.form_list[step], forms.ModelForm):
2407+            # If the form is based on ModelForm, add instance if available.
2408+            kwargs.update({'instance': self.get_form_instance(step)})
2409+        elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
2410+            # If the form is based on ModelFormSet, add queryset if available.
2411+            kwargs.update({'queryset': self.get_form_instance(step)})
2412+        return self.form_list[step](**kwargs)
2413+
2414+    def process_step(self, form):
2415+        """
2416+        This method is used to postprocess the form data. By default, it
2417+        returns the raw `form.data` dictionary.
2418+        """
2419+        return self.get_form_step_data(form)
2420+
2421+    def process_step_files(self, form):
2422+        """
2423+        This method is used to postprocess the form files. By default, it
2424+        returns the raw `form.files` dictionary.
2425+        """
2426+        return self.get_form_step_files(form)
2427+
2428+    def render_revalidation_failure(self, step, form, **kwargs):
2429+        """
2430+        Gets called when a form doesn't validate when rendering the done
2431+        view. By default, it changed the current step to failing forms step
2432+        and renders the form.
2433+        """
2434+        self.storage.set_current_step(step)
2435+        return self.render(form, **kwargs)
2436+
2437+    def get_form_step_data(self, form):
2438+        """
2439+        Is used to return the raw form data. You may use this method to
2440+        manipulate the data.
2441+        """
2442+        return form.data
2443+
2444+    def get_form_step_files(self, form):
2445+        """
2446+        Is used to return the raw form files. You may use this method to
2447+        manipulate the data.
2448+        """
2449+        return form.files
2450+
2451+    def get_all_cleaned_data(self):
2452+        """
2453+        Returns a merged dictionary of all step cleaned_data dictionaries.
2454+        If a step contains a `FormSet`, the key will be prefixed with formset
2455+        and contain a list of the formset' cleaned_data dictionaries.
2456+        """
2457+        cleaned_data = {}
2458+        for form_key in self.get_form_list():
2459+            form_obj = self.get_form(
2460+                step=form_key,
2461+                data=self.storage.get_step_data(form_key),
2462+                files=self.storage.get_step_files(form_key)
2463+            )
2464+            if form_obj.is_valid():
2465+                if isinstance(form_obj.cleaned_data, (tuple, list)):
2466+                    cleaned_data.update({
2467+                        'formset-%s' % form_key: form_obj.cleaned_data
2468+                    })
2469+                else:
2470+                    cleaned_data.update(form_obj.cleaned_data)
2471+        return cleaned_data
2472+
2473+    def get_cleaned_data_for_step(self, step):
2474+        """
2475+        Returns the cleaned data for a given `step`. Before returning the
2476+        cleaned data, the stored values are being revalidated through the
2477+        form. If the data doesn't validate, None will be returned.
2478+        """
2479+        if step in self.form_list:
2480+            form_obj = self.get_form(step=step,
2481+                data=self.storage.get_step_data(step),
2482+                files=self.storage.get_step_files(step))
2483+            if form_obj.is_valid():
2484+                return form_obj.cleaned_data
2485+        return None
2486+
2487+    def determine_step(self):
2488+        """
2489+        Returns the current step. If no current step is stored in the storage
2490+        backend, the first step will be returned.
2491+        """
2492+        return self.storage.get_current_step() or self.get_first_step()
2493+
2494+    def get_first_step(self):
2495+        """
2496+        Returns the name of the first step.
2497+        """
2498+        return self.get_form_list().keys()[0]
2499+
2500+    def get_last_step(self):
2501+        """
2502+        Returns the name of the last step.
2503+        """
2504+        return self.get_form_list().keys()[-1]
2505+
2506+    def get_next_step(self, step=None):
2507+        """
2508+        Returns the next step after the given `step`. If no more steps are
2509+        available, None will be returned. If the `step` argument is None, the
2510+        current step will be determined automatically.
2511+        """
2512+        if step is None:
2513+            step = self.determine_step()
2514+        form_list = self.get_form_list()
2515+        key = form_list.keyOrder.index(step) + 1
2516+        if len(form_list.keyOrder) > key:
2517+            return form_list.keyOrder[key]
2518+        return None
2519+
2520+    def get_prev_step(self, step=None):
2521+        """
2522+        Returns the previous step before the given `step`. If there are no
2523+        steps available, None will be returned. If the `step` argument is
2524+        None, the current step will be determined automatically.
2525+        """
2526+        if step is None:
2527+            step = self.determine_step()
2528+        form_list = self.get_form_list()
2529+        key = form_list.keyOrder.index(step) - 1
2530+        if key >= 0:
2531+            return form_list.keyOrder[key]
2532+        return None
2533+
2534+    def get_step_index(self, step=None):
2535+        """
2536+        Returns the index for the given `step` name. If no step is given,
2537+        the current step will be used to get the index.
2538+        """
2539+        if step is None:
2540+            step = self.determine_step()
2541+        return self.get_form_list().keyOrder.index(step)
2542+
2543+    def get_num_steps(self):
2544+        """
2545+        Returns the total number of steps/forms in this the wizard.
2546+        """
2547+        return len(self.get_form_list())
2548+
2549+    def reset_wizard(self):
2550+        """
2551+        Resets the user-state of the wizard.
2552+        """
2553+        self.storage.reset()
2554+
2555+    def get_context_data(self, form, *args, **kwargs):
2556+        """
2557+        Returns the template context for a step. You can overwrite this method
2558+        to add more data for all or some steps.
2559+        Example:
2560+
2561+        .. code-block:: python
2562+
2563+            class MyWizard(FormWizard):
2564+                def get_context_data(self, form, **kwargs):
2565+                    context = super(MyWizard, self).get_context_data(form, **kwargs)
2566+                    if self.storage.get_current_step() == 'my_step_name':
2567+                        context.update({'another_var': True})
2568+                    return context
2569+        """
2570+        context = super(WizardView, self).get_context_data(*args, **kwargs)
2571+        context.update({
2572+            'extra_context': self.get_extra_context(),
2573+            'form_step': self.determine_step(),
2574+            'form_first_step': self.get_first_step(),
2575+            'form_last_step': self.get_last_step(),
2576+            'form_prev_step': self.get_prev_step(),
2577+            'form_next_step': self.get_next_step(),
2578+            'form_step0': int(self.get_step_index()),
2579+            'form_step1': int(self.get_step_index()) + 1,
2580+            'form_step_count': self.get_num_steps(),
2581+            'form': form,
2582+        })
2583+        # if there is an extra_context item in the kwars, pass the data to the
2584+        # storage engine.
2585+        self.update_extra_context(kwargs.get('extra_context', {}))
2586+        return context
2587+
2588+    def get_extra_context(self):
2589+        """
2590+        Returns the extra data currently stored in the storage backend.
2591+        """
2592+        return self.storage.get_extra_context_data()
2593+
2594+    def update_extra_context(self, new_context):
2595+        """
2596+        Updates the currently stored extra context data. Already stored extra
2597+        context will be kept!
2598+        """
2599+        context = self.get_extra_context()
2600+        context.update(new_context)
2601+        return self.storage.set_extra_context_data(context)
2602+
2603+    def render(self, form, **kwargs):
2604+        """
2605+        Renders the acutal `form`. This method can be used to pre-process data
2606+        or conditionally skip steps.
2607+        """
2608+        return self.render_template(form, **kwargs)
2609+
2610+    def render_template(self, form=None, **kwargs):
2611+        """
2612+        Returns a `HttpResponse` containing the rendered form step. Available
2613+        template context variables are:
2614+
2615+         * `extra_context` - current extra context data
2616+         * `form_step` - name of the current step
2617+         * `form_first_step` - name of the first step
2618+         * `form_last_step` - name of the last step
2619+         * `form_prev_step`- name of the previous step
2620+         * `form_next_step` - name of the next step
2621+         * `form_step0` - index of the current step
2622+         * `form_step1` - index of the current step as a 1-index
2623+         * `form_step_count` - total number of steps
2624+         * `form` - form instance of the current step
2625+        """
2626+
2627+        form = form or self.get_form()
2628+        context = self.get_context_data(form, **kwargs)
2629+        return self.render_to_response(context)
2630+
2631+    def done(self, form_list, **kwargs):
2632+        """
2633+        This method muss be overrided by a subclass to process to form data
2634+        after processing all steps.
2635+        """
2636+        raise NotImplementedError("Your %s class has not defined a done() "
2637+            "method, which is required." % self.__class__.__name__)
2638+
2639+
2640+class SessionWizardView(WizardView):
2641+    """
2642+    A WizardView with pre-configured SessionStorage backend.
2643+    """
2644+    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
2645+
2646+
2647+class CookieWizardView(WizardView):
2648+    """
2649+    A WizardView with pre-configured CookieStorage backend.
2650+    """
2651+    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
2652+
2653+
2654+class NamedUrlWizardView(WizardView):
2655+    """
2656+    A WizardView with url-named steps support.
2657+    """
2658+    url_name = None
2659+    done_step_name = None
2660+
2661+    @classmethod
2662+    def get_initkwargs(cls, *args, **kwargs):
2663+        """
2664+        We require a url_name to reverse urls later. Additionally users can
2665+        pass a done_step_name to change the url-name of the "done" view.
2666+        """
2667+        extra_kwargs = {
2668+            'done_step_name': 'done'
2669+        }
2670+        assert 'url_name' in kwargs, 'url name is needed to resolve correct wizard urls'
2671+        extra_kwargs['url_name'] = kwargs['url_name']
2672+        del kwargs['url_name']
2673+
2674+        if 'done_step_name' in kwargs:
2675+            extra_kwargs['done_step_name'] = kwargs['done_step_name']
2676+            del kwargs['done_step_name']
2677+
2678+        initkwargs = super(NamedUrlWizardView, cls).get_initkwargs(*args, **kwargs)
2679+        initkwargs.update(extra_kwargs)
2680+
2681+        assert initkwargs['done_step_name'] not in initkwargs['form_list'], \
2682+            'step name "%s" is reserved for "done" view' % initkwargs['done_step_name']
2683+
2684+        return initkwargs
2685+
2686+    def get(self, *args, **kwargs):
2687+        """
2688+        This renders the form or, if needed, does the http redirects.
2689+        """
2690+        self.update_extra_context(kwargs.get('extra_context', {}))
2691+        step_url = kwargs.get('step', None)
2692+        if step_url is None:
2693+            if 'reset' in self.request.GET:
2694+                self.reset_wizard()
2695+                self.storage.set_current_step(self.get_first_step())
2696+
2697+            if self.request.GET:
2698+                query_string = "?%s" % self.request.GET.urlencode()
2699+            else:
2700+                query_string = ""
2701+            next_step_url = reverse(self.url_name, kwargs={
2702+                'step': self.determine_step()
2703+            }) + query_string
2704+            return HttpResponseRedirect(next_step_url)
2705+        else:
2706+            # is the current step the "done" name/view?
2707+            if step_url == self.done_step_name:
2708+                last_step = self.get_last_step()
2709+                return self.render_done(self.get_form(step=last_step,
2710+                    data=self.storage.get_step_data(last_step),
2711+                    files=self.storage.get_step_files(last_step)
2712+                ), **kwargs)
2713+
2714+            # is the url step name not equal to the step in the storage?
2715+            # if yes, change the step in the storage (if name exists)
2716+            if step_url == self.determine_step():
2717+                # url step name and storage step name are equal, render!
2718+                return self.render(self.get_form(
2719+                    data=self.storage.get_current_step_data(),
2720+                    files=self.storage.get_current_step_files()
2721+                ), **kwargs)
2722+            if step_url in self.get_form_list():
2723+                self.storage.set_current_step(step_url)
2724+                return self.render(self.get_form(
2725+                    data=self.storage.get_current_step_data(),
2726+                    files=self.storage.get_current_step_files()
2727+                ), **kwargs)
2728+            else:
2729+                # invalid step name, reset to first and redirect.
2730+                self.storage.set_current_step(self.get_first_step())
2731+                first_step_url = reverse(self.url_name, kwargs={
2732+                    'step': self.storage.get_current_step()
2733+                })
2734+                return HttpResponseRedirect(first_step_url)
2735+
2736+    def post(self, *args, **kwargs):
2737+        """
2738+        Do a redirect if user presses the prev. step button. The rest of this
2739+        is super'd from FormWizard.
2740+        """
2741+        prev_step = self.request.POST.get('form_prev_step', None)
2742+        if prev_step and prev_step in self.get_form_list():
2743+            self.storage.set_current_step(prev_step)
2744+            current_step_url = reverse(self.url_name, kwargs={
2745+                'step': self.storage.get_current_step(),
2746+            })
2747+            return HttpResponseRedirect(current_step_url)
2748+        return super(NamedUrlWizardView, self).post(*args, **kwargs)
2749+
2750+    def render_next_step(self, form, **kwargs):
2751+        """
2752+        When using the NamedUrlFormWizard, we have to redirect to update the
2753+        browser's url to match the shown step.
2754+        """
2755+        next_step = self.get_next_step()
2756+        next_step_url = reverse(self.url_name, kwargs={
2757+            'step': next_step,
2758+        })
2759+        self.storage.set_current_step(next_step)
2760+        return HttpResponseRedirect(next_step_url)
2761+
2762+    def render_revalidation_failure(self, failed_step, form, **kwargs):
2763+        """
2764+        When a step fails, we have to redirect the user to the first failing
2765+        step.
2766+        """
2767+        self.storage.set_current_step(failed_step)
2768+        return HttpResponseRedirect(reverse(self.url_name, kwargs={
2769+            'step': self.storage.get_current_step()
2770+        }))
2771+
2772+    def render_done(self, form, **kwargs):
2773+        """
2774+        When rendering the done view, we have to redirect first (if the url
2775+        name doesn't fit).
2776+        """
2777+        step_url = kwargs.get('step', None)
2778+        if step_url != self.done_step_name:
2779+            return HttpResponseRedirect(reverse(self.url_name, kwargs={
2780+                'step': self.done_step_name
2781+            }))
2782+        return super(NamedUrlWizardView, self).render_done(form, **kwargs)
2783+
2784+class NamedUrlSessionWizardView(NamedUrlWizardView):
2785+    """
2786+    A NamedUrlWizardView with pre-configured SessionStorage backend.
2787+    """
2788+    storage_name = 'django.contrib.formtools.wizard.storage.session.SessionStorage'
2789+
2790+
2791+class NamedUrlCookieWizardView(NamedUrlWizardView):
2792+    """
2793+    A NamedUrlFormWizard with pre-configured CookieStorageBackend.
2794+    """
2795+    storage_name = 'django.contrib.formtools.wizard.storage.cookie.CookieStorage'
2796+
2797diff --git a/django/core/signing.py b/django/core/signing.py
2798new file mode 100644
2799index 0000000..70fcc44
2800--- /dev/null
2801+++ b/django/core/signing.py
2802@@ -0,0 +1,183 @@
2803+"""
2804+Functions for creating and restoring url-safe signed JSON objects.
2805+
2806+The format used looks like this:
2807+
2808+>>> signed.dumps("hello")
2809+'ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8'
2810+
2811+There are two components here, separatad by a '.'. The first component is a
2812+URLsafe base64 encoded JSON of the object passed to dumps(). The second
2813+component is a base64 encoded hmac/SHA1 hash of "$first_component.$secret"
2814+
2815+signed.loads(s) checks the signature and returns the deserialised object.
2816+If the signature fails, a BadSignature exception is raised.
2817+
2818+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8")
2819+u'hello'
2820+>>> signed.loads("ImhlbGxvIg.RjVSUCt6S64WBilMYxG89-l0OA8-modified")
2821+...
2822+BadSignature: Signature failed: RjVSUCt6S64WBilMYxG89-l0OA8-modified
2823+
2824+You can optionally compress the JSON prior to base64 encoding it to save
2825+space, using the compress=True argument. This checks if compression actually
2826+helps and only applies compression if the result is a shorter string:
2827+
2828+>>> signed.dumps(range(1, 20), compress=True)
2829+'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml.oFq6lAAEbkHXBHfGnVX7Qx6NlZ8'
2830+
2831+The fact that the string is compressed is signalled by the prefixed '.' at the
2832+start of the base64 JSON.
2833+
2834+There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
2835+These functions make use of all of them.
2836+"""
2837+import hmac
2838+import base64
2839+import time
2840+
2841+from django.conf import settings
2842+from django.utils.hashcompat import sha_constructor
2843+from django.utils import baseconv, simplejson
2844+from django.utils.crypto import constant_time_compare
2845+from django.utils.encoding import force_unicode, smart_str
2846+from django.utils.importlib import import_module
2847+
2848+class BadSignature(Exception):
2849+    """
2850+    Signature does not match
2851+    """
2852+    pass
2853+
2854+
2855+class SignatureExpired(BadSignature):
2856+    """
2857+    Signature timestamp is older than required max_age
2858+    """
2859+    pass
2860+
2861+
2862+def b64_encode(s):
2863+    return base64.urlsafe_b64encode(s).strip('=')
2864+
2865+
2866+def b64_decode(s):
2867+    pad = '=' * (-len(s) % 4)
2868+    return base64.urlsafe_b64decode(s + pad)
2869+
2870+
2871+def base64_hmac(value, key):
2872+    return b64_encode((hmac.new(key, value, sha_constructor).digest()))
2873+
2874+
2875+def get_cookie_signer():
2876+    modpath = settings.SIGNING_BACKEND
2877+    module, attr = modpath.rsplit('.', 1)
2878+    try:
2879+        mod = import_module(module)
2880+    except ImportError, e:
2881+        raise ImproperlyConfigured(
2882+            'Error importing cookie signer %s: "%s"' % (modpath, e))
2883+    try:
2884+        Signer = getattr(mod, attr)
2885+    except AttributeError, e:
2886+        raise ImproperlyConfigured(
2887+            'Error importing cookie signer %s: "%s"' % (modpath, e))
2888+    return Signer('django.http.cookies' + settings.SECRET_KEY)
2889+
2890+
2891+def dumps(obj, key=None, salt='', compress=False):
2892+    """
2893+    Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
2894+    None, settings.SECRET_KEY is used instead.
2895+
2896+    If compress is True (not the default) checks if compressing using zlib can
2897+    save some space. Prepends a '.' to signify compression. This is included
2898+    in the signature, to protect against zip bombs.
2899+
2900+    salt can be used to further salt the hash, in case you're worried
2901+    that the NSA might try to brute-force your SHA-1 protected secret.
2902+    """
2903+    json = simplejson.dumps(obj, separators=(',', ':'))
2904+
2905+    # Flag for if it's been compressed or not
2906+    is_compressed = False
2907+
2908+    if compress:
2909+        # Avoid zlib dependency unless compress is being used
2910+        import zlib
2911+        compressed = zlib.compress(json)
2912+        if len(compressed) < (len(json) - 1):
2913+            json = compressed
2914+            is_compressed = True
2915+    base64d = b64_encode(json)
2916+    if is_compressed:
2917+        base64d = '.' + base64d
2918+    return TimestampSigner(key).sign(base64d, salt=salt)
2919+
2920+
2921+def loads(s, key=None, salt='', max_age=None):
2922+    """
2923+    Reverse of dumps(), raises BadSignature if signature fails
2924+    """
2925+    base64d = smart_str(
2926+        TimestampSigner(key).unsign(s, salt=salt, max_age=max_age))
2927+    decompress = False
2928+    if base64d[0] == '.':
2929+        # It's compressed; uncompress it first
2930+        base64d = base64d[1:]
2931+        decompress = True
2932+    json = b64_decode(base64d)
2933+    if decompress:
2934+        import zlib
2935+        jsond = zlib.decompress(json)
2936+    return simplejson.loads(json)
2937+
2938+
2939+class Signer(object):
2940+    def __init__(self, key=None, sep=':'):
2941+        self.sep = sep
2942+        self.key = key or settings.SECRET_KEY
2943+
2944+    def signature(self, value, salt=''):
2945+        # Derive a new key from the SECRET_KEY, using the optional salt
2946+        key = sha_constructor(salt + 'signer' + self.key).hexdigest()
2947+        return base64_hmac(value, key)
2948+
2949+    def sign(self, value, salt=''):
2950+        value = smart_str(value)
2951+        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
2952+
2953+    def unsign(self, signed_value, salt=''):
2954+        signed_value = smart_str(signed_value)
2955+        if not self.sep in signed_value:
2956+            raise BadSignature('No "%s" found in value' % self.sep)
2957+        value, sig = signed_value.rsplit(self.sep, 1)
2958+        expected = self.signature(value, salt=salt)
2959+        if constant_time_compare(sig, expected):
2960+            return force_unicode(value)
2961+        # Important: do NOT include the expected sig in the exception
2962+        # message, since it might leak up to an attacker!
2963+        # TODO: Can we enforce this in the Django debug templates?
2964+        raise BadSignature('Signature "%s" does not match' % sig)
2965+
2966+
2967+class TimestampSigner(Signer):
2968+    def timestamp(self):
2969+        return baseconv.base62.from_int(int(time.time()))
2970+
2971+    def sign(self, value, salt=''):
2972+        value = smart_str('%s%s%s' % (value, self.sep, self.timestamp()))
2973+        return '%s%s%s' % (value, self.sep, self.signature(value, salt=salt))
2974+
2975+    def unsign(self, value, salt='', max_age=None):
2976+        value, timestamp = super(TimestampSigner, self).unsign(
2977+            value, salt=salt).rsplit(self.sep, 1)
2978+        timestamp = baseconv.base62.to_int(timestamp)
2979+        if max_age is not None:
2980+            # Check timestamp is not older than max_age
2981+            age = time.time() - timestamp
2982+            if age > max_age:
2983+                raise SignatureExpired(
2984+                    'Signature age %s > %s seconds' % (age, max_age))
2985+        return value
2986diff --git a/django/http/__init__.py b/django/http/__init__.py
2987index 0d28ec0..0a0d665 100644
2988--- a/django/http/__init__.py
2989+++ b/django/http/__init__.py
2990@@ -122,6 +122,7 @@ from django.utils.encoding import smart_str, iri_to_uri, force_unicode
2991 from django.utils.http import cookie_date
2992 from django.http.multipartparser import MultiPartParser
2993 from django.conf import settings
2994+from django.core import signing
2995 from django.core.files import uploadhandler
2996 from utils import *
2997 
2998@@ -132,6 +133,8 @@ absolute_http_url_re = re.compile(r"^https?://", re.I)
2999 class Http404(Exception):
3000     pass
3001 
3002+RAISE_ERROR = object()
3003+
3004 class HttpRequest(object):
3005     """A basic HTTP request."""
3006 
3007@@ -170,6 +173,30 @@ class HttpRequest(object):
3008         # Rather than crash if this doesn't happen, we encode defensively.
3009         return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '')
3010 
3011+    def get_signed_cookie(self, key, default=RAISE_ERROR, salt='',
3012+                          max_age=None):
3013+        """
3014+        Attempts to return a signed cookie. If the signature fails or the
3015+        cookie has expired, raises an exception... unless you provide the
3016+        default argument in which case that value will be returned instead.
3017+        """
3018+        try:
3019+            cookie_value = self.COOKIES[key].encode('utf-8')
3020+        except KeyError:
3021+            if default is not RAISE_ERROR:
3022+                return default
3023+            else:
3024+                raise
3025+        try:
3026+            value = signing.get_cookie_signer().unsign(
3027+                cookie_value, salt=key + salt, max_age=max_age)
3028+        except signing.BadSignature:
3029+            if default is not RAISE_ERROR:
3030+                return default
3031+            else:
3032+                raise
3033+        return value
3034+
3035     def build_absolute_uri(self, location=None):
3036         """
3037         Builds an absolute URI from the location and the variables available in
3038@@ -584,6 +611,10 @@ class HttpResponse(object):
3039         if httponly:
3040             self.cookies[key]['httponly'] = True
3041 
3042+    def set_signed_cookie(self, key, value, salt='', **kwargs):
3043+        value = signing.get_cookie_signer().sign(value, salt=key + salt)
3044+        return self.set_cookie(key, value, **kwargs)
3045+
3046     def delete_cookie(self, key, path='/', domain=None):
3047         self.set_cookie(key, max_age=0, path=path, domain=domain,
3048                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
3049@@ -686,4 +717,3 @@ def str_to_unicode(s, encoding):
3050         return unicode(s, encoding, 'replace')
3051     else:
3052         return s
3053-
3054diff --git a/django/utils/baseconv.py b/django/utils/baseconv.py
3055new file mode 100644
3056index 0000000..db152f7
3057--- /dev/null
3058+++ b/django/utils/baseconv.py
3059@@ -0,0 +1,58 @@
3060+"""
3061+Convert numbers from base 10 integers to base X strings and back again.
3062+
3063+Sample usage:
3064+
3065+>>> base20 = BaseConverter('0123456789abcdefghij')
3066+>>> base20.from_int(1234)
3067+'31e'
3068+>>> base20.to_int('31e')
3069+1234
3070+"""
3071+
3072+
3073+class BaseConverter(object):
3074+    decimal_digits = "0123456789"
3075+
3076+    def __init__(self, digits):
3077+        self.digits = digits
3078+
3079+    def from_int(self, i):
3080+        return self.convert(i, self.decimal_digits, self.digits)
3081+
3082+    def to_int(self, s):
3083+        return int(self.convert(s, self.digits, self.decimal_digits))
3084+
3085+    def convert(number, fromdigits, todigits):
3086+        # Based on http://code.activestate.com/recipes/111286/
3087+        if str(number)[0] == '-':
3088+            number = str(number)[1:]
3089+            neg = 1
3090+        else:
3091+            neg = 0
3092+
3093+        # make an integer out of the number
3094+        x = 0
3095+        for digit in str(number):
3096+            x = x * len(fromdigits) + fromdigits.index(digit)
3097+
3098+        # create the result in base 'len(todigits)'
3099+        if x == 0:
3100+            res = todigits[0]
3101+        else:
3102+            res = ""
3103+            while x > 0:
3104+                digit = x % len(todigits)
3105+                res = todigits[digit] + res
3106+                x = int(x / len(todigits))
3107+            if neg:
3108+                res = '-' + res
3109+        return res
3110+    convert = staticmethod(convert)
3111+
3112+base2 = BaseConverter('01')
3113+base16 = BaseConverter('0123456789ABCDEF')
3114+base36 = BaseConverter('0123456789abcdefghijklmnopqrstuvwxyz')
3115+base62 = BaseConverter(
3116+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
3117+)
3118diff --git a/docs/index.txt b/docs/index.txt
3119index 9135d32..8b4ae53 100644
3120--- a/docs/index.txt
3121+++ b/docs/index.txt
3122@@ -171,6 +171,7 @@ Other batteries included
3123     * :doc:`Comments <ref/contrib/comments/index>` | :doc:`Moderation <ref/contrib/comments/moderation>` | :doc:`Custom comments <ref/contrib/comments/custom>`
3124     * :doc:`Content types <ref/contrib/contenttypes>`
3125     * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>`
3126+    * :doc:`Cryptographic signing <topics/signing>`
3127     * :doc:`Databrowse <ref/contrib/databrowse>`
3128     * :doc:`E-mail (sending) <topics/email>`
3129     * :doc:`Flatpages <ref/contrib/flatpages>`
3130diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
3131index 6281120..e17c0a7 100644
3132--- a/docs/ref/request-response.txt
3133+++ b/docs/ref/request-response.txt
3134@@ -240,6 +240,43 @@ Methods
3135 
3136    Example: ``"http://example.com/music/bands/the_beatles/?print=true"``
3137 
3138+.. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None)
3139+
3140+   .. versionadded:: 1.4
3141+
3142+   Returns a cookie value for a signed cookie, or raises a
3143+   :class:`~django.core.signing.BadSignature` exception if the signature is
3144+   no longer valid. If you provide the ``default`` argument the exception
3145+   will be suppressed and that default value will be returned instead.
3146+
3147+   The optional ``salt`` argument can be used to provide extra protection
3148+   against brute force attacks on your secret key. If supplied, the
3149+   ``max_age`` argument will be checked against the signed timestamp
3150+   attached to the cookie value to ensure the cookie is not older than
3151+   ``max_age`` seconds.
3152+
3153+   For example::
3154+
3155+          >>> request.get_signed_cookie('name')
3156+          'Tony'
3157+          >>> request.get_signed_cookie('name', salt='name-salt')
3158+          'Tony' # assuming cookie was set using the same salt
3159+          >>> request.get_signed_cookie('non-existing-cookie')
3160+          ...
3161+          KeyError: 'non-existing-cookie'
3162+          >>> request.get_signed_cookie('non-existing-cookie', False)
3163+          False
3164+          >>> request.get_signed_cookie('cookie-that-was-tampered-with')
3165+          ...
3166+          BadSignature: ...
3167+          >>> request.get_signed_cookie('name', max_age=60)
3168+          ...
3169+          SignatureExpired: Signature age 1677.3839159 > 60 seconds
3170+          >>> request.get_signed_cookie('name', False, max_age=60)
3171+          False
3172+
3173+   See :ref:`cryptographic signing <topics-signing>` for more information.
3174+
3175 .. method:: HttpRequest.is_secure()
3176 
3177    Returns ``True`` if the request is secure; that is, if it was made with
3178@@ -618,6 +655,17 @@ Methods
3179     .. _`cookie Morsel`: http://docs.python.org/library/cookie.html#Cookie.Morsel
3180     .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
3181 
3182+.. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
3183+
3184+    .. versionadded:: 1.4
3185+
3186+    Like :meth:`~HttpResponse.set_cookie()`, but
3187+    :ref:`cryptographically signs <topics-signing>` the cookie before setting
3188+    it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`.
3189+    You can use the optional ``salt`` argument for added key strength, but
3190+    you will need to remember to pass it to the corresponding
3191+    :meth:`HttpRequest.get_signed_cookie` call.
3192+
3193 .. method:: HttpResponse.delete_cookie(key, path='/', domain=None)
3194 
3195     Deletes the cookie with the given key. Fails silently if the key doesn't
3196diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
3197index f5f1226..38977e8 100644
3198--- a/docs/ref/settings.txt
3199+++ b/docs/ref/settings.txt
3200@@ -1647,6 +1647,19 @@ See :tfilter:`allowed date format strings <date>`.
3201 
3202 See also ``DATE_FORMAT`` and ``SHORT_DATETIME_FORMAT``.
3203 
3204+.. setting:: SIGNING_BACKEND
3205+
3206+SIGNING_BACKEND
3207+---------------
3208+
3209+.. versionadded:: 1.4
3210+
3211+Default: 'django.core.signing.TimestampSigner'
3212+
3213+The backend used for signing cookies and other data.
3214+
3215+See also the :ref:`topics-signing` documentation.
3216+
3217 .. setting:: SITE_ID
3218 
3219 SITE_ID
3220diff --git a/docs/topics/index.txt b/docs/topics/index.txt
3221index 49a03be..84f9e9f 100644
3222--- a/docs/topics/index.txt
3223+++ b/docs/topics/index.txt
3224@@ -18,6 +18,7 @@ Introductions to all the key parts of Django you'll need to know:
3225    auth
3226    cache
3227    conditional-view-processing
3228+   signing
3229    email
3230    i18n/index
3231    logging
3232diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt
3233new file mode 100644
3234index 0000000..c94462c
3235--- /dev/null
3236+++ b/docs/topics/signing.txt
3237@@ -0,0 +1,136 @@
3238+.. _topics-signing:
3239+
3240+=====================
3241+Cryptographic signing
3242+=====================
3243+
3244+.. module:: django.core.signing
3245+   :synopsis: Django's signing framework.
3246+
3247+.. versionadded:: 1.4
3248+
3249+The golden rule of Web application security is to never trust data from
3250+untrusted sources. Sometimes it can be useful to pass data through an
3251+untrusted medium. Cryptographically signed values can be passed through an
3252+untrusted channel safe in the knowledge that any tampering will be detected.
3253+
3254+Django provides both a low-level API for signing values and a high-level API
3255+for setting and reading signed cookies, one of the most common uses of
3256+signing in Web applications.
3257+
3258+You may also find signing useful for the following:
3259+
3260+    * Generating "recover my account" URLs for sending to users who have
3261+      lost their password.
3262+
3263+    * Ensuring data stored in hidden form fields has not been tampered with.
3264+
3265+    * Generating one-time secret URLs for allowing temporary access to a
3266+      protected resource, for example a downloadable file that a user has
3267+      paid for.
3268+
3269+Protecting the SECRET_KEY
3270+=========================
3271+
3272+When you create a new Django project using :djadmin:`startproject`, the
3273+``settings.py`` file it generates automatically gets a random
3274+:setting:`SECRET_KEY` value. This value is the key to securing signed
3275+data -- it is vital you keep this secure, or attackers could use it to
3276+generate their own signed values.
3277+
3278+Using the low-level API
3279+=======================
3280+
3281+.. class:: Signer
3282+
3283+Django's signing methods live in the ``django.core.signing`` module.
3284+To sign a value, first instantiate a ``Signer`` instance::
3285+
3286+    >>> from django.core.signing import Signer
3287+    >>> signer = Signer()
3288+    >>> value = signer.sign('My string')
3289+    >>> value
3290+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
3291+
3292+The signature is appended to the end of the string, following the colon.
3293+You can retrieve the original value using the ``unsign`` method::
3294+
3295+    >>> original = signer.unsign(value)
3296+    >>> original
3297+    u'My string'
3298+
3299+If the signature or value have been altered in any way, a
3300+``django.core.signing.BadSigature`` exception will be raised::
3301+
3302+    >>> value += 'm'
3303+    >>> try:
3304+    ...    original = signer.unsign(value)
3305+    ... except signing.BadSignature:
3306+    ...    print "Tampering detected!"
3307+
3308+By default, the ``Signer`` class uses the :setting:`SECRET_KEY` setting to
3309+generate signatures. You can use a different secret by passing it to the
3310+``Signer`` constructor::
3311+
3312+    >>> signer = Signer('my-other-secret')
3313+    >>> value = signer.sign('My string')
3314+    >>> value
3315+    'My string:EkfQJafvGyiofrdGnuthdxImIJw'
3316+
3317+Using the salt argument
3318+-----------------------
3319+
3320+If you do not wish to use the same key for every signing operation in your
3321+application, you can use the optional ``salt`` argument to the ``sign`` and
3322+``unsign`` methods to further strengthen your :setting:`SECRET_KEY` against
3323+brute force attacks. Using a salt will cause a new key to be derived from
3324+both the salt and your :setting:`SECRET_KEY`::
3325+
3326+    >>> signer = Signer()
3327+    >>> signer.sign('My string')
3328+    'My string:GdMGD6HNQ_qdgxYP8yBZAdAIV1w'
3329+    >>> signer.sign('My string', salt='extra')
3330+    'My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw'
3331+    >>> signer.unsign('My string:Ee7vGi-ING6n02gkcJ-QLHg6vFw', salt='extra')
3332+    u'My string'
3333+
3334+Unlike your :setting:`SECRET_KEY`, your salt argument does not need to stay
3335+secret.
3336+
3337+Verifying timestamped values
3338+----------------------------
3339+
3340+.. class:: TimestampSigner
3341+
3342+``TimestampSigner`` is a subclass of :class:`~Signer` that appends a signed
3343+timestamp to the value. This allows you to confirm that a signed value was
3344+created within a specified period of time::
3345+
3346+    >>> from django.core.signing import TimestampSigner
3347+    >>> signer = TimestampSigner()
3348+    >>> value = signer.sign('hello')
3349+    >>> value
3350+    'hello:1NMg5H:oPVuCqlJWmChm1rA2lyTUtelC-c'
3351+    >>> signer.unsign(value)
3352+    u'hello'
3353+    >>> signer.unsign(value, max_age=10)
3354+    ...
3355+    SignatureExpired: Signature age 15.5289158821 > 10 seconds
3356+    >>> signer.unsign(value, max_age=20)
3357+    u'hello'
3358+
3359+Protecting complex data structures
3360+----------------------------------
3361+
3362+If you wish to protect a list, tuple or dictionary you can do so using the
3363+signing module's dumps and loads functions. These imitate Python's pickle
3364+module, but uses JSON serialization under the hood. JSON ensures that even
3365+if your :setting:`SECRET_KEY` is stolen an attacker will not be able to
3366+execute arbitrary commands by exploiting the pickle format.::
3367+
3368+    >>> from django.core import signing
3369+    >>> value = signing.dumps({"foo": "bar"})
3370+    >>> value
3371+    'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI'
3372+    >>> signing.loads(value)
3373+    {'foo': 'bar'}
3374diff --git a/tests/regressiontests/signed_cookies_tests/__init__.py b/tests/regressiontests/signed_cookies_tests/__init__.py
3375new file mode 100644
3376index 0000000..e69de29
3377diff --git a/tests/regressiontests/signed_cookies_tests/models.py b/tests/regressiontests/signed_cookies_tests/models.py
3378new file mode 100644
3379index 0000000..71abcc5
3380--- /dev/null
3381+++ b/tests/regressiontests/signed_cookies_tests/models.py
3382@@ -0,0 +1 @@
3383+# models.py file for tests to run.
3384diff --git a/tests/regressiontests/signed_cookies_tests/tests.py b/tests/regressiontests/signed_cookies_tests/tests.py
3385new file mode 100644
3386index 0000000..c28892a
3387--- /dev/null
3388+++ b/tests/regressiontests/signed_cookies_tests/tests.py
3389@@ -0,0 +1,61 @@
3390+import time
3391+
3392+from django.core import signing
3393+from django.http import HttpRequest, HttpResponse
3394+from django.test import TestCase
3395+
3396+class SignedCookieTest(TestCase):
3397+
3398+    def test_can_set_and_read_signed_cookies(self):
3399+        response = HttpResponse()
3400+        response.set_signed_cookie('c', 'hello')
3401+        self.assertIn('c', response.cookies)
3402+        self.assertTrue(response.cookies['c'].value.startswith('hello:'))
3403+        request = HttpRequest()
3404+        request.COOKIES['c'] = response.cookies['c'].value
3405+        value = request.get_signed_cookie('c')
3406+        self.assertEqual(value, u'hello')
3407+
3408+    def test_can_use_salt(self):
3409+        response = HttpResponse()
3410+        response.set_signed_cookie('a', 'hello', salt='one')
3411+        request = HttpRequest()
3412+        request.COOKIES['a'] = response.cookies['a'].value
3413+        value = request.get_signed_cookie('a', salt='one')
3414+        self.assertEqual(value, u'hello')
3415+        self.assertRaises(signing.BadSignature,
3416+            request.get_signed_cookie, 'a', salt='two')
3417+
3418+    def test_detects_tampering(self):
3419+        response = HttpResponse()
3420+        response.set_signed_cookie('c', 'hello')
3421+        request = HttpRequest()
3422+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
3423+        self.assertRaises(signing.BadSignature,
3424+            request.get_signed_cookie, 'c')
3425+
3426+    def test_default_argument_supresses_exceptions(self):
3427+        response = HttpResponse()
3428+        response.set_signed_cookie('c', 'hello')
3429+        request = HttpRequest()
3430+        request.COOKIES['c'] = response.cookies['c'].value[:-2] + '$$'
3431+        self.assertEqual(request.get_signed_cookie('c', default=None), None)
3432+
3433+    def test_max_age_argument(self):
3434+        value = u'hello'
3435+        _time = time.time
3436+        time.time = lambda: 123456789
3437+        try:
3438+            response = HttpResponse()
3439+            response.set_signed_cookie('c', value)
3440+            request = HttpRequest()
3441+            request.COOKIES['c'] = response.cookies['c'].value
3442+            self.assertEqual(request.get_signed_cookie('c'), value)
3443+
3444+            time.time = lambda: 123456800
3445+            self.assertEqual(request.get_signed_cookie('c', max_age=12), value)
3446+            self.assertEqual(request.get_signed_cookie('c', max_age=11), value)
3447+            self.assertRaises(signing.SignatureExpired,
3448+                request.get_signed_cookie, 'c', max_age = 10)
3449+        finally:
3450+            time.time = _time
3451diff --git a/tests/regressiontests/signing/__init__.py b/tests/regressiontests/signing/__init__.py
3452new file mode 100644
3453index 0000000..e69de29
3454diff --git a/tests/regressiontests/signing/models.py b/tests/regressiontests/signing/models.py
3455new file mode 100644
3456index 0000000..71abcc5
3457--- /dev/null
3458+++ b/tests/regressiontests/signing/models.py
3459@@ -0,0 +1 @@
3460+# models.py file for tests to run.
3461diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py
3462new file mode 100644
3463index 0000000..0c28f53
3464--- /dev/null
3465+++ b/tests/regressiontests/signing/tests.py
3466@@ -0,0 +1,120 @@
3467+import time
3468+
3469+from django.core import signing
3470+from django.test import TestCase
3471+from django.utils.encoding import force_unicode
3472+from django.utils.hashcompat import sha_constructor
3473+
3474+class TestSigner(TestCase):
3475+
3476+    def test_signature(self):
3477+        "signature() method should generate a signature"
3478+        signer = signing.Signer('predictable-secret')
3479+        signer2 = signing.Signer('predictable-secret2')
3480+        for s in (
3481+            'hello',
3482+            '3098247:529:087:',
3483+            u'\u2019'.encode('utf8'),
3484+        ):
3485+            self.assertEqual(
3486+                signer.signature(s),
3487+                signing.base64_hmac(s, sha_constructor(
3488+                    'signer' + 'predictable-secret'
3489+                ).hexdigest())
3490+            )
3491+            self.assertNotEqual(signer.signature(s), signer2.signature(s))
3492+
3493+    def test_signature_with_salt(self):
3494+        "signature(value, salt=...) should work"
3495+        signer = signing.Signer('predictable-secret')
3496+        self.assertEqual(
3497+            signer.signature('hello', salt='extra-salt'),
3498+            signing.base64_hmac('hello', sha_constructor(
3499+                'extra-salt' + 'signer' + 'predictable-secret'
3500+            ).hexdigest())
3501+        )
3502+        self.assertNotEqual(
3503+            signer.signature('hello', salt='one'),
3504+            signer.signature('hello', salt='two'))
3505+
3506+    def test_sign_unsign(self):
3507+        "sign/unsign should be reversible"
3508+        signer = signing.Signer('predictable-secret')
3509+        examples = (
3510+            'q;wjmbk;wkmb',
3511+            '3098247529087',
3512+            '3098247:529:087:',
3513+            'jkw osanteuh ,rcuh nthu aou oauh ,ud du',
3514+            u'\u2019',
3515+        )
3516+        for example in examples:
3517+            self.assertNotEqual(
3518+                force_unicode(example), force_unicode(signer.sign(example)))
3519+            self.assertEqual(example, signer.unsign(signer.sign(example)))
3520+
3521+    def unsign_detects_tampering(self):
3522+        "unsign should raise an exception if the value has been tampered with"
3523+        signer = signing.Signer('predictable-secret')
3524+        value = 'Another string'
3525+        signed_value = signer.sign(value)
3526+        transforms = (
3527+            lambda s: s.upper(),
3528+            lambda s: s + 'a',
3529+            lambda s: 'a' + s[1:],
3530+            lambda s: s.replace(':', ''),
3531+        )
3532+        self.assertEqual(value, signer.unsign(signed_value))
3533+        for transform in transforms:
3534+            self.assertRaises(
3535+                signing.BadSignature, signer.unsign, transform(signed_value))
3536+
3537+    def test_dumps_loads(self):
3538+        "dumps and loads be reversible for any JSON serializable object"
3539+        objects = (
3540+            ['a', 'list'],
3541+            'a string',
3542+            u'a unicode string \u2019',
3543+            {'a': 'dictionary'},
3544+        )
3545+        for o in objects:
3546+            self.assertNotEqual(o, signing.dumps(o))
3547+            self.assertEqual(o, signing.loads(signing.dumps(o)))
3548+
3549+    def test_decode_detects_tampering(self):
3550+        "loads should raise exception for tampered objects"
3551+        transforms = (
3552+            lambda s: s.upper(),
3553+            lambda s: s + 'a',
3554+            lambda s: 'a' + s[1:],
3555+            lambda s: s.replace(':', ''),
3556+        )
3557+        value = {
3558+            'foo': 'bar',
3559+            'baz': 1,
3560+        }
3561+        encoded = signing.dumps(value)
3562+        self.assertEqual(value, signing.loads(encoded))
3563+        for transform in transforms:
3564+            self.assertRaises(
3565+                signing.BadSignature, signing.loads, transform(encoded))
3566+
3567+class TestTimestampSigner(TestCase):
3568+
3569+    def test_timestamp_signer(self):
3570+        value = u'hello'
3571+        _time = time.time
3572+        time.time = lambda: 123456789
3573+        try:
3574+            signer = signing.TimestampSigner('predictable-key')
3575+            ts = signer.sign(value)
3576+            self.assertNotEqual(ts,
3577+                signing.Signer('predictable-key').sign(value))
3578+
3579+            self.assertEqual(signer.unsign(ts), value)
3580+            time.time = lambda: 123456800
3581+            self.assertEqual(signer.unsign(ts, max_age=12), value)
3582+            self.assertEqual(signer.unsign(ts, max_age=11), value)
3583+            self.assertRaises(
3584+                signing.SignatureExpired, signer.unsign, ts, max_age=10)
3585+        finally:
3586+            time.time = _time
3587diff --git a/tests/regressiontests/utils/baseconv.py b/tests/regressiontests/utils/baseconv.py
3588new file mode 100644
3589index 0000000..90fe77f
3590--- /dev/null
3591+++ b/tests/regressiontests/utils/baseconv.py
3592@@ -0,0 +1,13 @@
3593+from unittest import TestCase
3594+from django.utils.baseconv import base2, base16, base36, base62
3595+
3596+class TestBaseConv(TestCase):
3597+
3598+    def test_baseconv(self):
3599+        nums = [-10 ** 10, 10 ** 10] + range(-100, 100)
3600+        for convertor in [base2, base16, base36, base62]:
3601+            for i in nums:
3602+                self.assertEqual(
3603+                    i, convertor.to_int(convertor.from_int(i))
3604+                )
3605+
3606diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
3607index 5c4c060..2b61627 100644
3608--- a/tests/regressiontests/utils/tests.py
3609+++ b/tests/regressiontests/utils/tests.py
3610@@ -17,3 +17,4 @@ from timesince import *
3611 from datastructures import *
3612 from tzinfo import *
3613 from datetime_safe import *
3614+from baseconv import *