Django

Code

root/django/branches/newforms-admin/django/newforms/formsets.py

Revision 7855, 12.2 kB (checked in by brosner, 5 months ago)

newforms-admin: Allow an overridden _construct_form to easily pass parameters through to the form constructor in formsets.

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