diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index aa7d5ff7d4..8caefd0e67 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -10,7 +10,6 @@ import warnings from django import http from django.conf import settings from django.contrib.formtools import preview, utils -from django.contrib.formtools.wizard import FormWizard from django.test import TestCase from django.test.html import parse_html from django.test.utils import override_settings @@ -187,255 +186,3 @@ class FormHmacTests(unittest.TestCase): hash1 = utils.form_hmac(f1) hash2 = utils.form_hmac(f2) self.assertEqual(hash1, hash2) - - -# -# FormWizard tests -# - -class TestWizardClass(FormWizard): - - def get_template(self, step): - return 'forms/wizard.html' - - def done(self, request, cleaned_data): - return http.HttpResponse(success_string) - - -class DummyRequest(http.HttpRequest): - - def __init__(self, POST=None): - super(DummyRequest, self).__init__() - self.method = POST and "POST" or "GET" - if POST is not None: - self.POST.update(POST) - self._dont_enforce_csrf_checks = True - - -@override_settings( - SECRET_KEY="123", - TEMPLATE_DIRS=( - os.path.join(os.path.dirname(upath(__file__)), 'templates'), - ), -) -class WizardTests(TestCase): - urls = 'django.contrib.formtools.tests.urls' - wizard_step_data = ( - { - '0-name': 'Pony', - '0-thirsty': '2', - }, - { - '1-address1': '123 Main St', - '1-address2': 'Djangoland', - }, - { - '2-random_crap': 'blah blah', - } - ) - - def setUp(self): - super(WizardTests, self).setUp() - self.save_warnings_state() - warnings.filterwarnings('ignore', category=DeprecationWarning, - module='django.contrib.formtools.wizard') - - def tearDown(self): - super(WizardTests, self).tearDown() - self.restore_warnings_state() - - def test_step_starts_at_zero(self): - """ - step should be zero for the first form - """ - response = self.client.get('/wizard1/') - self.assertEqual(0, response.context['step0']) - - def test_step_increments(self): - """ - step should be incremented when we go to the next page - """ - response = self.client.post('/wizard1/', {"0-field":"test", "wizard_step":"0"}) - self.assertEqual(1, response.context['step0']) - - def test_bad_hash(self): - """ - Form should not advance if the hash is missing or bad - """ - response = self.client.post('/wizard1/', - {"0-field":"test", - "1-field":"test2", - "wizard_step": "1"}) - self.assertEqual(0, response.context['step0']) - - def test_good_hash(self): - """ - Form should advance if the hash is present and good, as calculated using - current method. - """ - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "1"} - response = self.client.post('/wizard1/', data) - self.assertEqual(2, response.context['step0']) - - def test_11726(self): - """ - Regression test for ticket #11726. - Wizard should not raise Http404 when steps are added dynamically. - """ - reached = [False] - that = self - - class WizardWithProcessStep(TestWizardClass): - def process_step(self, request, form, step): - if step == 0: - if self.num_steps() < 2: - self.form_list.append(WizardPageTwoForm) - if step == 1: - that.assertTrue(isinstance(form, WizardPageTwoForm)) - reached[0] = True - - wizard = WizardWithProcessStep([WizardPageOneForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "1"} - wizard(DummyRequest(POST=data)) - self.assertTrue(reached[0]) - - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "hash_1": { - 2: "1e6f6315da42e62f33a30640ec7e007ad3fbf1a1", - 3: "c33142ef9d01b1beae238adf22c3c6c57328f51a", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "2"} - self.assertRaises(http.Http404, wizard, DummyRequest(POST=data)) - - def test_14498(self): - """ - Regression test for ticket #14498. All previous steps' forms should be - validated. - """ - reached = [False] - that = self - - class WizardWithProcessStep(TestWizardClass): - def process_step(self, request, form, step): - that.assertTrue(form.is_valid()) - reached[0] = True - - wizard = WizardWithProcessStep([WizardPageOneForm, - WizardPageTwoForm, - WizardPageThreeForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "1"} - wizard(DummyRequest(POST=data)) - self.assertTrue(reached[0]) - - def test_14576(self): - """ - Regression test for ticket #14576. - - The form of the last step is not passed to the done method. - """ - reached = [False] - that = self - - class Wizard(TestWizardClass): - def done(self, request, form_list): - reached[0] = True - that.assertTrue(len(form_list) == 2) - - wizard = Wizard([WizardPageOneForm, - WizardPageTwoForm]) - - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "1"} - wizard(DummyRequest(POST=data)) - self.assertTrue(reached[0]) - - def test_15075(self): - """ - Regression test for ticket #15075. Allow modifying wizard's form_list - in process_step. - """ - reached = [False] - that = self - - class WizardWithProcessStep(TestWizardClass): - def process_step(self, request, form, step): - if step == 0: - self.form_list[1] = WizardPageTwoAlternativeForm - if step == 1: - that.assertTrue(isinstance(form, WizardPageTwoAlternativeForm)) - reached[0] = True - - wizard = WizardWithProcessStep([WizardPageOneForm, - WizardPageTwoForm, - WizardPageThreeForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": { - 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", - }[pickle.HIGHEST_PROTOCOL], - "wizard_step": "1"} - wizard(DummyRequest(POST=data)) - self.assertTrue(reached[0]) - - def grab_field_data(self, response): - """ - Pull the appropriate field data from the context to pass to the next wizard step - """ - previous_fields = parse_html(response.context['previous_fields']) - fields = {'wizard_step': response.context['step0']} - - for input_field in previous_fields: - input_attrs = dict(input_field.attributes) - fields[input_attrs["name"]] = input_attrs["value"] - - return fields - - def check_wizard_step(self, response, step_no): - """ - Helper function to test each step of the wizard - - Make sure the call succeeded - - Make sure response is the proper step number - - return the result from the post for the next step - """ - step_count = len(self.wizard_step_data) - - self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) - - data = self.grab_field_data(response) - data.update(self.wizard_step_data[step_no - 1]) - - return self.client.post('/wizard2/', data) - - def test_9473(self): - response = self.client.get('/wizard2/') - for step_no in range(1, len(self.wizard_step_data) + 1): - response = self.check_wizard_step(response, step_no) diff --git a/django/contrib/formtools/tests/forms.py b/django/contrib/formtools/tests/forms.py index 1ed2ab48bb..6f3ce4b0bd 100644 --- a/django/contrib/formtools/tests/forms.py +++ b/django/contrib/formtools/tests/forms.py @@ -1,21 +1,4 @@ from django import forms -from django.contrib.formtools.wizard import FormWizard -from django.http import HttpResponse - -class Page1(forms.Form): - name = forms.CharField(max_length=100) - thirsty = forms.NullBooleanField() - -class Page2(forms.Form): - address1 = forms.CharField(max_length=100) - address2 = forms.CharField(max_length=100) - -class Page3(forms.Form): - random_crap = forms.CharField(max_length=100) - -class ContactWizard(FormWizard): - def done(self, request, form_list): - return HttpResponse("") class TestForm(forms.Form): field1 = forms.CharField() @@ -30,15 +13,3 @@ class HashTestForm(forms.Form): class HashTestBlankForm(forms.Form): name = forms.CharField(required=False) bio = forms.CharField(required=False) - -class WizardPageOneForm(forms.Form): - field = forms.CharField() - -class WizardPageTwoForm(forms.Form): - field = forms.CharField() - -class WizardPageTwoAlternativeForm(forms.Form): - field = forms.CharField() - -class WizardPageThreeForm(forms.Form): - field = forms.CharField() diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py index 99bf7eaefa..739095d69f 100644 --- a/django/contrib/formtools/tests/urls.py +++ b/django/contrib/formtools/tests/urls.py @@ -5,15 +5,11 @@ This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. from __future__ import absolute_import from django.conf.urls import patterns, url -from django.contrib.formtools.tests import TestFormPreview, TestWizardClass +from django.contrib.formtools.tests import TestFormPreview -from django.contrib.formtools.tests.forms import (ContactWizard, Page1, Page2, - Page3, TestForm, WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm) +from django.contrib.formtools.tests.forms import TestForm urlpatterns = patterns('', url(r'^preview/', TestFormPreview(TestForm)), - url(r'^wizard1/$', TestWizardClass( - [WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm])), - url(r'^wizard2/$', ContactWizard([Page1, Page2, Page3])), ) diff --git a/django/contrib/formtools/wizard/__init__.py b/django/contrib/formtools/wizard/__init__.py index 8e51a3170d..e69de29bb2 100644 --- a/django/contrib/formtools/wizard/__init__.py +++ b/django/contrib/formtools/wizard/__init__.py @@ -1 +0,0 @@ -from django.contrib.formtools.wizard.legacy import FormWizard diff --git a/django/contrib/formtools/wizard/legacy.py b/django/contrib/formtools/wizard/legacy.py deleted file mode 100644 index d07e959de0..0000000000 --- a/django/contrib/formtools/wizard/legacy.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -FormWizard class -- implements a multi-page form, validating between each -step and storing the form's state as HTML hidden fields so that no state is -stored on the server side. -""" -from django.forms import HiddenInput -from django.http import Http404 -from django.shortcuts import render_to_response -from django.template.context import RequestContext -from django.utils.crypto import constant_time_compare -from django.utils.translation import ugettext_lazy as _ -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect - -from django.contrib.formtools.utils import form_hmac - -class FormWizard(object): - # The HTML (and POST data) field name for the "step" variable. - step_field_name="wizard_step" - - # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### - - def __init__(self, form_list, initial=None): - """ - Start a new wizard with a list of forms. - - form_list should be a list of Form classes (not instances). - """ - self.form_list = form_list[:] - self.initial = initial or {} - - # Dictionary of extra template context variables. - self.extra_context = {} - - # A zero-based counter keeping track of which step we're in. - self.step = 0 - - import warnings - warnings.warn( - 'Old-style form wizards have been deprecated; use the class-based ' - 'views in django.contrib.formtools.wizard.views instead.', - DeprecationWarning) - - def __repr__(self): - return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) - - def get_form(self, step, data=None): - "Helper method that returns the Form instance for the given step." - # Sanity check. - if step >= self.num_steps(): - raise Http404('Step %s does not exist' % step) - return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None)) - - def num_steps(self): - "Helper method that returns the number of steps." - # You might think we should just set "self.num_steps = len(form_list)" - # in __init__(), but this calculation needs to be dynamic, because some - # hook methods might alter self.form_list. - return len(self.form_list) - - def _check_security_hash(self, token, request, form): - expected = self.security_hash(request, form) - return constant_time_compare(token, expected) - - @method_decorator(csrf_protect) - def __call__(self, request, *args, **kwargs): - """ - Main method that does all the hard work, conforming to the Django view - interface. - """ - if 'extra_context' in kwargs: - self.extra_context.update(kwargs['extra_context']) - current_step = self.get_current_or_first_step(request, *args, **kwargs) - self.parse_params(request, *args, **kwargs) - - # Validate and process all the previous forms before instantiating the - # current step's form in case self.process_step makes changes to - # self.form_list. - - # If any of them fails validation, that must mean the validator relied - # on some other input, such as an external Web site. - - # It is also possible that alidation might fail under certain attack - # situations: an attacker might be able to bypass previous stages, and - # generate correct security hashes for all the skipped stages by virtue - # of: - # 1) having filled out an identical form which doesn't have the - # validation (and does something different at the end), - # 2) or having filled out a previous version of the same form which - # had some validation missing, - # 3) or previously having filled out the form when they had more - # privileges than they do now. - # - # Since the hashes only take into account values, and not other other - # validation the form might do, we must re-do validation now for - # security reasons. - previous_form_list = [] - for i in range(current_step): - f = self.get_form(i, request.POST) - if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), - request, f): - return self.render_hash_failure(request, i) - - if not f.is_valid(): - return self.render_revalidation_failure(request, i, f) - else: - self.process_step(request, f, i) - previous_form_list.append(f) - - # Process the current step. If it's valid, go to the next step or call - # done(), depending on whether any steps remain. - if request.method == 'POST': - form = self.get_form(current_step, request.POST) - else: - form = self.get_form(current_step) - - if form.is_valid(): - self.process_step(request, form, current_step) - next_step = current_step + 1 - - if next_step == self.num_steps(): - return self.done(request, previous_form_list + [form]) - else: - form = self.get_form(next_step) - self.step = current_step = next_step - - return self.render(form, request, current_step) - - def render(self, form, request, step, context=None): - "Renders the given Form object, returning an HttpResponse." - old_data = request.POST - prev_fields = [] - if old_data: - hidden = HiddenInput() - # Collect all data from previous steps and render it as HTML hidden fields. - for i in range(step): - old_form = self.get_form(i, old_data) - hash_name = 'hash_%s' % i - prev_fields.extend([bf.as_hidden() for bf in old_form]) - prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form)))) - return self.render_template(request, form, ''.join(prev_fields), step, context) - - # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## - - def prefix_for_step(self, step): - "Given the step, returns a Form prefix to use." - return str(step) - - def render_hash_failure(self, request, step): - """ - Hook for rendering a template if a hash check failed. - - step is the step that failed. Any previous step is guaranteed to be - valid. - - This default implementation simply renders the form for the given step, - but subclasses may want to display an error message, etc. - """ - return self.render(self.get_form(step), request, step, context={'wizard_error': _('We apologize, but your form has expired. Please continue filling out the form from this page.')}) - - def render_revalidation_failure(self, request, step, form): - """ - Hook for rendering a template if final revalidation failed. - - It is highly unlikely that this point would ever be reached, but See - the comment in __call__() for an explanation. - """ - return self.render(form, request, step) - - def security_hash(self, request, form): - """ - Calculates the security hash for the given HttpRequest and Form instances. - - Subclasses may want to take into account request-specific information, - such as the IP address. - """ - return form_hmac(form) - - def get_current_or_first_step(self, request, *args, **kwargs): - """ - Given the request object and whatever *args and **kwargs were passed to - __call__(), returns the current step (which is zero-based). - - Note that the result should not be trusted. It may even be a completely - invalid number. It's not the job of this method to validate it. - """ - if not request.POST: - return 0 - try: - step = int(request.POST.get(self.step_field_name, 0)) - except ValueError: - return 0 - return step - - def parse_params(self, request, *args, **kwargs): - """ - Hook for setting some state, given the request object and whatever - *args and **kwargs were passed to __call__(), sets some state. - - This is called at the beginning of __call__(). - """ - pass - - def get_template(self, step): - """ - Hook for specifying the name of the template to use for a given step. - - Note that this can return a tuple of template names if you'd like to - use the template system's select_template() hook. - """ - return 'forms/wizard.html' - - def render_template(self, request, form, previous_fields, step, context=None): - """ - Renders the template for the given step, returning an HttpResponse object. - - Override this method if you want to add a custom context, return a - different MIME type, etc. If you only need to override the template - name, use get_template() instead. - - The template will be rendered with the following context: - step_field -- The name of the hidden field containing the step. - step0 -- The current step (zero-based). - step -- The current step (one-based). - step_count -- The total number of steps. - form -- The Form instance for the current step (either empty - or with errors). - previous_fields -- A string representing every previous data field, - plus hashes for completed forms, all in the form of - hidden fields. Note that you'll need to run this - through the "safe" template filter, to prevent - auto-escaping, because it's raw HTML. - """ - context = context or {} - context.update(self.extra_context) - return render_to_response(self.get_template(step), dict(context, - step_field=self.step_field_name, - step0=step, - step=step + 1, - step_count=self.num_steps(), - form=form, - previous_fields=previous_fields - ), context_instance=RequestContext(request)) - - def process_step(self, request, form, step): - """ - Hook for modifying the FormWizard's internal state, given a fully - validated Form object. The Form is guaranteed to have clean, valid - data. - - This method should *not* modify any of that data. Rather, it might want - to set self.extra_context or dynamically alter self.form_list, based on - previously submitted forms. - - Note that this method is called every time a page is rendered for *all* - submitted steps. - """ - pass - - # METHODS SUBCLASSES MUST OVERRIDE ######################################## - - def done(self, request, form_list): - """ - Hook for doing something with the validated data. This is responsible - for the final processing. - - form_list is a list of Form instances, each containing clean, valid - data. - """ - raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)