From d49551bc261666fad753b0f55f1150467190e3f7 Mon Sep 17 00:00:00 2001
From: Claude Paroz <claude@2xlibre.net>
Date: Thu, 1 Dec 2016 20:17:25 +0100
Subject: [PATCH] Fixed #27119 -- Cached BaseFormSet.management_form property

Thanks Tim Graham for the review.
---
 django/forms/formsets.py                 |  2 +-
 tests/forms_tests/tests/test_formsets.py | 33 +++++++++++++++++++++---
 2 files changed, 31 insertions(+), 4 deletions(-)

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