diff --git a/AUTHORS b/AUTHORS index 81858e7b29..b10852f407 100644 --- a/AUTHORS +++ b/AUTHORS @@ -627,6 +627,7 @@ answer newbie questions, and generally made Django that much better: Maximilian Merz Maximillian Dornseif mccutchen@gmail.com + Meghana Bhange Meir Kriheli Michael S. Brown Michael Hall diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 414ec70d28..32b3b4ad11 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -342,15 +342,15 @@ class BaseFormSet: self.total_form_count() - len(self.deleted_forms) > self.max_num) or \ self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max: raise ValidationError(ngettext( - "Please submit %d or fewer forms.", - "Please submit %d or fewer forms.", self.max_num) % self.max_num, + "Please submit at most %d form.", + "Please submit at most %d forms.", self.max_num) % self.max_num, code='too_many_forms', ) if (self.validate_min and self.total_form_count() - len(self.deleted_forms) - empty_forms_count < self.min_num): raise ValidationError(ngettext( - "Please submit %d or more forms.", - "Please submit %d or more forms.", self.min_num) % self.min_num, + "Please submit at least %d form.", + "Please submit at least %d forms.", self.min_num) % self.min_num, code='too_few_forms') # Give self.clean() a chance to do cross-form validation. self.clean() diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 3d71183ea7..37f6e300c4 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -151,7 +151,7 @@ protects against memory exhaustion attacks using forged ``POST`` requests:: >>> formset.is_valid() False >>> formset.non_form_errors() - ['Please submit 1000 or fewer forms.'] + ['Please submit at most 1000 forms.'] When ``absolute_max`` is None, it defaults to ``max_num + 1000``. (If ``max_num`` is ``None``, it defaults to ``2000``). @@ -371,7 +371,7 @@ deletion, is less than or equal to ``max_num``. >>> formset.errors [{}, {}] >>> formset.non_form_errors() - ['Please submit 1 or fewer forms.'] + ['Please submit at most 1 form.'] ``validate_max=True`` validates against ``max_num`` strictly even if ``max_num`` was exceeded because the amount of initial data supplied was @@ -413,7 +413,7 @@ deletion, is greater than or equal to ``min_num``. >>> formset.errors [{}, {}] >>> formset.non_form_errors() - ['Please submit 3 or more forms.'] + ['Please submit at least 3 forms.'] .. note:: diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 842843e8cd..267b949461 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -336,7 +336,7 @@ class FormsFormsetTestCase(SimpleTestCase): ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True) formset = ChoiceFormSet(data, auto_id=False, prefix='choices') self.assertFalse(formset.is_valid()) - self.assertEqual(formset.non_form_errors(), ['Please submit 1 or fewer forms.']) + self.assertEqual(formset.non_form_errors(), ['Please submit at most 1 form.']) def test_formset_validate_min_flag(self): """ @@ -358,7 +358,7 @@ class FormsFormsetTestCase(SimpleTestCase): ChoiceFormSet = formset_factory(Choice, extra=1, min_num=3, validate_min=True) formset = ChoiceFormSet(data, auto_id=False, prefix='choices') self.assertFalse(formset.is_valid()) - self.assertEqual(formset.non_form_errors(), ['Please submit 3 or more forms.']) + self.assertEqual(formset.non_form_errors(), ['Please submit at least 3 forms.']) def test_formset_validate_min_unchanged_forms(self): """ @@ -394,7 +394,7 @@ class FormsFormsetTestCase(SimpleTestCase): formset = ChoiceFormSet(data, prefix='choices') self.assertFalse(formset.has_changed()) self.assertFalse(formset.is_valid()) - self.assertEqual(formset.non_form_errors(), ['Please submit 1 or more forms.']) + self.assertEqual(formset.non_form_errors(), ['Please submit at least 1 form.']) def test_second_form_partially_filled_2(self): """A partially completed form is invalid.""" @@ -888,7 +888,7 @@ class FormsFormsetTestCase(SimpleTestCase): self.assertIs(formset.is_valid(), False) self.assertEqual( formset.non_form_errors(), - ['Please submit 1000 or fewer forms.'], + ['Please submit at most 1000 forms.'], ) self.assertEqual(formset.absolute_max, 2000) @@ -912,7 +912,7 @@ class FormsFormsetTestCase(SimpleTestCase): self.assertEqual(len(formset.forms), 3000) self.assertEqual( formset.non_form_errors(), - ['Please submit 1000 or fewer forms.'], + ['Please submit at most 1000 forms.'], ) def test_absolute_max_with_max_num(self): @@ -931,7 +931,7 @@ class FormsFormsetTestCase(SimpleTestCase): self.assertEqual(len(formset.forms), 1000) self.assertEqual( formset.non_form_errors(), - ['Please submit 30 or fewer forms.'], + ['Please submit at most 30 forms.'], ) def test_absolute_max_invalid(self): diff --git a/tests/generic_relations/test_forms.py b/tests/generic_relations/test_forms.py index f5e76fd3f5..e8caeba1f6 100644 --- a/tests/generic_relations/test_forms.py +++ b/tests/generic_relations/test_forms.py @@ -250,7 +250,7 @@ id="id_generic_relations-taggeditem-content_type-object_id-1-id">

""" % tagge self.assertEqual(len(formset.forms), 1500) self.assertEqual( formset.non_form_errors(), - ['Please submit 1000 or fewer forms.'], + ['Please submit at most 1000 forms.'], ) def test_absolute_max_with_max_num(self): @@ -269,7 +269,7 @@ id="id_generic_relations-taggeditem-content_type-object_id-1-id">

""" % tagge self.assertEqual(len(formset.forms), 100) self.assertEqual( formset.non_form_errors(), - ['Please submit 20 or fewer forms.'], + ['Please submit at most 20 forms.'], ) def test_can_delete_extra(self): diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index a01dd75e87..aaf76fa7ec 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1267,7 +1267,7 @@ class ModelFormsetTest(TestCase): FormSet = modelformset_factory(Price, fields="__all__", extra=1, max_num=1, validate_max=True) formset = FormSet(data) self.assertFalse(formset.is_valid()) - self.assertEqual(formset.non_form_errors(), ['Please submit 1 or fewer forms.']) + self.assertEqual(formset.non_form_errors(), ['Please submit at most 1 form.']) # Now test the same thing without the validate_max flag to ensure # default behavior is unchanged @@ -1275,6 +1275,48 @@ class ModelFormsetTest(TestCase): formset = FormSet(data) self.assertTrue(formset.is_valid()) + def test_modelformset_min_num_equals_max_num_less_than(self): + data = { + 'form-TOTAL_FORMS': '3', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '2', + 'form-0-slug': 'car-red', + 'form-1-slug': 'car-blue', + 'form-2-slug': 'car-black', + } + FormSet = modelformset_factory( + Product, + fields='__all__', + extra=1, + max_num=2, + validate_max=True, + min_num=2, + validate_min=True, + ) + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.non_form_errors(), ['Please submit at most 2 forms.']) + + def test_modelformset_min_num_equals_max_num_more_than(self): + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '2', + 'form-0-slug': 'car-red', + } + FormSet = modelformset_factory( + Product, + fields='__all__', + extra=1, + max_num=2, + validate_max=True, + min_num=2, + validate_min=True, + ) + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.non_form_errors(), ['Please submit at least 2 forms.']) + def test_unique_together_validation(self): FormSet = modelformset_factory(Price, fields="__all__", extra=1) data = { @@ -1851,7 +1893,7 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): self.assertEqual(len(formset.forms), 1500) self.assertEqual( formset.non_form_errors(), - ['Please submit 1000 or fewer forms.'], + ['Please submit at most 1000 forms.'], ) def test_modelformset_factory_absolute_max_with_max_num(self): @@ -1871,7 +1913,7 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): self.assertEqual(len(formset.forms), 100) self.assertEqual( formset.non_form_errors(), - ['Please submit 20 or fewer forms.'], + ['Please submit at most 20 forms.'], ) def test_inlineformset_factory_absolute_max(self): @@ -1892,7 +1934,7 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): self.assertEqual(len(formset.forms), 1500) self.assertEqual( formset.non_form_errors(), - ['Please submit 1000 or fewer forms.'], + ['Please submit at most 1000 forms.'], ) def test_inlineformset_factory_absolute_max_with_max_num(self): @@ -1914,7 +1956,7 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase): self.assertEqual(len(formset.forms), 100) self.assertEqual( formset.non_form_errors(), - ['Please submit 20 or fewer forms.'], + ['Please submit at most 20 forms.'], ) def test_modelformset_factory_can_delete_extra(self):