Commit df27803a authored by yokomizor's avatar yokomizor Committed by Tim Graham
Browse files

Fixed #9532 -- Added min_num and validate_min on formsets.

Thanks gsf for the suggestion.
parent 59a34c43
Loading
Loading
Loading
Loading
+22 −5
Original line number Diff line number Diff line
@@ -18,10 +18,14 @@ __all__ = ('BaseFormSet', 'all_valid')
# special field names
TOTAL_FORM_COUNT = 'TOTAL_FORMS'
INITIAL_FORM_COUNT = 'INITIAL_FORMS'
MIN_NUM_FORM_COUNT = 'MIN_NUM_FORMS'
MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'

# default minimum number of forms in a formset
DEFAULT_MIN_NUM = 0

# default maximum number of forms in a formset, to prevent memory exhaustion
DEFAULT_MAX_NUM = 1000

@@ -34,9 +38,10 @@ class ManagementForm(Form):
    def __init__(self, *args, **kwargs):
        self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
        self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
        # MAX_NUM_FORM_COUNT is output with the rest of the management form,
        # but only for the convenience of client-side code. The POST
        # value of MAX_NUM_FORM_COUNT returned from the client is not checked.
        # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of
        # the management form, but only for the convenience of client-side
        # code. The POST value of them returned from the client is not checked.
        self.base_fields[MIN_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
        self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
        super(ManagementForm, self).__init__(*args, **kwargs)

@@ -92,6 +97,7 @@ class BaseFormSet(object):
            form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
                TOTAL_FORM_COUNT: self.total_form_count(),
                INITIAL_FORM_COUNT: self.initial_form_count(),
                MIN_NUM_FORM_COUNT: self.min_num,
                MAX_NUM_FORM_COUNT: self.max_num
            })
        return form
@@ -323,6 +329,12 @@ class BaseFormSet(object):
                    "Please submit %d or fewer forms.", self.max_num) % self.max_num,
                    code='too_many_forms',
                )
            if (self.validate_min and
                self.total_form_count() - len(self.deleted_forms) < self.min_num):
                raise ValidationError(ungettext(
                    "Please submit %d or more forms.",
                    "Please submit %d or more forms.", self.min_num) % self.min_num,
                    code='too_few_forms')
            # Give self.clean() a chance to do cross-form validation.
            self.clean()
        except ValidationError as e:
@@ -395,17 +407,22 @@ class BaseFormSet(object):
        return mark_safe('\n'.join([six.text_type(self.management_form), forms]))

def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
                    can_delete=False, max_num=None, validate_max=False):
                    can_delete=False, max_num=None, validate_max=False,
                    min_num=None, validate_min=False):
    """Return a FormSet for the given form class."""
    if min_num is None:
        min_num = DEFAULT_MIN_NUM
    if max_num is None:
        max_num = DEFAULT_MAX_NUM
    # hard limit on forms instantiated, to prevent memory-exhaustion attacks
    # limit is simply max_num + DEFAULT_MAX_NUM (which is 2*DEFAULT_MAX_NUM
    # if max_num is None in the first place)
    absolute_max = max_num + DEFAULT_MAX_NUM
    extra += min_num
    attrs = {'form': form, 'extra': extra,
             'can_order': can_order, 'can_delete': can_delete,
             'max_num': max_num, 'absolute_max': absolute_max,
             'min_num': min_num, 'max_num': max_num,
             'absolute_max': absolute_max, 'validate_min' : validate_min,
             'validate_max' : validate_max}
    return type(form.__name__ + str('FormSet'), (formset,), attrs)

+6 −2
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ Formset Functions
.. module:: django.forms.formsets
   :synopsis: Django's functions for building formsets.

.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False)
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False)

    Returns a ``FormSet`` class for the given ``form`` class.

@@ -14,3 +14,7 @@ Formset Functions
    .. versionchanged:: 1.6

    The ``validate_max`` parameter was added.

    .. versionchanged:: 1.7

    The ``min_num`` and ``validate_min`` parameters were added.
+4 −0
Original line number Diff line number Diff line
@@ -234,6 +234,10 @@ Forms
  <django.forms.extras.widgets.SelectDateWidget.months>` can be used to
  customize the wording of the months displayed in the select widget.

* The ``min_num`` and ``validate_min`` parameters were added to
  :func:`~django.forms.formsets.formset_factory` to allow validating
  a minimum number of submitted forms.

Management Commands
^^^^^^^^^^^^^^^^^^^

+44 −3
Original line number Diff line number Diff line
@@ -298,6 +298,13 @@ method on the formset.
Validating the number of forms in a formset
-------------------------------------------

Django provides a couple ways to validate the minimum or maximum number of
submitted forms. Applications which need more customizable validation of the
number of forms should use custom formset validation.

``validate_max``
~~~~~~~~~~~~~~~~

If ``validate_max=True`` is passed to
:func:`~django.forms.formsets.formset_factory`, validation will also check
that the number of forms in the data set, minus those marked for
@@ -309,6 +316,7 @@ deletion, is less than or equal to ``max_num``.
    >>> data = {
    ...     'form-TOTAL_FORMS': u'2',
    ...     'form-INITIAL_FORMS': u'0',
    ...     'form-MIN_NUM_FORMS': u'',
    ...     'form-MAX_NUM_FORMS': u'',
    ...     'form-0-title': u'Test',
    ...     'form-0-pub_date': u'1904-06-16',
@@ -327,9 +335,6 @@ deletion, is less than or equal to ``max_num``.
``max_num`` was exceeded because the amount of initial data supplied was
excessive.

Applications which need more customizable validation of the number of forms
should use custom formset validation.

.. note::

    Regardless of ``validate_max``, if the number of forms in a data set
@@ -344,6 +349,42 @@ should use custom formset validation.
   The ``validate_max`` parameter was added to
   :func:`~django.forms.formsets.formset_factory`.

``validate_min``
~~~~~~~~~~~~~~~~

.. versionadded:: 1.7

If ``validate_min=True`` is passed to
:func:`~django.forms.formsets.formset_factory`, validation will also check
that the number of forms in the data set, minus those marked for
deletion, is greater than or equal to ``min_num``.

    >>> from django.forms.formsets import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
    >>> data = {
    ...     'form-TOTAL_FORMS': u'2',
    ...     'form-INITIAL_FORMS': u'0',
    ...     'form-MIN_NUM_FORMS': u'',
    ...     'form-MAX_NUM_FORMS': u'',
    ...     'form-0-title': u'Test',
    ...     'form-0-pub_date': u'1904-06-16',
    ...     'form-1-title': u'Test 2',
    ...     'form-1-pub_date': u'1912-06-23',
    ... }
    >>> formset = ArticleFormSet(data)
    >>> formset.is_valid()
    False
    >>> formset.errors
    [{}, {}]
    >>> formset.non_form_errors()
    [u'Please submit 3 or more forms.']

.. versionchanged:: 1.7

   The ``min_num`` and ``validate_min`` parameters were added to
   :func:`~django.forms.formsets.formset_factory`.

Dealing with ordering and deletion of forms
-------------------------------------------

+5 −5
Original line number Diff line number Diff line
@@ -1874,14 +1874,14 @@ class AdminViewListEditable(TestCase):
    def test_changelist_input_html(self):
        response = self.client.get('/test_admin/admin/admin_views/person/')
        # 2 inputs per object(the field and the hidden id field) = 6
        # 3 management hidden fields = 3
        # 4 management hidden fields = 4
        # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
        # main form submit button = 1
        # search field and search submit button = 2
        # CSRF field = 1
        # field to track 'select all' across paginated views = 1
        # 6 + 3 + 4 + 1 + 2 + 1 + 1 = 18 inputs
        self.assertContains(response, "<input", count=18)
        # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
        self.assertContains(response, "<input", count=19)
        # 1 select per object = 3 selects
        self.assertContains(response, "<select", count=4)

@@ -3629,9 +3629,9 @@ class ReadonlyTest(TestCase):
        response = self.client.get('/test_admin/admin/admin_views/post/add/')
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, 'name="posted"')
        # 3 fields + 2 submit buttons + 4 inline management form fields, + 2
        # 3 fields + 2 submit buttons + 5 inline management form fields, + 2
        # hidden fields for inlines + 1 field for the inline + 2 empty form
        self.assertContains(response, "<input", count=14)
        self.assertContains(response, "<input", count=15)
        self.assertContains(response, formats.localize(datetime.date.today()))
        self.assertContains(response,
            "<label>Awesomeness level:</label>")
Loading