Loading django/forms/formsets.py +22 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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) Loading docs/ref/forms/formsets.txt +6 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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. docs/releases/1.7.txt +4 −0 Original line number Diff line number Diff line Loading @@ -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 ^^^^^^^^^^^^^^^^^^^ Loading docs/topics/forms/formsets.txt +44 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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', Loading @@ -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 Loading @@ -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 ------------------------------------------- Loading tests/admin_views/tests.py +5 −5 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading
django/forms/formsets.py +22 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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) Loading
docs/ref/forms/formsets.txt +6 −2 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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.
docs/releases/1.7.txt +4 −0 Original line number Diff line number Diff line Loading @@ -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 ^^^^^^^^^^^^^^^^^^^ Loading
docs/topics/forms/formsets.txt +44 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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', Loading @@ -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 Loading @@ -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 ------------------------------------------- Loading
tests/admin_views/tests.py +5 −5 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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