2 from django
.utils
.encoding
import StrAndUnicode
3 from django
.utils
.safestring
import mark_safe
4 from django
.utils
.translation
import ugettext
as _
5 from fields
import IntegerField
, BooleanField
6 from widgets
import Media
, HiddenInput
7 from util
import ErrorList
, ValidationError
9 __all__
= ('BaseFormSet', 'all_valid')
12 TOTAL_FORM_COUNT
= 'TOTAL_FORMS'
13 INITIAL_FORM_COUNT
= 'INITIAL_FORMS'
14 ORDERING_FIELD_NAME
= 'ORDER'
15 DELETION_FIELD_NAME
= 'DELETE'
17 class ManagementForm(Form
):
19 ``ManagementForm`` is used to keep track of how many form instances
20 are displayed on the page. If adding new forms via javascript, you should
21 increment the count field of this form as well.
23 def __init__(self
, *args
, **kwargs
):
24 self
.base_fields
[TOTAL_FORM_COUNT
] = IntegerField(widget
=HiddenInput
)
25 self
.base_fields
[INITIAL_FORM_COUNT
] = IntegerField(widget
=HiddenInput
)
26 super(ManagementForm
, self
).__init
__(*args
, **kwargs
)
28 class BaseFormSet(StrAndUnicode
):
30 A collection of instances of the same Form class.
32 def __init__(self
, data
=None, files
=None, auto_id
='id_%s', prefix
=None,
33 initial
=None, error_class
=ErrorList
):
34 self
.is_bound
= data
is not None or files
is not None
35 self
.prefix
= prefix
or 'form'
36 self
.auto_id
= auto_id
39 self
.initial
= initial
40 self
.error_class
= error_class
42 self
._non
_form
_errors
= None
43 # initialization is different depending on whether we recieved data, initial, or nothing
45 self
.management_form
= ManagementForm(data
, auto_id
=self
.auto_id
, prefix
=self
.prefix
)
46 if self
.management_form
.is_valid():
47 self
._total
_form
_count
= self
.management_form
.cleaned_data
[TOTAL_FORM_COUNT
]
48 self
._initial
_form
_count
= self
.management_form
.cleaned_data
[INITIAL_FORM_COUNT
]
50 raise ValidationError('ManagementForm data is missing or has been tampered with')
53 self
._initial
_form
_count
= len(initial
)
54 if self
._initial
_form
_count
> self
.max_num
and self
.max_num
> 0:
55 self
._initial
_form
_count
= self
.max_num
56 self
._total
_form
_count
= self
._initial
_form
_count
+ self
.extra
58 self
._initial
_form
_count
= 0
59 self
._total
_form
_count
= self
.extra
60 if self
._total
_form
_count
> self
.max_num
and self
.max_num
> 0:
61 self
._total
_form
_count
= self
.max_num
62 initial
= {TOTAL_FORM_COUNT
: self
._total
_form
_count
,
63 INITIAL_FORM_COUNT
: self
._initial
_form
_count
}
64 self
.management_form
= ManagementForm(initial
=initial
, auto_id
=self
.auto_id
, prefix
=self
.prefix
)
66 # construct the forms in the formset
67 self
._construct
_forms
()
69 def __unicode__(self
):
70 return self
.as_table()
72 def _construct_forms(self
):
73 # instantiate all the forms and put them in self.forms
75 for i
in xrange(self
._total
_form
_count
):
76 self
.forms
.append(self
._construct
_form
(i
))
78 def _construct_form(self
, i
, **kwargs
):
80 Instantiates and returns the i-th form instance in a formset.
82 defaults
= {'auto_id': self
.auto_id
, 'prefix': self
.add_prefix(i
)}
83 if self
.data
or self
.files
:
84 defaults
['data'] = self
.data
85 defaults
['files'] = self
.files
88 defaults
['initial'] = self
.initial
[i
]
91 # Allow extra forms to be empty.
92 if i
>= self
._initial
_form
_count
:
93 defaults
['empty_permitted'] = True
94 defaults
.update(kwargs
)
95 form
= self
.form(**defaults
)
96 self
.add_fields(form
, i
)
99 def _get_initial_forms(self
):
100 """Return a list of all the intial forms in this formset."""
101 return self
.forms
[:self
._initial
_form
_count
]
102 initial_forms
= property(_get_initial_forms
)
104 def _get_extra_forms(self
):
105 """Return a list of all the extra forms in this formset."""
106 return self
.forms
[self
._initial
_form
_count
:]
107 extra_forms
= property(_get_extra_forms
)
109 # Maybe this should just go away?
110 def _get_cleaned_data(self
):
112 Returns a list of form.cleaned_data dicts for every form in self.forms.
114 if not self
.is_valid():
115 raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self
.__class
__.__name
__)
116 return [form
.cleaned_data
for form
in self
.forms
]
117 cleaned_data
= property(_get_cleaned_data
)
119 def _get_deleted_forms(self
):
121 Returns a list of forms that have been marked for deletion. Raises an
122 AttributeError if deletion is not allowed.
124 if not self
.is_valid() or not self
.can_delete
:
125 raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self
.__class
__.__name
__)
126 # construct _deleted_form_indexes which is just a list of form indexes
127 # that have had their deletion widget set to True
128 if not hasattr(self
, '_deleted_form_indexes'):
129 self
._deleted
_form
_indexes
= []
130 for i
in range(0, self
._total
_form
_count
):
132 # if this is an extra form and hasn't changed, don't consider it
133 if i
>= self
._initial
_form
_count
and not form
.has_changed():
135 if form
.cleaned_data
[DELETION_FIELD_NAME
]:
136 self
._deleted
_form
_indexes
.append(i
)
137 return [self
.forms
[i
] for i
in self
._deleted
_form
_indexes
]
138 deleted_forms
= property(_get_deleted_forms
)
140 def _get_ordered_forms(self
):
142 Returns a list of form in the order specified by the incoming data.
143 Raises an AttributeError if deletion is not allowed.
145 if not self
.is_valid() or not self
.can_order
:
146 raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self
.__class
__.__name
__)
147 # Construct _ordering, which is a list of (form_index, order_field_value)
148 # tuples. After constructing this list, we'll sort it by order_field_value
149 # so we have a way to get to the form indexes in the order specified
151 if not hasattr(self
, '_ordering'):
153 for i
in range(0, self
._total
_form
_count
):
155 # if this is an extra form and hasn't changed, don't consider it
156 if i
>= self
._initial
_form
_count
and not form
.has_changed():
158 # don't add data marked for deletion to self.ordered_data
159 if self
.can_delete
and form
.cleaned_data
[DELETION_FIELD_NAME
]:
161 # A sort function to order things numerically ascending, but
162 # None should be sorted below anything else. Allowing None as
163 # a comparison value makes it so we can leave ordering fields
165 def compare_ordering_values(x
, y
):
171 self
._ordering
.append((i
, form
.cleaned_data
[ORDERING_FIELD_NAME
]))
172 # After we're done populating self._ordering, sort it.
173 self
._ordering
.sort(compare_ordering_values
)
174 # Return a list of form.cleaned_data dicts in the order spcified by
176 return [self
.forms
[i
[0]] for i
in self
._ordering
]
177 ordered_forms
= property(_get_ordered_forms
)
179 def non_form_errors(self
):
181 Returns an ErrorList of errors that aren't associated with a particular
182 form -- i.e., from formset.clean(). Returns an empty ErrorList if there
185 if self
._non
_form
_errors
is not None:
186 return self
._non
_form
_errors
187 return self
.error_class()
189 def _get_errors(self
):
191 Returns a list of form.errors for every form in self.forms.
193 if self
._errors
is None:
196 errors
= property(_get_errors
)
200 Returns True if form.errors is empty for every form in self.forms.
202 if not self
.is_bound
:
204 # We loop over every form.errors here rather than short circuiting on the
205 # first failure to make sure validation gets triggered for every form.
207 for errors
in self
.errors
:
210 return forms_valid
and not bool(self
.non_form_errors())
212 def full_clean(self
):
214 Cleans all of self.data and populates self._errors.
217 if not self
.is_bound
: # Stop further processing.
219 for i
in range(0, self
._total
_form
_count
):
221 self
._errors
.append(form
.errors
)
222 # Give self.clean() a chance to do cross-form validation.
225 except ValidationError
, e
:
226 self
._non
_form
_errors
= e
.messages
230 Hook for doing any extra formset-wide cleaning after Form.clean() has
231 been called on every form. Any ValidationError raised by this method
232 will not be associated with a particular form; it will be accesible
233 via formset.non_form_errors()
237 def add_fields(self
, form
, index
):
238 """A hook for adding extra fields on to each form instance."""
240 # Only pre-fill the ordering field for initial forms.
241 if index
< self
._initial
_form
_count
:
242 form
.fields
[ORDERING_FIELD_NAME
] = IntegerField(label
=_(u
'Order'), initial
=index
+1, required
=False)
244 form
.fields
[ORDERING_FIELD_NAME
] = IntegerField(label
=_(u
'Order'), required
=False)
246 form
.fields
[DELETION_FIELD_NAME
] = BooleanField(label
=_(u
'Delete'), required
=False)
248 def add_prefix(self
, index
):
249 return '%s-%s' % (self
.prefix
, index
)
251 def is_multipart(self
):
253 Returns True if the formset needs to be multipart-encrypted, i.e. it
254 has FileInput. Otherwise, False.
256 return self
.forms
[0].is_multipart()
258 def _get_media(self
):
259 # All the forms on a FormSet are the same, so you only need to
260 # interrogate the first form for media.
262 return self
.forms
[0].media
265 media
= property(_get_media
)
268 "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
269 # XXX: there is no semantic division between forms here, there
270 # probably should be. It might make sense to render each form as a
271 # table row with each field as a td.
272 forms
= u
' '.join([form
.as_table() for form
in self
.forms
])
273 return mark_safe(u
'\n'.join([unicode(self
.management_form
), forms
]))
275 def formset_factory(form
, formset
=BaseFormSet
, extra
=1, can_order
=False,
276 can_delete
=False, max_num
=0):
277 """Return a FormSet for the given form class."""
278 attrs
= {'form': form
, 'extra': extra
,
279 'can_order': can_order
, 'can_delete': can_delete
,
281 return type(form
.__name
__ + 'FormSet', (formset
,), attrs
)
283 def all_valid(formsets
):
284 """Returns true if every formset in formsets is valid."""
286 for formset
in formsets
:
287 if not formset
.is_valid():