Kaydet (Commit) df27803a authored tarafından yokomizor's avatar yokomizor Kaydeden (comit) Tim Graham

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

Thanks gsf for the suggestion.
üst 59a34c43
...@@ -18,10 +18,14 @@ __all__ = ('BaseFormSet', 'all_valid') ...@@ -18,10 +18,14 @@ __all__ = ('BaseFormSet', 'all_valid')
# special field names # special field names
TOTAL_FORM_COUNT = 'TOTAL_FORMS' TOTAL_FORM_COUNT = 'TOTAL_FORMS'
INITIAL_FORM_COUNT = 'INITIAL_FORMS' INITIAL_FORM_COUNT = 'INITIAL_FORMS'
MIN_NUM_FORM_COUNT = 'MIN_NUM_FORMS'
MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS' MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS'
ORDERING_FIELD_NAME = 'ORDER' ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE' 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 maximum number of forms in a formset, to prevent memory exhaustion
DEFAULT_MAX_NUM = 1000 DEFAULT_MAX_NUM = 1000
...@@ -34,9 +38,10 @@ class ManagementForm(Form): ...@@ -34,9 +38,10 @@ class ManagementForm(Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput) self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
self.base_fields[INITIAL_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, # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of
# but only for the convenience of client-side code. The POST # the management form, but only for the convenience of client-side
# value of MAX_NUM_FORM_COUNT returned from the client is not checked. # 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) self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs) super(ManagementForm, self).__init__(*args, **kwargs)
...@@ -92,6 +97,7 @@ class BaseFormSet(object): ...@@ -92,6 +97,7 @@ class BaseFormSet(object):
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={ form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(), TOTAL_FORM_COUNT: self.total_form_count(),
INITIAL_FORM_COUNT: self.initial_form_count(), INITIAL_FORM_COUNT: self.initial_form_count(),
MIN_NUM_FORM_COUNT: self.min_num,
MAX_NUM_FORM_COUNT: self.max_num MAX_NUM_FORM_COUNT: self.max_num
}) })
return form return form
...@@ -323,6 +329,12 @@ class BaseFormSet(object): ...@@ -323,6 +329,12 @@ class BaseFormSet(object):
"Please submit %d or fewer forms.", self.max_num) % self.max_num, "Please submit %d or fewer forms.", self.max_num) % self.max_num,
code='too_many_forms', 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. # Give self.clean() a chance to do cross-form validation.
self.clean() self.clean()
except ValidationError as e: except ValidationError as e:
...@@ -395,17 +407,22 @@ class BaseFormSet(object): ...@@ -395,17 +407,22 @@ class BaseFormSet(object):
return mark_safe('\n'.join([six.text_type(self.management_form), forms])) return mark_safe('\n'.join([six.text_type(self.management_form), forms]))
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, 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.""" """Return a FormSet for the given form class."""
if min_num is None:
min_num = DEFAULT_MIN_NUM
if max_num is None: if max_num is None:
max_num = DEFAULT_MAX_NUM max_num = DEFAULT_MAX_NUM
# hard limit on forms instantiated, to prevent memory-exhaustion attacks # hard limit on forms instantiated, to prevent memory-exhaustion attacks
# limit is simply max_num + DEFAULT_MAX_NUM (which is 2*DEFAULT_MAX_NUM # limit is simply max_num + DEFAULT_MAX_NUM (which is 2*DEFAULT_MAX_NUM
# if max_num is None in the first place) # if max_num is None in the first place)
absolute_max = max_num + DEFAULT_MAX_NUM absolute_max = max_num + DEFAULT_MAX_NUM
extra += min_num
attrs = {'form': form, 'extra': extra, attrs = {'form': form, 'extra': extra,
'can_order': can_order, 'can_delete': can_delete, '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} 'validate_max' : validate_max}
return type(form.__name__ + str('FormSet'), (formset,), attrs) return type(form.__name__ + str('FormSet'), (formset,), attrs)
......
...@@ -5,12 +5,16 @@ Formset Functions ...@@ -5,12 +5,16 @@ Formset Functions
.. module:: django.forms.formsets .. module:: django.forms.formsets
:synopsis: Django's functions for building 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. Returns a ``FormSet`` class for the given ``form`` class.
See :ref:`formsets` for example usage. See :ref:`formsets` for example usage.
.. versionchanged:: 1.6 .. versionchanged:: 1.6
The ``validate_max`` parameter was added. The ``validate_max`` parameter was added.
.. versionchanged:: 1.7
The ``min_num`` and ``validate_min`` parameters were added.
...@@ -234,6 +234,10 @@ Forms ...@@ -234,6 +234,10 @@ Forms
<django.forms.extras.widgets.SelectDateWidget.months>` can be used to <django.forms.extras.widgets.SelectDateWidget.months>` can be used to
customize the wording of the months displayed in the select widget. 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 Management Commands
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
......
...@@ -298,6 +298,13 @@ method on the formset. ...@@ -298,6 +298,13 @@ method on the formset.
Validating the number of forms in a 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 If ``validate_max=True`` is passed to
:func:`~django.forms.formsets.formset_factory`, validation will also check :func:`~django.forms.formsets.formset_factory`, validation will also check
that the number of forms in the data set, minus those marked for 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``. ...@@ -309,6 +316,7 @@ deletion, is less than or equal to ``max_num``.
>>> data = { >>> data = {
... 'form-TOTAL_FORMS': u'2', ... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0', ... 'form-INITIAL_FORMS': u'0',
... 'form-MIN_NUM_FORMS': u'',
... 'form-MAX_NUM_FORMS': u'', ... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test', ... 'form-0-title': u'Test',
... 'form-0-pub_date': u'1904-06-16', ... 'form-0-pub_date': u'1904-06-16',
...@@ -327,9 +335,6 @@ deletion, is less than or equal to ``max_num``. ...@@ -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 ``max_num`` was exceeded because the amount of initial data supplied was
excessive. excessive.
Applications which need more customizable validation of the number of forms
should use custom formset validation.
.. note:: .. note::
Regardless of ``validate_max``, if the number of forms in a data set Regardless of ``validate_max``, if the number of forms in a data set
...@@ -344,6 +349,42 @@ should use custom formset validation. ...@@ -344,6 +349,42 @@ should use custom formset validation.
The ``validate_max`` parameter was added to The ``validate_max`` parameter was added to
:func:`~django.forms.formsets.formset_factory`. :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 Dealing with ordering and deletion of forms
------------------------------------------- -------------------------------------------
......
...@@ -1874,14 +1874,14 @@ class AdminViewListEditable(TestCase): ...@@ -1874,14 +1874,14 @@ class AdminViewListEditable(TestCase):
def test_changelist_input_html(self): def test_changelist_input_html(self):
response = self.client.get('/test_admin/admin/admin_views/person/') response = self.client.get('/test_admin/admin/admin_views/person/')
# 2 inputs per object(the field and the hidden id field) = 6 # 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) # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
# main form submit button = 1 # main form submit button = 1
# search field and search submit button = 2 # search field and search submit button = 2
# CSRF field = 1 # CSRF field = 1
# field to track 'select all' across paginated views = 1 # field to track 'select all' across paginated views = 1
# 6 + 3 + 4 + 1 + 2 + 1 + 1 = 18 inputs # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
self.assertContains(response, "<input", count=18) self.assertContains(response, "<input", count=19)
# 1 select per object = 3 selects # 1 select per object = 3 selects
self.assertContains(response, "<select", count=4) self.assertContains(response, "<select", count=4)
...@@ -3629,9 +3629,9 @@ class ReadonlyTest(TestCase): ...@@ -3629,9 +3629,9 @@ class ReadonlyTest(TestCase):
response = self.client.get('/test_admin/admin/admin_views/post/add/') response = self.client.get('/test_admin/admin/admin_views/post/add/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'name="posted"') 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 # 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, formats.localize(datetime.date.today()))
self.assertContains(response, self.assertContains(response,
"<label>Awesomeness level:</label>") "<label>Awesomeness level:</label>")
......
...@@ -57,7 +57,7 @@ SplitDateTimeFormSet = formset_factory(SplitDateTimeForm) ...@@ -57,7 +57,7 @@ SplitDateTimeFormSet = formset_factory(SplitDateTimeForm)
class FormsFormsetTestCase(TestCase): class FormsFormsetTestCase(TestCase):
def make_choiceformset(self, formset_data=None, formset_class=ChoiceFormSet, def make_choiceformset(self, formset_data=None, formset_class=ChoiceFormSet,
total_forms=None, initial_forms=0, max_num_forms=0, **kwargs): total_forms=None, initial_forms=0, max_num_forms=0, min_num_forms=0, **kwargs):
""" """
Make a ChoiceFormset from the given formset_data. Make a ChoiceFormset from the given formset_data.
The data should be given as a list of (choice, votes) tuples. The data should be given as a list of (choice, votes) tuples.
...@@ -79,6 +79,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -79,6 +79,7 @@ class FormsFormsetTestCase(TestCase):
prefixed('TOTAL_FORMS'): str(total_forms), prefixed('TOTAL_FORMS'): str(total_forms),
prefixed('INITIAL_FORMS'): str(initial_forms), prefixed('INITIAL_FORMS'): str(initial_forms),
prefixed('MAX_NUM_FORMS'): str(max_num_forms), prefixed('MAX_NUM_FORMS'): str(max_num_forms),
prefixed('MIN_NUM_FORMS'): str(min_num_forms),
} }
for i, (choice, votes) in enumerate(formset_data): for i, (choice, votes) in enumerate(formset_data):
data[prefixed(str(i), 'choice')] = choice data[prefixed(str(i), 'choice')] = choice
...@@ -91,8 +92,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -91,8 +92,7 @@ class FormsFormsetTestCase(TestCase):
# for adding data. By default, it displays 1 blank form. It can display more, # for adding data. By default, it displays 1 blank form. It can display more,
# but we'll look at how to do so later. # but we'll look at how to do so later.
formset = self.make_choiceformset() formset = self.make_choiceformset()
self.assertHTMLEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MIN_NUM_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
self.assertHTMLEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<tr><th>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""") <tr><th>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""")
...@@ -200,6 +200,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -200,6 +200,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': '', 'choices-0-choice': '',
'choices-0-votes': '', 'choices-0-votes': '',
...@@ -213,12 +214,46 @@ class FormsFormsetTestCase(TestCase): ...@@ -213,12 +214,46 @@ class FormsFormsetTestCase(TestCase):
self.assertTrue(formset.is_valid()) self.assertTrue(formset.is_valid())
self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}]) self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}])
def test_min_num_displaying_more_than_one_blank_form(self):
# We can also display more than 1 empty form passing min_num argument
# to formset_factory. It will increment the extra argument
ChoiceFormSet = formset_factory(Choice, extra=1, min_num=1)
formset = ChoiceFormSet(auto_id=False, prefix='choices')
form_output = []
for form in formset.forms:
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="number" name="choices-0-votes" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="number" name="choices-1-votes" /></li>""")
def test_min_num_displaying_more_than_one_blank_form_with_zero_extra(self):
# We can also display more than 1 empty form passing min_num argument
ChoiceFormSet = formset_factory(Choice, extra=0, min_num=3)
formset = ChoiceFormSet(auto_id=False, prefix='choices')
form_output = []
for form in formset.forms:
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="number" name="choices-0-votes" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="number" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="number" name="choices-2-votes" /></li>""")
def test_single_form_completed(self): def test_single_form_completed(self):
# We can just fill out one of the forms. # We can just fill out one of the forms.
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -242,6 +277,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -242,6 +277,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '2', # the number of forms rendered 'choices-TOTAL_FORMS': '2', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored 'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored
'choices-0-choice': 'Zero', 'choices-0-choice': 'Zero',
'choices-0-votes': '0', 'choices-0-votes': '0',
...@@ -254,12 +290,35 @@ class FormsFormsetTestCase(TestCase): ...@@ -254,12 +290,35 @@ class FormsFormsetTestCase(TestCase):
self.assertFalse(formset.is_valid()) 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 1 or fewer forms.'])
def test_formset_validate_min_flag(self):
# If validate_min is set and min_num is more than TOTAL_FORMS in the
# data, then throw an exception. MIN_NUM_FORMS in the data is
# irrelevant here (it's output as a hint for the client but its
# value in the returned data is not checked)
data = {
'choices-TOTAL_FORMS': '2', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms - should be ignored
'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')
self.assertFalse(formset.is_valid())
self.assertEqual(formset.non_form_errors(), ['Please submit 3 or more forms.'])
def test_second_form_partially_filled_2(self): def test_second_form_partially_filled_2(self):
# And once again, if we try to partially complete a form, validation will fail. # And once again, if we try to partially complete a form, validation will fail.
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -281,6 +340,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -281,6 +340,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -344,6 +404,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -344,6 +404,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '2', # the number of forms with initial data 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -371,6 +432,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -371,6 +432,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'check-TOTAL_FORMS': '3', # the number of forms rendered 'check-TOTAL_FORMS': '3', # the number of forms rendered
'check-INITIAL_FORMS': '2', # the number of forms with initial data 'check-INITIAL_FORMS': '2', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'check-MAX_NUM_FORMS': '0', # max number of forms 'check-MAX_NUM_FORMS': '0', # max number of forms
'check-0-field': '200', 'check-0-field': '200',
'check-0-DELETE': '', 'check-0-DELETE': '',
...@@ -401,7 +463,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -401,7 +463,7 @@ class FormsFormsetTestCase(TestCase):
p = PeopleForm( p = PeopleForm(
{'form-0-name': '', 'form-0-DELETE': 'on', # no name! {'form-0-name': '', 'form-0-DELETE': 'on', # no name!
'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 1, 'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 1,
'form-MAX_NUM_FORMS': 1}) 'form-MIN_NUM_FORMS': 0, 'form-MAX_NUM_FORMS': 1})
self.assertTrue(p.is_valid()) self.assertTrue(p.is_valid())
self.assertEqual(len(p.deleted_forms), 1) self.assertEqual(len(p.deleted_forms), 1)
...@@ -438,6 +500,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -438,6 +500,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '2', # the number of forms with initial data 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -470,6 +533,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -470,6 +533,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '4', # the number of forms rendered 'choices-TOTAL_FORMS': '4', # the number of forms rendered
'choices-INITIAL_FORMS': '3', # the number of forms with initial data 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -506,6 +570,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -506,6 +570,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered 'choices-TOTAL_FORMS': '3', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
} }
...@@ -558,6 +623,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -558,6 +623,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '4', # the number of forms rendered 'choices-TOTAL_FORMS': '4', # the number of forms rendered
'choices-INITIAL_FORMS': '3', # the number of forms with initial data 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -604,6 +670,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -604,6 +670,7 @@ class FormsFormsetTestCase(TestCase):
'form-0-DELETE': 'on', # no name! 'form-0-DELETE': 'on', # no name!
'form-TOTAL_FORMS': 1, 'form-TOTAL_FORMS': 1,
'form-INITIAL_FORMS': 1, 'form-INITIAL_FORMS': 1,
'form-MIN_NUM_FORMS': 0,
'form-MAX_NUM_FORMS': 1 'form-MAX_NUM_FORMS': 1
}) })
...@@ -620,6 +687,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -620,6 +687,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'drinks-TOTAL_FORMS': '2', # the number of forms rendered 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
'drinks-MIN_NUM_FORMS': '0', # min number of forms
'drinks-MAX_NUM_FORMS': '0', # max number of forms 'drinks-MAX_NUM_FORMS': '0', # max number of forms
'drinks-0-name': 'Gin and Tonic', 'drinks-0-name': 'Gin and Tonic',
'drinks-1-name': 'Gin and Tonic', 'drinks-1-name': 'Gin and Tonic',
...@@ -639,6 +707,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -639,6 +707,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'drinks-TOTAL_FORMS': '2', # the number of forms rendered 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
'drinks-MIN_NUM_FORMS': '0', # min number of forms
'drinks-MAX_NUM_FORMS': '0', # max number of forms 'drinks-MAX_NUM_FORMS': '0', # max number of forms
'drinks-0-name': 'Gin and Tonic', 'drinks-0-name': 'Gin and Tonic',
'drinks-1-name': 'Bloody Mary', 'drinks-1-name': 'Bloody Mary',
...@@ -791,6 +860,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -791,6 +860,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'form-TOTAL_FORMS': '2', 'form-TOTAL_FORMS': '2',
'form-INITIAL_FORMS': '0', 'form-INITIAL_FORMS': '0',
'form-MIN_NUM_FORMS': '0',
'form-MAX_NUM_FORMS': '0', 'form-MAX_NUM_FORMS': '0',
} }
formset = FavoriteDrinksFormSet(data=data) formset = FavoriteDrinksFormSet(data=data)
...@@ -805,6 +875,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -805,6 +875,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'drinks-TOTAL_FORMS': '2', # the number of forms rendered 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
'drinks-MIN_NUM_FORMS': '0', # min number of forms
'drinks-MAX_NUM_FORMS': '0', # max number of forms 'drinks-MAX_NUM_FORMS': '0', # max number of forms
'drinks-0-name': 'Gin and Tonic', 'drinks-0-name': 'Gin and Tonic',
'drinks-1-name': 'Gin and Tonic', 'drinks-1-name': 'Gin and Tonic',
...@@ -894,6 +965,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -894,6 +965,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '1', # number of forms rendered 'choices-TOTAL_FORMS': '1', # number of forms rendered
'choices-INITIAL_FORMS': '0', # number of forms with initial data 'choices-INITIAL_FORMS': '0', # number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -914,6 +986,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -914,6 +986,7 @@ class FormsFormsetTestCase(TestCase):
{ {
'choices-TOTAL_FORMS': '4', 'choices-TOTAL_FORMS': '4',
'choices-INITIAL_FORMS': '0', 'choices-INITIAL_FORMS': '0',
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '4', 'choices-MAX_NUM_FORMS': '4',
'choices-0-choice': 'Zero', 'choices-0-choice': 'Zero',
'choices-0-votes': '0', 'choices-0-votes': '0',
...@@ -945,6 +1018,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -945,6 +1018,7 @@ class FormsFormsetTestCase(TestCase):
{ {
'choices-TOTAL_FORMS': '4', 'choices-TOTAL_FORMS': '4',
'choices-INITIAL_FORMS': '0', 'choices-INITIAL_FORMS': '0',
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '4', 'choices-MAX_NUM_FORMS': '4',
'choices-0-choice': 'Zero', 'choices-0-choice': 'Zero',
'choices-0-votes': '0', 'choices-0-votes': '0',
...@@ -1032,6 +1106,7 @@ class FormsFormsetTestCase(TestCase): ...@@ -1032,6 +1106,7 @@ class FormsFormsetTestCase(TestCase):
data = { data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered 'choices-TOTAL_FORMS': '1', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
'choices-MIN_NUM_FORMS': '0', # min number of forms
'choices-MAX_NUM_FORMS': '0', # max number of forms 'choices-MAX_NUM_FORMS': '0', # max number of forms
'choices-0-choice': 'Calexico', 'choices-0-choice': 'Calexico',
'choices-0-votes': '100', 'choices-0-votes': '100',
...@@ -1046,19 +1121,19 @@ ChoiceFormSet = formset_factory(Choice) ...@@ -1046,19 +1121,19 @@ ChoiceFormSet = formset_factory(Choice)
class FormsetAsFooTests(TestCase): class FormsetAsFooTests(TestCase):
def test_as_table(self): def test_as_table(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices') formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_table(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> self.assertHTMLEqual(formset.as_table(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MIN_NUM_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" value="Calexico" /></td></tr> <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" value="Calexico" /></td></tr>
<tr><th>Votes:</th><td><input type="number" name="choices-0-votes" value="100" /></td></tr>""") <tr><th>Votes:</th><td><input type="number" name="choices-0-votes" value="100" /></td></tr>""")
def test_as_p(self): def test_as_p(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices') formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_p(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> self.assertHTMLEqual(formset.as_p(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MIN_NUM_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<p>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></p> <p>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></p>
<p>Votes: <input type="number" name="choices-0-votes" value="100" /></p>""") <p>Votes: <input type="number" name="choices-0-votes" value="100" /></p>""")
def test_as_ul(self): def test_as_ul(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices') formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_ul(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> self.assertHTMLEqual(formset.as_ul(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MIN_NUM_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> <li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>""") <li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>""")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment