diff --git a/django/forms/formsets.py b/django/forms/formsets.py index a0a38f336f..3893cc54ba 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -267,7 +267,7 @@ class BaseFormSet(object): def is_valid(self): """ - Returns True if form.errors is empty for every form in self.forms. + Returns True if every form in self.forms is valid. """ if not self.is_bound: return False @@ -282,8 +282,7 @@ class BaseFormSet(object): # This form is going to be deleted so any of its errors # should not cause the entire formset to be invalid. continue - if bool(self.errors[i]): - forms_valid = False + forms_valid &= form.is_valid() return forms_valid and not bool(self.non_form_errors()) def full_clean(self): diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py index b3ceee551b..bf893c4c1d 100644 --- a/tests/regressiontests/forms/tests/formsets.py +++ b/tests/regressiontests/forms/tests/formsets.py @@ -856,6 +856,27 @@ class FormsFormsetTestCase(TestCase): formset = FavoriteDrinksFormSet(error_class=CustomErrorList) self.assertEqual(formset.forms[0].error_class, CustomErrorList) + def test_formset_calls_forms_is_valid(self): + # Regression tests for #18574 -- make sure formsets call + # is_valid() on each form. + + class AnotherChoice(Choice): + def is_valid(self): + self.is_valid_called = True + return super(AnotherChoice, self).is_valid() + + AnotherChoiceFormSet = formset_factory(AnotherChoice) + data = { + 'choices-TOTAL_FORMS': '1', # number of forms rendered + 'choices-INITIAL_FORMS': '0', # number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + } + formset = AnotherChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertTrue(all([form.is_valid_called for form in formset.forms])) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered