diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index aea13b94d77..9c78e758f0d 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -445,6 +445,10 @@ class InlineAdminFormSet: def is_bound(self): return self.formset.is_bound + @property + def total_form_count(self): + return self.formset.total_form_count + @property def media(self): media = self.opts.media + self.formset.media diff --git a/django/test/testcases.py b/django/test/testcases.py index 0d24bf0d408..a29b77c2773 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -686,13 +686,21 @@ class SimpleTestCase(unittest.TestCase): for i, context in enumerate(contexts): if formset not in context or not hasattr(context[formset], "forms"): continue + formset_repr = repr(context[formset]) if not context[formset].is_bound: - formset_repr = repr(context[formset]) self.fail( f"{msg_prefix}The formset {formset_repr} is not bound, it will " f"never have any errors." ) found_formset = True + if form_index is not None: + form_count = context[formset].total_form_count() + if form_index >= form_count: + form_or_forms = "forms" if form_count > 1 else "form" + self.fail( + f"{msg_prefix}The formset {formset_repr} only has " + f"{form_count} {form_or_forms}." + ) for err in errors: if field is not None: if field in context[formset].forms[form_index].errors: diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index bfc9633122f..038e79732b6 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -1629,6 +1629,33 @@ class AssertFormsetErrorTests(SimpleTestCase): ) self.assertFormsetError(response, "formset", None, None, "error") + def test_form_index_too_big(self): + msg = ( + "The formset only has " + "1 form." + ) + response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}]) + with self.assertRaisesMessage(AssertionError, msg): + self.assertFormsetError(response, "formset", 2, "field", "error") + + def test_form_index_too_big_plural(self): + formset = TestFormset( + { + "form-TOTAL_FORMS": "2", + "form-INITIAL_FORMS": "0", + "form-0-field": "valid", + "form-1-field": "valid", + } + ) + formset.full_clean() + msg = ( + "The formset only has 2 " + "forms." + ) + response = mock.Mock(context=[{}, {"formset": formset}]) + with self.assertRaisesMessage(AssertionError, msg): + self.assertFormsetError(response, "formset", 2, "field", "error") + def test_formset_named_form(self): formset = TestFormset.invalid() # The mocked context emulates the template-based rendering of the