Django

Code

root/django/trunk/django/forms/formsets.py

Revision 8630, 12.0 kB (checked in by brosner, 4 months ago)

Corrected a couple of typos in docstrings of methods in BaseFormSet?.

Line 
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
8
9 __all__ = ('BaseFormSet', 'all_valid')
10
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'
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         super(ManagementForm, self).__init__(*args, **kwargs)
27
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)
65        
66         # construct the forms in the formset
67         self._construct_forms()
68
69     def __unicode__(self):
70         return self.as_table()
71
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))
77    
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
98
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)
103
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)
108
109     # Maybe this should just go away?
110     def _get_cleaned_data(self):
111         """
112         Returns a list of form.cleaned_data dicts for every form in self.forms.
113         """
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)
118
119     def _get_deleted_forms(self):
120         """
121         Returns a list of forms that have been marked for deletion. Raises an
122         AttributeError if deletion is not allowed.
123         """
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)
139
140     def _get_ordered_forms(self):
141         """
142         Returns a list of form in the order specified by the incoming data.
143         Raises an AttributeError if deletion is not allowed.
144         """
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)
178
179     def non_form_errors(self):
180         """
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.
184         """
185         if self._non_form_errors is not None:
186             return self._non_form_errors
187         return self.error_class()
188
189     def _get_errors(self):
190         """
191         Returns a list of form.errors for every form in self.forms.
192         """
193         if self._errors is None:
194             self.full_clean()
195         return self._errors
196     errors = property(_get_errors)
197
198     def is_valid(self):
199         """
200         Returns True if form.errors is empty for every form in self.forms.
201         """
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())
211
212     def full_clean(self):
213         """
214         Cleans all of self.data and populates self._errors.
215         """
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
227
228     def clean(self):
229         """
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()
234         """
235         pass
236
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)
247
248     def add_prefix(self, index):
249         return '%s-%s' % (self.prefix, index)
250
251     def is_multipart(self):
252         """
253         Returns True if the formset needs to be multipart-encrypted, i.e. it
254         has FileInput. Otherwise, False.
255         """
256         return self.forms[0].is_multipart()
257
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)
266
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]))
274
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)
282
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
Note: See TracBrowser for help on using the browser.