/tests/regressiontests/forms/tests/formsets.py
Python | 911 lines | 886 code | 16 blank | 9 comment | 5 complexity | 405025b1a4dba096029ef748e44c6361 MD5 | raw file
1# -*- coding: utf-8 -*- 2from django.forms import Form, CharField, IntegerField, ValidationError, DateField 3from django.forms.formsets import formset_factory, BaseFormSet 4from django.utils.unittest import TestCase 5 6 7class Choice(Form): 8 choice = CharField() 9 votes = IntegerField() 10 11 12# FormSet allows us to use multiple instance of the same form on 1 page. For now, 13# the best way to create a FormSet is by using the formset_factory function. 14ChoiceFormSet = formset_factory(Choice) 15 16 17class FavoriteDrinkForm(Form): 18 name = CharField() 19 20 21class BaseFavoriteDrinksFormSet(BaseFormSet): 22 def clean(self): 23 seen_drinks = [] 24 25 for drink in self.cleaned_data: 26 if drink['name'] in seen_drinks: 27 raise ValidationError('You may only specify a drink once.') 28 29 seen_drinks.append(drink['name']) 30 31 32class EmptyFsetWontValidate(BaseFormSet): 33 def clean(self): 34 raise ValidationError("Clean method called") 35 36 37# Let's define a FormSet that takes a list of favorite drinks, but raises an 38# error if there are any duplicates. Used in ``test_clean_hook``, 39# ``test_regression_6926`` & ``test_regression_12878``. 40FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, 41 formset=BaseFavoriteDrinksFormSet, extra=3) 42 43 44class FormsFormsetTestCase(TestCase): 45 def test_basic_formset(self): 46 # A FormSet constructor takes the same arguments as Form. Let's create a FormSet 47 # for adding data. By default, it displays 1 blank form. It can display more, 48 # but we'll look at how to do so later. 49 formset = ChoiceFormSet(auto_id=False, prefix='choices') 50 self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" /> 51<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> 52<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""") 53 54 # On thing to note is that there needs to be a special value in the data. This 55 # value tells the FormSet how many forms were displayed so it can tell how 56 # many forms it needs to clean and validate. You could use javascript to create 57 # new forms on the client side, but they won't get validated unless you increment 58 # the TOTAL_FORMS field appropriately. 59 60 data = { 61 'choices-TOTAL_FORMS': '1', # the number of forms rendered 62 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 63 'choices-MAX_NUM_FORMS': '0', # max number of forms 64 'choices-0-choice': 'Calexico', 65 'choices-0-votes': '100', 66 } 67 # We treat FormSet pretty much like we would treat a normal Form. FormSet has an 68 # is_valid method, and a cleaned_data or errors attribute depending on whether all 69 # the forms passed validation. However, unlike a Form instance, cleaned_data and 70 # errors will be a list of dicts rather than just a single dict. 71 72 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 73 self.assertTrue(formset.is_valid()) 74 self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}]) 75 76 # If a FormSet was not passed any data, its is_valid method should return False. 77 formset = ChoiceFormSet() 78 self.assertFalse(formset.is_valid()) 79 80 def test_formset_validation(self): 81 # FormSet instances can also have an error attribute if validation failed for 82 # any of the forms. 83 84 data = { 85 'choices-TOTAL_FORMS': '1', # the number of forms rendered 86 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 87 'choices-MAX_NUM_FORMS': '0', # max number of forms 88 'choices-0-choice': 'Calexico', 89 'choices-0-votes': '', 90 } 91 92 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 93 self.assertFalse(formset.is_valid()) 94 self.assertEqual(formset.errors, [{'votes': [u'This field is required.']}]) 95 96 def test_formset_initial_data(self): 97 # We can also prefill a FormSet with existing data by providing an ``initial`` 98 # argument to the constructor. ``initial`` should be a list of dicts. By default, 99 # an extra blank form is included. 100 101 initial = [{'choice': u'Calexico', 'votes': 100}] 102 formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 103 form_output = [] 104 105 for form in formset.forms: 106 form_output.append(form.as_ul()) 107 108 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 109<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> 110<li>Choice: <input type="text" name="choices-1-choice" /></li> 111<li>Votes: <input type="text" name="choices-1-votes" /></li>""") 112 113 # Let's simulate what would happen if we submitted this form. 114 115 data = { 116 'choices-TOTAL_FORMS': '2', # the number of forms rendered 117 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 118 'choices-MAX_NUM_FORMS': '0', # max number of forms 119 'choices-0-choice': 'Calexico', 120 'choices-0-votes': '100', 121 'choices-1-choice': '', 122 'choices-1-votes': '', 123 } 124 125 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 126 self.assertTrue(formset.is_valid()) 127 self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}, {}]) 128 129 def test_second_form_partially_filled(self): 130 # But the second form was blank! Shouldn't we get some errors? No. If we display 131 # a form as blank, it's ok for it to be submitted as blank. If we fill out even 132 # one of the fields of a blank form though, it will be validated. We may want to 133 # required that at least x number of forms are completed, but we'll show how to 134 # handle that later. 135 136 data = { 137 'choices-TOTAL_FORMS': '2', # the number of forms rendered 138 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 139 'choices-MAX_NUM_FORMS': '0', # max number of forms 140 'choices-0-choice': 'Calexico', 141 'choices-0-votes': '100', 142 'choices-1-choice': 'The Decemberists', 143 'choices-1-votes': '', # missing value 144 } 145 146 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 147 self.assertFalse(formset.is_valid()) 148 self.assertEqual(formset.errors, [{}, {'votes': [u'This field is required.']}]) 149 150 def test_delete_prefilled_data(self): 151 # If we delete data that was pre-filled, we should get an error. Simply removing 152 # data from form fields isn't the proper way to delete it. We'll see how to 153 # handle that case later. 154 155 data = { 156 'choices-TOTAL_FORMS': '2', # the number of forms rendered 157 'choices-INITIAL_FORMS': '1', # the number of forms with initial data 158 'choices-MAX_NUM_FORMS': '0', # max number of forms 159 'choices-0-choice': '', # deleted value 160 'choices-0-votes': '', # deleted value 161 'choices-1-choice': '', 162 'choices-1-votes': '', 163 } 164 165 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 166 self.assertFalse(formset.is_valid()) 167 self.assertEqual(formset.errors, [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]) 168 169 def test_displaying_more_than_one_blank_form(self): 170 # Displaying more than 1 blank form ########################################### 171 # We can also display more than 1 empty form at a time. To do so, pass a 172 # extra argument to formset_factory. 173 ChoiceFormSet = formset_factory(Choice, extra=3) 174 175 formset = ChoiceFormSet(auto_id=False, prefix='choices') 176 form_output = [] 177 178 for form in formset.forms: 179 form_output.append(form.as_ul()) 180 181 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" /></li> 182<li>Votes: <input type="text" name="choices-0-votes" /></li> 183<li>Choice: <input type="text" name="choices-1-choice" /></li> 184<li>Votes: <input type="text" name="choices-1-votes" /></li> 185<li>Choice: <input type="text" name="choices-2-choice" /></li> 186<li>Votes: <input type="text" name="choices-2-votes" /></li>""") 187 188 # Since we displayed every form as blank, we will also accept them back as blank. 189 # This may seem a little strange, but later we will show how to require a minimum 190 # number of forms to be completed. 191 192 data = { 193 'choices-TOTAL_FORMS': '3', # the number of forms rendered 194 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 195 'choices-MAX_NUM_FORMS': '0', # max number of forms 196 'choices-0-choice': '', 197 'choices-0-votes': '', 198 'choices-1-choice': '', 199 'choices-1-votes': '', 200 'choices-2-choice': '', 201 'choices-2-votes': '', 202 } 203 204 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 205 self.assertTrue(formset.is_valid()) 206 self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}]) 207 208 def test_single_form_completed(self): 209 # We can just fill out one of the forms. 210 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-MAX_NUM_FORMS': '0', # max number of forms 215 'choices-0-choice': 'Calexico', 216 'choices-0-votes': '100', 217 'choices-1-choice': '', 218 'choices-1-votes': '', 219 'choices-2-choice': '', 220 'choices-2-votes': '', 221 } 222 223 ChoiceFormSet = formset_factory(Choice, extra=3) 224 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 225 self.assertTrue(formset.is_valid()) 226 self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}, {}, {}]) 227 228 def test_second_form_partially_filled_2(self): 229 # And once again, if we try to partially complete a form, validation will fail. 230 231 data = { 232 'choices-TOTAL_FORMS': '3', # the number of forms rendered 233 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 234 'choices-MAX_NUM_FORMS': '0', # max number of forms 235 'choices-0-choice': 'Calexico', 236 'choices-0-votes': '100', 237 'choices-1-choice': 'The Decemberists', 238 'choices-1-votes': '', # missing value 239 'choices-2-choice': '', 240 'choices-2-votes': '', 241 } 242 243 ChoiceFormSet = formset_factory(Choice, extra=3) 244 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 245 self.assertFalse(formset.is_valid()) 246 self.assertEqual(formset.errors, [{}, {'votes': [u'This field is required.']}, {}]) 247 248 def test_more_initial_data(self): 249 # The extra argument also works when the formset is pre-filled with initial 250 # data. 251 252 data = { 253 'choices-TOTAL_FORMS': '3', # the number of forms rendered 254 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 255 'choices-MAX_NUM_FORMS': '0', # max number of forms 256 'choices-0-choice': 'Calexico', 257 'choices-0-votes': '100', 258 'choices-1-choice': '', 259 'choices-1-votes': '', # missing value 260 'choices-2-choice': '', 261 'choices-2-votes': '', 262 } 263 264 initial = [{'choice': u'Calexico', 'votes': 100}] 265 ChoiceFormSet = formset_factory(Choice, extra=3) 266 formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 267 form_output = [] 268 269 for form in formset.forms: 270 form_output.append(form.as_ul()) 271 272 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 273<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> 274<li>Choice: <input type="text" name="choices-1-choice" /></li> 275<li>Votes: <input type="text" name="choices-1-votes" /></li> 276<li>Choice: <input type="text" name="choices-2-choice" /></li> 277<li>Votes: <input type="text" name="choices-2-votes" /></li> 278<li>Choice: <input type="text" name="choices-3-choice" /></li> 279<li>Votes: <input type="text" name="choices-3-votes" /></li>""") 280 281 # Make sure retrieving an empty form works, and it shows up in the form list 282 283 self.assertTrue(formset.empty_form.empty_permitted) 284 self.assertEqual(formset.empty_form.as_ul(), """<li>Choice: <input type="text" name="choices-__prefix__-choice" /></li> 285<li>Votes: <input type="text" name="choices-__prefix__-votes" /></li>""") 286 287 def test_formset_with_deletion(self): 288 # FormSets with deletion ###################################################### 289 # We can easily add deletion ability to a FormSet with an argument to 290 # formset_factory. This will add a boolean field to each form instance. When 291 # that boolean field is True, the form will be in formset.deleted_forms 292 293 ChoiceFormSet = formset_factory(Choice, can_delete=True) 294 295 initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 296 formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 297 form_output = [] 298 299 for form in formset.forms: 300 form_output.append(form.as_ul()) 301 302 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 303<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> 304<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> 305<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> 306<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> 307<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> 308<li>Choice: <input type="text" name="choices-2-choice" /></li> 309<li>Votes: <input type="text" name="choices-2-votes" /></li> 310<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>""") 311 312 # To delete something, we just need to set that form's special delete field to 313 # 'on'. Let's go ahead and delete Fergie. 314 315 data = { 316 'choices-TOTAL_FORMS': '3', # the number of forms rendered 317 'choices-INITIAL_FORMS': '2', # the number of forms with initial data 318 'choices-MAX_NUM_FORMS': '0', # max number of forms 319 'choices-0-choice': 'Calexico', 320 'choices-0-votes': '100', 321 'choices-0-DELETE': '', 322 'choices-1-choice': 'Fergie', 323 'choices-1-votes': '900', 324 'choices-1-DELETE': 'on', 325 'choices-2-choice': '', 326 'choices-2-votes': '', 327 'choices-2-DELETE': '', 328 } 329 330 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 331 self.assertTrue(formset.is_valid()) 332 self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]) 333 self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]) 334 335 # If we fill a form with something and then we check the can_delete checkbox for 336 # that form, that form's errors should not make the entire formset invalid since 337 # it's going to be deleted. 338 339 class CheckForm(Form): 340 field = IntegerField(min_value=100) 341 342 data = { 343 'check-TOTAL_FORMS': '3', # the number of forms rendered 344 'check-INITIAL_FORMS': '2', # the number of forms with initial data 345 'check-MAX_NUM_FORMS': '0', # max number of forms 346 'check-0-field': '200', 347 'check-0-DELETE': '', 348 'check-1-field': '50', 349 'check-1-DELETE': 'on', 350 'check-2-field': '', 351 'check-2-DELETE': '', 352 } 353 CheckFormSet = formset_factory(CheckForm, can_delete=True) 354 formset = CheckFormSet(data, prefix='check') 355 self.assertTrue(formset.is_valid()) 356 357 # If we remove the deletion flag now we will have our validation back. 358 data['check-1-DELETE'] = '' 359 formset = CheckFormSet(data, prefix='check') 360 self.assertFalse(formset.is_valid()) 361 362 # Should be able to get deleted_forms from a valid formset even if a 363 # deleted form would have been invalid. 364 365 class Person(Form): 366 name = CharField() 367 368 PeopleForm = formset_factory( 369 form=Person, 370 can_delete=True) 371 372 p = PeopleForm( 373 {'form-0-name': u'', 'form-0-DELETE': u'on', # no name! 374 'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 1, 375 'form-MAX_NUM_FORMS': 1}) 376 377 self.assertTrue(p.is_valid()) 378 self.assertEqual(len(p.deleted_forms), 1) 379 380 def test_formsets_with_ordering(self): 381 # FormSets with ordering ###################################################### 382 # We can also add ordering ability to a FormSet with an argument to 383 # formset_factory. This will add a integer field to each form instance. When 384 # form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct 385 # order specified by the ordering fields. If a number is duplicated in the set 386 # of ordering fields, for instance form 0 and form 3 are both marked as 1, then 387 # the form index used as a secondary ordering criteria. In order to put 388 # something at the front of the list, you'd need to set it's order to 0. 389 390 ChoiceFormSet = formset_factory(Choice, can_order=True) 391 392 initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] 393 formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 394 form_output = [] 395 396 for form in formset.forms: 397 form_output.append(form.as_ul()) 398 399 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 400<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> 401<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> 402<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> 403<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> 404<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> 405<li>Choice: <input type="text" name="choices-2-choice" /></li> 406<li>Votes: <input type="text" name="choices-2-votes" /></li> 407<li>Order: <input type="text" name="choices-2-ORDER" /></li>""") 408 409 data = { 410 'choices-TOTAL_FORMS': '3', # the number of forms rendered 411 'choices-INITIAL_FORMS': '2', # the number of forms with initial data 412 'choices-MAX_NUM_FORMS': '0', # max number of forms 413 'choices-0-choice': 'Calexico', 414 'choices-0-votes': '100', 415 'choices-0-ORDER': '1', 416 'choices-1-choice': 'Fergie', 417 'choices-1-votes': '900', 418 'choices-1-ORDER': '2', 419 'choices-2-choice': 'The Decemberists', 420 'choices-2-votes': '500', 421 'choices-2-ORDER': '0', 422 } 423 424 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 425 self.assertTrue(formset.is_valid()) 426 form_output = [] 427 428 for form in formset.ordered_forms: 429 form_output.append(form.cleaned_data) 430 431 self.assertEqual(form_output, [ 432 {'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}, 433 {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}, 434 {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}, 435 ]) 436 437 def test_empty_ordered_fields(self): 438 # Ordering fields are allowed to be left blank, and if they *are* left blank, 439 # they will be sorted below everything else. 440 441 data = { 442 'choices-TOTAL_FORMS': '4', # the number of forms rendered 443 'choices-INITIAL_FORMS': '3', # the number of forms with initial data 444 'choices-MAX_NUM_FORMS': '0', # max number of forms 445 'choices-0-choice': 'Calexico', 446 'choices-0-votes': '100', 447 'choices-0-ORDER': '1', 448 'choices-1-choice': 'Fergie', 449 'choices-1-votes': '900', 450 'choices-1-ORDER': '2', 451 'choices-2-choice': 'The Decemberists', 452 'choices-2-votes': '500', 453 'choices-2-ORDER': '', 454 'choices-3-choice': 'Basia Bulat', 455 'choices-3-votes': '50', 456 'choices-3-ORDER': '', 457 } 458 459 ChoiceFormSet = formset_factory(Choice, can_order=True) 460 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 461 self.assertTrue(formset.is_valid()) 462 form_output = [] 463 464 for form in formset.ordered_forms: 465 form_output.append(form.cleaned_data) 466 467 self.assertEqual(form_output, [ 468 {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}, 469 {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}, 470 {'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'}, 471 {'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'}, 472 ]) 473 474 def test_ordering_blank_fieldsets(self): 475 # Ordering should work with blank fieldsets. 476 477 data = { 478 'choices-TOTAL_FORMS': '3', # the number of forms rendered 479 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 480 'choices-MAX_NUM_FORMS': '0', # max number of forms 481 } 482 483 ChoiceFormSet = formset_factory(Choice, can_order=True) 484 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 485 self.assertTrue(formset.is_valid()) 486 form_output = [] 487 488 for form in formset.ordered_forms: 489 form_output.append(form.cleaned_data) 490 491 self.assertEqual(form_output, []) 492 493 def test_formset_with_ordering_and_deletion(self): 494 # FormSets with ordering + deletion ########################################### 495 # Let's try throwing ordering and deletion into the same form. 496 497 ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True) 498 499 initial = [ 500 {'choice': u'Calexico', 'votes': 100}, 501 {'choice': u'Fergie', 'votes': 900}, 502 {'choice': u'The Decemberists', 'votes': 500}, 503 ] 504 formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') 505 form_output = [] 506 507 for form in formset.forms: 508 form_output.append(form.as_ul()) 509 510 self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 511<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> 512<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> 513<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> 514<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> 515<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> 516<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> 517<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> 518<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li> 519<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li> 520<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li> 521<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> 522<li>Choice: <input type="text" name="choices-3-choice" /></li> 523<li>Votes: <input type="text" name="choices-3-votes" /></li> 524<li>Order: <input type="text" name="choices-3-ORDER" /></li> 525<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>""") 526 527 # Let's delete Fergie, and put The Decemberists ahead of Calexico. 528 529 data = { 530 'choices-TOTAL_FORMS': '4', # the number of forms rendered 531 'choices-INITIAL_FORMS': '3', # the number of forms with initial data 532 'choices-MAX_NUM_FORMS': '0', # max number of forms 533 'choices-0-choice': 'Calexico', 534 'choices-0-votes': '100', 535 'choices-0-ORDER': '1', 536 'choices-0-DELETE': '', 537 'choices-1-choice': 'Fergie', 538 'choices-1-votes': '900', 539 'choices-1-ORDER': '2', 540 'choices-1-DELETE': 'on', 541 'choices-2-choice': 'The Decemberists', 542 'choices-2-votes': '500', 543 'choices-2-ORDER': '0', 544 'choices-2-DELETE': '', 545 'choices-3-choice': '', 546 'choices-3-votes': '', 547 'choices-3-ORDER': '', 548 'choices-3-DELETE': '', 549 } 550 551 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 552 self.assertTrue(formset.is_valid()) 553 form_output = [] 554 555 for form in formset.ordered_forms: 556 form_output.append(form.cleaned_data) 557 558 self.assertEqual(form_output, [ 559 {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}, 560 {'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}, 561 ]) 562 self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]) 563 564 def test_invalid_deleted_form_with_ordering(self): 565 # Should be able to get ordered forms from a valid formset even if a 566 # deleted form would have been invalid. 567 568 class Person(Form): 569 name = CharField() 570 571 PeopleForm = formset_factory(form=Person, can_delete=True, can_order=True) 572 573 p = PeopleForm({ 574 'form-0-name': u'', 575 'form-0-DELETE': u'on', # no name! 576 'form-TOTAL_FORMS': 1, 577 'form-INITIAL_FORMS': 1, 578 'form-MAX_NUM_FORMS': 1 579 }) 580 581 self.assertTrue(p.is_valid()) 582 self.assertEqual(p.ordered_forms, []) 583 584 def test_clean_hook(self): 585 # FormSet clean hook ########################################################## 586 # FormSets have a hook for doing extra validation that shouldn't be tied to any 587 # particular form. It follows the same pattern as the clean hook on Forms. 588 589 # We start out with a some duplicate data. 590 591 data = { 592 'drinks-TOTAL_FORMS': '2', # the number of forms rendered 593 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 594 'drinks-MAX_NUM_FORMS': '0', # max number of forms 595 'drinks-0-name': 'Gin and Tonic', 596 'drinks-1-name': 'Gin and Tonic', 597 } 598 599 formset = FavoriteDrinksFormSet(data, prefix='drinks') 600 self.assertFalse(formset.is_valid()) 601 602 # Any errors raised by formset.clean() are available via the 603 # formset.non_form_errors() method. 604 605 for error in formset.non_form_errors(): 606 self.assertEqual(str(error), 'You may only specify a drink once.') 607 608 # Make sure we didn't break the valid case. 609 610 data = { 611 'drinks-TOTAL_FORMS': '2', # the number of forms rendered 612 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 613 'drinks-MAX_NUM_FORMS': '0', # max number of forms 614 'drinks-0-name': 'Gin and Tonic', 615 'drinks-1-name': 'Bloody Mary', 616 } 617 618 formset = FavoriteDrinksFormSet(data, prefix='drinks') 619 self.assertTrue(formset.is_valid()) 620 self.assertEqual(formset.non_form_errors(), []) 621 622 def test_limiting_max_forms(self): 623 # Limiting the maximum number of forms ######################################## 624 # Base case for max_num. 625 626 # When not passed, max_num will take its default value of None, i.e. unlimited 627 # number of forms, only controlled by the value of the extra parameter. 628 629 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3) 630 formset = LimitedFavoriteDrinkFormSet() 631 form_output = [] 632 633 for form in formset.forms: 634 form_output.append(str(form)) 635 636 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> 637<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> 638<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>""") 639 640 # If max_num is 0 then no form is rendered at all. 641 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=0) 642 formset = LimitedFavoriteDrinkFormSet() 643 form_output = [] 644 645 for form in formset.forms: 646 form_output.append(str(form)) 647 648 self.assertEqual('\n'.join(form_output), "") 649 650 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2) 651 formset = LimitedFavoriteDrinkFormSet() 652 form_output = [] 653 654 for form in formset.forms: 655 form_output.append(str(form)) 656 657 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> 658<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>""") 659 660 # Ensure that max_num has no effect when extra is less than max_num. 661 662 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) 663 formset = LimitedFavoriteDrinkFormSet() 664 form_output = [] 665 666 for form in formset.forms: 667 form_output.append(str(form)) 668 669 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>""") 670 671 def test_max_num_with_initial_data(self): 672 # max_num with initial data 673 674 # When not passed, max_num will take its default value of None, i.e. unlimited 675 # number of forms, only controlled by the values of the initial and extra 676 # parameters. 677 678 initial = [ 679 {'name': 'Fernet and Coke'}, 680 ] 681 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1) 682 formset = LimitedFavoriteDrinkFormSet(initial=initial) 683 form_output = [] 684 685 for form in formset.forms: 686 form_output.append(str(form)) 687 688 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Fernet and Coke" id="id_form-0-name" /></td></tr> 689<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>""") 690 691 def test_max_num_zero(self): 692 # If max_num is 0 then no form is rendered at all, even if extra and initial 693 # are specified. 694 695 initial = [ 696 {'name': 'Fernet and Coke'}, 697 {'name': 'Bloody Mary'}, 698 ] 699 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0) 700 formset = LimitedFavoriteDrinkFormSet(initial=initial) 701 form_output = [] 702 703 for form in formset.forms: 704 form_output.append(str(form)) 705 706 self.assertEqual('\n'.join(form_output), "") 707 708 def test_more_initial_than_max_num(self): 709 # More initial forms than max_num will result in only the first max_num of 710 # them to be displayed with no extra forms. 711 712 initial = [ 713 {'name': 'Gin Tonic'}, 714 {'name': 'Bloody Mary'}, 715 {'name': 'Jack and Coke'}, 716 ] 717 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) 718 formset = LimitedFavoriteDrinkFormSet(initial=initial) 719 form_output = [] 720 721 for form in formset.forms: 722 form_output.append(str(form)) 723 724 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> 725<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr>""") 726 727 # One form from initial and extra=3 with max_num=2 should result in the one 728 # initial form and one extra. 729 initial = [ 730 {'name': 'Gin Tonic'}, 731 ] 732 LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2) 733 formset = LimitedFavoriteDrinkFormSet(initial=initial) 734 form_output = [] 735 736 for form in formset.forms: 737 form_output.append(str(form)) 738 739 self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> 740<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>""") 741 742 def test_regression_6926(self): 743 # Regression test for #6926 ################################################## 744 # Make sure the management form has the correct prefix. 745 746 formset = FavoriteDrinksFormSet() 747 self.assertEqual(formset.management_form.prefix, 'form') 748 749 data = { 750 'form-TOTAL_FORMS': '2', 751 'form-INITIAL_FORMS': '0', 752 'form-MAX_NUM_FORMS': '0', 753 } 754 formset = FavoriteDrinksFormSet(data=data) 755 self.assertEqual(formset.management_form.prefix, 'form') 756 757 formset = FavoriteDrinksFormSet(initial={}) 758 self.assertEqual(formset.management_form.prefix, 'form') 759 760 def test_regression_12878(self): 761 # Regression test for #12878 ################################################# 762 763 data = { 764 'drinks-TOTAL_FORMS': '2', # the number of forms rendered 765 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data 766 'drinks-MAX_NUM_FORMS': '0', # max number of forms 767 'drinks-0-name': 'Gin and Tonic', 768 'drinks-1-name': 'Gin and Tonic', 769 } 770 771 formset = FavoriteDrinksFormSet(data, prefix='drinks') 772 self.assertFalse(formset.is_valid()) 773 self.assertEqual(formset.non_form_errors(), [u'You may only specify a drink once.']) 774 775 def test_formset_iteration(self): 776 # Regression tests for #16455 -- formset instances are iterable 777 ChoiceFormset = formset_factory(Choice, extra=3) 778 formset = ChoiceFormset() 779 780 # confirm iterated formset yields formset.forms 781 forms = list(formset) 782 self.assertEqual(forms, formset.forms) 783 self.assertEqual(len(formset), len(forms)) 784 785 # confirm indexing of formset 786 self.assertEqual(formset[0], forms[0]) 787 try: 788 formset[3] 789 self.fail('Requesting an invalid formset index should raise an exception') 790 except IndexError: 791 pass 792 793 # Formets can override the default iteration order 794 class BaseReverseFormSet(BaseFormSet): 795 def __iter__(self): 796 for form in reversed(self.forms): 797 yield form 798 799 ReverseChoiceFormset = formset_factory(Choice, BaseReverseFormSet, extra=3) 800 reverse_formset = ReverseChoiceFormset() 801 802 # confirm that __iter__ modifies rendering order 803 # compare forms from "reverse" formset with forms from original formset 804 self.assertEqual(str(reverse_formset[0]), str(forms[-1])) 805 self.assertEqual(str(reverse_formset[1]), str(forms[-2])) 806 self.assertEqual(len(reverse_formset), len(forms)) 807 808data = { 809 'choices-TOTAL_FORMS': '1', # the number of forms rendered 810 'choices-INITIAL_FORMS': '0', # the number of forms with initial data 811 'choices-MAX_NUM_FORMS': '0', # max number of forms 812 'choices-0-choice': 'Calexico', 813 'choices-0-votes': '100', 814} 815 816class Choice(Form): 817 choice = CharField() 818 votes = IntegerField() 819 820ChoiceFormSet = formset_factory(Choice) 821 822class FormsetAsFooTests(TestCase): 823 def test_as_table(self): 824 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 825 self.assertEqual(formset.as_table(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> 826<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" value="Calexico" /></td></tr> 827<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" value="100" /></td></tr>""") 828 829 def test_as_p(self): 830 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 831 self.assertEqual(formset.as_p(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> 832<p>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></p> 833<p>Votes: <input type="text" name="choices-0-votes" value="100" /></p>""") 834 835 def test_as_ul(self): 836 formset = ChoiceFormSet(data, auto_id=False, prefix='choices') 837 self.assertEqual(formset.as_ul(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" /> 838<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> 839<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>""") 840 841 842# Regression test for #11418 ################################################# 843class ArticleForm(Form): 844 title = CharField() 845 pub_date = DateField() 846 847ArticleFormSet = formset_factory(ArticleForm) 848 849class TestIsBoundBehavior(TestCase): 850 def test_no_data_raises_validation_error(self): 851 self.assertRaises(ValidationError, ArticleFormSet, {}) 852 853 def test_with_management_data_attrs_work_fine(self): 854 data = { 855 'form-TOTAL_FORMS': u'1', 856 'form-INITIAL_FORMS': u'0', 857 } 858 formset = ArticleFormSet(data) 859 self.assertEqual(0, formset.initial_form_count()) 860 self.assertEqual(1, formset.total_form_count()) 861 self.assertTrue(formset.is_bound) 862 self.assertTrue(formset.forms[0].is_bound) 863 self.assertTrue(formset.is_valid()) 864 self.assertTrue(formset.forms[0].is_valid()) 865 self.assertEqual([{}], formset.cleaned_data) 866 867 868 def test_form_errors_are_cought_by_formset(self): 869 data = { 870 'form-TOTAL_FORMS': u'2', 871 'form-INITIAL_FORMS': u'0', 872 'form-0-title': u'Test', 873 'form-0-pub_date': u'1904-06-16', 874 'form-1-title': u'Test', 875 'form-1-pub_date': u'', # <-- this date is missing but required 876 } 877 formset = ArticleFormSet(data) 878 self.assertFalse(formset.is_valid()) 879 self.assertEqual([{}, {'pub_date': [u'This field is required.']}], formset.errors) 880 881 def test_empty_forms_are_unbound(self): 882 data = { 883 'form-TOTAL_FORMS': u'1', 884 'form-INITIAL_FORMS': u'0', 885 'form-0-title': u'Test', 886 'form-0-pub_date': u'1904-06-16', 887 } 888 unbound_formset = ArticleFormSet() 889 bound_formset = ArticleFormSet(data) 890 891 empty_forms = [] 892 893 empty_forms.append(unbound_formset.empty_form) 894 empty_forms.append(bound_formset.empty_form) 895 896 # Empty forms should be unbound 897 self.assertFalse(empty_forms[0].is_bound) 898 self.assertFalse(empty_forms[1].is_bound) 899 900 # The empty forms should be equal. 901 self.assertEqual(empty_forms[0].as_p(), empty_forms[1].as_p()) 902 903class TestEmptyFormSet(TestCase): 904 "Test that an empty formset still calls clean()" 905 def test_empty_formset_is_valid(self): 906 EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate) 907 formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form") 908 formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form") 909 self.assertFalse(formset.is_valid()) 910 self.assertFalse(formset2.is_valid()) 911