Ticket #3706: superform.py

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

Updated version to work w/1.2 (should work with all 1.0+).

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: data = []
58 self.key_order = [d[0] for d in data]
59 SortedDict.__init__(self, dict(data))
60
61class 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
103class 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
188class 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
217class 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
349class SuperForm(BaseSuperForm):
350 __metaclass__ = DeclarativeSubFormsMetaclass
351
352
Back to Top