summaryrefslogtreecommitdiff
path: root/tests/regressiontests/forms/formsets.py
blob: 1c4f3d91b89995e013386ecfc765dcda32449c7f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# -*- coding: utf-8 -*-
formset_tests = """
# Basic FormSet creation and usage ############################################

FormSet allows us to use multiple instance of the same form on 1 page. For now,
the best way to create a FormSet is by using the formset_for_form function.

>>> from django.newforms import Form, CharField, IntegerField
>>> from django.newforms.formsets import formset_for_form

>>> class Choice(Form):
...     choice = CharField()
...     votes = IntegerField()

>>> ChoiceFormSet = formset_for_form(Choice)


A FormSet constructor takes the same arguments as Form. Let's create a FormSet
for adding data. By default, it displays 1 blank form. It can display more,
but we'll look at how to do so later.

>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="text" name="choices-0-votes" /></li>

On thing to note is that there needs to be a special value in the data. This
value tells the FormSet how many forms were displayed so it can tell how
many forms it needs to clean and validate. You could use javascript to create
new forms on the client side, but they won't get validated unless you increment
the COUNT field appropriately.

>>> data = {
...     'choices-COUNT': '1', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
... }

We treat FormSet pretty much like we would treat a normal Form. FormSet has an
is_valid method, and a cleaned_data or errors attribute depending on whether all
the forms passed validation. However, unlike a Form instance, cleaned_data and
errors will be a list of dicts rather than just a single dict.

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
[{'votes': 100, 'choice': u'Calexico'}]


FormSet instances can also have an error attribute if validation failed for
any of the forms.

>>> data = {
...     'choices-COUNT': '1', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
[{'votes': [u'This field is required.']}]

Like a Form instance, cleaned_data won't exist if the formset wasn't validated.

>>> formset.cleaned_data
Traceback (most recent call last):
...
AttributeError: 'ChoiceFormSet' object has no attribute 'cleaned_data'


We can also prefill a FormSet with existing data by providing an ``initial``
argument to the constructor. ``initial`` should be a list of dicts. By default,
an extra blank form is included.

>>> initial = [{'choice': u'Calexico', 'votes': 100}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>


Let's simulate what would happen if we submitted this form.

>>> data = {
...     'choices-COUNT': '2', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-1-choice': '',
...     'choices-1-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
[{'votes': 100, 'choice': u'Calexico'}]

But the second form was blank! Shouldn't we get some errors? No. If we display
a form as blank, it's ok for it to be submitted as blank. If we fill out even
one of the fields of a blank form though, it will be validated. We may want to
required that at least x number of forms are completed, but we'll show how to
handle that later.

>>> data = {
...     'choices-COUNT': '2', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-1-choice': 'The Decemberists',
...     'choices-1-votes': '', # missing value
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'votes': [u'This field is required.']}]


If we delete data that was pre-filled, we should get an error. Simply removing
data from form fields isn't the proper way to delete it. We'll see how to
handle that case later.

>>> data = {
...     'choices-COUNT': '2', # the number of forms rendered
...     'choices-0-choice': '', # deleted value
...     'choices-0-votes': '', # deleted value
...     'choices-1-choice': '',
...     'choices-1-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]


# Displaying more than 1 blank form ###########################################

We can also display more than 1 empty form at a time. To do so, pass a
num_extra argument to formset_for_form.

>>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)

>>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" /></li>
<li>Votes: <input type="text" name="choices-0-votes" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>

Since we displayed every form as blank, we will also accept them back as blank.
This may seem a little strange, but later we will show how to require a minimum
number of forms to be completed.

>>> data = {
...     'choices-COUNT': '3', # the number of forms rendered
...     'choices-0-choice': '',
...     'choices-0-votes': '',
...     'choices-1-choice': '',
...     'choices-1-votes': '',
...     'choices-2-choice': '',
...     'choices-2-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
[]


We can just fill out one of the forms.

>>> data = {
...     'choices-COUNT': '3', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-1-choice': '',
...     'choices-1-votes': '',
...     'choices-2-choice': '',
...     'choices-2-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
[{'votes': 100, 'choice': u'Calexico'}]


And once again, if we try to partially complete a form, validation will fail.

>>> data = {
...     'choices-COUNT': '3', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-1-choice': 'The Decemberists',
...     'choices-1-votes': '', # missing value
...     'choices-2-choice': '',
...     'choices-2-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'votes': [u'This field is required.']}]


The num_extra argument also works when the formset is pre-filled with initial
data.

>>> initial = [{'choice': u'Calexico', 'votes': 100}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
<li>Votes: <input type="text" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>


If we try to skip a form, even if it was initially displayed as blank, we will
get an error.

>>> data = {
...     'choices-COUNT': '4', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-1-choice': '',
...     'choices-1-votes': '',
...     'choices-2-choice': 'The Decemberists',
...     'choices-2-votes': '12',
...     'choices-3-choice': '',
...     'choices-3-votes': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]


# FormSets with deletion ######################################################

We can easily add deletion ability to a FormSet with an agrument to
formset_for_form. This will add a boolean field to each form instance. When
that boolean field is True, the cleaned data will be in formset.deleted_data
rather than formset.cleaned_data

>>> ChoiceFormSet = formset_for_form(Choice, deletable=True)

>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>

To delete something, we just need to set that form's special delete field to
'on'. Let's go ahead and delete Fergie.

>>> data = {
...     'choices-COUNT': '3', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-0-DELETE': '',
...     'choices-1-choice': 'Fergie',
...     'choices-1-votes': '900',
...     'choices-1-DELETE': 'on',
...     'choices-2-choice': '',
...     'choices-2-votes': '',
...     'choices-2-DELETE': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> formset.cleaned_data
[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}]
>>> formset.deleted_data
[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]

# FormSets with ordering ######################################################

We can also add ordering ability to a FormSet with an agrument to
formset_for_form. This will add a integer field to each form instance. When
form validation succeeds, formset.cleaned_data will have the data in the correct
order specified by the ordering fields. If a number is duplicated in the set
of ordering fields, for instance form 0 and form 3 are both marked as 1, then
the form index used as a secondary ordering criteria. In order to put
something at the front of the list, you'd need to set it's order to 0.

>>> ChoiceFormSet = formset_for_form(Choice, orderable=True)

>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
<li>Votes: <input type="text" name="choices-2-votes" /></li>
<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>

>>> data = {
...     'choices-COUNT': '3', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-0-ORDER': '1',
...     'choices-1-choice': 'Fergie',
...     'choices-1-votes': '900',
...     'choices-1-ORDER': '2',
...     'choices-2-choice': 'The Decemberists',
...     'choices-2-votes': '500',
...     'choices-2-ORDER': '0',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> for cleaned_data in formset.cleaned_data:
...    print cleaned_data
{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}

# FormSets with ordering + deletion ###########################################

Let's try throwing ordering and deletion into the same form.

>>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)

>>> initial = [
...     {'choice': u'Calexico', 'votes': 100},
...     {'choice': u'Fergie', 'votes': 900},
...     {'choice': u'The Decemberists', 'votes': 500},
... ]
>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
>>> for form in formset.forms:
...    print form.as_ul()
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li>
<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li>
<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
<li>Votes: <input type="text" name="choices-3-votes" /></li>
<li>Order: <input type="text" name="choices-3-ORDER" value="4" /></li>
<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>

Let's delete Fergie, and put The Decemberists ahead of Calexico.

>>> data = {
...     'choices-COUNT': '4', # the number of forms rendered
...     'choices-0-choice': 'Calexico',
...     'choices-0-votes': '100',
...     'choices-0-ORDER': '1',
...     'choices-0-DELETE': '',
...     'choices-1-choice': 'Fergie',
...     'choices-1-votes': '900',
...     'choices-1-ORDER': '2',
...     'choices-1-DELETE': 'on',
...     'choices-2-choice': 'The Decemberists',
...     'choices-2-votes': '500',
...     'choices-2-ORDER': '0',
...     'choices-2-DELETE': '',
...     'choices-3-choice': '',
...     'choices-3-votes': '',
...     'choices-3-ORDER': '4',
...     'choices-3-DELETE': '',
... }

>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
>>> formset.is_valid()
True
>>> for cleaned_data in formset.cleaned_data:
...    print cleaned_data
{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}
{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}
>>> formset.deleted_data
[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]


"""