Django

Code

root/django/branches/gis/django/forms/formsets.py

Revision 8215, 11.9 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7981-8001,8003-8011,8013-8033,8035-8036,8038-8039,8041-8063,8065-8076,8078-8139,8141-8154,8156-8214 via svnmerge from trunk.

Line 
1 from forms import Form
2 from django.utils.encoding import StrAndUnicode
3 from django.utils.safestring import mark_safe
4 from fields import IntegerField, BooleanField
5 from widgets import Media, HiddenInput
6 from util import ErrorList, ValidationError
7
8 __all__ = ('BaseFormSet', 'all_valid')
9
10 # special field names
11 TOTAL_FORM_COUNT = 'TOTAL_FORMS'
12 INITIAL_FORM_COUNT = 'INITIAL_FORMS'
13 ORDERING_FIELD_NAME = 'ORDER'
14 DELETION_FIELD_NAME = 'DELETE'
15
16 class ManagementForm(Form):
17     """
18     ``ManagementForm`` is used to keep track of how many form instances
19     are displayed on the page. If adding new forms via javascript, you should
20     increment the count field of this form as well.
21     """
22     def __init__(self, *args, **kwargs):
23         self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
24         self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
25         super(ManagementForm, self).__init__(*args, **kwargs)
26
27 class BaseFormSet(StrAndUnicode):
28     """
29     A collection of instances of the same Form class.
30     """
31     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
32                  initial=None, error_class=ErrorList):
33         self.is_bound = data is not None or files is not None
34         self.prefix = prefix or 'form'
35         self.auto_id = auto_id
36         self.data = data
37         self.files = files
38         self.initial = initial
39         self.error_class = error_class
40         self._errors = None
41         self._non_form_errors = None
42         # initialization is different depending on whether we recieved data, initial, or nothing
43         if data or files:
44             self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
45             if self.management_form.is_valid():
46                 self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
47                 self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
48             else:
49                 raise ValidationError('ManagementForm data is missing or has been tampered with')
50         else:
51             if initial:
52                 self._initial_form_count = len(initial)
53                 if self._initial_form_count > self.max_num and self.max_num > 0:
54                     self._initial_form_count = self.max_num
55                 self._total_form_count = self._initial_form_count + self.extra
56             else:
57                 self._initial_form_count = 0
58                 self._total_form_count = self.extra
59             if self._total_form_count > self.max_num and self.max_num > 0:
60                 self._total_form_count = self.max_num
61             initial = {TOTAL_FORM_COUNT: self._total_form_count,
62                        INITIAL_FORM_COUNT: self._initial_form_count}
63             self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
64        
65         # construct the forms in the formset
66         self._construct_forms()
67
68     def __unicode__(self):
69         return self.as_table()
70
71     def _construct_forms(self):
72         # instantiate all the forms and put them in self.forms
73         self.forms = []
74         for i in xrange(self._total_form_count):
75             self.forms.append(self._construct_form(i))
76    
77     def _construct_form(self, i, **kwargs):
78         """
79         Instantiates and returns the i-th form instance in a formset.
80         """
81         defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
82         if self.data or self.files:
83             defaults['data'] = self.data
84             defaults['files'] = self.files
85         if self.initial:
86             try:
87                 defaults['initial'] = self.initial[i]
88             except IndexError:
89                 pass
90         # Allow extra forms to be empty.
91         if i >= self._initial_form_count:
92             defaults['empty_permitted'] = True
93         defaults.update(kwargs)
94         form = self.form(**defaults)
95         self.add_fields(form, i)
96         return form
97
98     def _get_initial_forms(self):
99         """Return a list of all the intial forms in this formset."""
100         return self.forms[:self._initial_form_count]
101     initial_forms = property(_get_initial_forms)
102
103     def _get_extra_forms(self):
104         """Return a list of all the extra forms in this formset."""
105         return self.forms[self._initial_form_count:]
106     extra_forms = property(_get_extra_forms)
107
108     # Maybe this should just go away?
109     def _get_cleaned_data(self):
110         """
111         Returns a list of form.cleaned_data dicts for every form in self.forms.
112         """
113         if not self.is_valid():
114             raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
115         return [form.cleaned_data for form in self.forms]
116     cleaned_data = property(_get_cleaned_data)
117
118     def _get_deleted_forms(self):
119         """
120         Returns a list of forms that have been marked for deletion. Raises an
121         AttributeError is deletion is not allowed.
122         """
123         if not self.is_valid() or not self.can_delete:
124             raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
125         # construct _deleted_form_indexes which is just a list of form indexes
126         # that have had their deletion widget set to True
127         if not hasattr(self, '_deleted_form_indexes'):
128             self._deleted_form_indexes = []
129             for i in range(0, self._total_form_count):
130                 form = self.forms[i]
131                 # if this is an extra form and hasn't changed, don't consider it
132                 if i >= self._initial_form_count and not form.has_changed():
133                     continue
134                 if form.cleaned_data[DELETION_FIELD_NAME]:
135                     self._deleted_form_indexes.append(i)
136         return [self.forms[i] for i in self._deleted_form_indexes]
137     deleted_forms = property(_get_deleted_forms)
138
139     def _get_ordered_forms(self):
140         """
141         Returns a list of form in the order specified by the incoming data.
142         Raises an AttributeError is deletion is not allowed.
143         """
144         if not self.is_valid() or not self.can_order:
145             raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
146         # Construct _ordering, which is a list of (form_index, order_field_value)
147         # tuples. After constructing this list, we'll sort it by order_field_value
148         # so we have a way to get to the form indexes in the order specified
149         # by the form data.
150         if not hasattr(self, '_ordering'):
151             self._ordering = []
152             for i in range(0, self._total_form_count):
153                 form = self.forms[i]
154                 # if this is an extra form and hasn't changed, don't consider it
155                 if i >= self._initial_form_count and not form.has_changed():
156                     continue
157                 # don't add data marked for deletion to self.ordered_data
158                 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
159                     continue
160                 # A sort function to order things numerically ascending, but
161                 # None should be sorted below anything else. Allowing None as
162                 # a comparison value makes it so we can leave ordering fields
163                 # blamk.
164                 def compare_ordering_values(x, y):
165                     if x[1] is None:
166                         return 1
167                     if y[1] is None:
168                         return -1
169                     return x[1] - y[1]
170                 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
171             # After we're done populating self._ordering, sort it.
172             self._ordering.sort(compare_ordering_values)
173         # Return a list of form.cleaned_data dicts in the order spcified by
174         # the form data.
175         return [self.forms[i[0]] for i in self._ordering]
176     ordered_forms = property(_get_ordered_forms)
177
178     def non_form_errors(self):
179         """
180         Returns an ErrorList of errors that aren't associated with a particular
181         form -- i.e., from formset.clean(). Returns an empty ErrorList if there
182         are none.
183         """
184         if self._non_form_errors is not None:
185             return self._non_form_errors
186         return self.error_class()
187
188     def _get_errors(self):
189         """
190         Returns a list of form.errors for every form in self.forms.
191         """
192         if self._errors is None:
193             self.full_clean()
194         return self._errors
195     errors = property(_get_errors)
196
197     def is_valid(self):
198         """
199         Returns True if form.errors is empty for every form in self.forms.
200         """
201         if not self.is_bound:
202             return False
203         # We loop over every form.errors here rather than short circuiting on the
204         # first failure to make sure validation gets triggered for every form.
205         forms_valid = True
206         for errors in self.errors:
207             if bool(errors):
208                 forms_valid = False
209         return forms_valid and not bool(self.non_form_errors())
210
211     def full_clean(self):
212         """
213         Cleans all of self.data and populates self._errors.
214         """
215         self._errors = []
216         if not self.is_bound: # Stop further processing.
217             return
218         for i in range(0, self._total_form_count):
219             form = self.forms[i]
220             self._errors.append(form.errors)
221         # Give self.clean() a chance to do cross-form validation.
222         try:
223             self.clean()
224         except ValidationError, e:
225             self._non_form_errors = e.messages
226
227     def clean(self):
228         """
229         Hook for doing any extra formset-wide cleaning after Form.clean() has
230         been called on every form. Any ValidationError raised by this method
231         will not be associated with a particular form; it will be accesible
232         via formset.non_form_errors()
233         """
234         pass
235
236     def add_fields(self, form, index):
237         """A hook for adding extra fields on to each form instance."""
238         if self.can_order:
239             # Only pre-fill the ordering field for initial forms.
240             if index < self._initial_form_count:
241                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
242             else:
243                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
244         if self.can_delete:
245             form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
246
247     def add_prefix(self, index):
248         return '%s-%s' % (self.prefix, index)
249
250     def is_multipart(self):
251         """
252         Returns True if the formset needs to be multipart-encrypted, i.e. it
253         has FileInput. Otherwise, False.
254         """
255         return self.forms[0].is_multipart()
256
257     def _get_media(self):
258         # All the forms on a FormSet are the same, so you only need to
259         # interrogate the first form for media.
260         if self.forms:
261             return self.forms[0].media
262         else:
263             return Media()
264     media = property(_get_media)
265
266     def as_table(self):
267         "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
268         # XXX: there is no semantic division between forms here, there
269         # probably should be. It might make sense to render each form as a
270         # table row with each field as a td.
271         forms = u' '.join([form.as_table() for form in self.forms])
272         return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
273
274 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
275                     can_delete=False, max_num=0):
276     """Return a FormSet for the given form class."""
277     attrs = {'form': form, 'extra': extra,
278              'can_order': can_order, 'can_delete': can_delete,
279              'max_num': max_num}
280     return type(form.__name__ + 'FormSet', (formset,), attrs)
281
282 def all_valid(formsets):
283     """Returns true if every formset in formsets is valid."""
284     valid = True
285     for formset in formsets:
286         if not formset.is_valid():
287             valid = False
288     return valid
Note: See TracBrowser for help on using the browser.