test_formsets.py 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530
  1. import datetime
  2. from collections import Counter
  3. from unittest import mock
  4. from django.core.exceptions import ValidationError
  5. from django.forms import (
  6. BaseForm, CharField, DateField, FileField, Form, IntegerField,
  7. SplitDateTimeField, formsets,
  8. )
  9. from django.forms.formsets import BaseFormSet, all_valid, formset_factory
  10. from django.forms.utils import ErrorList
  11. from django.forms.widgets import HiddenInput
  12. from django.test import SimpleTestCase
  13. class Choice(Form):
  14. choice = CharField()
  15. votes = IntegerField()
  16. ChoiceFormSet = formset_factory(Choice)
  17. class FavoriteDrinkForm(Form):
  18. name = CharField()
  19. class BaseFavoriteDrinksFormSet(BaseFormSet):
  20. def clean(self):
  21. seen_drinks = []
  22. for drink in self.cleaned_data:
  23. if drink['name'] in seen_drinks:
  24. raise ValidationError('You may only specify a drink once.')
  25. seen_drinks.append(drink['name'])
  26. # A FormSet that takes a list of favorite drinks and raises an error if
  27. # there are any duplicates.
  28. FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, formset=BaseFavoriteDrinksFormSet, extra=3)
  29. class CustomKwargForm(Form):
  30. def __init__(self, *args, custom_kwarg, **kwargs):
  31. self.custom_kwarg = custom_kwarg
  32. super().__init__(*args, **kwargs)
  33. class FormsFormsetTestCase(SimpleTestCase):
  34. def make_choiceformset(
  35. self, formset_data=None, formset_class=ChoiceFormSet,
  36. total_forms=None, initial_forms=0, max_num_forms=0, min_num_forms=0, **kwargs):
  37. """
  38. Make a ChoiceFormset from the given formset_data.
  39. The data should be given as a list of (choice, votes) tuples.
  40. """
  41. kwargs.setdefault('prefix', 'choices')
  42. kwargs.setdefault('auto_id', False)
  43. if formset_data is None:
  44. return formset_class(**kwargs)
  45. if total_forms is None:
  46. total_forms = len(formset_data)
  47. def prefixed(*args):
  48. args = (kwargs['prefix'],) + args
  49. return '-'.join(args)
  50. data = {
  51. prefixed('TOTAL_FORMS'): str(total_forms),
  52. prefixed('INITIAL_FORMS'): str(initial_forms),
  53. prefixed('MAX_NUM_FORMS'): str(max_num_forms),
  54. prefixed('MIN_NUM_FORMS'): str(min_num_forms),
  55. }
  56. for i, (choice, votes) in enumerate(formset_data):
  57. data[prefixed(str(i), 'choice')] = choice
  58. data[prefixed(str(i), 'votes')] = votes
  59. return formset_class(data, **kwargs)
  60. def test_basic_formset(self):
  61. """
  62. A FormSet constructor takes the same arguments as Form. Create a
  63. FormSet for adding data. By default, it displays 1 blank form.
  64. """
  65. formset = self.make_choiceformset()
  66. self.assertHTMLEqual(
  67. str(formset),
  68. """<input type="hidden" name="choices-TOTAL_FORMS" value="1">
  69. <input type="hidden" name="choices-INITIAL_FORMS" value="0">
  70. <input type="hidden" name="choices-MIN_NUM_FORMS" value="0">
  71. <input type="hidden" name="choices-MAX_NUM_FORMS" value="1000">
  72. <tr><th>Choice:</th><td><input type="text" name="choices-0-choice"></td></tr>
  73. <tr><th>Votes:</th><td><input type="number" name="choices-0-votes"></td></tr>"""
  74. )
  75. # FormSet are treated similarly to Forms. FormSet has an is_valid()
  76. # method, and a cleaned_data or errors attribute depending on whether
  77. # all the forms passed validation. However, unlike a Form, cleaned_data
  78. # and errors will be a list of dicts rather than a single dict.
  79. formset = self.make_choiceformset([('Calexico', '100')])
  80. self.assertTrue(formset.is_valid())
  81. self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])
  82. # If a FormSet wasn't passed any data, is_valid() and has_changed()
  83. # return False.
  84. formset = self.make_choiceformset()
  85. self.assertFalse(formset.is_valid())
  86. self.assertFalse(formset.has_changed())
  87. def test_form_kwargs_formset(self):
  88. """
  89. Custom kwargs set on the formset instance are passed to the
  90. underlying forms.
  91. """
  92. FormSet = formset_factory(CustomKwargForm, extra=2)
  93. formset = FormSet(form_kwargs={'custom_kwarg': 1})
  94. for form in formset:
  95. self.assertTrue(hasattr(form, 'custom_kwarg'))
  96. self.assertEqual(form.custom_kwarg, 1)
  97. def test_form_kwargs_formset_dynamic(self):
  98. """Form kwargs can be passed dynamically in a formset."""
  99. class DynamicBaseFormSet(BaseFormSet):
  100. def get_form_kwargs(self, index):
  101. return {'custom_kwarg': index}
  102. DynamicFormSet = formset_factory(CustomKwargForm, formset=DynamicBaseFormSet, extra=2)
  103. formset = DynamicFormSet(form_kwargs={'custom_kwarg': 'ignored'})
  104. for i, form in enumerate(formset):
  105. self.assertTrue(hasattr(form, 'custom_kwarg'))
  106. self.assertEqual(form.custom_kwarg, i)
  107. def test_form_kwargs_empty_form(self):
  108. FormSet = formset_factory(CustomKwargForm)
  109. formset = FormSet(form_kwargs={'custom_kwarg': 1})
  110. self.assertTrue(hasattr(formset.empty_form, 'custom_kwarg'))
  111. self.assertEqual(formset.empty_form.custom_kwarg, 1)
  112. def test_formset_validation(self):
  113. # FormSet instances can also have an error attribute if validation failed for
  114. # any of the forms.
  115. formset = self.make_choiceformset([('Calexico', '')])
  116. self.assertFalse(formset.is_valid())
  117. self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])
  118. def test_formset_validation_count(self):
  119. """
  120. A formset's ManagementForm is validated once per FormSet.is_valid()
  121. call and each form of the formset is cleaned once.
  122. """
  123. def make_method_counter(func):
  124. """Add a counter to func for the number of times it's called."""
  125. counter = Counter()
  126. counter.call_count = 0
  127. def mocked_func(*args, **kwargs):
  128. counter.call_count += 1
  129. return func(*args, **kwargs)
  130. return mocked_func, counter
  131. mocked_is_valid, is_valid_counter = make_method_counter(formsets.ManagementForm.is_valid)
  132. mocked_full_clean, full_clean_counter = make_method_counter(BaseForm.full_clean)
  133. formset = self.make_choiceformset([('Calexico', '100'), ('Any1', '42'), ('Any2', '101')])
  134. with mock.patch('django.forms.formsets.ManagementForm.is_valid', mocked_is_valid), \
  135. mock.patch('django.forms.forms.BaseForm.full_clean', mocked_full_clean):
  136. self.assertTrue(formset.is_valid())
  137. self.assertEqual(is_valid_counter.call_count, 1)
  138. self.assertEqual(full_clean_counter.call_count, 4)
  139. def test_formset_has_changed(self):
  140. """
  141. FormSet.has_changed() is True if any data is passed to its forms, even
  142. if the formset didn't validate.
  143. """
  144. blank_formset = self.make_choiceformset([('', '')])
  145. self.assertFalse(blank_formset.has_changed())
  146. # invalid formset
  147. invalid_formset = self.make_choiceformset([('Calexico', '')])
  148. self.assertFalse(invalid_formset.is_valid())
  149. self.assertTrue(invalid_formset.has_changed())
  150. # valid formset
  151. valid_formset = self.make_choiceformset([('Calexico', '100')])
  152. self.assertTrue(valid_formset.is_valid())
  153. self.assertTrue(valid_formset.has_changed())
  154. def test_formset_initial_data(self):
  155. """
  156. A FormSet can be prefilled with existing data by providing a list of
  157. dicts to the `initial` argument. By default, an extra blank form is
  158. included.
  159. """
  160. formset = self.make_choiceformset(initial=[{'choice': 'Calexico', 'votes': 100}])
  161. self.assertHTMLEqual(
  162. '\n'.join(form.as_ul() for form in formset.forms),
  163. """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
  164. <li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
  165. <li>Choice: <input type="text" name="choices-1-choice"></li>
  166. <li>Votes: <input type="number" name="choices-1-votes"></li>"""
  167. )
  168. def test_blank_form_unfilled(self):
  169. """A form that's displayed as blank may be submitted as blank."""
  170. formset = self.make_choiceformset([('Calexico', '100'), ('', '')], initial_forms=1)
  171. self.assertTrue(formset.is_valid())
  172. self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}])
  173. def test_second_form_partially_filled(self):
  174. """
  175. If at least one field is filled out on a blank form, it will be
  176. validated.
  177. """
  178. formset = self.make_choiceformset([('Calexico', '100'), ('The Decemberists', '')], initial_forms=1)
  179. self.assertFalse(formset.is_valid())
  180. self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])
  181. def test_delete_prefilled_data(self):
  182. """
  183. Deleting prefilled data is an error. Removing data from form fields
  184. isn't the proper way to delete it.
  185. """
  186. formset = self.make_choiceformset([('', ''), ('', '')], initial_forms=1)
  187. self.assertFalse(formset.is_valid())
  188. self.assertEqual(
  189. formset.errors,
  190. [{'votes': ['This field is required.'], 'choice': ['This field is required.']}, {}]
  191. )
  192. def test_displaying_more_than_one_blank_form(self):
  193. """
  194. More than 1 empty form can be displayed using formset_factory's
  195. `extra` argument.
  196. """
  197. ChoiceFormSet = formset_factory(Choice, extra=3)
  198. formset = ChoiceFormSet(auto_id=False, prefix='choices')
  199. self.assertHTMLEqual(
  200. '\n'.join(form.as_ul() for form in formset.forms),
  201. """<li>Choice: <input type="text" name="choices-0-choice"></li>
  202. <li>Votes: <input type="number" name="choices-0-votes"></li>
  203. <li>Choice: <input type="text" name="choices-1-choice"></li>
  204. <li>Votes: <input type="number" name="choices-1-votes"></li>
  205. <li>Choice: <input type="text" name="choices-2-choice"></li>
  206. <li>Votes: <input type="number" name="choices-2-votes"></li>"""
  207. )
  208. # Since every form was displayed as blank, they are also accepted as
  209. # blank. This may seem a little strange, but min_num is used to require
  210. # a minimum number of forms to be completed.
  211. data = {
  212. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  213. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  214. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  215. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  216. 'choices-0-choice': '',
  217. 'choices-0-votes': '',
  218. 'choices-1-choice': '',
  219. 'choices-1-votes': '',
  220. 'choices-2-choice': '',
  221. 'choices-2-votes': '',
  222. }
  223. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  224. self.assertTrue(formset.is_valid())
  225. self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}])
  226. def test_min_num_displaying_more_than_one_blank_form(self):
  227. """
  228. More than 1 empty form can also be displayed using formset_factory's
  229. min_num argument. It will (essentially) increment the extra argument.
  230. """
  231. ChoiceFormSet = formset_factory(Choice, extra=1, min_num=1)
  232. formset = ChoiceFormSet(auto_id=False, prefix='choices')
  233. # Min_num forms are required; extra forms can be empty.
  234. self.assertFalse(formset.forms[0].empty_permitted)
  235. self.assertTrue(formset.forms[1].empty_permitted)
  236. self.assertHTMLEqual(
  237. '\n'.join(form.as_ul() for form in formset.forms),
  238. """<li>Choice: <input type="text" name="choices-0-choice"></li>
  239. <li>Votes: <input type="number" name="choices-0-votes"></li>
  240. <li>Choice: <input type="text" name="choices-1-choice"></li>
  241. <li>Votes: <input type="number" name="choices-1-votes"></li>"""
  242. )
  243. def test_min_num_displaying_more_than_one_blank_form_with_zero_extra(self):
  244. """More than 1 empty form can be displayed using min_num."""
  245. ChoiceFormSet = formset_factory(Choice, extra=0, min_num=3)
  246. formset = ChoiceFormSet(auto_id=False, prefix='choices')
  247. self.assertHTMLEqual(
  248. '\n'.join(form.as_ul() for form in formset.forms),
  249. """<li>Choice: <input type="text" name="choices-0-choice"></li>
  250. <li>Votes: <input type="number" name="choices-0-votes"></li>
  251. <li>Choice: <input type="text" name="choices-1-choice"></li>
  252. <li>Votes: <input type="number" name="choices-1-votes"></li>
  253. <li>Choice: <input type="text" name="choices-2-choice"></li>
  254. <li>Votes: <input type="number" name="choices-2-votes"></li>"""
  255. )
  256. def test_single_form_completed(self):
  257. """Just one form may be completed."""
  258. data = {
  259. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  260. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  261. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  262. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  263. 'choices-0-choice': 'Calexico',
  264. 'choices-0-votes': '100',
  265. 'choices-1-choice': '',
  266. 'choices-1-votes': '',
  267. 'choices-2-choice': '',
  268. 'choices-2-votes': '',
  269. }
  270. ChoiceFormSet = formset_factory(Choice, extra=3)
  271. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  272. self.assertTrue(formset.is_valid())
  273. self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}, {}])
  274. def test_formset_validate_max_flag(self):
  275. """
  276. If validate_max is set and max_num is less than TOTAL_FORMS in the
  277. data, a ValidationError is raised. MAX_NUM_FORMS in the data is
  278. irrelevant here (it's output as a hint for the client but its value
  279. in the returned data is not checked).
  280. """
  281. data = {
  282. 'choices-TOTAL_FORMS': '2', # the number of forms rendered
  283. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  284. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  285. 'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored
  286. 'choices-0-choice': 'Zero',
  287. 'choices-0-votes': '0',
  288. 'choices-1-choice': 'One',
  289. 'choices-1-votes': '1',
  290. }
  291. ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True)
  292. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  293. self.assertFalse(formset.is_valid())
  294. self.assertEqual(formset.non_form_errors(), ['Please submit at most 1 form.'])
  295. self.assertEqual(
  296. str(formset.non_form_errors()),
  297. '<ul class="errorlist nonform"><li>Please submit at most 1 form.</li></ul>',
  298. )
  299. def test_formset_validate_min_flag(self):
  300. """
  301. If validate_min is set and min_num is more than TOTAL_FORMS in the
  302. data, a ValidationError is raised. MIN_NUM_FORMS in the data is
  303. irrelevant here (it's output as a hint for the client but its value
  304. in the returned data is not checked).
  305. """
  306. data = {
  307. 'choices-TOTAL_FORMS': '2', # the number of forms rendered
  308. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  309. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  310. 'choices-MAX_NUM_FORMS': '0', # max number of forms - should be ignored
  311. 'choices-0-choice': 'Zero',
  312. 'choices-0-votes': '0',
  313. 'choices-1-choice': 'One',
  314. 'choices-1-votes': '1',
  315. }
  316. ChoiceFormSet = formset_factory(Choice, extra=1, min_num=3, validate_min=True)
  317. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  318. self.assertFalse(formset.is_valid())
  319. self.assertEqual(formset.non_form_errors(), ['Please submit at least 3 forms.'])
  320. self.assertEqual(
  321. str(formset.non_form_errors()),
  322. '<ul class="errorlist nonform"><li>'
  323. 'Please submit at least 3 forms.</li></ul>',
  324. )
  325. def test_formset_validate_min_unchanged_forms(self):
  326. """
  327. min_num validation doesn't consider unchanged forms with initial data
  328. as "empty".
  329. """
  330. initial = [
  331. {'choice': 'Zero', 'votes': 0},
  332. {'choice': 'One', 'votes': 0},
  333. ]
  334. data = {
  335. 'choices-TOTAL_FORMS': '2',
  336. 'choices-INITIAL_FORMS': '2',
  337. 'choices-MIN_NUM_FORMS': '0',
  338. 'choices-MAX_NUM_FORMS': '2',
  339. 'choices-0-choice': 'Zero',
  340. 'choices-0-votes': '0',
  341. 'choices-1-choice': 'One',
  342. 'choices-1-votes': '1', # changed from initial
  343. }
  344. ChoiceFormSet = formset_factory(Choice, min_num=2, validate_min=True)
  345. formset = ChoiceFormSet(data, auto_id=False, prefix='choices', initial=initial)
  346. self.assertFalse(formset.forms[0].has_changed())
  347. self.assertTrue(formset.forms[1].has_changed())
  348. self.assertTrue(formset.is_valid())
  349. def test_formset_validate_min_excludes_empty_forms(self):
  350. data = {
  351. 'choices-TOTAL_FORMS': '2',
  352. 'choices-INITIAL_FORMS': '0',
  353. }
  354. ChoiceFormSet = formset_factory(Choice, extra=2, min_num=1, validate_min=True, can_delete=True)
  355. formset = ChoiceFormSet(data, prefix='choices')
  356. self.assertFalse(formset.has_changed())
  357. self.assertFalse(formset.is_valid())
  358. self.assertEqual(formset.non_form_errors(), ['Please submit at least 1 form.'])
  359. def test_second_form_partially_filled_2(self):
  360. """A partially completed form is invalid."""
  361. data = {
  362. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  363. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  364. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  365. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  366. 'choices-0-choice': 'Calexico',
  367. 'choices-0-votes': '100',
  368. 'choices-1-choice': 'The Decemberists',
  369. 'choices-1-votes': '', # missing value
  370. 'choices-2-choice': '',
  371. 'choices-2-votes': '',
  372. }
  373. ChoiceFormSet = formset_factory(Choice, extra=3)
  374. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  375. self.assertFalse(formset.is_valid())
  376. self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}, {}])
  377. def test_more_initial_data(self):
  378. """
  379. The extra argument works when the formset is pre-filled with initial
  380. data.
  381. """
  382. initial = [{'choice': 'Calexico', 'votes': 100}]
  383. ChoiceFormSet = formset_factory(Choice, extra=3)
  384. formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
  385. self.assertHTMLEqual(
  386. '\n'.join(form.as_ul() for form in formset.forms),
  387. """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
  388. <li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
  389. <li>Choice: <input type="text" name="choices-1-choice"></li>
  390. <li>Votes: <input type="number" name="choices-1-votes"></li>
  391. <li>Choice: <input type="text" name="choices-2-choice"></li>
  392. <li>Votes: <input type="number" name="choices-2-votes"></li>
  393. <li>Choice: <input type="text" name="choices-3-choice"></li>
  394. <li>Votes: <input type="number" name="choices-3-votes"></li>"""
  395. )
  396. # Retrieving an empty form works. Tt shows up in the form list.
  397. self.assertTrue(formset.empty_form.empty_permitted)
  398. self.assertHTMLEqual(
  399. formset.empty_form.as_ul(),
  400. """<li>Choice: <input type="text" name="choices-__prefix__-choice"></li>
  401. <li>Votes: <input type="number" name="choices-__prefix__-votes"></li>"""
  402. )
  403. def test_formset_with_deletion(self):
  404. """
  405. formset_factory's can_delete argument adds a boolean "delete" field to
  406. each form. When that boolean field is True, the form will be in
  407. formset.deleted_forms.
  408. """
  409. ChoiceFormSet = formset_factory(Choice, can_delete=True)
  410. initial = [{'choice': 'Calexico', 'votes': 100}, {'choice': 'Fergie', 'votes': 900}]
  411. formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
  412. self.assertHTMLEqual(
  413. '\n'.join(form.as_ul() for form in formset.forms),
  414. """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
  415. <li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
  416. <li>Delete: <input type="checkbox" name="choices-0-DELETE"></li>
  417. <li>Choice: <input type="text" name="choices-1-choice" value="Fergie"></li>
  418. <li>Votes: <input type="number" name="choices-1-votes" value="900"></li>
  419. <li>Delete: <input type="checkbox" name="choices-1-DELETE"></li>
  420. <li>Choice: <input type="text" name="choices-2-choice"></li>
  421. <li>Votes: <input type="number" name="choices-2-votes"></li>
  422. <li>Delete: <input type="checkbox" name="choices-2-DELETE"></li>"""
  423. )
  424. # To delete something, set that form's special delete field to 'on'.
  425. # Let's go ahead and delete Fergie.
  426. data = {
  427. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  428. 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
  429. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  430. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  431. 'choices-0-choice': 'Calexico',
  432. 'choices-0-votes': '100',
  433. 'choices-0-DELETE': '',
  434. 'choices-1-choice': 'Fergie',
  435. 'choices-1-votes': '900',
  436. 'choices-1-DELETE': 'on',
  437. 'choices-2-choice': '',
  438. 'choices-2-votes': '',
  439. 'choices-2-DELETE': '',
  440. }
  441. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  442. self.assertTrue(formset.is_valid())
  443. self.assertEqual(
  444. [form.cleaned_data for form in formset.forms],
  445. [
  446. {'votes': 100, 'DELETE': False, 'choice': 'Calexico'},
  447. {'votes': 900, 'DELETE': True, 'choice': 'Fergie'},
  448. {},
  449. ]
  450. )
  451. self.assertEqual(
  452. [form.cleaned_data for form in formset.deleted_forms],
  453. [{'votes': 900, 'DELETE': True, 'choice': 'Fergie'}]
  454. )
  455. def test_formset_with_deletion_remove_deletion_flag(self):
  456. """
  457. If a form is filled with something and can_delete is also checked, that
  458. form's errors shouldn't make the entire formset invalid since it's
  459. going to be deleted.
  460. """
  461. class CheckForm(Form):
  462. field = IntegerField(min_value=100)
  463. data = {
  464. 'check-TOTAL_FORMS': '3', # the number of forms rendered
  465. 'check-INITIAL_FORMS': '2', # the number of forms with initial data
  466. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  467. 'check-MAX_NUM_FORMS': '0', # max number of forms
  468. 'check-0-field': '200',
  469. 'check-0-DELETE': '',
  470. 'check-1-field': '50',
  471. 'check-1-DELETE': 'on',
  472. 'check-2-field': '',
  473. 'check-2-DELETE': '',
  474. }
  475. CheckFormSet = formset_factory(CheckForm, can_delete=True)
  476. formset = CheckFormSet(data, prefix='check')
  477. self.assertTrue(formset.is_valid())
  478. # If the deletion flag is removed, validation is enabled.
  479. data['check-1-DELETE'] = ''
  480. formset = CheckFormSet(data, prefix='check')
  481. self.assertFalse(formset.is_valid())
  482. def test_formset_with_deletion_invalid_deleted_form(self):
  483. """
  484. deleted_forms works on a valid formset even if a deleted form would
  485. have been invalid.
  486. """
  487. FavoriteDrinkFormset = formset_factory(form=FavoriteDrinkForm, can_delete=True)
  488. formset = FavoriteDrinkFormset({
  489. 'form-0-name': '',
  490. 'form-0-DELETE': 'on', # no name!
  491. 'form-TOTAL_FORMS': 1,
  492. 'form-INITIAL_FORMS': 1,
  493. 'form-MIN_NUM_FORMS': 0,
  494. 'form-MAX_NUM_FORMS': 1,
  495. })
  496. self.assertTrue(formset.is_valid())
  497. self.assertEqual(formset._errors, [])
  498. self.assertEqual(len(formset.deleted_forms), 1)
  499. def test_formset_with_deletion_custom_widget(self):
  500. class DeletionAttributeFormSet(BaseFormSet):
  501. deletion_widget = HiddenInput
  502. class DeletionMethodFormSet(BaseFormSet):
  503. def get_deletion_widget(self):
  504. return HiddenInput(attrs={'class': 'deletion'})
  505. tests = [
  506. (DeletionAttributeFormSet, '<input type="hidden" name="form-0-DELETE">'),
  507. (
  508. DeletionMethodFormSet,
  509. '<input class="deletion" type="hidden" name="form-0-DELETE">',
  510. ),
  511. ]
  512. for formset_class, delete_html in tests:
  513. with self.subTest(formset_class=formset_class.__name__):
  514. ArticleFormSet = formset_factory(
  515. ArticleForm,
  516. formset=formset_class,
  517. can_delete=True,
  518. )
  519. formset = ArticleFormSet(auto_id=False)
  520. self.assertHTMLEqual(
  521. '\n'.join([form.as_ul() for form in formset.forms]),
  522. (
  523. f'<li>Title: <input type="text" name="form-0-title"></li>'
  524. f'<li>Pub date: <input type="text" name="form-0-pub_date">'
  525. f'{delete_html}</li>'
  526. ),
  527. )
  528. def test_formsets_with_ordering(self):
  529. """
  530. formset_factory's can_order argument adds an integer field to each
  531. form. When form validation succeeds, [form.cleaned_data for form in formset.forms]
  532. will have the data in the correct order specified by the ordering
  533. fields. If a number is duplicated in the set of ordering fields, for
  534. instance form 0 and form 3 are both marked as 1, then the form index
  535. used as a secondary ordering criteria. In order to put something at the
  536. front of the list, you'd need to set its order to 0.
  537. """
  538. ChoiceFormSet = formset_factory(Choice, can_order=True)
  539. initial = [{'choice': 'Calexico', 'votes': 100}, {'choice': 'Fergie', 'votes': 900}]
  540. formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
  541. self.assertHTMLEqual(
  542. '\n'.join(form.as_ul() for form in formset.forms),
  543. """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
  544. <li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
  545. <li>Order: <input type="number" name="choices-0-ORDER" value="1"></li>
  546. <li>Choice: <input type="text" name="choices-1-choice" value="Fergie"></li>
  547. <li>Votes: <input type="number" name="choices-1-votes" value="900"></li>
  548. <li>Order: <input type="number" name="choices-1-ORDER" value="2"></li>
  549. <li>Choice: <input type="text" name="choices-2-choice"></li>
  550. <li>Votes: <input type="number" name="choices-2-votes"></li>
  551. <li>Order: <input type="number" name="choices-2-ORDER"></li>"""
  552. )
  553. data = {
  554. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  555. 'choices-INITIAL_FORMS': '2', # the number of forms with initial data
  556. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  557. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  558. 'choices-0-choice': 'Calexico',
  559. 'choices-0-votes': '100',
  560. 'choices-0-ORDER': '1',
  561. 'choices-1-choice': 'Fergie',
  562. 'choices-1-votes': '900',
  563. 'choices-1-ORDER': '2',
  564. 'choices-2-choice': 'The Decemberists',
  565. 'choices-2-votes': '500',
  566. 'choices-2-ORDER': '0',
  567. }
  568. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  569. self.assertTrue(formset.is_valid())
  570. self.assertEqual(
  571. [form.cleaned_data for form in formset.ordered_forms],
  572. [
  573. {'votes': 500, 'ORDER': 0, 'choice': 'The Decemberists'},
  574. {'votes': 100, 'ORDER': 1, 'choice': 'Calexico'},
  575. {'votes': 900, 'ORDER': 2, 'choice': 'Fergie'},
  576. ],
  577. )
  578. def test_formsets_with_ordering_custom_widget(self):
  579. class OrderingAttributeFormSet(BaseFormSet):
  580. ordering_widget = HiddenInput
  581. class OrderingMethodFormSet(BaseFormSet):
  582. def get_ordering_widget(self):
  583. return HiddenInput(attrs={'class': 'ordering'})
  584. tests = (
  585. (OrderingAttributeFormSet, '<input type="hidden" name="form-0-ORDER">'),
  586. (OrderingMethodFormSet, '<input class="ordering" type="hidden" name="form-0-ORDER">'),
  587. )
  588. for formset_class, order_html in tests:
  589. with self.subTest(formset_class=formset_class.__name__):
  590. ArticleFormSet = formset_factory(ArticleForm, formset=formset_class, can_order=True)
  591. formset = ArticleFormSet(auto_id=False)
  592. self.assertHTMLEqual(
  593. '\n'.join(form.as_ul() for form in formset.forms),
  594. (
  595. '<li>Title: <input type="text" name="form-0-title"></li>'
  596. '<li>Pub date: <input type="text" name="form-0-pub_date">'
  597. '%s</li>' % order_html
  598. ),
  599. )
  600. def test_empty_ordered_fields(self):
  601. """
  602. Ordering fields are allowed to be left blank. If they are left blank,
  603. they'll be sorted below everything else.
  604. """
  605. data = {
  606. 'choices-TOTAL_FORMS': '4', # the number of forms rendered
  607. 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
  608. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  609. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  610. 'choices-0-choice': 'Calexico',
  611. 'choices-0-votes': '100',
  612. 'choices-0-ORDER': '1',
  613. 'choices-1-choice': 'Fergie',
  614. 'choices-1-votes': '900',
  615. 'choices-1-ORDER': '2',
  616. 'choices-2-choice': 'The Decemberists',
  617. 'choices-2-votes': '500',
  618. 'choices-2-ORDER': '',
  619. 'choices-3-choice': 'Basia Bulat',
  620. 'choices-3-votes': '50',
  621. 'choices-3-ORDER': '',
  622. }
  623. ChoiceFormSet = formset_factory(Choice, can_order=True)
  624. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  625. self.assertTrue(formset.is_valid())
  626. self.assertEqual(
  627. [form.cleaned_data for form in formset.ordered_forms],
  628. [
  629. {'votes': 100, 'ORDER': 1, 'choice': 'Calexico'},
  630. {'votes': 900, 'ORDER': 2, 'choice': 'Fergie'},
  631. {'votes': 500, 'ORDER': None, 'choice': 'The Decemberists'},
  632. {'votes': 50, 'ORDER': None, 'choice': 'Basia Bulat'},
  633. ],
  634. )
  635. def test_ordering_blank_fieldsets(self):
  636. """Ordering works with blank fieldsets."""
  637. data = {
  638. 'choices-TOTAL_FORMS': '3', # the number of forms rendered
  639. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  640. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  641. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  642. }
  643. ChoiceFormSet = formset_factory(Choice, can_order=True)
  644. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  645. self.assertTrue(formset.is_valid())
  646. self.assertEqual(formset.ordered_forms, [])
  647. def test_formset_with_ordering_and_deletion(self):
  648. """FormSets with ordering + deletion."""
  649. ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True)
  650. initial = [
  651. {'choice': 'Calexico', 'votes': 100},
  652. {'choice': 'Fergie', 'votes': 900},
  653. {'choice': 'The Decemberists', 'votes': 500},
  654. ]
  655. formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
  656. self.assertHTMLEqual(
  657. '\n'.join(form.as_ul() for form in formset.forms),
  658. """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>
  659. <li>Votes: <input type="number" name="choices-0-votes" value="100"></li>
  660. <li>Order: <input type="number" name="choices-0-ORDER" value="1"></li>
  661. <li>Delete: <input type="checkbox" name="choices-0-DELETE"></li>
  662. <li>Choice: <input type="text" name="choices-1-choice" value="Fergie"></li>
  663. <li>Votes: <input type="number" name="choices-1-votes" value="900"></li>
  664. <li>Order: <input type="number" name="choices-1-ORDER" value="2"></li>
  665. <li>Delete: <input type="checkbox" name="choices-1-DELETE"></li>
  666. <li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists"></li>
  667. <li>Votes: <input type="number" name="choices-2-votes" value="500"></li>
  668. <li>Order: <input type="number" name="choices-2-ORDER" value="3"></li>
  669. <li>Delete: <input type="checkbox" name="choices-2-DELETE"></li>
  670. <li>Choice: <input type="text" name="choices-3-choice"></li>
  671. <li>Votes: <input type="number" name="choices-3-votes"></li>
  672. <li>Order: <input type="number" name="choices-3-ORDER"></li>
  673. <li>Delete: <input type="checkbox" name="choices-3-DELETE"></li>"""
  674. )
  675. # Let's delete Fergie, and put The Decemberists ahead of Calexico.
  676. data = {
  677. 'choices-TOTAL_FORMS': '4', # the number of forms rendered
  678. 'choices-INITIAL_FORMS': '3', # the number of forms with initial data
  679. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  680. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  681. 'choices-0-choice': 'Calexico',
  682. 'choices-0-votes': '100',
  683. 'choices-0-ORDER': '1',
  684. 'choices-0-DELETE': '',
  685. 'choices-1-choice': 'Fergie',
  686. 'choices-1-votes': '900',
  687. 'choices-1-ORDER': '2',
  688. 'choices-1-DELETE': 'on',
  689. 'choices-2-choice': 'The Decemberists',
  690. 'choices-2-votes': '500',
  691. 'choices-2-ORDER': '0',
  692. 'choices-2-DELETE': '',
  693. 'choices-3-choice': '',
  694. 'choices-3-votes': '',
  695. 'choices-3-ORDER': '',
  696. 'choices-3-DELETE': '',
  697. }
  698. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  699. self.assertTrue(formset.is_valid())
  700. self.assertEqual(
  701. [form.cleaned_data for form in formset.ordered_forms],
  702. [
  703. {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': 'The Decemberists'},
  704. {'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': 'Calexico'},
  705. ],
  706. )
  707. self.assertEqual(
  708. [form.cleaned_data for form in formset.deleted_forms],
  709. [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': 'Fergie'}]
  710. )
  711. def test_invalid_deleted_form_with_ordering(self):
  712. """
  713. Can get ordered_forms from a valid formset even if a deleted form
  714. would have been invalid.
  715. """
  716. FavoriteDrinkFormset = formset_factory(form=FavoriteDrinkForm, can_delete=True, can_order=True)
  717. formset = FavoriteDrinkFormset({
  718. 'form-0-name': '',
  719. 'form-0-DELETE': 'on', # no name!
  720. 'form-TOTAL_FORMS': 1,
  721. 'form-INITIAL_FORMS': 1,
  722. 'form-MIN_NUM_FORMS': 0,
  723. 'form-MAX_NUM_FORMS': 1
  724. })
  725. self.assertTrue(formset.is_valid())
  726. self.assertEqual(formset.ordered_forms, [])
  727. def test_clean_hook(self):
  728. """
  729. FormSets have a clean() hook for doing extra validation that isn't tied
  730. to any form. It follows the same pattern as the clean() hook on Forms.
  731. """
  732. # Start out with a some duplicate data.
  733. data = {
  734. 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
  735. 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
  736. 'drinks-MIN_NUM_FORMS': '0', # min number of forms
  737. 'drinks-MAX_NUM_FORMS': '0', # max number of forms
  738. 'drinks-0-name': 'Gin and Tonic',
  739. 'drinks-1-name': 'Gin and Tonic',
  740. }
  741. formset = FavoriteDrinksFormSet(data, prefix='drinks')
  742. self.assertFalse(formset.is_valid())
  743. # Any errors raised by formset.clean() are available via the
  744. # formset.non_form_errors() method.
  745. for error in formset.non_form_errors():
  746. self.assertEqual(str(error), 'You may only specify a drink once.')
  747. # The valid case still works.
  748. data['drinks-1-name'] = 'Bloody Mary'
  749. formset = FavoriteDrinksFormSet(data, prefix='drinks')
  750. self.assertTrue(formset.is_valid())
  751. self.assertEqual(formset.non_form_errors(), [])
  752. def test_limiting_max_forms(self):
  753. """Limiting the maximum number of forms with max_num."""
  754. # When not passed, max_num will take a high default value, leaving the
  755. # number of forms only controlled by the value of the extra parameter.
  756. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3)
  757. formset = LimitedFavoriteDrinkFormSet()
  758. self.assertHTMLEqual(
  759. '\n'.join(str(form) for form in formset.forms),
  760. """<tr><th><label for="id_form-0-name">Name:</label></th>
  761. <td><input type="text" name="form-0-name" id="id_form-0-name"></td></tr>
  762. <tr><th><label for="id_form-1-name">Name:</label></th>
  763. <td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>
  764. <tr><th><label for="id_form-2-name">Name:</label></th>
  765. <td><input type="text" name="form-2-name" id="id_form-2-name"></td></tr>"""
  766. )
  767. # If max_num is 0 then no form is rendered at all.
  768. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=0)
  769. formset = LimitedFavoriteDrinkFormSet()
  770. self.assertEqual(formset.forms, [])
  771. def test_limited_max_forms_two(self):
  772. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2)
  773. formset = LimitedFavoriteDrinkFormSet()
  774. self.assertHTMLEqual(
  775. '\n'.join(str(form) for form in formset.forms),
  776. """<tr><th><label for="id_form-0-name">Name:</label></th><td>
  777. <input type="text" name="form-0-name" id="id_form-0-name"></td></tr>
  778. <tr><th><label for="id_form-1-name">Name:</label></th>
  779. <td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>"""
  780. )
  781. def test_limiting_extra_lest_than_max_num(self):
  782. """max_num has no effect when extra is less than max_num."""
  783. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
  784. formset = LimitedFavoriteDrinkFormSet()
  785. self.assertHTMLEqual(
  786. '\n'.join(str(form) for form in formset.forms),
  787. """<tr><th><label for="id_form-0-name">Name:</label></th>
  788. <td><input type="text" name="form-0-name" id="id_form-0-name"></td></tr>"""
  789. )
  790. def test_max_num_with_initial_data(self):
  791. # When not passed, max_num will take a high default value, leaving the
  792. # number of forms only controlled by the value of the initial and extra
  793. # parameters.
  794. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1)
  795. formset = LimitedFavoriteDrinkFormSet(initial=[{'name': 'Fernet and Coke'}])
  796. self.assertHTMLEqual(
  797. '\n'.join(str(form) for form in formset.forms),
  798. """<tr><th><label for="id_form-0-name">Name:</label></th>
  799. <td><input type="text" name="form-0-name" value="Fernet and Coke" id="id_form-0-name"></td></tr>
  800. <tr><th><label for="id_form-1-name">Name:</label></th>
  801. <td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>"""
  802. )
  803. def test_max_num_zero(self):
  804. """
  805. If max_num is 0 then no form is rendered at all, regardless of extra,
  806. unless initial data is present.
  807. """
  808. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0)
  809. formset = LimitedFavoriteDrinkFormSet()
  810. self.assertEqual(formset.forms, [])
  811. def test_max_num_zero_with_initial(self):
  812. # initial trumps max_num
  813. initial = [
  814. {'name': 'Fernet and Coke'},
  815. {'name': 'Bloody Mary'},
  816. ]
  817. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0)
  818. formset = LimitedFavoriteDrinkFormSet(initial=initial)
  819. self.assertHTMLEqual(
  820. '\n'.join(str(form) for form in formset.forms),
  821. """<tr><th><label for="id_form-0-name">Name:</label></th>
  822. <td><input id="id_form-0-name" name="form-0-name" type="text" value="Fernet and Coke"></td></tr>
  823. <tr><th><label for="id_form-1-name">Name:</label></th>
  824. <td><input id="id_form-1-name" name="form-1-name" type="text" value="Bloody Mary"></td></tr>"""
  825. )
  826. def test_more_initial_than_max_num(self):
  827. """
  828. More initial forms than max_num results in all initial forms being
  829. displayed (but no extra forms).
  830. """
  831. initial = [
  832. {'name': 'Gin Tonic'},
  833. {'name': 'Bloody Mary'},
  834. {'name': 'Jack and Coke'},
  835. ]
  836. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
  837. formset = LimitedFavoriteDrinkFormSet(initial=initial)
  838. self.assertHTMLEqual(
  839. '\n'.join(str(form) for form in formset.forms),
  840. """<tr><th><label for="id_form-0-name">Name:</label></th>
  841. <td><input id="id_form-0-name" name="form-0-name" type="text" value="Gin Tonic"></td></tr>
  842. <tr><th><label for="id_form-1-name">Name:</label></th>
  843. <td><input id="id_form-1-name" name="form-1-name" type="text" value="Bloody Mary"></td></tr>
  844. <tr><th><label for="id_form-2-name">Name:</label></th>
  845. <td><input id="id_form-2-name" name="form-2-name" type="text" value="Jack and Coke"></td></tr>"""
  846. )
  847. def test_default_absolute_max(self):
  848. # absolute_max defaults to 2 * DEFAULT_MAX_NUM if max_num is None.
  849. data = {
  850. 'form-TOTAL_FORMS': 2001,
  851. 'form-INITIAL_FORMS': '0',
  852. 'form-MAX_NUM_FORMS': '0',
  853. }
  854. formset = FavoriteDrinksFormSet(data=data)
  855. self.assertIs(formset.is_valid(), False)
  856. self.assertEqual(
  857. formset.non_form_errors(),
  858. ['Please submit at most 1000 forms.'],
  859. )
  860. self.assertEqual(formset.absolute_max, 2000)
  861. def test_absolute_max(self):
  862. data = {
  863. 'form-TOTAL_FORMS': '2001',
  864. 'form-INITIAL_FORMS': '0',
  865. 'form-MAX_NUM_FORMS': '0',
  866. }
  867. AbsoluteMaxFavoriteDrinksFormSet = formset_factory(
  868. FavoriteDrinkForm,
  869. absolute_max=3000,
  870. )
  871. formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
  872. self.assertIs(formset.is_valid(), True)
  873. self.assertEqual(len(formset.forms), 2001)
  874. # absolute_max provides a hard limit.
  875. data['form-TOTAL_FORMS'] = '3001'
  876. formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
  877. self.assertIs(formset.is_valid(), False)
  878. self.assertEqual(len(formset.forms), 3000)
  879. self.assertEqual(
  880. formset.non_form_errors(),
  881. ['Please submit at most 1000 forms.'],
  882. )
  883. def test_absolute_max_with_max_num(self):
  884. data = {
  885. 'form-TOTAL_FORMS': '1001',
  886. 'form-INITIAL_FORMS': '0',
  887. 'form-MAX_NUM_FORMS': '0',
  888. }
  889. LimitedFavoriteDrinksFormSet = formset_factory(
  890. FavoriteDrinkForm,
  891. max_num=30,
  892. absolute_max=1000,
  893. )
  894. formset = LimitedFavoriteDrinksFormSet(data=data)
  895. self.assertIs(formset.is_valid(), False)
  896. self.assertEqual(len(formset.forms), 1000)
  897. self.assertEqual(
  898. formset.non_form_errors(),
  899. ['Please submit at most 30 forms.'],
  900. )
  901. def test_absolute_max_invalid(self):
  902. msg = "'absolute_max' must be greater or equal to 'max_num'."
  903. for max_num in [None, 31]:
  904. with self.subTest(max_num=max_num):
  905. with self.assertRaisesMessage(ValueError, msg):
  906. formset_factory(FavoriteDrinkForm, max_num=max_num, absolute_max=30)
  907. def test_more_initial_form_result_in_one(self):
  908. """
  909. One form from initial and extra=3 with max_num=2 results in the one
  910. initial form and one extra.
  911. """
  912. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2)
  913. formset = LimitedFavoriteDrinkFormSet(initial=[{'name': 'Gin Tonic'}])
  914. self.assertHTMLEqual(
  915. '\n'.join(str(form) for form in formset.forms),
  916. """<tr><th><label for="id_form-0-name">Name:</label></th>
  917. <td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name"></td></tr>
  918. <tr><th><label for="id_form-1-name">Name:</label></th>
  919. <td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>"""
  920. )
  921. def test_management_form_prefix(self):
  922. """The management form has the correct prefix."""
  923. formset = FavoriteDrinksFormSet()
  924. self.assertEqual(formset.management_form.prefix, 'form')
  925. data = {
  926. 'form-TOTAL_FORMS': '2',
  927. 'form-INITIAL_FORMS': '0',
  928. 'form-MIN_NUM_FORMS': '0',
  929. 'form-MAX_NUM_FORMS': '0',
  930. }
  931. formset = FavoriteDrinksFormSet(data=data)
  932. self.assertEqual(formset.management_form.prefix, 'form')
  933. formset = FavoriteDrinksFormSet(initial={})
  934. self.assertEqual(formset.management_form.prefix, 'form')
  935. def test_non_form_errors(self):
  936. data = {
  937. 'drinks-TOTAL_FORMS': '2', # the number of forms rendered
  938. 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data
  939. 'drinks-MIN_NUM_FORMS': '0', # min number of forms
  940. 'drinks-MAX_NUM_FORMS': '0', # max number of forms
  941. 'drinks-0-name': 'Gin and Tonic',
  942. 'drinks-1-name': 'Gin and Tonic',
  943. }
  944. formset = FavoriteDrinksFormSet(data, prefix='drinks')
  945. self.assertFalse(formset.is_valid())
  946. self.assertEqual(formset.non_form_errors(), ['You may only specify a drink once.'])
  947. self.assertEqual(
  948. str(formset.non_form_errors()),
  949. '<ul class="errorlist nonform"><li>'
  950. 'You may only specify a drink once.</li></ul>',
  951. )
  952. def test_formset_iteration(self):
  953. """Formset instances are iterable."""
  954. ChoiceFormset = formset_factory(Choice, extra=3)
  955. formset = ChoiceFormset()
  956. # An iterated formset yields formset.forms.
  957. forms = list(formset)
  958. self.assertEqual(forms, formset.forms)
  959. self.assertEqual(len(formset), len(forms))
  960. # A formset may be indexed to retrieve its forms.
  961. self.assertEqual(formset[0], forms[0])
  962. with self.assertRaises(IndexError):
  963. formset[3]
  964. # Formsets can override the default iteration order
  965. class BaseReverseFormSet(BaseFormSet):
  966. def __iter__(self):
  967. return reversed(self.forms)
  968. def __getitem__(self, idx):
  969. return super().__getitem__(len(self) - idx - 1)
  970. ReverseChoiceFormset = formset_factory(Choice, BaseReverseFormSet, extra=3)
  971. reverse_formset = ReverseChoiceFormset()
  972. # __iter__() modifies the rendering order.
  973. # Compare forms from "reverse" formset with forms from original formset
  974. self.assertEqual(str(reverse_formset[0]), str(forms[-1]))
  975. self.assertEqual(str(reverse_formset[1]), str(forms[-2]))
  976. self.assertEqual(len(reverse_formset), len(forms))
  977. def test_formset_nonzero(self):
  978. """A formsets without any forms evaluates as True."""
  979. ChoiceFormset = formset_factory(Choice, extra=0)
  980. formset = ChoiceFormset()
  981. self.assertEqual(len(formset.forms), 0)
  982. self.assertTrue(formset)
  983. def test_formset_splitdatetimefield(self):
  984. """
  985. Formset works with SplitDateTimeField(initial=datetime.datetime.now).
  986. """
  987. class SplitDateTimeForm(Form):
  988. when = SplitDateTimeField(initial=datetime.datetime.now)
  989. SplitDateTimeFormSet = formset_factory(SplitDateTimeForm)
  990. data = {
  991. 'form-TOTAL_FORMS': '1',
  992. 'form-INITIAL_FORMS': '0',
  993. 'form-0-when_0': '1904-06-16',
  994. 'form-0-when_1': '15:51:33',
  995. }
  996. formset = SplitDateTimeFormSet(data)
  997. self.assertTrue(formset.is_valid())
  998. def test_formset_error_class(self):
  999. """Formset's forms use the formset's error_class."""
  1000. class CustomErrorList(ErrorList):
  1001. pass
  1002. formset = FavoriteDrinksFormSet(error_class=CustomErrorList)
  1003. self.assertEqual(formset.forms[0].error_class, CustomErrorList)
  1004. def test_formset_calls_forms_is_valid(self):
  1005. """Formsets call is_valid() on each form."""
  1006. class AnotherChoice(Choice):
  1007. def is_valid(self):
  1008. self.is_valid_called = True
  1009. return super().is_valid()
  1010. AnotherChoiceFormSet = formset_factory(AnotherChoice)
  1011. data = {
  1012. 'choices-TOTAL_FORMS': '1', # number of forms rendered
  1013. 'choices-INITIAL_FORMS': '0', # number of forms with initial data
  1014. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  1015. 'choices-MAX_NUM_FORMS': '0', # max number of forms
  1016. 'choices-0-choice': 'Calexico',
  1017. 'choices-0-votes': '100',
  1018. }
  1019. formset = AnotherChoiceFormSet(data, auto_id=False, prefix='choices')
  1020. self.assertTrue(formset.is_valid())
  1021. self.assertTrue(all(form.is_valid_called for form in formset.forms))
  1022. def test_hard_limit_on_instantiated_forms(self):
  1023. """A formset has a hard limit on the number of forms instantiated."""
  1024. # reduce the default limit of 1000 temporarily for testing
  1025. _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
  1026. try:
  1027. formsets.DEFAULT_MAX_NUM = 2
  1028. ChoiceFormSet = formset_factory(Choice, max_num=1)
  1029. # someone fiddles with the mgmt form data...
  1030. formset = ChoiceFormSet(
  1031. {
  1032. 'choices-TOTAL_FORMS': '4',
  1033. 'choices-INITIAL_FORMS': '0',
  1034. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  1035. 'choices-MAX_NUM_FORMS': '4',
  1036. 'choices-0-choice': 'Zero',
  1037. 'choices-0-votes': '0',
  1038. 'choices-1-choice': 'One',
  1039. 'choices-1-votes': '1',
  1040. 'choices-2-choice': 'Two',
  1041. 'choices-2-votes': '2',
  1042. 'choices-3-choice': 'Three',
  1043. 'choices-3-votes': '3',
  1044. },
  1045. prefix='choices',
  1046. )
  1047. # But we still only instantiate 3 forms
  1048. self.assertEqual(len(formset.forms), 3)
  1049. # and the formset isn't valid
  1050. self.assertFalse(formset.is_valid())
  1051. finally:
  1052. formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
  1053. def test_increase_hard_limit(self):
  1054. """Can increase the built-in forms limit via a higher max_num."""
  1055. # reduce the default limit of 1000 temporarily for testing
  1056. _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
  1057. try:
  1058. formsets.DEFAULT_MAX_NUM = 3
  1059. # for this form, we want a limit of 4
  1060. ChoiceFormSet = formset_factory(Choice, max_num=4)
  1061. formset = ChoiceFormSet(
  1062. {
  1063. 'choices-TOTAL_FORMS': '4',
  1064. 'choices-INITIAL_FORMS': '0',
  1065. 'choices-MIN_NUM_FORMS': '0', # min number of forms
  1066. 'choices-MAX_NUM_FORMS': '4',
  1067. 'choices-0-choice': 'Zero',
  1068. 'choices-0-votes': '0',
  1069. 'choices-1-choice': 'One',
  1070. 'choices-1-votes': '1',
  1071. 'choices-2-choice': 'Two',
  1072. 'choices-2-votes': '2',
  1073. 'choices-3-choice': 'Three',
  1074. 'choices-3-votes': '3',
  1075. },
  1076. prefix='choices',
  1077. )
  1078. # Four forms are instantiated and no exception is raised
  1079. self.assertEqual(len(formset.forms), 4)
  1080. finally:
  1081. formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
  1082. def test_non_form_errors_run_full_clean(self):
  1083. """
  1084. If non_form_errors() is called without calling is_valid() first,
  1085. it should ensure that full_clean() is called.
  1086. """
  1087. class BaseCustomFormSet(BaseFormSet):
  1088. def clean(self):
  1089. raise ValidationError("This is a non-form error")
  1090. ChoiceFormSet = formset_factory(Choice, formset=BaseCustomFormSet)
  1091. data = {
  1092. 'choices-TOTAL_FORMS': '1',
  1093. 'choices-INITIAL_FORMS': '0',
  1094. }
  1095. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1096. self.assertIsInstance(formset.non_form_errors(), ErrorList)
  1097. self.assertEqual(list(formset.non_form_errors()), ['This is a non-form error'])
  1098. def test_validate_max_ignores_forms_marked_for_deletion(self):
  1099. class CheckForm(Form):
  1100. field = IntegerField()
  1101. data = {
  1102. 'check-TOTAL_FORMS': '2',
  1103. 'check-INITIAL_FORMS': '0',
  1104. 'check-MAX_NUM_FORMS': '1',
  1105. 'check-0-field': '200',
  1106. 'check-0-DELETE': '',
  1107. 'check-1-field': '50',
  1108. 'check-1-DELETE': 'on',
  1109. }
  1110. CheckFormSet = formset_factory(CheckForm, max_num=1, validate_max=True, can_delete=True)
  1111. formset = CheckFormSet(data, prefix='check')
  1112. self.assertTrue(formset.is_valid())
  1113. def test_formset_total_error_count(self):
  1114. """A valid formset should have 0 total errors."""
  1115. data = [ # formset_data, expected error count
  1116. ([('Calexico', '100')], 0),
  1117. ([('Calexico', '')], 1),
  1118. ([('', 'invalid')], 2),
  1119. ([('Calexico', '100'), ('Calexico', '')], 1),
  1120. ([('Calexico', ''), ('Calexico', '')], 2),
  1121. ]
  1122. for formset_data, expected_error_count in data:
  1123. formset = self.make_choiceformset(formset_data)
  1124. self.assertEqual(formset.total_error_count(), expected_error_count)
  1125. def test_formset_total_error_count_with_non_form_errors(self):
  1126. data = {
  1127. 'choices-TOTAL_FORMS': '2', # the number of forms rendered
  1128. 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
  1129. 'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored
  1130. 'choices-0-choice': 'Zero',
  1131. 'choices-0-votes': '0',
  1132. 'choices-1-choice': 'One',
  1133. 'choices-1-votes': '1',
  1134. }
  1135. ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True)
  1136. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1137. self.assertEqual(formset.total_error_count(), 1)
  1138. data['choices-1-votes'] = ''
  1139. formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1140. self.assertEqual(formset.total_error_count(), 2)
  1141. def test_html_safe(self):
  1142. formset = self.make_choiceformset()
  1143. self.assertTrue(hasattr(formset, '__html__'))
  1144. self.assertEqual(str(formset), formset.__html__())
  1145. def test_can_delete_extra_formset_forms(self):
  1146. ChoiceFormFormset = formset_factory(form=Choice, can_delete=True, extra=2)
  1147. formset = ChoiceFormFormset()
  1148. self.assertEqual(len(formset), 2)
  1149. self.assertIn('DELETE', formset.forms[0].fields)
  1150. self.assertIn('DELETE', formset.forms[1].fields)
  1151. def test_disable_delete_extra_formset_forms(self):
  1152. ChoiceFormFormset = formset_factory(
  1153. form=Choice,
  1154. can_delete=True,
  1155. can_delete_extra=False,
  1156. extra=2,
  1157. )
  1158. formset = ChoiceFormFormset()
  1159. self.assertEqual(len(formset), 2)
  1160. self.assertNotIn('DELETE', formset.forms[0].fields)
  1161. self.assertNotIn('DELETE', formset.forms[1].fields)
  1162. formset = ChoiceFormFormset(initial=[{'choice': 'Zero', 'votes': '1'}])
  1163. self.assertEqual(len(formset), 3)
  1164. self.assertIn('DELETE', formset.forms[0].fields)
  1165. self.assertNotIn('DELETE', formset.forms[1].fields)
  1166. self.assertNotIn('DELETE', formset.forms[2].fields)
  1167. formset = ChoiceFormFormset(data={
  1168. 'form-0-choice': 'Zero',
  1169. 'form-0-votes': '0',
  1170. 'form-0-DELETE': 'on',
  1171. 'form-1-choice': 'One',
  1172. 'form-1-votes': '1',
  1173. 'form-2-choice': '',
  1174. 'form-2-votes': '',
  1175. 'form-TOTAL_FORMS': '3',
  1176. 'form-INITIAL_FORMS': '1',
  1177. }, initial=[{'choice': 'Zero', 'votes': '1'}])
  1178. self.assertEqual(formset.cleaned_data, [
  1179. {'choice': 'Zero', 'votes': 0, 'DELETE': True},
  1180. {'choice': 'One', 'votes': 1},
  1181. {},
  1182. ])
  1183. self.assertIs(formset._should_delete_form(formset.forms[0]), True)
  1184. self.assertIs(formset._should_delete_form(formset.forms[1]), False)
  1185. self.assertIs(formset._should_delete_form(formset.forms[2]), False)
  1186. class FormsetAsTagTests(SimpleTestCase):
  1187. def setUp(self):
  1188. data = {
  1189. 'choices-TOTAL_FORMS': '1',
  1190. 'choices-INITIAL_FORMS': '0',
  1191. 'choices-MIN_NUM_FORMS': '0',
  1192. 'choices-MAX_NUM_FORMS': '0',
  1193. 'choices-0-choice': 'Calexico',
  1194. 'choices-0-votes': '100',
  1195. }
  1196. self.formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1197. self.management_form_html = (
  1198. '<input type="hidden" name="choices-TOTAL_FORMS" value="1">'
  1199. '<input type="hidden" name="choices-INITIAL_FORMS" value="0">'
  1200. '<input type="hidden" name="choices-MIN_NUM_FORMS" value="0">'
  1201. '<input type="hidden" name="choices-MAX_NUM_FORMS" value="0">'
  1202. )
  1203. def test_as_table(self):
  1204. self.assertHTMLEqual(
  1205. self.formset.as_table(),
  1206. self.management_form_html + (
  1207. '<tr><th>Choice:</th><td>'
  1208. '<input type="text" name="choices-0-choice" value="Calexico"></td></tr>'
  1209. '<tr><th>Votes:</th><td>'
  1210. '<input type="number" name="choices-0-votes" value="100"></td></tr>'
  1211. )
  1212. )
  1213. def test_as_p(self):
  1214. self.assertHTMLEqual(
  1215. self.formset.as_p(),
  1216. self.management_form_html + (
  1217. '<p>Choice: <input type="text" name="choices-0-choice" value="Calexico"></p>'
  1218. '<p>Votes: <input type="number" name="choices-0-votes" value="100"></p>'
  1219. )
  1220. )
  1221. def test_as_ul(self):
  1222. self.assertHTMLEqual(
  1223. self.formset.as_ul(),
  1224. self.management_form_html + (
  1225. '<li>Choice: <input type="text" name="choices-0-choice" value="Calexico"></li>'
  1226. '<li>Votes: <input type="number" name="choices-0-votes" value="100"></li>'
  1227. )
  1228. )
  1229. class ArticleForm(Form):
  1230. title = CharField()
  1231. pub_date = DateField()
  1232. ArticleFormSet = formset_factory(ArticleForm)
  1233. class TestIsBoundBehavior(SimpleTestCase):
  1234. def test_no_data_error(self):
  1235. formset = ArticleFormSet({})
  1236. self.assertIs(formset.is_valid(), False)
  1237. self.assertEqual(
  1238. formset.non_form_errors(),
  1239. [
  1240. 'ManagementForm data is missing or has been tampered with. '
  1241. 'Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. '
  1242. 'You may need to file a bug report if the issue persists.',
  1243. ],
  1244. )
  1245. self.assertEqual(formset.errors, [])
  1246. # Can still render the formset.
  1247. self.assertEqual(
  1248. str(formset),
  1249. '<tr><td colspan="2">'
  1250. '<ul class="errorlist nonfield">'
  1251. '<li>(Hidden field TOTAL_FORMS) This field is required.</li>'
  1252. '<li>(Hidden field INITIAL_FORMS) This field is required.</li>'
  1253. '</ul>'
  1254. '<input type="hidden" name="form-TOTAL_FORMS" id="id_form-TOTAL_FORMS">'
  1255. '<input type="hidden" name="form-INITIAL_FORMS" id="id_form-INITIAL_FORMS">'
  1256. '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
  1257. '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
  1258. '</td></tr>\n'
  1259. )
  1260. def test_management_form_invalid_data(self):
  1261. data = {
  1262. 'form-TOTAL_FORMS': 'two',
  1263. 'form-INITIAL_FORMS': 'one',
  1264. }
  1265. formset = ArticleFormSet(data)
  1266. self.assertIs(formset.is_valid(), False)
  1267. self.assertEqual(
  1268. formset.non_form_errors(),
  1269. [
  1270. 'ManagementForm data is missing or has been tampered with. '
  1271. 'Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. '
  1272. 'You may need to file a bug report if the issue persists.',
  1273. ],
  1274. )
  1275. self.assertEqual(formset.errors, [])
  1276. # Can still render the formset.
  1277. self.assertEqual(
  1278. str(formset),
  1279. '<tr><td colspan="2">'
  1280. '<ul class="errorlist nonfield">'
  1281. '<li>(Hidden field TOTAL_FORMS) Enter a whole number.</li>'
  1282. '<li>(Hidden field INITIAL_FORMS) Enter a whole number.</li>'
  1283. '</ul>'
  1284. '<input type="hidden" name="form-TOTAL_FORMS" value="two" id="id_form-TOTAL_FORMS">'
  1285. '<input type="hidden" name="form-INITIAL_FORMS" value="one" id="id_form-INITIAL_FORMS">'
  1286. '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
  1287. '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
  1288. '</td></tr>\n',
  1289. )
  1290. def test_customize_management_form_error(self):
  1291. formset = ArticleFormSet({}, error_messages={'missing_management_form': 'customized'})
  1292. self.assertIs(formset.is_valid(), False)
  1293. self.assertEqual(formset.non_form_errors(), ['customized'])
  1294. self.assertEqual(formset.errors, [])
  1295. def test_with_management_data_attrs_work_fine(self):
  1296. data = {
  1297. 'form-TOTAL_FORMS': '1',
  1298. 'form-INITIAL_FORMS': '0',
  1299. }
  1300. formset = ArticleFormSet(data)
  1301. self.assertEqual(0, formset.initial_form_count())
  1302. self.assertEqual(1, formset.total_form_count())
  1303. self.assertTrue(formset.is_bound)
  1304. self.assertTrue(formset.forms[0].is_bound)
  1305. self.assertTrue(formset.is_valid())
  1306. self.assertTrue(formset.forms[0].is_valid())
  1307. self.assertEqual([{}], formset.cleaned_data)
  1308. def test_form_errors_are_caught_by_formset(self):
  1309. data = {
  1310. 'form-TOTAL_FORMS': '2',
  1311. 'form-INITIAL_FORMS': '0',
  1312. 'form-0-title': 'Test',
  1313. 'form-0-pub_date': '1904-06-16',
  1314. 'form-1-title': 'Test',
  1315. 'form-1-pub_date': '', # <-- this date is missing but required
  1316. }
  1317. formset = ArticleFormSet(data)
  1318. self.assertFalse(formset.is_valid())
  1319. self.assertEqual([{}, {'pub_date': ['This field is required.']}], formset.errors)
  1320. def test_empty_forms_are_unbound(self):
  1321. data = {
  1322. 'form-TOTAL_FORMS': '1',
  1323. 'form-INITIAL_FORMS': '0',
  1324. 'form-0-title': 'Test',
  1325. 'form-0-pub_date': '1904-06-16',
  1326. }
  1327. unbound_formset = ArticleFormSet()
  1328. bound_formset = ArticleFormSet(data)
  1329. empty_forms = [
  1330. unbound_formset.empty_form,
  1331. bound_formset.empty_form
  1332. ]
  1333. # Empty forms should be unbound
  1334. self.assertFalse(empty_forms[0].is_bound)
  1335. self.assertFalse(empty_forms[1].is_bound)
  1336. # The empty forms should be equal.
  1337. self.assertHTMLEqual(empty_forms[0].as_p(), empty_forms[1].as_p())
  1338. class TestEmptyFormSet(SimpleTestCase):
  1339. def test_empty_formset_is_valid(self):
  1340. """An empty formset still calls clean()"""
  1341. class EmptyFsetWontValidate(BaseFormSet):
  1342. def clean(self):
  1343. raise ValidationError('Clean method called')
  1344. EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
  1345. formset = EmptyFsetWontValidateFormset(
  1346. data={'form-INITIAL_FORMS': '0', 'form-TOTAL_FORMS': '0'},
  1347. prefix="form",
  1348. )
  1349. formset2 = EmptyFsetWontValidateFormset(
  1350. data={'form-INITIAL_FORMS': '0', 'form-TOTAL_FORMS': '1', 'form-0-name': 'bah'},
  1351. prefix="form",
  1352. )
  1353. self.assertFalse(formset.is_valid())
  1354. self.assertFalse(formset2.is_valid())
  1355. def test_empty_formset_media(self):
  1356. """Media is available on empty formset."""
  1357. class MediaForm(Form):
  1358. class Media:
  1359. js = ('some-file.js',)
  1360. self.assertIn('some-file.js', str(formset_factory(MediaForm, extra=0)().media))
  1361. def test_empty_formset_is_multipart(self):
  1362. """is_multipart() works with an empty formset."""
  1363. class FileForm(Form):
  1364. file = FileField()
  1365. self.assertTrue(formset_factory(FileForm, extra=0)().is_multipart())
  1366. class AllValidTests(SimpleTestCase):
  1367. def test_valid(self):
  1368. data = {
  1369. 'choices-TOTAL_FORMS': '2',
  1370. 'choices-INITIAL_FORMS': '0',
  1371. 'choices-MIN_NUM_FORMS': '0',
  1372. 'choices-0-choice': 'Zero',
  1373. 'choices-0-votes': '0',
  1374. 'choices-1-choice': 'One',
  1375. 'choices-1-votes': '1',
  1376. }
  1377. ChoiceFormSet = formset_factory(Choice)
  1378. formset1 = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1379. formset2 = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1380. self.assertIs(all_valid((formset1, formset2)), True)
  1381. expected_errors = [{}, {}]
  1382. self.assertEqual(formset1._errors, expected_errors)
  1383. self.assertEqual(formset2._errors, expected_errors)
  1384. def test_invalid(self):
  1385. """all_valid() validates all forms, even when some are invalid."""
  1386. data = {
  1387. 'choices-TOTAL_FORMS': '2',
  1388. 'choices-INITIAL_FORMS': '0',
  1389. 'choices-MIN_NUM_FORMS': '0',
  1390. 'choices-0-choice': 'Zero',
  1391. 'choices-0-votes': '',
  1392. 'choices-1-choice': 'One',
  1393. 'choices-1-votes': '',
  1394. }
  1395. ChoiceFormSet = formset_factory(Choice)
  1396. formset1 = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1397. formset2 = ChoiceFormSet(data, auto_id=False, prefix='choices')
  1398. self.assertIs(all_valid((formset1, formset2)), False)
  1399. expected_errors = [{'votes': ['This field is required.']}, {'votes': ['This field is required.']}]
  1400. self.assertEqual(formset1._errors, expected_errors)
  1401. self.assertEqual(formset2._errors, expected_errors)