Ticket #3706: superform.2.py

File superform.2.py, 13.0 KB (added by sdolan, 14 years ago)

Removed a debug raise statement that slipped by.

Line 
1""" SuperForm: a form that can contain sub-forms or a list of them.
2
3Copyright (c) 2007, Jeroen van Dongen (jeroen<at>jkwadraat.net)
4All rights reserved. Licensed under the same license as Django.
5
6Known bugs
7----------
8 - Nesting a FormList of SuperForm's does not work as expected yet.
9 At least the rendering is screwed up, however given the error
10 pattern I suspect a more serious flaw.
11
12Introduction
13------------
14Often you need part of a form multiple times. Either in the same form or
15in different forms. SuperForm allows you to compose forms by using
16forms in almost the same way you use fields. The resulting form
17behaves just like any other Form, so you can even nest SuperForms
18in SuperForms. And mix SuperForms and regular Forms. Also, you can
19mix SubForms and regular Fields in a single SuperForm (just take note
20of the field ordering, see below).
21
22Field naming
23------------
24If you include a Form in a SuperForm, with a name of 'postal_address',
25the subform gets 'postal_address' as a prefix. When rendered, the
26fields of the subform are named like 'postal_address-<name_of_field>'.
27
28Accessing the clean_data can be done like:
29 form.cleaned_data['postal_address']['name_of_field']
30
31Errors can be accessed in the same way:
32 form.errors['postal_address']['name_of_field']
33
34Field ordering during rendering
35-------------------------------
36SubForms are rendered in the order they're defined.
37If the SuperForm has fields of its own (instead of just
38subforms), those will be rendered together BEFORE
39any of the subforms, in the order they're defined.
40If you don't want that to happen, layout your forms
41manually.
42"""
43from django.utils.datastructures import SortedDict
44from django.forms.fields import Field
45from django.forms.util import StrAndUnicode, ErrorDict, ErrorList
46from django.forms import Form
47
48__all__ = ('SuperForm', 'SubForm', 'FormList')
49
50NON_FIELD_ERRORS = '__all__'
51
52class SortedDictFromList(SortedDict):
53 "A dictionary that keeps its keys in the order in which they're inserted."
54 # This is different than django.utils.datastructures.SortedDict, because
55 # this takes a list/tuple as the argument to __init__().
56 def __init__(self, data=None):
57 if data is None:
58 data = []
59 self.key_order = [d[0] for d in data]
60 SortedDict.__init__(self, dict(data))
61
62class SubForm(object):
63 # Tracks each time a SubForm instance is created. Used to retain order.
64 creation_counter = 0
65
66 def __init__(self, form_def, required=True, initial=None):
67 self.form_def = form_def
68 self.required = required
69 self.initial = initial
70 self._form = None
71 self.creation_counter = SubForm.creation_counter
72 SubForm.creation_counter += 1
73
74 def ignore_errors(self):
75 return not (self.required or self._got_data(self._form))
76
77 def _got_data(self, form):
78 """ Determines if there's data submitted for this subform
79 """
80 for k in self.data.keys():
81 if k.startswith(form.prefix):
82 return True
83 return False
84
85 def is_valid(self):
86 if self._form.is_valid():
87 return True
88 else:
89 if self.ignore_errors():
90 return True
91 else:
92 return False
93
94 def init_form(self, prefix, auto_id="id_%s", initial=None,
95 data=None):
96 if initial is None:
97 initial = self.initial
98 self._form = self.form_def(data=data, prefix=prefix, auto_id=auto_id,
99 initial=initial)
100
101 def __getattr__(self, name):
102 return getattr(self._form, name)
103
104class FormList(SubForm):
105 def __init__(self, form_def, min_count=0, max_count=None,
106 initial_count=1, initial=None):
107 self.min_count = min_count
108 self.max_count = max_count
109 self.initial_count = initial_count
110 self._nf_errors = []
111 self.__errors = None
112 is_required = self.min_count > 0
113 super(FormList, self).__init__(form_def=form_def,
114 required=is_required,
115 initial=initial)
116
117 def init_form(self, prefix, auto_id="id_%s",
118 initial=None, data=None):
119 if initial is None:
120 initial = self.initial
121 if data is None:
122 count = self.initial_count
123 else:
124 # figure out how many items there are in the datadict
125 key = prefix
126 count = 0
127 for k in self.data.keys():
128 if k.startswith(key):
129 count += 1
130 self._forms = []
131 self.prefix = prefix
132 for i in range(0, count):
133 f = self.form_def(data=data, prefix=prefix+"-%s" % i,
134 auto_id=auto_id, initial=initial)
135 self._forms.append(f)
136
137 def _errors(self):
138 if self.__errors is None:
139 error_dict = ErrorDict()
140 for f in self._forms:
141 error_dict[self._forms.index(f)] = f.errors
142 if self._nf_errors:
143 error_dict[NON_FIELD_ERRORS]=self._nf_errors
144 self.__errors = error_dict
145 return self.__errors
146 errors = property(_errors)
147
148 def _cleaned_data(self):
149 if not hasattr(self, '__cleaned_data'):
150 cleaned_data = []
151 errors = False
152 for f in self.forms:
153 if hasattr(f, 'cleaned_data'):
154 cleaned_data.append(f.cleaned_data)
155 else:
156 if isinstance(f, SubForm) and f.ignore_errors():
157 continue
158 else:
159 raise AttributeError('cleaned_data did not exist on the form %s, and it is not a sublcass of SubForm, and not marked to ignore errors.' % f)
160 self.__cleaned_data = cleaned_data
161 return self.__cleaned_data
162 cleaned_data = property(_cleaned_data)
163
164 def is_valid(self):
165 valid_count = 0
166 for f in self._forms:
167 if f.is_valid():
168 valid_count += 1
169 continue
170 if self._got_data(f):
171 return False
172 if valid_count < self.min_count:
173 # not enough items
174 self._nf_errors.append(u'At least %s items are required' % self.min_count)
175 return False
176 if valid_count > self.max_count:
177 # too much items
178 self._nf_errors.append(u'No more than %s items are allowed' % self.max_count)
179 return False
180 return True
181
182 def as_table(self):
183 "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
184 subs = []
185 for f in self._forms:
186 subs.append(f.as_table())
187 return "\n".join(subs)
188
189class DeclarativeSubFormsMetaclass(type):
190 """
191 Metaclass that converts SubForm attributes to a dictionary called
192 'base_subforms', taking into account parent class 'base_subforms' as well.
193 """
194 def __new__(cls, name, bases, attrs):
195 subfields = [(fieldname, attrs.pop(fieldname))
196 for fieldname, obj in attrs.items()
197 if isinstance(obj, Field)]
198 subfields.sort(lambda x, y:
199 cmp(x[1].creation_counter,
200 y[1].creation_counter))
201
202 subforms = [(form_prefix, attrs.pop(form_prefix))
203 for form_prefix, obj in attrs.items()
204 if isinstance(obj, SubForm)]
205 subforms.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
206 # NOTE: we don't support subclassing of SuperForms yet.
207 # -----------------------------------------------------
208 # If this class is subclassing another SuperForm, add that SuperForm's subforms.
209 # Note that we loop over the bases in *reverse*. This is necessary in
210 # order to preserve the correct order of fields.
211 #for base in bases[::-1]:
212 # if hasattr(base, 'base_fields'):
213 # fields = base.base_fields.items() + fields
214 attrs['base_subfields'] = SortedDictFromList(subfields)
215 attrs['base_subforms'] = SortedDictFromList(subforms)
216 return type.__new__(cls, name, bases, attrs)
217
218class BaseSuperForm(StrAndUnicode):
219 def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
220 #print 'In BaseSuperForm', self.base_subforms
221 self.is_bound = data is not None
222 self.data = data
223 self.auto_id = auto_id
224 self.prefix = prefix
225 self.initial = initial or {}
226 self.__errors = None # Stores the errors after clean() has been called.
227
228 # create a list of subform instances
229 finst_list = []
230 # if we've fields of our own, collect them first and put
231 # 'm in a form of their own
232 if len(self.base_subfields) > 0:
233 self_form = Form(data=data, auto_id=auto_id,
234 prefix=self.prefix, initial=initial)
235 self_form.fields = self.base_subfields.copy()
236 finst_list.append(("_self", self_form,))
237
238 # now do our subforms ...
239 for name, fd in self.base_subforms.items():
240 subform_prefix = self.add_prefix(name)
241 fd.init_form(prefix=subform_prefix,
242 auto_id=auto_id,
243 initial=initial,
244 data=data)
245 finst_list.append((subform_prefix, fd))
246
247 self.forms = SortedDictFromList(finst_list)
248
249 def __unicode__(self):
250 return self.as_table()
251
252 def __iter__(self):
253 for form in self.forms.values():
254 for field in form:
255 yield field
256
257 def __getitem__(self, name):
258 """Returns a BoundField with the given name."""
259 try:
260 return self.forms[name]
261 except KeyError:
262 return self.forms['_self'][name]
263
264
265 def is_valid(self):
266 """
267 Returns True if all subforms are either valid or
268 empty and not required. False otherwise.
269 """
270 # first check if we're bound ...
271 if self.is_bound:
272 # then check every subform ...
273 for form in self.forms.values():
274 if not form.is_valid():
275 return False
276 else:
277 return False
278 return True
279
280 def add_prefix(self, field_name):
281 """
282 Returns the field name with a prefix appended, if this Form has a
283 prefix set.
284
285 Subclasses may wish to override.
286 """
287 return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
288
289 def as_table(self):
290 "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
291 subs = []
292 for f in self.forms.values():
293 subs.append(f.as_table())
294 return "\n".join(subs)
295
296 def as_ul(self):
297 "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
298 subs = []
299 for f in self.forms.values():
300 subs.append(f.as_ul())
301 return "\n".join(subs)
302
303 def as_p(self):
304 "Returns this form rendered as HTML <p>s."
305 subs = []
306 for f in self.forms.values():
307 subs.append(f.as_p())
308 return "\n".join(subs)
309
310 def _errors(self):
311 "Returns an ErrorDict for self.data"
312 if self.__errors is None:
313 # We may only have SubForms
314 error_dict = self.forms['_self'].errors if self.forms.get('_self') else {}
315 for k,f in self.forms.items():
316 if k == '_self':
317 continue
318 error_dict[k] = f.errors
319 self.__errors = error_dict
320 return self.__errors
321 errors = property(_errors)
322
323 def non_field_errors(self):
324 """
325 Returns an ErrorList of errors that aren't associated with a particular
326 field -- i.e., from Form.clean(). Returns an empty ErrorList if there
327 are none.
328 """
329 return self.errors.get(NON_FIELD_ERRORS, ErrorList())
330
331 def _cleaned_data(self):
332 if not hasattr(self, '__cleaned_data'):
333 cleaned_data = {}
334 errors = False
335 for k, f in self.forms.items():
336 if hasattr(f, 'cleaned_data'):
337 if k == '_self':
338 cleaned_data.update(f.cleaned_data)
339 else:
340 cleaned_data[k] = f.cleaned_data
341 else:
342 if isinstance(f, SubForm) and f.ignore_errors():
343 continue
344 else:
345 raise AttributeError('Form was not a subform and `ignore_errors` was not flagged: %s' % f)
346 self.__cleaned_data = cleaned_data
347 return self.__cleaned_data
348 cleaned_data = property(_cleaned_data)
349
350class SuperForm(BaseSuperForm):
351 __metaclass__ = DeclarativeSubFormsMetaclass
352
353
Back to Top