| 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 |
|
|---|