Ticket #3706: superforms.py

File superforms.py, 15.4 KB (added by Jeroen van Dongen <jeroen at jkwadraat.net>, 17 years ago)
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
22See the doctests below for examples.
23
24Field naming
25------------
26If you include a Form in a SuperForm, with a name of 'postal_address',
27the subform gets 'postal_address' as a prefix. When rendered, the
28fields of the subform are named like 'postal_address-<name_of_field>'.
29
30Accessing the clean_data can be done like:
31 form.clean_data['postal_address']['name_of_field']
32
33Errors can be accessed in the same way:
34 form.errors['postal_address']['name_of_field']
35
36Field ordering during rendering
37-------------------------------
38SubForms are rendered in the order they're defined.
39If the SuperForm has fields of its own (instead of just
40subforms), those will be rendered together BEFORE
41any of the subforms, in the order they're defined.
42If you don't want that to happen, layout your forms
43manually.
44
45Doc tests
46---------
47
48>>> import django.newforms as forms
49>>> class AddressForm(forms.Form):
50... street = forms.CharField()
51... city = forms.CharField()
52>>> class BusinessLocationForm(SuperForm):
53... phone_num = forms.CharField()
54... visiting_address = SubForm(AddressForm)
55... postal_address = SubForm(AddressForm, required=False)
56>>> f = BusinessLocationForm()
57>>> f.is_bound
58False
59>>> f.as_table()
60u'<tr><th><label for="id_phone_num">Phone num:</label></th><td><input type="text" name="phone_num" id="id_phone_num" /></td></tr>\\n<tr><th><label for="id_visiting_address-street">Street:</label></th><td><input type="text" name="visiting_address-street" id="id_visiting_address-street" /></td></tr>\\n<tr><th><label for="id_visiting_address-city">City:</label></th><td><input type="text" name="visiting_address-city" id="id_visiting_address-city" /></td></tr>\\n<tr><th><label for="id_postal_address-street">Street:</label></th><td><input type="text" name="postal_address-street" id="id_postal_address-street" /></td></tr>\\n<tr><th><label for="id_postal_address-city">City:</label></th><td><input type="text" name="postal_address-city" id="id_postal_address-city" /></td></tr>'
61>>> data = {
62... 'phone_num': '010 2207061',
63... 'visiting_address-street': 'visiting street',
64... 'visiting_address-city': 'visiting city',
65... 'postal_address-street': 'postal street',
66... 'postal_address-city': 'postal city',
67... }
68>>> f = BusinessLocationForm(data=data)
69>>> f.is_valid()
70True
71>>> f.clean_data['phone_num']
72u'010 2207061'
73>>> f.clean_data['visiting_address']['street']
74u'visiting street'
75>>> f.clean_data['postal_address']['street']
76u'postal street'
77>>> del data['postal_address-street']
78>>> f = BusinessLocationForm(data=data)
79>>> f.is_valid()
80False
81>>> f.clean_data['visiting_address']['street']
82Traceback (most recent call last):
83 ...
84AttributeError: clean_data
85>>> f.errors['postal_address']['street']
86[u'This field is required.']
87>>> del data['postal_address-city']
88>>> f = BusinessLocationForm(data=data)
89>>> f.is_valid()
90True
91>>> f.clean_data['phone_num']
92u'010 2207061'
93>>> f.clean_data['visiting_address']['street']
94u'visiting street'
95>>> f.clean_data['postal_address']['street']
96Traceback (most recent call last):
97 ...
98KeyError: 'postal_address'
99>>> class TestForm1(SuperForm):
100... visiting_address = SubForm(AddressForm)
101... postal_address = SubForm(AddressForm)
102>>> class TestForm2(SuperForm):
103... name = forms.CharField()
104... locations = FormList(TestForm1, initial_count=2)
105>>> f = TestForm2()
106>>> f.is_bound
107False
108>>> f.as_table()
109"""
110
111from django.utils.datastructures import SortedDict
112from django.newforms.fields import Field
113from django.newforms.util import StrAndUnicode, ErrorDict, ErrorList, ValidationError
114from django.newforms.forms import Form
115import copy
116
117__all__ = ('SuperForm', 'SubForm', 'FormList')
118
119NON_FIELD_ERRORS = '__all__'
120class SortedDictFromList(SortedDict):
121 "A dictionary that keeps its keys in the order in which they're inserted."
122 # This is different than django.utils.datastructures.SortedDict, because
123 # this takes a list/tuple as the argument to __init__().
124 def __init__(self, data=None):
125 if data is None: data = []
126 self.keyOrder = [d[0] for d in data]
127 dict.__init__(self, dict(data))
128
129class SubForm(object):
130 # Tracks each time a SubForm instance is created. Used to retain order.
131 creation_counter = 0
132
133 def __init__(self, form_def, required=True, initial=None):
134 self.form_def = form_def
135 self.required=required
136 self.initial = initial
137 self._form = None
138 self.creation_counter = SubForm.creation_counter
139 SubForm.creation_counter += 1
140
141 def ignore_errors(self):
142 return not (self.required or self._got_data(self._form))
143
144 def _got_data(self, form):
145 """ Determines if there's data submitted for this subform
146 """
147 for k in self.data.keys():
148 if k.startswith(form.prefix):
149 return True
150 return False
151
152 def is_valid(self):
153 if self._form.is_valid():
154 return True
155 else:
156 if self.ignore_errors():
157 return True
158 else:
159 return False
160
161 def init_form(self, prefix, auto_id="id_%s", initial=None,
162 data=None):
163 if initial is None:
164 initial = self.initial
165 self._form = self.form_def(data=data, prefix=prefix, auto_id=auto_id,
166 initial=initial)
167
168 def __getattr__(self, name):
169 return getattr(self._form, name)
170
171class FormList(SubForm):
172 def __init__(self, form_def, min_count=0, max_count=None,
173 initial_count=1, initial=None):
174 self.min_count=min_count
175 self.max_count=max_count
176 self.initial_count = initial_count
177 self._nf_errors = []
178 self.__errors = None
179 super(FormList, self).__init__(form_def=form_def,
180 required=(min_count>0),
181 initial=initial)
182
183 def init_form(self, prefix, auto_id="id_%s", initial=None,
184 data=None):
185 if initial is None:
186 initial = self.initial
187 if data is None:
188 count = self.initial_count
189 else:
190 # figure out how many items there are in the datadict
191 key = prefix
192 count = 0
193 for k in self.data.keys():
194 if k.startswith(key):
195 count += 1
196 self._forms = []
197 self.prefix = prefix
198 for i in range(0, count):
199 f = self.form_def(data=data, prefix=prefix+"-%s" % i,
200 auto_id=auto_id, initial=initial)
201 self._forms.append(f)
202
203 def _errors(self):
204 if self.__errors is None:
205 error_dict = ErrorDict()
206 for f in self._forms:
207 error_dict[self._forms.index(f)] = f.errors
208 if self._nf_errors:
209 error_dict[NON_FIELD_ERRORS]=self._nf_errors
210 self.__errors = error_dict
211 return self.__errors
212 errors = property(_errors)
213
214 def _clean_data(self):
215 if not hasattr(self, '__clean_data'):
216 clean_data = []
217 errors = False
218 for f in self.forms:
219 if hasattr(f, 'clean_data'):
220 clean_data.append(f.clean_data)
221 else:
222 if isinstance(f, SubForm) and f.ignore_errors():
223 continue
224 else:
225 raise AttributeError, 'clean_data'
226 self.__clean_data = clean_data
227 return self.__clean_data
228 clean_data = property(_clean_data)
229
230 def is_valid(self):
231 valid_count = 0
232 for f in self._forms:
233 if f.is_valid():
234 valid_count += 1
235 continue
236 if self._got_data(f):
237 return False
238 if valid_count < self.min_count:
239 # not enough items
240 self._nf_errors.append(u'At least %s items are required' % self.min_count)
241 return False
242 if valid_count > self.max_count:
243 # too much items
244 self._nf_errors.append(u'No more than %s items are allowed' % self.max_count)
245 return False
246 return True
247
248 def as_table(self):
249 "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
250 subs = []
251 for f in self._forms:
252 subs.append(f.as_table())
253 return "\n".join(subs)
254
255class DeclarativeSubFormsMetaclass(type):
256 """
257 Metaclass that converts SubForm attributes to a dictionary called
258 'base_subforms', taking into account parent class 'base_subforms' as well.
259 """
260 def __new__(cls, name, bases, attrs):
261 subfields = [(fieldname, attrs.pop(fieldname)) for fieldname, obj in attrs.items() if isinstance(obj, Field)]
262 subfields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
263
264 subforms = [(form_prefix, attrs.pop(form_prefix)) for form_prefix, obj in attrs.items() if isinstance(obj, SubForm)]
265 subforms.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
266
267 # NOTE: we don't support subclassing of SuperForms yet.
268 # -----------------------------------------------------
269 # If this class is subclassing another SuperForm, add that SuperForm's subforms.
270 # Note that we loop over the bases in *reverse*. This is necessary in
271 # order to preserve the correct order of fields.
272 #for base in bases[::-1]:
273 # if hasattr(base, 'base_fields'):
274 # fields = base.base_fields.items() + fields
275
276 attrs['base_subfields'] = SortedDictFromList(subfields)
277 attrs['base_subforms'] = SortedDictFromList(subforms)
278 return type.__new__(cls, name, bases, attrs)
279
280class BaseSuperForm(StrAndUnicode):
281 def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
282 self.is_bound = data is not None
283 self.data = data
284 self.auto_id = auto_id
285 self.prefix = prefix
286 self.initial = initial or {}
287 self.__errors = None # Stores the errors after clean() has been called.
288
289 # create a list of subform instances
290 finst_list = []
291 # if we've fields of our own, collect them first and put
292 # 'm in a form of their own
293 if len(self.base_subfields) > 0:
294 self_form = Form(data=data, auto_id=auto_id,
295 prefix=self.prefix, initial=initial)
296 self_form.fields = self.base_subfields.copy()
297 finst_list.append( ("_self", self_form,) )
298
299 # now do our subforms ...
300 for (name, fd) in self.base_subforms.items():
301 subform_prefix = self.add_prefix(name)
302 fd.init_form(prefix=subform_prefix,
303 auto_id=auto_id,
304 initial=initial,
305 data=data)
306 finst_list.append( (subform_prefix, fd,) )
307 self.forms = SortedDictFromList(finst_list)
308
309 def __unicode__(self):
310 return self.as_table()
311
312 def __iter__(self):
313 for form in self.forms.values():
314 for field in form:
315 yield field
316
317 def __getitem__(self, name):
318 """Returns a BoundField with the given name.
319
320 """
321 try:
322 return self.forms[name]
323 except KeyError:
324 return self.forms['_self'][name]
325
326
327 def is_valid(self):
328 """
329 Returns True if all subforms are either valid or
330 empty and not required. False otherwise.
331 """
332 # first check if we're bound ...
333 if self.is_bound:
334 # then check every subform ...
335 for form in self.forms.values():
336 if not form.is_valid():
337 return False
338 else:
339 return False
340 return True
341
342 def add_prefix(self, field_name):
343 """
344 Returns the field name with a prefix appended, if this Form has a
345 prefix set.
346
347 Subclasses may wish to override.
348 """
349 return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
350
351 def as_table(self):
352 "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
353 subs = []
354 for f in self.forms.values():
355 subs.append(f.as_table())
356 return "\n".join(subs)
357
358 def as_ul(self):
359 "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
360 subs = []
361 for f in self.forms.values():
362 subs.append(f.as_ul())
363 return "\n".join(subs)
364
365 def as_p(self):
366 "Returns this form rendered as HTML <p>s."
367 subs = []
368 for f in self.forms.values():
369 subs.append(f.as_p())
370 return "\n".join(subs)
371
372 def _errors(self):
373 "Returns an ErrorDict for self.data"
374 if self.__errors is None:
375 error_dict = self.forms['_self'].errors
376 for k,f in self.forms.items():
377 if k == '_self':
378 continue
379 error_dict[k] = f.errors
380 self.__errors = error_dict
381 return self.__errors
382 errors = property(_errors)
383
384 def non_field_errors(self):
385 """
386 Returns an ErrorList of errors that aren't associated with a particular
387 field -- i.e., from Form.clean(). Returns an empty ErrorList if there
388 are none.
389 """
390 return self.errors.get(NON_FIELD_ERRORS, ErrorList())
391
392 def _clean_data(self):
393 if not hasattr(self, '__clean_data'):
394 clean_data = {}
395 errors = False
396 for k, f in self.forms.items():
397 if hasattr(f, 'clean_data'):
398 if k == '_self':
399 clean_data.update(f.clean_data)
400 else:
401 clean_data[k] = f.clean_data
402 else:
403 if isinstance(f, SubForm) and f.ignore_errors():
404 continue
405 else:
406 raise AttributeError, 'clean_data'
407 self.__clean_data = clean_data
408 return self.__clean_data
409 clean_data = property(_clean_data)
410
411class SuperForm(BaseSuperForm):
412 __metaclass__ = DeclarativeSubFormsMetaclass
413
414
415def _test():
416 import doctest
417 doctest.testmod()
418
419if __name__ == "__main__":
420 _test()
421
422
Back to Top