1 | """ SuperForm: a form that can contain sub-forms or a list of them.
|
---|
2 |
|
---|
3 | Copyright (c) 2007, Jeroen van Dongen (jeroen<at>jkwadraat.net)
|
---|
4 | All rights reserved. Licensed under the same license as Django.
|
---|
5 |
|
---|
6 | Known 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 |
|
---|
12 | Introduction
|
---|
13 | ------------
|
---|
14 | Often you need part of a form multiple times. Either in the same form or
|
---|
15 | in different forms. SuperForm allows you to compose forms by using
|
---|
16 | forms in almost the same way you use fields. The resulting form
|
---|
17 | behaves just like any other Form, so you can even nest SuperForms
|
---|
18 | in SuperForms. And mix SuperForms and regular Forms. Also, you can
|
---|
19 | mix SubForms and regular Fields in a single SuperForm (just take note
|
---|
20 | of the field ordering, see below).
|
---|
21 |
|
---|
22 | See the doctests below for examples.
|
---|
23 |
|
---|
24 | Field naming
|
---|
25 | ------------
|
---|
26 | If you include a Form in a SuperForm, with a name of 'postal_address',
|
---|
27 | the subform gets 'postal_address' as a prefix. When rendered, the
|
---|
28 | fields of the subform are named like 'postal_address-<name_of_field>'.
|
---|
29 |
|
---|
30 | Accessing the clean_data can be done like:
|
---|
31 | form.clean_data['postal_address']['name_of_field']
|
---|
32 |
|
---|
33 | Errors can be accessed in the same way:
|
---|
34 | form.errors['postal_address']['name_of_field']
|
---|
35 |
|
---|
36 | Field ordering during rendering
|
---|
37 | -------------------------------
|
---|
38 | SubForms are rendered in the order they're defined.
|
---|
39 | If the SuperForm has fields of its own (instead of just
|
---|
40 | subforms), those will be rendered together BEFORE
|
---|
41 | any of the subforms, in the order they're defined.
|
---|
42 | If you don't want that to happen, layout your forms
|
---|
43 | manually.
|
---|
44 |
|
---|
45 | Doc 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
|
---|
58 | False
|
---|
59 | >>> f.as_table()
|
---|
60 | u'<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()
|
---|
70 | True
|
---|
71 | >>> f.clean_data['phone_num']
|
---|
72 | u'010 2207061'
|
---|
73 | >>> f.clean_data['visiting_address']['street']
|
---|
74 | u'visiting street'
|
---|
75 | >>> f.clean_data['postal_address']['street']
|
---|
76 | u'postal street'
|
---|
77 | >>> del data['postal_address-street']
|
---|
78 | >>> f = BusinessLocationForm(data=data)
|
---|
79 | >>> f.is_valid()
|
---|
80 | False
|
---|
81 | >>> f.clean_data['visiting_address']['street']
|
---|
82 | Traceback (most recent call last):
|
---|
83 | ...
|
---|
84 | AttributeError: 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()
|
---|
90 | True
|
---|
91 | >>> f.clean_data['phone_num']
|
---|
92 | u'010 2207061'
|
---|
93 | >>> f.clean_data['visiting_address']['street']
|
---|
94 | u'visiting street'
|
---|
95 | >>> f.clean_data['postal_address']['street']
|
---|
96 | Traceback (most recent call last):
|
---|
97 | ...
|
---|
98 | KeyError: '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
|
---|
107 | False
|
---|
108 | >>> f.as_table()
|
---|
109 | """
|
---|
110 |
|
---|
111 | from django.utils.datastructures import SortedDict
|
---|
112 | from django.newforms.fields import Field
|
---|
113 | from django.newforms.util import StrAndUnicode, ErrorDict, ErrorList, ValidationError
|
---|
114 | from django.newforms.forms import Form
|
---|
115 | import copy
|
---|
116 |
|
---|
117 | __all__ = ('SuperForm', 'SubForm', 'FormList')
|
---|
118 |
|
---|
119 | NON_FIELD_ERRORS = '__all__'
|
---|
120 | class 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 |
|
---|
129 | class 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 |
|
---|
171 | class 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 |
|
---|
255 | class 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 |
|
---|
280 | class 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 |
|
---|
411 | class SuperForm(BaseSuperForm):
|
---|
412 | __metaclass__ = DeclarativeSubFormsMetaclass
|
---|
413 |
|
---|
414 |
|
---|
415 | def _test():
|
---|
416 | import doctest
|
---|
417 | doctest.testmod()
|
---|
418 |
|
---|
419 | if __name__ == "__main__":
|
---|
420 | _test()
|
---|
421 |
|
---|
422 |
|
---|