123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530 |
- import datetime
- from collections import Counter
- from unittest import mock
- from django.core.exceptions import ValidationError
- from django.forms import (
- BaseForm, CharField, DateField, FileField, Form, IntegerField,
- SplitDateTimeField, formsets,
- )
- from django.forms.formsets import BaseFormSet, all_valid, formset_factory
- from django.forms.utils import ErrorList
- from django.forms.widgets import HiddenInput
- from django.test import SimpleTestCase
- class Choice(Form):
- choice = CharField()
- votes = IntegerField()
- 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'])
- # A FormSet that takes a list of favorite drinks and raises an error if
- # there are any duplicates.
- FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, formset=BaseFavoriteDrinksFormSet, extra=3)
- class CustomKwargForm(Form):
- def __init__(self, *args, custom_kwarg, **kwargs):
- self.custom_kwarg = custom_kwarg
- super().__init__(*args, **kwargs)
- class FormsFormsetTestCase(SimpleTestCase):
- def make_choiceformset(
- self, formset_data=None, formset_class=ChoiceFormSet,
- total_forms=None, initial_forms=0, max_num_forms=0, min_num_forms=0, **kwargs):
- """
- Make a ChoiceFormset from the given formset_data.
- The data should be given as a list of (choice, votes) tuples.
- """
- kwargs.setdefault('prefix', 'choices')
- kwargs.setdefault('auto_id', False)
- if formset_data is None:
- return formset_class(**kwargs)
- if total_forms is None:
- total_forms = len(formset_data)
- def prefixed(*args):
- args = (kwargs['prefix'],) + args
- return '-'.join(args)
- data = {
- prefixed('TOTAL_FORMS'): str(total_forms),
- prefixed('INITIAL_FORMS'): str(initial_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):
- data[prefixed(str(i), 'choice')] = choice
- data[prefixed(str(i), 'votes')] = votes
- return formset_class(data, **kwargs)
- def test_basic_formset(self):
- """
- A FormSet constructor takes the same arguments as Form. Create a
- FormSet for adding data. By default, it displays 1 blank form.
- """
- 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">
- <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>"""
- )
- # FormSet are treated similarly to Forms. 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, cleaned_data
- # and errors will be a list of dicts rather than a single dict.
- formset = self.make_choiceformset([('Calexico', '100')])
- self.assertTrue(formset.is_valid())
- self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])
- # If a FormSet wasn't passed any data, is_valid() and has_changed()
- # return False.
- formset = self.make_choiceformset()
- self.assertFalse(formset.is_valid())
- self.assertFalse(formset.has_changed())
- def test_form_kwargs_formset(self):
- """
- Custom kwargs set on the formset instance are passed to the
- underlying forms.
- """
- FormSet = formset_factory(CustomKwargForm, extra=2)
- formset = FormSet(form_kwargs={'custom_kwarg': 1})
- for form in formset:
- self.assertTrue(hasattr(form, 'custom_kwarg'))
- self.assertEqual(form.custom_kwarg, 1)
- def test_form_kwargs_formset_dynamic(self):
- """Form kwargs can be passed dynamically in a formset."""
- class DynamicBaseFormSet(BaseFormSet):
- def get_form_kwargs(self, index):
- return {'custom_kwarg': index}
- DynamicFormSet = formset_factory(CustomKwargForm, formset=DynamicBaseFormSet, extra=2)
- formset = DynamicFormSet(form_kwargs={'custom_kwarg': 'ignored'})
- for i, form in enumerate(formset):
- self.assertTrue(hasattr(form, 'custom_kwarg'))
- self.assertEqual(form.custom_kwarg, i)
- def test_form_kwargs_empty_form(self):
- FormSet = formset_factory(CustomKwargForm)
- formset = FormSet(form_kwargs={'custom_kwarg': 1})
- self.assertTrue(hasattr(formset.empty_form, 'custom_kwarg'))
- self.assertEqual(formset.empty_form.custom_kwarg, 1)
- def test_formset_validation(self):
- # FormSet instances can also have an error attribute if validation failed for
- # any of the forms.
- formset = self.make_choiceformset([('Calexico', '')])
- self.assertFalse(formset.is_valid())
- self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])
- def test_formset_validation_count(self):
- """
- A formset's ManagementForm is validated once per FormSet.is_valid()
- call and each form of the formset is cleaned once.
- """
- def make_method_counter(func):
- """Add a counter to func for the number of times it's called."""
- counter = Counter()
- counter.call_count = 0
- def mocked_func(*args, **kwargs):
- counter.call_count += 1
- return func(*args, **kwargs)
- return mocked_func, counter
- mocked_is_valid, is_valid_counter = make_method_counter(formsets.ManagementForm.is_valid)
- mocked_full_clean, full_clean_counter = make_method_counter(BaseForm.full_clean)
- formset = self.make_choiceformset([('Calexico', '100'), ('Any1', '42'), ('Any2', '101')])
- with mock.patch('django.forms.formsets.ManagementForm.is_valid', mocked_is_valid), \
- mock.patch('django.forms.forms.BaseForm.full_clean', mocked_full_clean):
- self.assertTrue(formset.is_valid())
- self.assertEqual(is_valid_counter.call_count, 1)
- self.assertEqual(full_clean_counter.call_count, 4)
- def test_formset_has_changed(self):
- """
- FormSet.has_changed() is True if any data is passed to its forms, even
- if the formset didn't validate.
- """
- blank_formset = self.make_choiceformset([('', '')])
- self.assertFalse(blank_formset.has_changed())
- # invalid formset
- invalid_formset = self.make_choiceformset([('Calexico', '')])
- self.assertFalse(invalid_formset.is_valid())
- self.assertTrue(invalid_formset.has_changed())
- # valid formset
- valid_formset = self.make_choiceformset([('Calexico', '100')])
- self.assertTrue(valid_formset.is_valid())
- self.assertTrue(valid_formset.has_changed())
- def test_formset_initial_data(self):
- """
- A FormSet can be prefilled with existing data by providing a list of
- dicts to the `initial` argument. By default, an extra blank form is
- included.
- """
- formset = self.make_choiceformset(initial=[{'choice': 'Calexico', 'votes': 100}])
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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>"""
- )
- def test_blank_form_unfilled(self):
- """A form that's displayed as blank may be submitted as blank."""
- formset = self.make_choiceformset([('Calexico', '100'), ('', '')], initial_forms=1)
- 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):
- """
- If at least one field is filled out on a blank form, it will be
- validated.
- """
- formset = self.make_choiceformset([('Calexico', '100'), ('The Decemberists', '')], initial_forms=1)
- self.assertFalse(formset.is_valid())
- self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])
- def test_delete_prefilled_data(self):
- """
- Deleting prefilled data is an error. Removing data from form fields
- isn't the proper way to delete it.
- """
- formset = self.make_choiceformset([('', ''), ('', '')], initial_forms=1)
- 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):
- """
- More than 1 empty form can be displayed using formset_factory's
- `extra` argument.
- """
- ChoiceFormSet = formset_factory(Choice, extra=3)
- formset = ChoiceFormSet(auto_id=False, prefix='choices')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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 every form was displayed as blank, they are also accepted as
- # blank. This may seem a little strange, but min_num is used 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-MIN_NUM_FORMS': '0', # min number of forms
- '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_min_num_displaying_more_than_one_blank_form(self):
- """
- More than 1 empty form can also be displayed using formset_factory's
- min_num argument. It will (essentially) increment the extra argument.
- """
- ChoiceFormSet = formset_factory(Choice, extra=1, min_num=1)
- formset = ChoiceFormSet(auto_id=False, prefix='choices')
- # Min_num forms are required; extra forms can be empty.
- self.assertFalse(formset.forms[0].empty_permitted)
- self.assertTrue(formset.forms[1].empty_permitted)
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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):
- """More than 1 empty form can be displayed using min_num."""
- ChoiceFormSet = formset_factory(Choice, extra=0, min_num=3)
- formset = ChoiceFormSet(auto_id=False, prefix='choices')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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):
- """Just one form may be completed."""
- data = {
- 'choices-TOTAL_FORMS': '3', # 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
- '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_formset_validate_max_flag(self):
- """
- If validate_max is set and max_num is less than TOTAL_FORMS in the
- data, a ValidationError is raised. MAX_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': '2', # 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, max_num=1, validate_max=True)
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertFalse(formset.is_valid())
- self.assertEqual(formset.non_form_errors(), ['Please submit at most 1 form.'])
- self.assertEqual(
- str(formset.non_form_errors()),
- '<ul class="errorlist nonform"><li>Please submit at most 1 form.</li></ul>',
- )
- def test_formset_validate_min_flag(self):
- """
- If validate_min is set and min_num is more than TOTAL_FORMS in the
- data, a ValidationError is raised. 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 at least 3 forms.'])
- self.assertEqual(
- str(formset.non_form_errors()),
- '<ul class="errorlist nonform"><li>'
- 'Please submit at least 3 forms.</li></ul>',
- )
- def test_formset_validate_min_unchanged_forms(self):
- """
- min_num validation doesn't consider unchanged forms with initial data
- as "empty".
- """
- initial = [
- {'choice': 'Zero', 'votes': 0},
- {'choice': 'One', 'votes': 0},
- ]
- data = {
- 'choices-TOTAL_FORMS': '2',
- 'choices-INITIAL_FORMS': '2',
- 'choices-MIN_NUM_FORMS': '0',
- 'choices-MAX_NUM_FORMS': '2',
- 'choices-0-choice': 'Zero',
- 'choices-0-votes': '0',
- 'choices-1-choice': 'One',
- 'choices-1-votes': '1', # changed from initial
- }
- ChoiceFormSet = formset_factory(Choice, min_num=2, validate_min=True)
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices', initial=initial)
- self.assertFalse(formset.forms[0].has_changed())
- self.assertTrue(formset.forms[1].has_changed())
- self.assertTrue(formset.is_valid())
- def test_formset_validate_min_excludes_empty_forms(self):
- data = {
- 'choices-TOTAL_FORMS': '2',
- 'choices-INITIAL_FORMS': '0',
- }
- ChoiceFormSet = formset_factory(Choice, extra=2, min_num=1, validate_min=True, can_delete=True)
- formset = ChoiceFormSet(data, prefix='choices')
- self.assertFalse(formset.has_changed())
- self.assertFalse(formset.is_valid())
- self.assertEqual(formset.non_form_errors(), ['Please submit at least 1 form.'])
- def test_second_form_partially_filled_2(self):
- """A partially completed form is invalid."""
- data = {
- 'choices-TOTAL_FORMS': '3', # 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
- '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 works when the formset is pre-filled with initial
- data.
- """
- initial = [{'choice': 'Calexico', 'votes': 100}]
- ChoiceFormSet = formset_factory(Choice, extra=3)
- formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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>"""
- )
- # Retrieving an empty form works. Tt 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):
- """
- formset_factory's can_delete argument adds a boolean "delete" field to
- each form. 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')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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, 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-MIN_NUM_FORMS': '0', # min number of forms
- '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'}]
- )
- def test_formset_with_deletion_remove_deletion_flag(self):
- """
- If a form is filled with something and can_delete is also checked, that
- form's errors shouldn't 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
- 'choices-MIN_NUM_FORMS': '0', # min number of forms
- '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 the deletion flag is removed, validation is enabled.
- data['check-1-DELETE'] = ''
- formset = CheckFormSet(data, prefix='check')
- self.assertFalse(formset.is_valid())
- def test_formset_with_deletion_invalid_deleted_form(self):
- """
- deleted_forms works on a valid formset even if a deleted form would
- have been invalid.
- """
- FavoriteDrinkFormset = formset_factory(form=FavoriteDrinkForm, can_delete=True)
- formset = FavoriteDrinkFormset({
- 'form-0-name': '',
- 'form-0-DELETE': 'on', # no name!
- 'form-TOTAL_FORMS': 1,
- 'form-INITIAL_FORMS': 1,
- 'form-MIN_NUM_FORMS': 0,
- 'form-MAX_NUM_FORMS': 1,
- })
- self.assertTrue(formset.is_valid())
- self.assertEqual(formset._errors, [])
- self.assertEqual(len(formset.deleted_forms), 1)
- def test_formset_with_deletion_custom_widget(self):
- class DeletionAttributeFormSet(BaseFormSet):
- deletion_widget = HiddenInput
- class DeletionMethodFormSet(BaseFormSet):
- def get_deletion_widget(self):
- return HiddenInput(attrs={'class': 'deletion'})
- tests = [
- (DeletionAttributeFormSet, '<input type="hidden" name="form-0-DELETE">'),
- (
- DeletionMethodFormSet,
- '<input class="deletion" type="hidden" name="form-0-DELETE">',
- ),
- ]
- for formset_class, delete_html in tests:
- with self.subTest(formset_class=formset_class.__name__):
- ArticleFormSet = formset_factory(
- ArticleForm,
- formset=formset_class,
- can_delete=True,
- )
- formset = ArticleFormSet(auto_id=False)
- self.assertHTMLEqual(
- '\n'.join([form.as_ul() for form in formset.forms]),
- (
- f'<li>Title: <input type="text" name="form-0-title"></li>'
- f'<li>Pub date: <input type="text" name="form-0-pub_date">'
- f'{delete_html}</li>'
- ),
- )
- def test_formsets_with_ordering(self):
- """
- formset_factory's can_order argument adds an integer field to each
- form. 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 its 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')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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-MIN_NUM_FORMS': '0', # min number of forms
- '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())
- self.assertEqual(
- [form.cleaned_data for form in formset.ordered_forms],
- [
- {'votes': 500, 'ORDER': 0, 'choice': 'The Decemberists'},
- {'votes': 100, 'ORDER': 1, 'choice': 'Calexico'},
- {'votes': 900, 'ORDER': 2, 'choice': 'Fergie'},
- ],
- )
- def test_formsets_with_ordering_custom_widget(self):
- class OrderingAttributeFormSet(BaseFormSet):
- ordering_widget = HiddenInput
- class OrderingMethodFormSet(BaseFormSet):
- def get_ordering_widget(self):
- return HiddenInput(attrs={'class': 'ordering'})
- tests = (
- (OrderingAttributeFormSet, '<input type="hidden" name="form-0-ORDER">'),
- (OrderingMethodFormSet, '<input class="ordering" type="hidden" name="form-0-ORDER">'),
- )
- for formset_class, order_html in tests:
- with self.subTest(formset_class=formset_class.__name__):
- ArticleFormSet = formset_factory(ArticleForm, formset=formset_class, can_order=True)
- formset = ArticleFormSet(auto_id=False)
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- (
- '<li>Title: <input type="text" name="form-0-title"></li>'
- '<li>Pub date: <input type="text" name="form-0-pub_date">'
- '%s</li>' % order_html
- ),
- )
- def test_empty_ordered_fields(self):
- """
- Ordering fields are allowed to be left blank. If they are left blank,
- they'll 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-MIN_NUM_FORMS': '0', # min number of forms
- '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())
- self.assertEqual(
- [form.cleaned_data for form in formset.ordered_forms],
- [
- {'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 works 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-MIN_NUM_FORMS': '0', # min number of forms
- '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())
- self.assertEqual(formset.ordered_forms, [])
- def test_formset_with_ordering_and_deletion(self):
- """FormSets with ordering + deletion."""
- 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')
- self.assertHTMLEqual(
- '\n'.join(form.as_ul() for form in formset.forms),
- """<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-MIN_NUM_FORMS': '0', # min number of forms
- '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())
- self.assertEqual(
- [form.cleaned_data for form in formset.ordered_forms],
- [
- {'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):
- """
- Can get ordered_forms from a valid formset even if a deleted form
- would have been invalid.
- """
- FavoriteDrinkFormset = formset_factory(form=FavoriteDrinkForm, can_delete=True, can_order=True)
- formset = FavoriteDrinkFormset({
- 'form-0-name': '',
- 'form-0-DELETE': 'on', # no name!
- 'form-TOTAL_FORMS': 1,
- 'form-INITIAL_FORMS': 1,
- 'form-MIN_NUM_FORMS': 0,
- 'form-MAX_NUM_FORMS': 1
- })
- self.assertTrue(formset.is_valid())
- self.assertEqual(formset.ordered_forms, [])
- def test_clean_hook(self):
- """
- FormSets have a clean() hook for doing extra validation that isn't tied
- to any form. It follows the same pattern as the clean() hook on Forms.
- """
- # 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-MIN_NUM_FORMS': '0', # min number of forms
- '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.')
- # The valid case still works.
- data['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 with 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()
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<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()
- self.assertEqual(formset.forms, [])
- def test_limited_max_forms_two(self):
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2)
- formset = LimitedFavoriteDrinkFormSet()
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<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>"""
- )
- def test_limiting_extra_lest_than_max_num(self):
- """max_num has no effect when extra is less than max_num."""
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
- formset = LimitedFavoriteDrinkFormSet()
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<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):
- # 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.
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1)
- formset = LimitedFavoriteDrinkFormSet(initial=[{'name': 'Fernet and Coke'}])
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<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, regardless of extra,
- unless initial data is present.
- """
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0)
- formset = LimitedFavoriteDrinkFormSet()
- self.assertEqual(formset.forms, [])
- def test_max_num_zero_with_initial(self):
- # initial trumps max_num
- initial = [
- {'name': 'Fernet and Coke'},
- {'name': 'Bloody Mary'},
- ]
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0)
- formset = LimitedFavoriteDrinkFormSet(initial=initial)
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<tr><th><label for="id_form-0-name">Name:</label></th>
- <td><input id="id_form-0-name" name="form-0-name" type="text" value="Fernet and Coke"></td></tr>
- <tr><th><label for="id_form-1-name">Name:</label></th>
- <td><input id="id_form-1-name" name="form-1-name" type="text" value="Bloody Mary"></td></tr>"""
- )
- def test_more_initial_than_max_num(self):
- """
- More initial forms than max_num results in all initial forms being
- displayed (but 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)
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<tr><th><label for="id_form-0-name">Name:</label></th>
- <td><input id="id_form-0-name" name="form-0-name" type="text" value="Gin Tonic"></td></tr>
- <tr><th><label for="id_form-1-name">Name:</label></th>
- <td><input id="id_form-1-name" name="form-1-name" type="text" value="Bloody Mary"></td></tr>
- <tr><th><label for="id_form-2-name">Name:</label></th>
- <td><input id="id_form-2-name" name="form-2-name" type="text" value="Jack and Coke"></td></tr>"""
- )
- def test_default_absolute_max(self):
- # absolute_max defaults to 2 * DEFAULT_MAX_NUM if max_num is None.
- data = {
- 'form-TOTAL_FORMS': 2001,
- 'form-INITIAL_FORMS': '0',
- 'form-MAX_NUM_FORMS': '0',
- }
- formset = FavoriteDrinksFormSet(data=data)
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(
- formset.non_form_errors(),
- ['Please submit at most 1000 forms.'],
- )
- self.assertEqual(formset.absolute_max, 2000)
- def test_absolute_max(self):
- data = {
- 'form-TOTAL_FORMS': '2001',
- 'form-INITIAL_FORMS': '0',
- 'form-MAX_NUM_FORMS': '0',
- }
- AbsoluteMaxFavoriteDrinksFormSet = formset_factory(
- FavoriteDrinkForm,
- absolute_max=3000,
- )
- formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
- self.assertIs(formset.is_valid(), True)
- self.assertEqual(len(formset.forms), 2001)
- # absolute_max provides a hard limit.
- data['form-TOTAL_FORMS'] = '3001'
- formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(len(formset.forms), 3000)
- self.assertEqual(
- formset.non_form_errors(),
- ['Please submit at most 1000 forms.'],
- )
- def test_absolute_max_with_max_num(self):
- data = {
- 'form-TOTAL_FORMS': '1001',
- 'form-INITIAL_FORMS': '0',
- 'form-MAX_NUM_FORMS': '0',
- }
- LimitedFavoriteDrinksFormSet = formset_factory(
- FavoriteDrinkForm,
- max_num=30,
- absolute_max=1000,
- )
- formset = LimitedFavoriteDrinksFormSet(data=data)
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(len(formset.forms), 1000)
- self.assertEqual(
- formset.non_form_errors(),
- ['Please submit at most 30 forms.'],
- )
- def test_absolute_max_invalid(self):
- msg = "'absolute_max' must be greater or equal to 'max_num'."
- for max_num in [None, 31]:
- with self.subTest(max_num=max_num):
- with self.assertRaisesMessage(ValueError, msg):
- formset_factory(FavoriteDrinkForm, max_num=max_num, absolute_max=30)
- def test_more_initial_form_result_in_one(self):
- """
- One form from initial and extra=3 with max_num=2 results in the one
- initial form and one extra.
- """
- LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2)
- formset = LimitedFavoriteDrinkFormSet(initial=[{'name': 'Gin Tonic'}])
- self.assertHTMLEqual(
- '\n'.join(str(form) for form in formset.forms),
- """<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_management_form_prefix(self):
- """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-MIN_NUM_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_non_form_errors(self):
- data = {
- 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
- '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-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.'])
- self.assertEqual(
- str(formset.non_form_errors()),
- '<ul class="errorlist nonform"><li>'
- 'You may only specify a drink once.</li></ul>',
- )
- def test_formset_iteration(self):
- """Formset instances are iterable."""
- ChoiceFormset = formset_factory(Choice, extra=3)
- formset = ChoiceFormset()
- # An iterated formset yields formset.forms.
- forms = list(formset)
- self.assertEqual(forms, formset.forms)
- self.assertEqual(len(formset), len(forms))
- # A formset may be indexed to retrieve its forms.
- self.assertEqual(formset[0], forms[0])
- with self.assertRaises(IndexError):
- formset[3]
- # Formsets can override the default iteration order
- class BaseReverseFormSet(BaseFormSet):
- def __iter__(self):
- return reversed(self.forms)
- def __getitem__(self, idx):
- return super().__getitem__(len(self) - idx - 1)
- ReverseChoiceFormset = formset_factory(Choice, BaseReverseFormSet, extra=3)
- reverse_formset = ReverseChoiceFormset()
- # __iter__() modifies the 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):
- """A formsets without any forms evaluates as True."""
- ChoiceFormset = formset_factory(Choice, extra=0)
- formset = ChoiceFormset()
- self.assertEqual(len(formset.forms), 0)
- self.assertTrue(formset)
- def test_formset_splitdatetimefield(self):
- """
- Formset works with SplitDateTimeField(initial=datetime.datetime.now).
- """
- class SplitDateTimeForm(Form):
- when = SplitDateTimeField(initial=datetime.datetime.now)
- SplitDateTimeFormSet = formset_factory(SplitDateTimeForm)
- data = {
- 'form-TOTAL_FORMS': '1',
- 'form-INITIAL_FORMS': '0',
- 'form-0-when_0': '1904-06-16',
- 'form-0-when_1': '15:51:33',
- }
- formset = SplitDateTimeFormSet(data)
- self.assertTrue(formset.is_valid())
- def test_formset_error_class(self):
- """Formset's forms use the formset's 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):
- """Formsets call is_valid() on each form."""
- class AnotherChoice(Choice):
- def is_valid(self):
- self.is_valid_called = True
- return super().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-MIN_NUM_FORMS': '0', # min number of forms
- '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 = 2
- ChoiceFormSet = formset_factory(Choice, max_num=1)
- # someone fiddles with the mgmt form data...
- formset = ChoiceFormSet(
- {
- 'choices-TOTAL_FORMS': '4',
- 'choices-INITIAL_FORMS': '0',
- 'choices-MIN_NUM_FORMS': '0', # min number of forms
- '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)
- # and the formset isn't valid
- self.assertFalse(formset.is_valid())
- 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-MIN_NUM_FORMS': '0', # min number of forms
- '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',
- )
- # Four forms are instantiated and no exception is raised
- self.assertEqual(len(formset.forms), 4)
- finally:
- formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
- def test_non_form_errors_run_full_clean(self):
- """
- If non_form_errors() is called without calling is_valid() first,
- it should ensure that full_clean() is called.
- """
- class BaseCustomFormSet(BaseFormSet):
- def clean(self):
- raise ValidationError("This is a non-form error")
- ChoiceFormSet = formset_factory(Choice, formset=BaseCustomFormSet)
- data = {
- 'choices-TOTAL_FORMS': '1',
- 'choices-INITIAL_FORMS': '0',
- }
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertIsInstance(formset.non_form_errors(), ErrorList)
- self.assertEqual(list(formset.non_form_errors()), ['This is a non-form error'])
- def test_validate_max_ignores_forms_marked_for_deletion(self):
- class CheckForm(Form):
- field = IntegerField()
- data = {
- 'check-TOTAL_FORMS': '2',
- 'check-INITIAL_FORMS': '0',
- 'check-MAX_NUM_FORMS': '1',
- 'check-0-field': '200',
- 'check-0-DELETE': '',
- 'check-1-field': '50',
- 'check-1-DELETE': 'on',
- }
- CheckFormSet = formset_factory(CheckForm, max_num=1, validate_max=True, can_delete=True)
- formset = CheckFormSet(data, prefix='check')
- self.assertTrue(formset.is_valid())
- def test_formset_total_error_count(self):
- """A valid formset should have 0 total errors."""
- data = [ # formset_data, expected error count
- ([('Calexico', '100')], 0),
- ([('Calexico', '')], 1),
- ([('', 'invalid')], 2),
- ([('Calexico', '100'), ('Calexico', '')], 1),
- ([('Calexico', ''), ('Calexico', '')], 2),
- ]
- for formset_data, expected_error_count in data:
- formset = self.make_choiceformset(formset_data)
- self.assertEqual(formset.total_error_count(), expected_error_count)
- def test_formset_total_error_count_with_non_form_errors(self):
- data = {
- 'choices-TOTAL_FORMS': '2', # the number of forms rendered
- 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '2', # 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, max_num=1, validate_max=True)
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertEqual(formset.total_error_count(), 1)
- data['choices-1-votes'] = ''
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertEqual(formset.total_error_count(), 2)
- def test_html_safe(self):
- formset = self.make_choiceformset()
- self.assertTrue(hasattr(formset, '__html__'))
- self.assertEqual(str(formset), formset.__html__())
- def test_can_delete_extra_formset_forms(self):
- ChoiceFormFormset = formset_factory(form=Choice, can_delete=True, extra=2)
- formset = ChoiceFormFormset()
- self.assertEqual(len(formset), 2)
- self.assertIn('DELETE', formset.forms[0].fields)
- self.assertIn('DELETE', formset.forms[1].fields)
- def test_disable_delete_extra_formset_forms(self):
- ChoiceFormFormset = formset_factory(
- form=Choice,
- can_delete=True,
- can_delete_extra=False,
- extra=2,
- )
- formset = ChoiceFormFormset()
- self.assertEqual(len(formset), 2)
- self.assertNotIn('DELETE', formset.forms[0].fields)
- self.assertNotIn('DELETE', formset.forms[1].fields)
- formset = ChoiceFormFormset(initial=[{'choice': 'Zero', 'votes': '1'}])
- self.assertEqual(len(formset), 3)
- self.assertIn('DELETE', formset.forms[0].fields)
- self.assertNotIn('DELETE', formset.forms[1].fields)
- self.assertNotIn('DELETE', formset.forms[2].fields)
- formset = ChoiceFormFormset(data={
- 'form-0-choice': 'Zero',
- 'form-0-votes': '0',
- 'form-0-DELETE': 'on',
- 'form-1-choice': 'One',
- 'form-1-votes': '1',
- 'form-2-choice': '',
- 'form-2-votes': '',
- 'form-TOTAL_FORMS': '3',
- 'form-INITIAL_FORMS': '1',
- }, initial=[{'choice': 'Zero', 'votes': '1'}])
- self.assertEqual(formset.cleaned_data, [
- {'choice': 'Zero', 'votes': 0, 'DELETE': True},
- {'choice': 'One', 'votes': 1},
- {},
- ])
- self.assertIs(formset._should_delete_form(formset.forms[0]), True)
- self.assertIs(formset._should_delete_form(formset.forms[1]), False)
- self.assertIs(formset._should_delete_form(formset.forms[2]), False)
- class FormsetAsTagTests(SimpleTestCase):
- def setUp(self):
- data = {
- 'choices-TOTAL_FORMS': '1',
- 'choices-INITIAL_FORMS': '0',
- 'choices-MIN_NUM_FORMS': '0',
- 'choices-MAX_NUM_FORMS': '0',
- 'choices-0-choice': 'Calexico',
- 'choices-0-votes': '100',
- }
- self.formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.management_form_html = (
- '<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">'
- )
- def test_as_table(self):
- self.assertHTMLEqual(
- self.formset.as_table(),
- self.management_form_html + (
- '<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):
- self.assertHTMLEqual(
- self.formset.as_p(),
- self.management_form_html + (
- '<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):
- self.assertHTMLEqual(
- self.formset.as_ul(),
- self.management_form_html + (
- '<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>'
- '<li>Votes: <input type="number" name="choices-0-votes" value="100"></li>'
- )
- )
- class ArticleForm(Form):
- title = CharField()
- pub_date = DateField()
- ArticleFormSet = formset_factory(ArticleForm)
- class TestIsBoundBehavior(SimpleTestCase):
- def test_no_data_error(self):
- formset = ArticleFormSet({})
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(
- formset.non_form_errors(),
- [
- 'ManagementForm data is missing or has been tampered with. '
- 'Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. '
- 'You may need to file a bug report if the issue persists.',
- ],
- )
- self.assertEqual(formset.errors, [])
- # Can still render the formset.
- self.assertEqual(
- str(formset),
- '<tr><td colspan="2">'
- '<ul class="errorlist nonfield">'
- '<li>(Hidden field TOTAL_FORMS) This field is required.</li>'
- '<li>(Hidden field INITIAL_FORMS) This field is required.</li>'
- '</ul>'
- '<input type="hidden" name="form-TOTAL_FORMS" id="id_form-TOTAL_FORMS">'
- '<input type="hidden" name="form-INITIAL_FORMS" id="id_form-INITIAL_FORMS">'
- '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
- '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
- '</td></tr>\n'
- )
- def test_management_form_invalid_data(self):
- data = {
- 'form-TOTAL_FORMS': 'two',
- 'form-INITIAL_FORMS': 'one',
- }
- formset = ArticleFormSet(data)
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(
- formset.non_form_errors(),
- [
- 'ManagementForm data is missing or has been tampered with. '
- 'Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. '
- 'You may need to file a bug report if the issue persists.',
- ],
- )
- self.assertEqual(formset.errors, [])
- # Can still render the formset.
- self.assertEqual(
- str(formset),
- '<tr><td colspan="2">'
- '<ul class="errorlist nonfield">'
- '<li>(Hidden field TOTAL_FORMS) Enter a whole number.</li>'
- '<li>(Hidden field INITIAL_FORMS) Enter a whole number.</li>'
- '</ul>'
- '<input type="hidden" name="form-TOTAL_FORMS" value="two" id="id_form-TOTAL_FORMS">'
- '<input type="hidden" name="form-INITIAL_FORMS" value="one" id="id_form-INITIAL_FORMS">'
- '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
- '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
- '</td></tr>\n',
- )
- def test_customize_management_form_error(self):
- formset = ArticleFormSet({}, error_messages={'missing_management_form': 'customized'})
- self.assertIs(formset.is_valid(), False)
- self.assertEqual(formset.non_form_errors(), ['customized'])
- self.assertEqual(formset.errors, [])
- 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_caught_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 = [
- unbound_formset.empty_form,
- 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(SimpleTestCase):
- def test_empty_formset_is_valid(self):
- """An empty formset still calls clean()"""
- class EmptyFsetWontValidate(BaseFormSet):
- def clean(self):
- raise ValidationError('Clean method called')
- 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):
- """Media is available on empty formset."""
- 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):
- """is_multipart() works with an empty formset."""
- class FileForm(Form):
- file = FileField()
- self.assertTrue(formset_factory(FileForm, extra=0)().is_multipart())
- class AllValidTests(SimpleTestCase):
- def test_valid(self):
- data = {
- 'choices-TOTAL_FORMS': '2',
- 'choices-INITIAL_FORMS': '0',
- 'choices-MIN_NUM_FORMS': '0',
- 'choices-0-choice': 'Zero',
- 'choices-0-votes': '0',
- 'choices-1-choice': 'One',
- 'choices-1-votes': '1',
- }
- ChoiceFormSet = formset_factory(Choice)
- formset1 = ChoiceFormSet(data, auto_id=False, prefix='choices')
- formset2 = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertIs(all_valid((formset1, formset2)), True)
- expected_errors = [{}, {}]
- self.assertEqual(formset1._errors, expected_errors)
- self.assertEqual(formset2._errors, expected_errors)
- def test_invalid(self):
- """all_valid() validates all forms, even when some are invalid."""
- data = {
- 'choices-TOTAL_FORMS': '2',
- 'choices-INITIAL_FORMS': '0',
- 'choices-MIN_NUM_FORMS': '0',
- 'choices-0-choice': 'Zero',
- 'choices-0-votes': '',
- 'choices-1-choice': 'One',
- 'choices-1-votes': '',
- }
- ChoiceFormSet = formset_factory(Choice)
- formset1 = ChoiceFormSet(data, auto_id=False, prefix='choices')
- formset2 = ChoiceFormSet(data, auto_id=False, prefix='choices')
- self.assertIs(all_valid((formset1, formset2)), False)
- expected_errors = [{'votes': ['This field is required.']}, {'votes': ['This field is required.']}]
- self.assertEqual(formset1._errors, expected_errors)
- self.assertEqual(formset2._errors, expected_errors)
|