diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 9e185de1b3..7d9e84113f 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -87,7 +87,7 @@ class BaseFormSet(object): def __nonzero__(self): # Python 2 compatibility return type(self).__bool__(self) - @property + @cached_property def management_form(self): """Returns the ManagementForm instance for this FormSet.""" if self.is_bound: diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 8e04ce76cd..29f0befc18 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -2,14 +2,15 @@ from __future__ import unicode_literals import datetime +from collections import Counter from django.forms import ( - CharField, DateField, FileField, Form, IntegerField, SplitDateTimeField, - ValidationError, formsets, + BaseForm, CharField, DateField, FileField, Form, IntegerField, + SplitDateTimeField, ValidationError, formsets, ) from django.forms.formsets import BaseFormSet, formset_factory from django.forms.utils import ErrorList -from django.test import SimpleTestCase +from django.test import SimpleTestCase, mock from django.utils.encoding import force_text @@ -165,6 +166,32 @@ class FormsFormsetTestCase(SimpleTestCase): self.assertFalse(formset.is_valid()) self.assertEqual(formset.errors, [{'votes': ['This field is required.']}]) + def test_formset_validation_count(self): + """ + A formset's ManagementForm is validated once per FormSet.is_valid() + call and each form of the formset is cleaned once. + """ + def make_method_counter(func): + """Add a counter to func for the number of times it's called.""" + counter = Counter() + counter.call_count = 0 + + def mocked_func(*args, **kwargs): + counter.call_count += 1 + return func(*args, **kwargs) + + return mocked_func, counter + + mocked_is_valid, is_valid_counter = make_method_counter(formsets.ManagementForm.is_valid) + mocked_full_clean, full_clean_counter = make_method_counter(BaseForm.full_clean) + formset = self.make_choiceformset([('Calexico', '100'), ('Any1', '42'), ('Any2', '101')]) + + with mock.patch('django.forms.formsets.ManagementForm.is_valid', mocked_is_valid), \ + mock.patch('django.forms.forms.BaseForm.full_clean', mocked_full_clean): + self.assertTrue(formset.is_valid()) + self.assertEqual(is_valid_counter.call_count, 1) + self.assertEqual(full_clean_counter.call_count, 4) + def test_formset_has_changed(self): # FormSet instances has_changed method will be True if any data is # passed to his forms, even if the formset didn't validate