Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #20464 -- Added a `total_error_count` method on formsets.

Thanks to frog32 for the report and to Tim Graham for the review.
  • Loading branch information...
commit 1b7634a0d0e21faba71a27ae7951d7cb7aec0e49 1 parent aa22cbd
Baptiste Mispelon authored timgraham committed
2  django/contrib/admin/templates/admin/change_list.html
@@ -64,7 +64,7 @@
64 64
     {% endblock %}
65 65
     {% if cl.formset.errors %}
66 66
         <p class="errornote">
67  
-        {% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
  67
+        {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
68 68
         </p>
69 69
         {{ cl.formset.non_form_errors }}
70 70
     {% endif %}
7  django/forms/formsets.py
@@ -263,6 +263,13 @@ def errors(self):
263 263
             self.full_clean()
264 264
         return self._errors
265 265
 
  266
+    def total_error_count(self):
  267
+        """
  268
+        Returns the number of errors across all forms in the formset.
  269
+        """
  270
+        return len(self.non_form_errors()) +\
  271
+            sum(len(form_errors) for form_errors in self.errors)
  272
+
266 273
     def _should_delete_form(self, form):
267 274
         """
268 275
         Returns whether or not the form was marked for deletion.
3  docs/releases/1.6.txt
@@ -315,6 +315,9 @@ Minor features
315 315
   :class:`~django.contrib.admin.InlineModelAdmin` may be overridden to
316 316
   customize the extra and maximum number of inline forms.
317 317
 
  318
+* Formsets now have a
  319
+  :meth:`~django.forms.formsets.BaseFormSet.total_error_count` method.
  320
+
318 321
 Backwards incompatible changes in 1.6
319 322
 =====================================
320 323
 
17  docs/topics/forms/formsets.txt
@@ -164,6 +164,23 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the
164 164
 forms in the formset. Validation was performed for each of the two forms, and
165 165
 the expected error message appears for the second item.
166 166
 
  167
+.. currentmodule:: django.forms.formsets.BaseFormSet
  168
+
  169
+.. method:: total_error_count(self)
  170
+
  171
+.. versionadded:: 1.6
  172
+
  173
+To check how many errors there are in the formset, we can use the
  174
+``total_error_count`` method::
  175
+
  176
+    >>> # Using the previous example
  177
+    >>> formset.errors
  178
+    [{}, {'pub_date': [u'This field is required.']}]
  179
+    >>> len(formset.errors)
  180
+    2
  181
+    >>> formset.total_error_count()
  182
+    1
  183
+
167 184
 We can also check if form data differs from the initial data (i.e. the form was
168 185
 sent without any data)::
169 186
 
154  tests/forms_tests/tests/test_formsets.py
@@ -55,81 +55,82 @@ class SplitDateTimeForm(Form):
55 55
 
56 56
 
57 57
 class FormsFormsetTestCase(TestCase):
  58
+
  59
+    def make_choiceformset(self, formset_data=None, formset_class=ChoiceFormSet,
  60
+        total_forms=None, initial_forms=0, max_num_forms=0, **kwargs):
  61
+        """
  62
+        Make a ChoiceFormset from the given formset_data.
  63
+        The data should be given as a list of (choice, votes) tuples.
  64
+        """
  65
+        kwargs.setdefault('prefix', 'choices')
  66
+        kwargs.setdefault('auto_id', False)
  67
+
  68
+        if formset_data is None:
  69
+            return formset_class(**kwargs)
  70
+
  71
+        if total_forms is None:
  72
+            total_forms = len(formset_data)
  73
+
  74
+        def prefixed(*args):
  75
+            args = (kwargs['prefix'],) + args
  76
+            return '-'.join(args)
  77
+
  78
+        data = {
  79
+            prefixed('TOTAL_FORMS'): str(total_forms),
  80
+            prefixed('INITIAL_FORMS'): str(initial_forms),
  81
+            prefixed('MAX_NUM_FORMS'): str(max_num_forms),
  82
+        }
  83
+        for i, (choice, votes) in enumerate(formset_data):
  84
+            data[prefixed(str(i), 'choice')] = choice
  85
+            data[prefixed(str(i), 'votes')] = votes
  86
+
  87
+        return formset_class(data, **kwargs)
  88
+
58 89
     def test_basic_formset(self):
59 90
         # A FormSet constructor takes the same arguments as Form. Let's create a FormSet
60 91
         # for adding data. By default, it displays 1 blank form. It can display more,
61 92
         # but we'll look at how to do so later.
62  
-        formset = ChoiceFormSet(auto_id=False, prefix='choices')
  93
+        formset = self.make_choiceformset()
  94
+        
63 95
         self.assertHTMLEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
64 96
 <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
65 97
 <tr><th>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""")
66 98
 
67  
-        # On thing to note is that there needs to be a special value in the data. This
68  
-        # value tells the FormSet how many forms were displayed so it can tell how
69  
-        # many forms it needs to clean and validate. You could use javascript to create
70  
-        # new forms on the client side, but they won't get validated unless you increment
71  
-        # the TOTAL_FORMS field appropriately.
72  
-
73  
-        data = {
74  
-            'choices-TOTAL_FORMS': '1', # the number of forms rendered
75  
-            'choices-INITIAL_FORMS': '0', # the number of forms with initial data
76  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
77  
-            'choices-0-choice': 'Calexico',
78  
-            'choices-0-votes': '100',
79  
-        }
80 99
         # We treat FormSet pretty much like we would treat a normal Form. FormSet has an
81 100
         # is_valid method, and a cleaned_data or errors attribute depending on whether all
82 101
         # the forms passed validation. However, unlike a Form instance, cleaned_data and
83 102
         # errors will be a list of dicts rather than just a single dict.
84 103
 
85  
-        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  104
+        formset = self.make_choiceformset([('Calexico', '100')])
86 105
         self.assertTrue(formset.is_valid())
87 106
         self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])
88 107
 
89 108
         # If a FormSet was not passed any data, its is_valid and has_changed
90 109
         # methods should return False.
91  
-        formset = ChoiceFormSet()
  110
+        formset = self.make_choiceformset()
92 111
         self.assertFalse(formset.is_valid())
93 112
         self.assertFalse(formset.has_changed())
94 113
 
95 114
     def test_formset_validation(self):
96 115
         # FormSet instances can also have an error attribute if validation failed for
97 116
         # any of the forms.
98  
-
99  
-        data = {
100  
-            'choices-TOTAL_FORMS': '1', # the number of forms rendered
101  
-            'choices-INITIAL_FORMS': '0', # the number of forms with initial data
102  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
103  
-            'choices-0-choice': 'Calexico',
104  
-            'choices-0-votes': '',
105  
-        }
106  
-
107  
-        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  117
+        formset = self.make_choiceformset([('Calexico', '')])
108 118
         self.assertFalse(formset.is_valid())
109 119
         self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])
110 120
 
111 121
     def test_formset_has_changed(self):
112 122
         # FormSet instances has_changed method will be True if any data is
113 123
         # passed to his forms, even if the formset didn't validate
114  
-        data = {
115  
-            'choices-TOTAL_FORMS': '1', # the number of forms rendered
116  
-            'choices-INITIAL_FORMS': '0', # the number of forms with initial data
117  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
118  
-            'choices-0-choice': '',
119  
-            'choices-0-votes': '',
120  
-        }
121  
-        blank_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  124
+        blank_formset = self.make_choiceformset([('', '')])
122 125
         self.assertFalse(blank_formset.has_changed())
123 126
 
124 127
         # invalid formset test
125  
-        data['choices-0-choice'] = 'Calexico'
126  
-        invalid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  128
+        invalid_formset = self.make_choiceformset([('Calexico', '')])
127 129
         self.assertFalse(invalid_formset.is_valid())
128 130
         self.assertTrue(invalid_formset.has_changed())
129 131
 
130 132
         # valid formset test
131  
-        data['choices-0-votes'] = '100'
132  
-        valid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  133
+        valid_formset = self.make_choiceformset([('Calexico', '100')])
133 134
         self.assertTrue(valid_formset.is_valid())
134 135
         self.assertTrue(valid_formset.has_changed())
135 136
 
@@ -139,7 +140,7 @@ def test_formset_initial_data(self):
139 140
         # an extra blank form is included.
140 141
 
141 142
         initial = [{'choice': 'Calexico', 'votes': 100}]
142  
-        formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
  143
+        formset = self.make_choiceformset(initial=initial)
143 144
         form_output = []
144 145
 
145 146
         for form in formset.forms:
@@ -151,18 +152,7 @@ def test_formset_initial_data(self):
151 152
 <li>Votes: <input type="number" name="choices-1-votes" /></li>""")
152 153
 
153 154
         # Let's simulate what would happen if we submitted this form.
154  
-
155  
-        data = {
156  
-            'choices-TOTAL_FORMS': '2', # the number of forms rendered
157  
-            'choices-INITIAL_FORMS': '1', # the number of forms with initial data
158  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
159  
-            'choices-0-choice': 'Calexico',
160  
-            'choices-0-votes': '100',
161  
-            'choices-1-choice': '',
162  
-            'choices-1-votes': '',
163  
-        }
164  
-
165  
-        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  155
+        formset = self.make_choiceformset([('Calexico', '100'), ('', '')], initial_forms=1)
166 156
         self.assertTrue(formset.is_valid())
167 157
         self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}])
168 158
 
@@ -172,18 +162,7 @@ def test_second_form_partially_filled(self):
172 162
         # one of the fields of a blank form though, it will be validated. We may want to
173 163
         # required that at least x number of forms are completed, but we'll show how to
174 164
         # handle that later.
175  
-
176  
-        data = {
177  
-            'choices-TOTAL_FORMS': '2', # the number of forms rendered
178  
-            'choices-INITIAL_FORMS': '1', # the number of forms with initial data
179  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
180  
-            'choices-0-choice': 'Calexico',
181  
-            'choices-0-votes': '100',
182  
-            'choices-1-choice': 'The Decemberists',
183  
-            'choices-1-votes': '', # missing value
184  
-        }
185  
-
186  
-        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  165
+        formset = self.make_choiceformset([('Calexico', '100'), ('The Decemberists', '')], initial_forms=1)
187 166
         self.assertFalse(formset.is_valid())
188 167
         self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])
189 168
 
@@ -191,18 +170,7 @@ def test_delete_prefilled_data(self):
191 170
         # If we delete data that was pre-filled, we should get an error. Simply removing
192 171
         # data from form fields isn't the proper way to delete it. We'll see how to
193 172
         # handle that case later.
194  
-
195  
-        data = {
196  
-            'choices-TOTAL_FORMS': '2', # the number of forms rendered
197  
-            'choices-INITIAL_FORMS': '1', # the number of forms with initial data
198  
-            'choices-MAX_NUM_FORMS': '0', # max number of forms
199  
-            'choices-0-choice': '', # deleted value
200  
-            'choices-0-votes': '', # deleted value
201  
-            'choices-1-choice': '',
202  
-            'choices-1-votes': '',
203  
-        }
204  
-
205  
-        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  173
+        formset = self.make_choiceformset([('', ''), ('', '')], initial_forms=1)
206 174
         self.assertFalse(formset.is_valid())
207 175
         self.assertEqual(formset.errors, [{'votes': ['This field is required.'], 'choice': ['This field is required.']}, {}])
208 176
 
@@ -1027,6 +995,40 @@ class CheckForm(Form):
1027 995
         self.assertTrue(formset.is_valid())
1028 996
 
1029 997
 
  998
+    def test_formset_total_error_count(self):
  999
+        """A valid formset should have 0 total errors."""
  1000
+        data = [ #  formset_data, expected error count
  1001
+            ([('Calexico', '100')], 0),
  1002
+            ([('Calexico', '')], 1),
  1003
+            ([('', 'invalid')], 2),
  1004
+            ([('Calexico', '100'), ('Calexico', '')], 1),
  1005
+            ([('Calexico', ''), ('Calexico', '')], 2),
  1006
+        ]
  1007
+        
  1008
+        for formset_data, expected_error_count in data:
  1009
+            formset = self.make_choiceformset(formset_data)
  1010
+            self.assertEqual(formset.total_error_count(), expected_error_count)
  1011
+
  1012
+    def test_formset_total_error_count_with_non_form_errors(self):
  1013
+        data = {
  1014
+            'choices-TOTAL_FORMS': '2', # the number of forms rendered
  1015
+            'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  1016
+            'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored
  1017
+            'choices-0-choice': 'Zero',
  1018
+            'choices-0-votes': '0',
  1019
+            'choices-1-choice': 'One',
  1020
+            'choices-1-votes': '1',
  1021
+        }
  1022
+
  1023
+        ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True)
  1024
+        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1025
+        self.assertEqual(formset.total_error_count(), 1)
  1026
+
  1027
+        data['choices-1-votes'] = ''
  1028
+        formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1029
+        self.assertEqual(formset.total_error_count(), 2)
  1030
+
  1031
+
1030 1032
 data = {
1031 1033
     'choices-TOTAL_FORMS': '1', # the number of forms rendered
1032 1034
     'choices-INITIAL_FORMS': '0', # the number of forms with initial data
@@ -1087,7 +1089,7 @@ def test_with_management_data_attrs_work_fine(self):
1087 1089
         self.assertEqual([{}], formset.cleaned_data)
1088 1090
 
1089 1091
 
1090  
-    def test_form_errors_are_cought_by_formset(self):
  1092
+    def test_form_errors_are_caught_by_formset(self):
1091 1093
         data = {
1092 1094
             'form-TOTAL_FORMS': '2',
1093 1095
             'form-INITIAL_FORMS': '0',

0 notes on commit 1b7634a

Please sign in to comment.
Something went wrong with that request. Please try again.