summaryrefslogtreecommitdiff
path: root/tests/forms/tests/formsets.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/forms/tests/formsets.py')
-rw-r--r--tests/forms/tests/formsets.py1055
1 files changed, 1055 insertions, 0 deletions
diff --git a/tests/forms/tests/formsets.py b/tests/forms/tests/formsets.py
new file mode 100644
index 0000000000..2bef0c5c33
--- /dev/null
+++ b/tests/forms/tests/formsets.py
@@ -0,0 +1,1055 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.forms import (CharField, DateField, FileField, Form, IntegerField,
+ ValidationError, formsets)
+from django.forms.formsets import BaseFormSet, formset_factory
+from django.forms.util import ErrorList
+from django.test import TestCase
+
+
+class Choice(Form):
+ choice = CharField()
+ votes = IntegerField()
+
+
+# FormSet allows us to use multiple instance of the same form on 1 page. For now,
+# the best way to create a FormSet is by using the formset_factory function.
+ChoiceFormSet = formset_factory(Choice)
+
+
+class FavoriteDrinkForm(Form):
+ name = CharField()
+
+
+class BaseFavoriteDrinksFormSet(BaseFormSet):
+ def clean(self):
+ seen_drinks = []
+
+ for drink in self.cleaned_data:
+ if drink['name'] in seen_drinks:
+ raise ValidationError('You may only specify a drink once.')
+
+ seen_drinks.append(drink['name'])
+
+
+class EmptyFsetWontValidate(BaseFormSet):
+ def clean(self):
+ raise ValidationError("Clean method called")
+
+
+# Let's define a FormSet that takes a list of favorite drinks, but raises an
+# error if there are any duplicates. Used in ``test_clean_hook``,
+# ``test_regression_6926`` & ``test_regression_12878``.
+FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm,
+ formset=BaseFavoriteDrinksFormSet, extra=3)
+
+
+class FormsFormsetTestCase(TestCase):
+ def test_basic_formset(self):
+ # A FormSet constructor takes the same arguments as Form. Let's create a FormSet
+ # for adding data. By default, it displays 1 blank form. It can display more,
+ # but we'll look at how to do so later.
+ formset = ChoiceFormSet(auto_id=False, prefix='choices')
+ 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>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""")
+
+ # On thing to note is that there needs to be a special value in the data. This
+ # value tells the FormSet how many forms were displayed so it can tell how
+ # many forms it needs to clean and validate. You could use javascript to create
+ # new forms on the client side, but they won't get validated unless you increment
+ # the TOTAL_FORMS field appropriately.
+
+ data = {
+ 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ }
+ # We treat FormSet pretty much like we would treat a normal Form. FormSet has an
+ # is_valid method, and a cleaned_data or errors attribute depending on whether all
+ # the forms passed validation. However, unlike a Form instance, cleaned_data and
+ # errors will be a list of dicts rather than just a single dict.
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])
+
+ # If a FormSet was not passed any data, its is_valid and has_changed
+ # methods should return False.
+ formset = ChoiceFormSet()
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset.has_changed())
+
+ def test_formset_validation(self):
+ # FormSet instances can also have an error attribute if validation failed for
+ # any of the forms.
+
+ data = {
+ 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(formset.is_valid())
+ self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])
+
+ def test_formset_has_changed(self):
+ # FormSet instances has_changed method will be True if any data is
+ # passed to his forms, even if the formset didn't validate
+ data = {
+ 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': '',
+ 'choices-0-votes': '',
+ }
+ blank_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(blank_formset.has_changed())
+
+ # invalid formset test
+ data['choices-0-choice'] = 'Calexico'
+ invalid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(invalid_formset.is_valid())
+ self.assertTrue(invalid_formset.has_changed())
+
+ # valid formset test
+ data['choices-0-votes'] = '100'
+ valid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(valid_formset.is_valid())
+ self.assertTrue(valid_formset.has_changed())
+
+ def test_formset_initial_data(self):
+ # We can also prefill a FormSet with existing data by providing an ``initial``
+ # argument to the constructor. ``initial`` should be a list of dicts. By default,
+ # an extra blank form is included.
+
+ initial = [{'choice': 'Calexico', 'votes': 100}]
+ formset = ChoiceFormSet(initial=initial, 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" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" /></li>""")
+
+ # Let's simulate what would happen if we submitted this form.
+
+ data = {
+ 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-1-choice': '',
+ 'choices-1-votes': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}])
+
+ def test_second_form_partially_filled(self):
+ # But the second form was blank! Shouldn't we get some errors? No. If we display
+ # a form as blank, it's ok for it to be submitted as blank. If we fill out even
+ # one of the fields of a blank form though, it will be validated. We may want to
+ # required that at least x number of forms are completed, but we'll show how to
+ # handle that later.
+
+ data = {
+ 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-1-choice': 'The Decemberists',
+ 'choices-1-votes': '', # missing value
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(formset.is_valid())
+ self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])
+
+ def test_delete_prefilled_data(self):
+ # If we delete data that was pre-filled, we should get an error. Simply removing
+ # data from form fields isn't the proper way to delete it. We'll see how to
+ # handle that case later.
+
+ data = {
+ 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': '', # deleted value
+ 'choices-0-votes': '', # deleted value
+ 'choices-1-choice': '',
+ 'choices-1-votes': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(formset.is_valid())
+ self.assertEqual(formset.errors, [{'votes': ['This field is required.'], 'choice': ['This field is required.']}, {}])
+
+ def test_displaying_more_than_one_blank_form(self):
+ # Displaying more than 1 blank form ###########################################
+ # We can also display more than 1 empty form at a time. To do so, pass a
+ # extra argument to formset_factory.
+ ChoiceFormSet = formset_factory(Choice, extra=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>""")
+
+ # Since we displayed every form as blank, we will also accept them back as blank.
+ # This may seem a little strange, but later we will show how to require a minimum
+ # number of forms to be completed.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': '',
+ 'choices-0-votes': '',
+ 'choices-1-choice': '',
+ 'choices-1-votes': '',
+ 'choices-2-choice': '',
+ 'choices-2-votes': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}])
+
+ def test_single_form_completed(self):
+ # We can just fill out one of the forms.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-1-choice': '',
+ 'choices-1-votes': '',
+ 'choices-2-choice': '',
+ 'choices-2-votes': '',
+ }
+
+ ChoiceFormSet = formset_factory(Choice, extra=3)
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}, {}])
+
+ def test_second_form_partially_filled_2(self):
+ # And once again, if we try to partially complete a form, validation will fail.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-1-choice': 'The Decemberists',
+ 'choices-1-votes': '', # missing value
+ 'choices-2-choice': '',
+ 'choices-2-votes': '',
+ }
+
+ ChoiceFormSet = formset_factory(Choice, extra=3)
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertFalse(formset.is_valid())
+ self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}, {}])
+
+ def test_more_initial_data(self):
+ # The extra argument also works when the formset is pre-filled with initial
+ # data.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-1-choice': '',
+ 'choices-1-votes': '', # missing value
+ 'choices-2-choice': '',
+ 'choices-2-votes': '',
+ }
+
+ initial = [{'choice': 'Calexico', 'votes': 100}]
+ ChoiceFormSet = formset_factory(Choice, extra=3)
+ formset = ChoiceFormSet(initial=initial, 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" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></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>
+<li>Choice: <input type="text" name="choices-3-choice" /></li>
+<li>Votes: <input type="number" name="choices-3-votes" /></li>""")
+
+ # Make sure retrieving an empty form works, and it shows up in the form list
+
+ self.assertTrue(formset.empty_form.empty_permitted)
+ self.assertHTMLEqual(formset.empty_form.as_ul(), """<li>Choice: <input type="text" name="choices-__prefix__-choice" /></li>
+<li>Votes: <input type="number" name="choices-__prefix__-votes" /></li>""")
+
+ def test_formset_with_deletion(self):
+ # FormSets with deletion ######################################################
+ # We can easily add deletion ability to a FormSet with an argument to
+ # formset_factory. This will add a boolean field to each form instance. When
+ # that boolean field is True, the form will be in formset.deleted_forms
+
+ ChoiceFormSet = formset_factory(Choice, can_delete=True)
+
+ initial = [{'choice': 'Calexico', 'votes': 100}, {'choice': 'Fergie', 'votes': 900}]
+ formset = ChoiceFormSet(initial=initial, 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" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
+<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" /></li>
+<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>""")
+
+ # To delete something, we just need to set that form's special delete field to
+ # 'on'. Let's go ahead and delete Fergie.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-0-DELETE': '',
+ 'choices-1-choice': 'Fergie',
+ 'choices-1-votes': '900',
+ 'choices-1-DELETE': 'on',
+ 'choices-2-choice': '',
+ 'choices-2-votes': '',
+ 'choices-2-DELETE': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'DELETE': False, 'choice': 'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': 'Fergie'}, {}])
+ self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'choice': 'Fergie'}])
+
+ # If we fill a form with something and then we check the can_delete checkbox for
+ # that form, that form's errors should not make the entire formset invalid since
+ # it's going to be deleted.
+
+ class CheckForm(Form):
+ field = IntegerField(min_value=100)
+
+ data = {
+ 'check-TOTAL_FORMS': '3', # the number of forms rendered
+ 'check-INITIAL_FORMS': '2', # the number of forms with initial data
+ 'check-MAX_NUM_FORMS': '0', # max number of forms
+ 'check-0-field': '200',
+ 'check-0-DELETE': '',
+ 'check-1-field': '50',
+ 'check-1-DELETE': 'on',
+ 'check-2-field': '',
+ 'check-2-DELETE': '',
+ }
+ CheckFormSet = formset_factory(CheckForm, can_delete=True)
+ formset = CheckFormSet(data, prefix='check')
+ self.assertTrue(formset.is_valid())
+
+ # If we remove the deletion flag now we will have our validation back.
+ data['check-1-DELETE'] = ''
+ formset = CheckFormSet(data, prefix='check')
+ self.assertFalse(formset.is_valid())
+
+ # Should be able to get deleted_forms from a valid formset even if a
+ # deleted form would have been invalid.
+
+ class Person(Form):
+ name = CharField()
+
+ PeopleForm = formset_factory(
+ form=Person,
+ can_delete=True)
+
+ p = PeopleForm(
+ {'form-0-name': '', 'form-0-DELETE': 'on', # no name!
+ 'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 1,
+ 'form-MAX_NUM_FORMS': 1})
+
+ self.assertTrue(p.is_valid())
+ self.assertEqual(len(p.deleted_forms), 1)
+
+ def test_formsets_with_ordering(self):
+ # FormSets with ordering ######################################################
+ # We can also add ordering ability to a FormSet with an argument to
+ # formset_factory. This will add a integer field to each form instance. When
+ # form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct
+ # order specified by the ordering fields. If a number is duplicated in the set
+ # of ordering fields, for instance form 0 and form 3 are both marked as 1, then
+ # the form index used as a secondary ordering criteria. In order to put
+ # something at the front of the list, you'd need to set it's order to 0.
+
+ ChoiceFormSet = formset_factory(Choice, can_order=True)
+
+ initial = [{'choice': 'Calexico', 'votes': 100}, {'choice': 'Fergie', 'votes': 900}]
+ formset = ChoiceFormSet(initial=initial, 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" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Order: <input type="number" name="choices-0-ORDER" value="1" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
+<li>Order: <input type="number" name="choices-1-ORDER" value="2" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" /></li>
+<li>Order: <input type="number" name="choices-2-ORDER" /></li>""")
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-0-ORDER': '1',
+ 'choices-1-choice': 'Fergie',
+ 'choices-1-votes': '900',
+ 'choices-1-ORDER': '2',
+ 'choices-2-choice': 'The Decemberists',
+ 'choices-2-votes': '500',
+ 'choices-2-ORDER': '0',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ form_output = []
+
+ for form in formset.ordered_forms:
+ form_output.append(form.cleaned_data)
+
+ self.assertEqual(form_output, [
+ {'votes': 500, 'ORDER': 0, 'choice': 'The Decemberists'},
+ {'votes': 100, 'ORDER': 1, 'choice': 'Calexico'},
+ {'votes': 900, 'ORDER': 2, 'choice': 'Fergie'},
+ ])
+
+ def test_empty_ordered_fields(self):
+ # Ordering fields are allowed to be left blank, and if they *are* left blank,
+ # they will be sorted below everything else.
+
+ data = {
+ 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-0-ORDER': '1',
+ 'choices-1-choice': 'Fergie',
+ 'choices-1-votes': '900',
+ 'choices-1-ORDER': '2',
+ 'choices-2-choice': 'The Decemberists',
+ 'choices-2-votes': '500',
+ 'choices-2-ORDER': '',
+ 'choices-3-choice': 'Basia Bulat',
+ 'choices-3-votes': '50',
+ 'choices-3-ORDER': '',
+ }
+
+ ChoiceFormSet = formset_factory(Choice, can_order=True)
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ form_output = []
+
+ for form in formset.ordered_forms:
+ form_output.append(form.cleaned_data)
+
+ self.assertEqual(form_output, [
+ {'votes': 100, 'ORDER': 1, 'choice': 'Calexico'},
+ {'votes': 900, 'ORDER': 2, 'choice': 'Fergie'},
+ {'votes': 500, 'ORDER': None, 'choice': 'The Decemberists'},
+ {'votes': 50, 'ORDER': None, 'choice': 'Basia Bulat'},
+ ])
+
+ def test_ordering_blank_fieldsets(self):
+ # Ordering should work with blank fieldsets.
+
+ data = {
+ 'choices-TOTAL_FORMS': '3', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ }
+
+ ChoiceFormSet = formset_factory(Choice, can_order=True)
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ form_output = []
+
+ for form in formset.ordered_forms:
+ form_output.append(form.cleaned_data)
+
+ self.assertEqual(form_output, [])
+
+ def test_formset_with_ordering_and_deletion(self):
+ # FormSets with ordering + deletion ###########################################
+ # Let's try throwing ordering and deletion into the same form.
+
+ ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True)
+
+ initial = [
+ {'choice': 'Calexico', 'votes': 100},
+ {'choice': 'Fergie', 'votes': 900},
+ {'choice': 'The Decemberists', 'votes': 500},
+ ]
+ formset = ChoiceFormSet(initial=initial, 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" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Order: <input type="number" name="choices-0-ORDER" value="1" /></li>
+<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
+<li>Order: <input type="number" name="choices-1-ORDER" value="2" /></li>
+<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" value="500" /></li>
+<li>Order: <input type="number" name="choices-2-ORDER" value="3" /></li>
+<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
+<li>Choice: <input type="text" name="choices-3-choice" /></li>
+<li>Votes: <input type="number" name="choices-3-votes" /></li>
+<li>Order: <input type="number" name="choices-3-ORDER" /></li>
+<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>""")
+
+ # Let's delete Fergie, and put The Decemberists ahead of Calexico.
+
+ data = {
+ 'choices-TOTAL_FORMS': '4', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ 'choices-0-ORDER': '1',
+ 'choices-0-DELETE': '',
+ 'choices-1-choice': 'Fergie',
+ 'choices-1-votes': '900',
+ 'choices-1-ORDER': '2',
+ 'choices-1-DELETE': 'on',
+ 'choices-2-choice': 'The Decemberists',
+ 'choices-2-votes': '500',
+ 'choices-2-ORDER': '0',
+ 'choices-2-DELETE': '',
+ 'choices-3-choice': '',
+ 'choices-3-votes': '',
+ 'choices-3-ORDER': '',
+ 'choices-3-DELETE': '',
+ }
+
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ form_output = []
+
+ for form in formset.ordered_forms:
+ form_output.append(form.cleaned_data)
+
+ self.assertEqual(form_output, [
+ {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': 'The Decemberists'},
+ {'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': 'Calexico'},
+ ])
+ self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': 'Fergie'}])
+
+ def test_invalid_deleted_form_with_ordering(self):
+ # Should be able to get ordered forms from a valid formset even if a
+ # deleted form would have been invalid.
+
+ class Person(Form):
+ name = CharField()
+
+ PeopleForm = formset_factory(form=Person, can_delete=True, can_order=True)
+
+ p = PeopleForm({
+ 'form-0-name': '',
+ 'form-0-DELETE': 'on', # no name!
+ 'form-TOTAL_FORMS': 1,
+ 'form-INITIAL_FORMS': 1,
+ 'form-MAX_NUM_FORMS': 1
+ })
+
+ self.assertTrue(p.is_valid())
+ self.assertEqual(p.ordered_forms, [])
+
+ def test_clean_hook(self):
+ # FormSet clean hook ##########################################################
+ # FormSets have a hook for doing extra validation that shouldn't be tied to any
+ # particular form. It follows the same pattern as the clean hook on Forms.
+
+ # We start out with a some duplicate data.
+
+ data = {
+ 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+ 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'drinks-MAX_NUM_FORMS': '0', # max number of forms
+ 'drinks-0-name': 'Gin and Tonic',
+ 'drinks-1-name': 'Gin and Tonic',
+ }
+
+ formset = FavoriteDrinksFormSet(data, prefix='drinks')
+ self.assertFalse(formset.is_valid())
+
+ # Any errors raised by formset.clean() are available via the
+ # formset.non_form_errors() method.
+
+ for error in formset.non_form_errors():
+ self.assertEqual(str(error), 'You may only specify a drink once.')
+
+ # Make sure we didn't break the valid case.
+
+ data = {
+ 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+ 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'drinks-MAX_NUM_FORMS': '0', # max number of forms
+ 'drinks-0-name': 'Gin and Tonic',
+ 'drinks-1-name': 'Bloody Mary',
+ }
+
+ formset = FavoriteDrinksFormSet(data, prefix='drinks')
+ self.assertTrue(formset.is_valid())
+ self.assertEqual(formset.non_form_errors(), [])
+
+ def test_limiting_max_forms(self):
+ # Limiting the maximum number of forms ########################################
+ # Base case for max_num.
+
+ # When not passed, max_num will take a high default value, leaving the
+ # number of forms only controlled by the value of the extra parameter.
+
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3)
+ formset = LimitedFavoriteDrinkFormSet()
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>
+<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>
+<tr><th><label for="id_form-2-name">Name:</label></th><td><input type="text" name="form-2-name" id="id_form-2-name" /></td></tr>""")
+
+ # If max_num is 0 then no form is rendered at all.
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=0)
+ formset = LimitedFavoriteDrinkFormSet()
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertEqual('\n'.join(form_output), "")
+
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2)
+ formset = LimitedFavoriteDrinkFormSet()
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>
+<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""")
+
+ # Ensure that max_num has no effect when extra is less than max_num.
+
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
+ formset = LimitedFavoriteDrinkFormSet()
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>""")
+
+ def test_max_num_with_initial_data(self):
+ # max_num with initial data
+
+ # When not passed, max_num will take a high default value, leaving the
+ # number of forms only controlled by the value of the initial and extra
+ # parameters.
+
+ initial = [
+ {'name': 'Fernet and Coke'},
+ ]
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1)
+ formset = LimitedFavoriteDrinkFormSet(initial=initial)
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Fernet and Coke" id="id_form-0-name" /></td></tr>
+<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""")
+
+ def test_max_num_zero(self):
+ # If max_num is 0 then no form is rendered at all, even if extra and initial
+ # are specified.
+
+ initial = [
+ {'name': 'Fernet and Coke'},
+ {'name': 'Bloody Mary'},
+ ]
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0)
+ formset = LimitedFavoriteDrinkFormSet(initial=initial)
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertEqual('\n'.join(form_output), "")
+
+ def test_more_initial_than_max_num(self):
+ # More initial forms than max_num will result in only the first max_num of
+ # them to be displayed with no extra forms.
+
+ initial = [
+ {'name': 'Gin Tonic'},
+ {'name': 'Bloody Mary'},
+ {'name': 'Jack and Coke'},
+ ]
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
+ formset = LimitedFavoriteDrinkFormSet(initial=initial)
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr>
+<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr>""")
+
+ # One form from initial and extra=3 with max_num=2 should result in the one
+ # initial form and one extra.
+ initial = [
+ {'name': 'Gin Tonic'},
+ ]
+ LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2)
+ formset = LimitedFavoriteDrinkFormSet(initial=initial)
+ form_output = []
+
+ for form in formset.forms:
+ form_output.append(str(form))
+
+ self.assertHTMLEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr>
+<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""")
+
+ def test_regression_6926(self):
+ # Regression test for #6926 ##################################################
+ # Make sure the management form has the correct prefix.
+
+ formset = FavoriteDrinksFormSet()
+ self.assertEqual(formset.management_form.prefix, 'form')
+
+ data = {
+ 'form-TOTAL_FORMS': '2',
+ 'form-INITIAL_FORMS': '0',
+ 'form-MAX_NUM_FORMS': '0',
+ }
+ formset = FavoriteDrinksFormSet(data=data)
+ self.assertEqual(formset.management_form.prefix, 'form')
+
+ formset = FavoriteDrinksFormSet(initial={})
+ self.assertEqual(formset.management_form.prefix, 'form')
+
+ def test_regression_12878(self):
+ # Regression test for #12878 #################################################
+
+ data = {
+ 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
+ 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'drinks-MAX_NUM_FORMS': '0', # max number of forms
+ 'drinks-0-name': 'Gin and Tonic',
+ 'drinks-1-name': 'Gin and Tonic',
+ }
+
+ formset = FavoriteDrinksFormSet(data, prefix='drinks')
+ self.assertFalse(formset.is_valid())
+ self.assertEqual(formset.non_form_errors(), ['You may only specify a drink once.'])
+
+ def test_formset_iteration(self):
+ # Regression tests for #16455 -- formset instances are iterable
+ ChoiceFormset = formset_factory(Choice, extra=3)
+ formset = ChoiceFormset()
+
+ # confirm iterated formset yields formset.forms
+ forms = list(formset)
+ self.assertEqual(forms, formset.forms)
+ self.assertEqual(len(formset), len(forms))
+
+ # confirm indexing of formset
+ self.assertEqual(formset[0], forms[0])
+ try:
+ formset[3]
+ self.fail('Requesting an invalid formset index should raise an exception')
+ except IndexError:
+ pass
+
+ # Formets can override the default iteration order
+ class BaseReverseFormSet(BaseFormSet):
+ def __iter__(self):
+ return reversed(self.forms)
+
+ def __getitem__(self, idx):
+ return super(BaseReverseFormSet, self).__getitem__(len(self) - idx - 1)
+
+ ReverseChoiceFormset = formset_factory(Choice, BaseReverseFormSet, extra=3)
+ reverse_formset = ReverseChoiceFormset()
+
+ # confirm that __iter__ modifies rendering order
+ # compare forms from "reverse" formset with forms from original formset
+ self.assertEqual(str(reverse_formset[0]), str(forms[-1]))
+ self.assertEqual(str(reverse_formset[1]), str(forms[-2]))
+ self.assertEqual(len(reverse_formset), len(forms))
+
+ def test_formset_nonzero(self):
+ """
+ Formsets with no forms should still evaluate as true.
+ Regression test for #15722
+ """
+ ChoiceFormset = formset_factory(Choice, extra=0)
+ formset = ChoiceFormset()
+ self.assertEqual(len(formset.forms), 0)
+ self.assertTrue(formset)
+
+
+ def test_formset_error_class(self):
+ # Regression tests for #16479 -- formsets form use ErrorList instead of supplied error_class
+ class CustomErrorList(ErrorList):
+ pass
+
+ formset = FavoriteDrinksFormSet(error_class=CustomErrorList)
+ self.assertEqual(formset.forms[0].error_class, CustomErrorList)
+
+ def test_formset_calls_forms_is_valid(self):
+ # Regression tests for #18574 -- make sure formsets call
+ # is_valid() on each form.
+
+ class AnotherChoice(Choice):
+ def is_valid(self):
+ self.is_valid_called = True
+ return super(AnotherChoice, self).is_valid()
+
+ AnotherChoiceFormSet = formset_factory(AnotherChoice)
+ data = {
+ 'choices-TOTAL_FORMS': '1', # number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ }
+ formset = AnotherChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertTrue(all([form.is_valid_called for form in formset.forms]))
+
+ def test_hard_limit_on_instantiated_forms(self):
+ """A formset has a hard limit on the number of forms instantiated."""
+ # reduce the default limit of 1000 temporarily for testing
+ _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
+ try:
+ formsets.DEFAULT_MAX_NUM = 3
+ ChoiceFormSet = formset_factory(Choice)
+ # someone fiddles with the mgmt form data...
+ formset = ChoiceFormSet(
+ {
+ 'choices-TOTAL_FORMS': '4',
+ 'choices-INITIAL_FORMS': '0',
+ 'choices-MAX_NUM_FORMS': '4',
+ 'choices-0-choice': 'Zero',
+ 'choices-0-votes': '0',
+ 'choices-1-choice': 'One',
+ 'choices-1-votes': '1',
+ 'choices-2-choice': 'Two',
+ 'choices-2-votes': '2',
+ 'choices-3-choice': 'Three',
+ 'choices-3-votes': '3',
+ },
+ prefix='choices',
+ )
+ # But we still only instantiate 3 forms
+ self.assertEqual(len(formset.forms), 3)
+ finally:
+ formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
+
+ def test_increase_hard_limit(self):
+ """Can increase the built-in forms limit via a higher max_num."""
+ # reduce the default limit of 1000 temporarily for testing
+ _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
+ try:
+ formsets.DEFAULT_MAX_NUM = 3
+ # for this form, we want a limit of 4
+ ChoiceFormSet = formset_factory(Choice, max_num=4)
+ formset = ChoiceFormSet(
+ {
+ 'choices-TOTAL_FORMS': '4',
+ 'choices-INITIAL_FORMS': '0',
+ 'choices-MAX_NUM_FORMS': '4',
+ 'choices-0-choice': 'Zero',
+ 'choices-0-votes': '0',
+ 'choices-1-choice': 'One',
+ 'choices-1-votes': '1',
+ 'choices-2-choice': 'Two',
+ 'choices-2-votes': '2',
+ 'choices-3-choice': 'Three',
+ 'choices-3-votes': '3',
+ },
+ prefix='choices',
+ )
+ # This time four forms are instantiated
+ self.assertEqual(len(formset.forms), 4)
+ finally:
+ formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
+
+
+data = {
+ 'choices-TOTAL_FORMS': '1', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+}
+
+class Choice(Form):
+ choice = CharField()
+ votes = IntegerField()
+
+ChoiceFormSet = formset_factory(Choice)
+
+class FormsetAsFooTests(TestCase):
+ def test_as_table(self):
+ 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" />
+<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>""")
+
+ def test_as_p(self):
+ 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" />
+<p>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></p>
+<p>Votes: <input type="number" name="choices-0-votes" value="100" /></p>""")
+
+ def test_as_ul(self):
+ 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" />
+<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>""")
+
+
+# Regression test for #11418 #################################################
+class ArticleForm(Form):
+ title = CharField()
+ pub_date = DateField()
+
+ArticleFormSet = formset_factory(ArticleForm)
+
+class TestIsBoundBehavior(TestCase):
+ def test_no_data_raises_validation_error(self):
+ self.assertRaises(ValidationError, ArticleFormSet, {})
+
+ def test_with_management_data_attrs_work_fine(self):
+ data = {
+ 'form-TOTAL_FORMS': '1',
+ 'form-INITIAL_FORMS': '0',
+ }
+ formset = ArticleFormSet(data)
+ self.assertEqual(0, formset.initial_form_count())
+ self.assertEqual(1, formset.total_form_count())
+ self.assertTrue(formset.is_bound)
+ self.assertTrue(formset.forms[0].is_bound)
+ self.assertTrue(formset.is_valid())
+ self.assertTrue(formset.forms[0].is_valid())
+ self.assertEqual([{}], formset.cleaned_data)
+
+
+ def test_form_errors_are_cought_by_formset(self):
+ data = {
+ 'form-TOTAL_FORMS': '2',
+ 'form-INITIAL_FORMS': '0',
+ 'form-0-title': 'Test',
+ 'form-0-pub_date': '1904-06-16',
+ 'form-1-title': 'Test',
+ 'form-1-pub_date': '', # <-- this date is missing but required
+ }
+ formset = ArticleFormSet(data)
+ self.assertFalse(formset.is_valid())
+ self.assertEqual([{}, {'pub_date': ['This field is required.']}], formset.errors)
+
+ def test_empty_forms_are_unbound(self):
+ data = {
+ 'form-TOTAL_FORMS': '1',
+ 'form-INITIAL_FORMS': '0',
+ 'form-0-title': 'Test',
+ 'form-0-pub_date': '1904-06-16',
+ }
+ unbound_formset = ArticleFormSet()
+ bound_formset = ArticleFormSet(data)
+
+ empty_forms = []
+
+ empty_forms.append(unbound_formset.empty_form)
+ empty_forms.append(bound_formset.empty_form)
+
+ # Empty forms should be unbound
+ self.assertFalse(empty_forms[0].is_bound)
+ self.assertFalse(empty_forms[1].is_bound)
+
+ # The empty forms should be equal.
+ self.assertHTMLEqual(empty_forms[0].as_p(), empty_forms[1].as_p())
+
+class TestEmptyFormSet(TestCase):
+ def test_empty_formset_is_valid(self):
+ """Test that an empty formset still calls clean()"""
+ EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
+ formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form")
+ formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form")
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset2.is_valid())
+
+ def test_empty_formset_media(self):
+ """Make sure media is available on empty formset, refs #19545"""
+ class MediaForm(Form):
+ class Media:
+ js = ('some-file.js',)
+ self.assertIn('some-file.js', str(formset_factory(MediaForm, extra=0)().media))
+
+ def test_empty_formset_is_multipart(self):
+ """Make sure `is_multipart()` works with empty formset, refs #19545"""
+ class FileForm(Form):
+ file = FileField()
+ self.assertTrue(formset_factory(FileForm, extra=0)().is_multipart())