Ticket #3706: superforms.2.py

File superforms.2.py, 13.2 KB (added by mnbayazit, 14 years ago)

Added iter, getitem, and save()

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