Added name column to all role lists.
[Melange.git] / app / django / forms / formsets.py
blob887f13034725a90bd04b3b0042ee53574508fa30
1 from forms import Form
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')
11 # special field names
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):
18 """
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.
22 """
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):
29 """
30 A collection of instances of the same Form class.
31 """
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
37 self.data = data
38 self.files = files
39 self.initial = initial
40 self.error_class = error_class
41 self._errors = None
42 self._non_form_errors = None
43 # initialization is different depending on whether we recieved data, initial, or nothing
44 if data or files:
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]
49 else:
50 raise ValidationError('ManagementForm data is missing or has been tampered with')
51 else:
52 if initial:
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
57 else:
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
74 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):
79 """
80 Instantiates and returns the i-th form instance in a formset.
81 """
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
86 if self.initial:
87 try:
88 defaults['initial'] = self.initial[i]
89 except IndexError:
90 pass
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)
97 return form
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):
131 form = self.forms[i]
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():
134 continue
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
150 # by the form data.
151 if not hasattr(self, '_ordering'):
152 self._ordering = []
153 for i in range(0, self._total_form_count):
154 form = self.forms[i]
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():
157 continue
158 # don't add data marked for deletion to self.ordered_data
159 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
160 continue
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
164 # blamk.
165 def compare_ordering_values(x, y):
166 if x[1] is None:
167 return 1
168 if y[1] is None:
169 return -1
170 return x[1] - y[1]
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
175 # the form data.
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
183 are none.
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:
194 self.full_clean()
195 return self._errors
196 errors = property(_get_errors)
198 def is_valid(self):
200 Returns True if form.errors is empty for every form in self.forms.
202 if not self.is_bound:
203 return False
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.
206 forms_valid = True
207 for errors in self.errors:
208 if bool(errors):
209 forms_valid = False
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.
216 self._errors = []
217 if not self.is_bound: # Stop further processing.
218 return
219 for i in range(0, self._total_form_count):
220 form = self.forms[i]
221 self._errors.append(form.errors)
222 # Give self.clean() a chance to do cross-form validation.
223 try:
224 self.clean()
225 except ValidationError, e:
226 self._non_form_errors = e.messages
228 def clean(self):
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()
235 pass
237 def add_fields(self, form, index):
238 """A hook for adding extra fields on to each form instance."""
239 if self.can_order:
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)
243 else:
244 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), required=False)
245 if self.can_delete:
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.
261 if self.forms:
262 return self.forms[0].media
263 else:
264 return Media()
265 media = property(_get_media)
267 def as_table(self):
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,
280 'max_num': max_num}
281 return type(form.__name__ + 'FormSet', (formset,), attrs)
283 def all_valid(formsets):
284 """Returns true if every formset in formsets is valid."""
285 valid = True
286 for formset in formsets:
287 if not formset.is_valid():
288 valid = False
289 return valid