Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #9532 -- min_num on formsets #1444

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 22 additions & 5 deletions django/forms/formsets.py
Expand Up @@ -18,10 +18,14 @@
# 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

Expand All @@ -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)

Expand Down Expand Up @@ -92,6 +97,7 @@ def management_form(self):
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
Expand Down Expand Up @@ -323,6 +329,12 @@ def full_clean(self):
"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:
Expand Down Expand Up @@ -395,17 +407,22 @@ def as_ul(self):
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)

Expand Down
6 changes: 5 additions & 1 deletion docs/ref/forms/formsets.txt
Expand Up @@ -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.

Expand All @@ -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.
5 changes: 5 additions & 0 deletions docs/releases/1.7.txt
Expand Up @@ -234,6 +234,11 @@ 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`. It will be used to validate
if the number of forms in a formset, minus those marked for deletion, is
greater than or equal to ``min_num``.

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

Expand Down
40 changes: 40 additions & 0 deletions docs/topics/forms/formsets.txt
Expand Up @@ -298,6 +298,9 @@ method on the formset.
Validating the number of forms in a formset
-------------------------------------------

``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
Expand All @@ -309,6 +312,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',
Expand All @@ -327,6 +331,37 @@ deletion, is less than or equal to ``max_num``.
``max_num`` was exceeded because the amount of initial data supplied was
excessive.

``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.']

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

Expand All @@ -344,6 +379,11 @@ should use custom formset validation.
The ``validate_max`` parameter was added to
:func:`~django.forms.formsets.formset_factory`.

.. versionchanged:: 1.7

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

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

Expand Down