diff --git a/AUTHORS b/AUTHORS index 6f0096e7cb..9f25f5933e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -600,6 +600,7 @@ answer newbie questions, and generally made Django that much better: Marc Garcia Marcin Wróbel Marc Remolt + Marc Seguí Coll Marc Tamlyn Marc-Aurèle Brothier Marian Andre diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 2df80297d3..3adbc6979a 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -6,7 +6,7 @@ from django.forms.utils import ErrorList, RenderableFormMixin from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from django.utils.translation import ngettext +from django.utils.translation import ngettext_lazy __all__ = ("BaseFormSet", "formset_factory", "all_valid") @@ -61,6 +61,16 @@ class BaseFormSet(RenderableFormMixin): "ManagementForm data is missing or has been tampered with. Missing fields: " "%(field_names)s. You may need to file a bug report if the issue persists." ), + "too_many_forms": ngettext_lazy( + "Please submit at most %(num)d form.", + "Please submit at most %(num)d forms.", + "num", + ), + "too_few_forms": ngettext_lazy( + "Please submit at least %(num)d form.", + "Please submit at least %(num)d forms.", + "num", + ), } template_name_div = "django/forms/formsets/div.html" @@ -425,12 +435,7 @@ class BaseFormSet(RenderableFormMixin): TOTAL_FORM_COUNT ] > self.absolute_max: raise ValidationError( - ngettext( - "Please submit at most %d form.", - "Please submit at most %d forms.", - self.max_num, - ) - % self.max_num, + self.error_messages["too_many_forms"] % {"num": self.max_num}, code="too_many_forms", ) if ( @@ -441,12 +446,7 @@ class BaseFormSet(RenderableFormMixin): < self.min_num ): raise ValidationError( - ngettext( - "Please submit at least %d form.", - "Please submit at least %d forms.", - self.min_num, - ) - % self.min_num, + self.error_messages["too_few_forms"] % {"num": self.min_num}, code="too_few_forms", ) # Give self.clean() a chance to do cross-form validation. diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index fa5990393b..a9e6d71cd1 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -292,6 +292,11 @@ Forms attributes help to identify widgets where its inputs should be grouped in a ``
`` with a ````. +* The :ref:`formsets-error-messages` argument for + :class:`~django.forms.formsets.BaseFormSet` now allows customizing + error messages for invalid number of forms by passing ``'too_few_forms'`` + and ``'too_many_forms'`` keys. + Generic Views ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 1b54f2accb..c0f6df0855 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -287,12 +287,20 @@ sure you understand what they do before doing so. a form instance with a prefix of ``__prefix__`` for easier use in dynamic forms with JavaScript. +.. _formsets-error-messages: + ``error_messages`` ------------------ The ``error_messages`` argument lets you override the default messages that the formset will raise. Pass in a dictionary with keys matching the error messages -you want to override. For example, here is the default error message when the +you want to override. Error message keys include ``'too_few_forms'``, +``'too_many_forms'``, and ``'missing_management_form'``. The +``'too_few_forms'`` and ``'too_many_forms'`` error messages may contain +``%(num)d``, which will be replaced with ``min_num`` and ``max_num``, +respectively. + +For example, here is the default error message when the management form is missing:: >>> formset = ArticleFormSet({}) @@ -309,6 +317,10 @@ And here is a custom error message:: >>> formset.non_form_errors() ['Sorry, something went wrong.'] +.. versionchanged:: 4.1 + + The ``'too_few_forms'`` and ``'too_many_forms'`` keys were added. + Custom formset validation ------------------------- @@ -410,6 +422,9 @@ deletion, is less than or equal to ``max_num``. ``max_num`` was exceeded because the amount of initial data supplied was excessive. +The error message can be customized by passing the ``'too_many_forms'`` message +to the :ref:`formsets-error-messages` argument. + .. note:: Regardless of ``validate_max``, if the number of forms in a data set @@ -446,6 +461,9 @@ deletion, is greater than or equal to ``min_num``. >>> formset.non_form_errors() ['Please submit at least 3 forms.'] +The error message can be customized by passing the ``'too_few_forms'`` message +to the :ref:`formsets-error-messages` argument. + .. note:: Regardless of ``validate_min``, if a formset contains no data, then diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index c713c85bfb..0868b41644 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -404,6 +404,37 @@ class FormsFormsetTestCase(SimpleTestCase): '
  • Please submit at most 1 form.
', ) + def test_formset_validate_max_flag_custom_error(self): + data = { + "choices-TOTAL_FORMS": "2", + "choices-INITIAL_FORMS": "0", + "choices-MIN_NUM_FORMS": "0", + "choices-MAX_NUM_FORMS": "2", + "choices-0-choice": "Zero", + "choices-0-votes": "0", + "choices-1-choice": "One", + "choices-1-votes": "1", + } + ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True) + formset = ChoiceFormSet( + data, + auto_id=False, + prefix="choices", + error_messages={ + "too_many_forms": "Number of submitted forms should be at most %(num)d." + }, + ) + self.assertFalse(formset.is_valid()) + self.assertEqual( + formset.non_form_errors(), + ["Number of submitted forms should be at most 1."], + ) + self.assertEqual( + str(formset.non_form_errors()), + '
    ' + "
  • Number of submitted forms should be at most 1.
", + ) + def test_formset_validate_min_flag(self): """ If validate_min is set and min_num is more than TOTAL_FORMS in the @@ -431,6 +462,37 @@ class FormsFormsetTestCase(SimpleTestCase): "Please submit at least 3 forms.", ) + def test_formset_validate_min_flag_custom_formatted_error(self): + data = { + "choices-TOTAL_FORMS": "2", + "choices-INITIAL_FORMS": "0", + "choices-MIN_NUM_FORMS": "0", + "choices-MAX_NUM_FORMS": "0", + "choices-0-choice": "Zero", + "choices-0-votes": "0", + "choices-1-choice": "One", + "choices-1-votes": "1", + } + ChoiceFormSet = formset_factory(Choice, extra=1, min_num=3, validate_min=True) + formset = ChoiceFormSet( + data, + auto_id=False, + prefix="choices", + error_messages={ + "too_few_forms": "Number of submitted forms should be at least %(num)d." + }, + ) + self.assertFalse(formset.is_valid()) + self.assertEqual( + formset.non_form_errors(), + ["Number of submitted forms should be at least 3."], + ) + self.assertEqual( + str(formset.non_form_errors()), + '
    ' + "
  • Number of submitted forms should be at least 3.
", + ) + def test_formset_validate_min_unchanged_forms(self): """ min_num validation doesn't consider unchanged forms with initial data