diff --git a/AUTHORS b/AUTHORS index b5f73f73c6..9c0af6a03a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -253,6 +253,7 @@ answer newbie questions, and generally made Django that much better: pradeep.gowda@gmail.com Collin Grady Gabriel Grant + Martin Green Daniel Greenfeld Simon Greenhill Owen Griffiths diff --git a/django/test/testcases.py b/django/test/testcases.py index 311b50cfb7..463914c1e1 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -509,6 +509,83 @@ class SimpleTestCase(ut2.TestCase): self.fail(msg_prefix + "The form '%s' was not used to render the" " response" % form) + def assertFormsetError(self, response, formset, form_index, field, errors, + msg_prefix=''): + """ + Asserts that a formset used to render the response has a specific error. + + For field errors, specify the ``form_index`` and the ``field``. + For non-field errors, specify the ``form_index`` and the ``field`` as + None. + For non-form errors, specify ``form_index`` as None and the ``field`` + as None. + """ + # Add punctuation to msg_prefix + if msg_prefix: + msg_prefix += ": " + + # Put context(s) into a list to simplify processing. + contexts = to_list(response.context) + if not contexts: + self.fail(msg_prefix + 'Response did not use any contexts to ' + 'render the response') + + # Put error(s) into a list to simplify processing. + errors = to_list(errors) + + # Search all contexts for the error. + found_formset = False + for i, context in enumerate(contexts): + if formset not in context: + continue + found_formset = True + for err in errors: + if field is not None: + if field in context[formset].forms[form_index].errors: + field_errors = context[formset].forms[form_index].errors[field] + self.assertTrue(err in field_errors, + msg_prefix + "The field '%s' on formset '%s', " + "form %d in context %d does not contain the " + "error '%s' (actual errors: %s)" % + (field, formset, form_index, i, err, + repr(field_errors))) + elif field in context[formset].forms[form_index].fields: + self.fail(msg_prefix + "The field '%s' " + "on formset '%s', form %d in " + "context %d contains no errors" % + (field, formset, form_index, i)) + else: + self.fail(msg_prefix + "The formset '%s', form %d in " + "context %d does not contain the field '%s'" % + (formset, form_index, i, field)) + elif form_index is not None: + non_field_errors = context[formset].forms[form_index].non_field_errors() + self.assertFalse(len(non_field_errors) == 0, + msg_prefix + "The formset '%s', form %d in " + "context %d does not contain any non-field " + "errors." % (formset, form_index, i)) + self.assertTrue(err in non_field_errors, + msg_prefix + "The formset '%s', form %d " + "in context %d does not contain the " + "non-field error '%s' " + "(actual errors: %s)" % + (formset, form_index, i, err, + repr(non_field_errors))) + else: + non_form_errors = context[formset].non_form_errors() + self.assertFalse(len(non_form_errors) == 0, + msg_prefix + "The formset '%s' in " + "context %d does not contain any " + "non-form errors." % (formset, i)) + self.assertTrue(err in non_form_errors, + msg_prefix + "The formset '%s' in context " + "%d does not contain the " + "non-form error '%s' (actual errors: %s)" % + (formset, i, err, repr(non_form_errors))) + if not found_formset: + self.fail(msg_prefix + "The formset '%s' was not used to render " + "the response" % formset) + def assertTemplateUsed(self, response=None, template_name=None, msg_prefix=''): """ Asserts that the template with the provided name was used in rendering diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 4a8f7c6ecf..4f4b615835 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -283,6 +283,10 @@ Minor features * The :meth:`~django.db.models.query.QuerySet.get_or_create` method no longer requires at least one keyword argument. +* The :class:`~django.test.SimpleTestCase` class includes a new assertion + helper for testing formset errors: + :meth:`~django.test.SimpleTestCase.assertFormsetError`. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 3669bff8bf..470051e78e 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -1532,6 +1532,27 @@ your test suite. ``errors`` is an error string, or a list of error strings, that are expected as a result of form validation. +.. method:: SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='') + + .. versionadded:: 1.6 + + Asserts that the ``formset`` raises the provided list of errors when + rendered. + + ``formset`` is the name the ``Formset`` instance was given in the template + context. + + ``form_index`` is the number of the form within the ``Formset``. If + ``form_index`` has a value of ``None``, non-form errors (errors you can + access via ``formset.non_form_errors()``) will be checked. + + ``field`` is the name of the field on the form to check. If ``field`` + has a value of ``None``, non-field errors (errors you can access via + ``form.non_field_errors()``) will be checked. + + ``errors`` is an error string, or a list of error strings, that are + expected as a result of form validation. + .. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) Asserts that a ``Response`` instance produced the given ``status_code`` and diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 67c475eaff..bd395ca552 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -21,6 +21,7 @@ urlpatterns = patterns('', (r'^bad_view/$', views.bad_view), (r'^form_view/$', views.form_view), (r'^form_view_with_template/$', views.form_view_with_template), + (r'^formset_view/$', views.formset_view), (r'^login_protected_view/$', views.login_protected_view), (r'^login_protected_method_view/$', views.login_protected_method_view), (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index f760466497..76296cb80d 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -7,7 +7,8 @@ from xml.dom.minidom import parseString from django.contrib.auth.decorators import login_required, permission_required from django.core import mail from django.forms import fields -from django.forms.forms import Form +from django.forms.forms import Form, ValidationError +from django.forms.formsets import formset_factory, BaseFormSet from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.shortcuts import render_to_response from django.template import Context, Template @@ -95,6 +96,12 @@ class TestForm(Form): single = fields.ChoiceField(choices=TestChoices) multi = fields.MultipleChoiceField(choices=TestChoices) + def clean(self): + cleaned_data = self.cleaned_data + if cleaned_data.get("text") == "Raise non-field error": + raise ValidationError("Non-field error.") + return cleaned_data + def form_view(request): "A view that tests a simple form" if request.method == 'POST': @@ -130,6 +137,43 @@ def form_view_with_template(request): } ) +class BaseTestFormSet(BaseFormSet): + def clean(self): + """Checks that no two email addresses are the same.""" + if any(self.errors): + # Don't bother validating the formset unless each form is valid + return + + emails = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + email = form.cleaned_data['email'] + if email in emails: + raise ValidationError( + "Forms in a set must have distinct email addresses." + ) + emails.append(email) + +TestFormSet = formset_factory(TestForm, BaseTestFormSet) + +def formset_view(request): + "A view that tests a simple formset" + if request.method == 'POST': + formset = TestFormSet(request.POST) + if formset.is_valid(): + t = Template('Valid POST data.', name='Valid POST Template') + c = Context() + else: + t = Template('Invalid POST data. {{ my_formset.errors }}', + name='Invalid POST Template') + c = Context({'my_formset': formset}) + else: + formset = TestForm(request.GET) + t = Template('Viewing base formset. {{ my_formset }}.', + name='Formset GET Template') + c = Context({'my_formset': formset}) + return HttpResponse(t.render(c)) + def login_protected_view(request): "A simple view that is login protected." t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 6a58ef6344..654f65235a 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -543,6 +543,197 @@ class AssertFormErrorTests(TestCase): except AssertionError as e: self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) +class AssertFormsetErrorTests(TestCase): + msg_prefixes = [("", {}), ("abc: ", {"msg_prefix": "abc"})] + def setUp(self): + """Makes response object for testing field and non-field errors""" + # For testing field and non-field errors + self.response_form_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Raise non-field error', + 'form-0-email': 'not an email address', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + # For testing non-form errors + self.response_nonform_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Hello World', + 'form-0-email': 'email@domain.com', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + + def getResponse(self, post_data): + response = self.client.post('/test_client/formset_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + return response + + def test_unknown_formset(self): + "An assertion is raised if the formset name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'wrong_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'wrong_formset' was not " + "used to render the response", + str(cm.exception)) + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', " + "form 0 in context 0 " + "does not contain the field 'Some_field'", + str(cm.exception)) + + def test_no_error_field(self): + "An assertion is raised if the field doesn't have any errors" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + 'value', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The field 'value' " + "on formset 'my_formset', form 1 " + "in context 0 contains no errors", + str(cm.exception)) + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the specified error" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + "The field 'email' " + "on formset 'my_formset', form 0 in context 0 does not " + "contain the error 'Some error.' (actual errors: " + "[%(_)s'Enter a valid email address.'])"), + str(cm.exception)) + + def test_field_error(self): + "No assertion is raised if the field contains the provided error" + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + ['Enter a valid email address.'], + **kwargs) + + def test_no_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', form 1 in " + "context 0 does not contain any " + "non-field errors.", + str(cm.exception)) + + def test_unknown_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset', form 0 in context 0 does not " + "contain the non-field error 'Some error.' (actual errors: " + "[%(_)s'Non-field error.'])"), str(cm.exception)) + + def test_nonfield_error(self): + "No assertion is raised if the formsets non-field errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Non-field error.', + **kwargs) + + def test_no_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset' in context 0 " + "does not contain any non-form errors.", + str(cm.exception)) + + def test_unknown_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset' in context 0 does not contain the " + "non-form error 'Some error.' (actual errors: [%(_)s'Forms " + "in a set must have distinct email addresses.'])"), str(cm.exception)) + + def test_nonform_error(self): + "No assertion is raised if the formsets non-form errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Forms in a set must have distinct email ' + 'addresses.', + **kwargs) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class LoginTests(TestCase): fixtures = ['testdata']