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 | Field naming
|
---|
23 | ------------
|
---|
24 | If you include a Form in a SuperForm, with a name of 'postal_address',
|
---|
25 | the subform gets 'postal_address' as a prefix. When rendered, the
|
---|
26 | fields of the subform are named like 'postal_address-<name_of_field>'.
|
---|
27 |
|
---|
28 | Accessing the clean_data can be done like:
|
---|
29 | form.cleaned_data['postal_address']['name_of_field']
|
---|
30 |
|
---|
31 | Errors can be accessed in the same way:
|
---|
32 | form.errors['postal_address']['name_of_field']
|
---|
33 |
|
---|
34 | Field ordering during rendering
|
---|
35 | -------------------------------
|
---|
36 | SubForms are rendered in the order they're defined.
|
---|
37 | If the SuperForm has fields of its own (instead of just
|
---|
38 | subforms), those will be rendered together BEFORE
|
---|
39 | any of the subforms, in the order they're defined.
|
---|
40 | If you don't want that to happen, layout your forms
|
---|
41 | manually.
|
---|
42 | """
|
---|
43 | from django.utils.datastructures import SortedDict
|
---|
44 | from django.forms.fields import Field
|
---|
45 | from django.forms.util import StrAndUnicode, ErrorDict, ErrorList
|
---|
46 | from django.forms import Form
|
---|
47 |
|
---|
48 | __all__ = ('SuperForm', 'SubForm', 'FormList')
|
---|
49 |
|
---|
50 | NON_FIELD_ERRORS = '__all__'
|
---|
51 |
|
---|
52 | class 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 |
|
---|
62 | class 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 |
|
---|
104 | class 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 |
|
---|
189 | class 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 |
|
---|
218 | class 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 |
|
---|
350 | class SuperForm(BaseSuperForm):
|
---|
351 | __metaclass__ = DeclarativeSubFormsMetaclass
|
---|
352 |
|
---|
353 |
|
---|