diff --git a/django/test/testcases.py b/django/test/testcases.py index b56e8f5edf..28cf45e8ba 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -557,7 +557,7 @@ class SimpleTestCase(unittest.TestCase): # Search all contexts for the error. found_formset = False for i, context in enumerate(contexts): - if formset not in context: + if formset not in context or not hasattr(context[formset], 'forms'): continue found_formset = True for err in errors: diff --git a/docs/releases/4.0.1.txt b/docs/releases/4.0.1.txt index d5a674778e..fa4ebe826f 100644 --- a/docs/releases/4.0.1.txt +++ b/docs/releases/4.0.1.txt @@ -9,4 +9,6 @@ Django 4.0.1 fixes several bugs in 4.0. Bugfixes ======== -* ... +* Fixed a regression in Django 4.0 that caused a crash of + :meth:`~django.test.SimpleTestCase.assertFormsetError` on a formset named + ``form`` (:ticket:`33346`). diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 82ec36ab38..f7a964bfa0 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -13,7 +13,10 @@ from django.core.files.storage import default_storage from django.db import ( IntegrityError, connection, connections, models, router, transaction, ) -from django.forms import EmailField, IntegerField +from django.forms import ( + CharField, EmailField, Form, IntegerField, ValidationError, + formset_factory, +) from django.http import HttpResponse from django.template.loader import render_to_string from django.test import ( @@ -1241,6 +1244,44 @@ class AssertURLEqualTests(SimpleTestCase): ) +class TestForm(Form): + field = CharField() + + def clean_field(self): + value = self.cleaned_data.get('field', '') + if value == 'invalid': + raise ValidationError('invalid value') + return value + + +class TestFormset(formset_factory(TestForm)): + @classmethod + def _get_cleaned_formset(cls, field_value): + formset = cls({ + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-0-field': field_value, + }) + formset.full_clean() + return formset + + @classmethod + def invalid(cls): + return cls._get_cleaned_formset('invalid') + + +class AssertFormsetErrorTests(SimpleTestCase): + def test_formset_named_form(self): + formset = TestFormset.invalid() + # The mocked context emulates the template-based rendering of the + # formset. + response = mock.Mock(context=[ + {'form': formset}, + {'form': formset.management_form}, + ]) + self.assertFormsetError(response, 'form', 0, 'field', 'invalid value') + + class FirstUrls: urlpatterns = [path('first/', empty_response, name='first')]