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: data = []
|
---|
58 | self.key_order = [d[0] for d in data]
|
---|
59 | SortedDict.__init__(self, dict(data))
|
---|
60 |
|
---|
61 | class SubForm(object):
|
---|
62 | # Tracks each time a SubForm instance is created. Used to retain order.
|
---|
63 | creation_counter = 0
|
---|
64 |
|
---|
65 | def __init__(self, form_def, required=True, initial=None):
|
---|
66 | self.form_def = form_def
|
---|
67 | self.required=required
|
---|
68 | self.initial = initial
|
---|
69 | self._form = None
|
---|
70 | self.creation_counter = SubForm.creation_counter
|
---|
71 | SubForm.creation_counter += 1
|
---|
72 |
|
---|
73 | def ignore_errors(self):
|
---|
74 | return not (self.required or self._got_data(self._form))
|
---|
75 |
|
---|
76 | def _got_data(self, form):
|
---|
77 | """ Determines if there's data submitted for this subform
|
---|
78 | """
|
---|
79 | for k in self.data.keys():
|
---|
80 | if k.startswith(form.prefix):
|
---|
81 | return True
|
---|
82 | return False
|
---|
83 |
|
---|
84 | def is_valid(self):
|
---|
85 | if self._form.is_valid():
|
---|
86 | return True
|
---|
87 | else:
|
---|
88 | if self.ignore_errors():
|
---|
89 | return True
|
---|
90 | else:
|
---|
91 | return False
|
---|
92 |
|
---|
93 | def init_form(self, prefix, auto_id="id_%s", initial=None,
|
---|
94 | data=None):
|
---|
95 | if initial is None:
|
---|
96 | initial = self.initial
|
---|
97 | self._form = self.form_def(data=data, prefix=prefix, auto_id=auto_id,
|
---|
98 | initial=initial)
|
---|
99 |
|
---|
100 | def __getattr__(self, name):
|
---|
101 | return getattr(self._form, name)
|
---|
102 |
|
---|
103 | class FormList(SubForm):
|
---|
104 | def __init__(self, form_def, min_count=0, max_count=None,
|
---|
105 | initial_count=1, initial=None):
|
---|
106 | self.min_count=min_count
|
---|
107 | self.max_count=max_count
|
---|
108 | self.initial_count = initial_count
|
---|
109 | self._nf_errors = []
|
---|
110 | self.__errors = None
|
---|
111 | super(FormList, self).__init__(form_def=form_def,
|
---|
112 | required=(min_count>0),
|
---|
113 | initial=initial)
|
---|
114 |
|
---|
115 | def init_form(self, prefix, auto_id="id_%s", initial=None,
|
---|
116 | data=None):
|
---|
117 | if initial is None:
|
---|
118 | initial = self.initial
|
---|
119 | if data is None:
|
---|
120 | count = self.initial_count
|
---|
121 | else:
|
---|
122 | # figure out how many items there are in the datadict
|
---|
123 | key = prefix
|
---|
124 | count = 0
|
---|
125 | for k in self.data.keys():
|
---|
126 | if k.startswith(key):
|
---|
127 | count += 1
|
---|
128 | self._forms = []
|
---|
129 | self.prefix = prefix
|
---|
130 | for i in range(0, count):
|
---|
131 | f = self.form_def(data=data, prefix=prefix+"-%s" % i,
|
---|
132 | auto_id=auto_id, initial=initial)
|
---|
133 | self._forms.append(f)
|
---|
134 |
|
---|
135 | def _errors(self):
|
---|
136 | if self.__errors is None:
|
---|
137 | error_dict = ErrorDict()
|
---|
138 | for f in self._forms:
|
---|
139 | error_dict[self._forms.index(f)] = f.errors
|
---|
140 | if self._nf_errors:
|
---|
141 | error_dict[NON_FIELD_ERRORS]=self._nf_errors
|
---|
142 | self.__errors = error_dict
|
---|
143 | return self.__errors
|
---|
144 | errors = property(_errors)
|
---|
145 |
|
---|
146 | def _cleaned_data(self):
|
---|
147 | raise
|
---|
148 | if not hasattr(self, '__cleaned_data'):
|
---|
149 | cleaned_data = []
|
---|
150 | errors = False
|
---|
151 | for f in self.forms:
|
---|
152 | if hasattr(f, 'cleaned_data'):
|
---|
153 | cleaned_data.append(f.cleaned_data)
|
---|
154 | else:
|
---|
155 | if isinstance(f, SubForm) and f.ignore_errors():
|
---|
156 | continue
|
---|
157 | else:
|
---|
158 | raise AttributeError, 'cleaned_data'
|
---|
159 | self.__cleaned_data = cleaned_data
|
---|
160 | return self.__cleaned_data
|
---|
161 | cleaned_data = property(_cleaned_data)
|
---|
162 |
|
---|
163 | def is_valid(self):
|
---|
164 | valid_count = 0
|
---|
165 | for f in self._forms:
|
---|
166 | if f.is_valid():
|
---|
167 | valid_count += 1
|
---|
168 | continue
|
---|
169 | if self._got_data(f):
|
---|
170 | return False
|
---|
171 | if valid_count < self.min_count:
|
---|
172 | # not enough items
|
---|
173 | self._nf_errors.append(u'At least %s items are required' % self.min_count)
|
---|
174 | return False
|
---|
175 | if valid_count > self.max_count:
|
---|
176 | # too much items
|
---|
177 | self._nf_errors.append(u'No more than %s items are allowed' % self.max_count)
|
---|
178 | return False
|
---|
179 | return True
|
---|
180 |
|
---|
181 | def as_table(self):
|
---|
182 | "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
|
---|
183 | subs = []
|
---|
184 | for f in self._forms:
|
---|
185 | subs.append(f.as_table())
|
---|
186 | return "\n".join(subs)
|
---|
187 |
|
---|
188 | class DeclarativeSubFormsMetaclass(type):
|
---|
189 | """
|
---|
190 | Metaclass that converts SubForm attributes to a dictionary called
|
---|
191 | 'base_subforms', taking into account parent class 'base_subforms' as well.
|
---|
192 | """
|
---|
193 | def __new__(cls, name, bases, attrs):
|
---|
194 | subfields = [(fieldname, attrs.pop(fieldname))
|
---|
195 | for fieldname, obj in attrs.items()
|
---|
196 | if isinstance(obj, Field)]
|
---|
197 | subfields.sort(lambda x, y:
|
---|
198 | cmp(x[1].creation_counter,
|
---|
199 | y[1].creation_counter))
|
---|
200 |
|
---|
201 | subforms = [(form_prefix, attrs.pop(form_prefix))
|
---|
202 | for form_prefix, obj in attrs.items()
|
---|
203 | if isinstance(obj, SubForm)]
|
---|
204 | subforms.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
|
---|
205 | # NOTE: we don't support subclassing of SuperForms yet.
|
---|
206 | # -----------------------------------------------------
|
---|
207 | # If this class is subclassing another SuperForm, add that SuperForm's subforms.
|
---|
208 | # Note that we loop over the bases in *reverse*. This is necessary in
|
---|
209 | # order to preserve the correct order of fields.
|
---|
210 | #for base in bases[::-1]:
|
---|
211 | # if hasattr(base, 'base_fields'):
|
---|
212 | # fields = base.base_fields.items() + fields
|
---|
213 | attrs['base_subfields'] = SortedDictFromList(subfields)
|
---|
214 | attrs['base_subforms'] = SortedDictFromList(subforms)
|
---|
215 | return type.__new__(cls, name, bases, attrs)
|
---|
216 |
|
---|
217 | class BaseSuperForm(StrAndUnicode):
|
---|
218 | def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None):
|
---|
219 | #print 'In BaseSuperForm', self.base_subforms
|
---|
220 | self.is_bound = data is not None
|
---|
221 | self.data = data
|
---|
222 | self.auto_id = auto_id
|
---|
223 | self.prefix = prefix
|
---|
224 | self.initial = initial or {}
|
---|
225 | self.__errors = None # Stores the errors after clean() has been called.
|
---|
226 |
|
---|
227 | # create a list of subform instances
|
---|
228 | finst_list = []
|
---|
229 | # if we've fields of our own, collect them first and put
|
---|
230 | # 'm in a form of their own
|
---|
231 | if len(self.base_subfields) > 0:
|
---|
232 | self_form = Form(data=data, auto_id=auto_id,
|
---|
233 | prefix=self.prefix, initial=initial)
|
---|
234 | self_form.fields = self.base_subfields.copy()
|
---|
235 | finst_list.append( ("_self", self_form,) )
|
---|
236 |
|
---|
237 | # now do our subforms ...
|
---|
238 | for name, fd in self.base_subforms.items():
|
---|
239 | subform_prefix = self.add_prefix(name)
|
---|
240 | fd.init_form(prefix=subform_prefix,
|
---|
241 | auto_id=auto_id,
|
---|
242 | initial=initial,
|
---|
243 | data=data)
|
---|
244 | finst_list.append((subform_prefix, fd))
|
---|
245 |
|
---|
246 | self.forms = SortedDictFromList(finst_list)
|
---|
247 |
|
---|
248 | def __unicode__(self):
|
---|
249 | return self.as_table()
|
---|
250 |
|
---|
251 | def __iter__(self):
|
---|
252 | for form in self.forms.values():
|
---|
253 | for field in form:
|
---|
254 | yield field
|
---|
255 |
|
---|
256 | def __getitem__(self, name):
|
---|
257 | """Returns a BoundField with the given name."""
|
---|
258 | try:
|
---|
259 | return self.forms[name]
|
---|
260 | except KeyError:
|
---|
261 | return self.forms['_self'][name]
|
---|
262 |
|
---|
263 |
|
---|
264 | def is_valid(self):
|
---|
265 | """
|
---|
266 | Returns True if all subforms are either valid or
|
---|
267 | empty and not required. False otherwise.
|
---|
268 | """
|
---|
269 | # first check if we're bound ...
|
---|
270 | if self.is_bound:
|
---|
271 | # then check every subform ...
|
---|
272 | for form in self.forms.values():
|
---|
273 | if not form.is_valid():
|
---|
274 | return False
|
---|
275 | else:
|
---|
276 | return False
|
---|
277 | return True
|
---|
278 |
|
---|
279 | def add_prefix(self, field_name):
|
---|
280 | """
|
---|
281 | Returns the field name with a prefix appended, if this Form has a
|
---|
282 | prefix set.
|
---|
283 |
|
---|
284 | Subclasses may wish to override.
|
---|
285 | """
|
---|
286 | return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
|
---|
287 |
|
---|
288 | def as_table(self):
|
---|
289 | "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
|
---|
290 | subs = []
|
---|
291 | for f in self.forms.values():
|
---|
292 | subs.append(f.as_table())
|
---|
293 | return "\n".join(subs)
|
---|
294 |
|
---|
295 | def as_ul(self):
|
---|
296 | "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
|
---|
297 | subs = []
|
---|
298 | for f in self.forms.values():
|
---|
299 | subs.append(f.as_ul())
|
---|
300 | return "\n".join(subs)
|
---|
301 |
|
---|
302 | def as_p(self):
|
---|
303 | "Returns this form rendered as HTML <p>s."
|
---|
304 | subs = []
|
---|
305 | for f in self.forms.values():
|
---|
306 | subs.append(f.as_p())
|
---|
307 | return "\n".join(subs)
|
---|
308 |
|
---|
309 | def _errors(self):
|
---|
310 | "Returns an ErrorDict for self.data"
|
---|
311 | if self.__errors is None:
|
---|
312 | # We may only have SubForms
|
---|
313 | error_dict = self.forms['_self'].errors if self.forms.get('_self') else {}
|
---|
314 | for k,f in self.forms.items():
|
---|
315 | if k == '_self':
|
---|
316 | continue
|
---|
317 | error_dict[k] = f.errors
|
---|
318 | self.__errors = error_dict
|
---|
319 | return self.__errors
|
---|
320 | errors = property(_errors)
|
---|
321 |
|
---|
322 | def non_field_errors(self):
|
---|
323 | """
|
---|
324 | Returns an ErrorList of errors that aren't associated with a particular
|
---|
325 | field -- i.e., from Form.clean(). Returns an empty ErrorList if there
|
---|
326 | are none.
|
---|
327 | """
|
---|
328 | return self.errors.get(NON_FIELD_ERRORS, ErrorList())
|
---|
329 |
|
---|
330 | def _cleaned_data(self):
|
---|
331 | if not hasattr(self, '__cleaned_data'):
|
---|
332 | cleaned_data = {}
|
---|
333 | errors = False
|
---|
334 | for k, f in self.forms.items():
|
---|
335 | if hasattr(f, 'cleaned_data'):
|
---|
336 | if k == '_self':
|
---|
337 | cleaned_data.update(f.cleaned_data)
|
---|
338 | else:
|
---|
339 | cleaned_data[k] = f.cleaned_data
|
---|
340 | else:
|
---|
341 | if isinstance(f, SubForm) and f.ignore_errors():
|
---|
342 | continue
|
---|
343 | else:
|
---|
344 | raise AttributeError('Form was not a subform and `ignore_errors` was not flagged: %s' % f)
|
---|
345 | self.__cleaned_data = cleaned_data
|
---|
346 | return self.__cleaned_data
|
---|
347 | cleaned_data = property(_cleaned_data)
|
---|
348 |
|
---|
349 | class SuperForm(BaseSuperForm):
|
---|
350 | __metaclass__ = DeclarativeSubFormsMetaclass
|
---|
351 |
|
---|
352 |
|
---|