| 1 |
""" |
|---|
| 2 |
Form classes |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
from copy import deepcopy |
|---|
| 6 |
|
|---|
| 7 |
from django.utils.datastructures import SortedDict |
|---|
| 8 |
from django.utils.html import escape |
|---|
| 9 |
from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode |
|---|
| 10 |
from django.utils.safestring import mark_safe |
|---|
| 11 |
|
|---|
| 12 |
from fields import Field, FileField |
|---|
| 13 |
from widgets import Media, media_property, TextInput, Textarea |
|---|
| 14 |
from util import flatatt, ErrorDict, ErrorList, ValidationError |
|---|
| 15 |
|
|---|
| 16 |
__all__ = ('BaseForm', 'Form') |
|---|
| 17 |
|
|---|
| 18 |
NON_FIELD_ERRORS = '__all__' |
|---|
| 19 |
|
|---|
| 20 |
def pretty_name(name): |
|---|
| 21 |
"Converts 'first_name' to 'First name'" |
|---|
| 22 |
name = name[0].upper() + name[1:] |
|---|
| 23 |
return name.replace('_', ' ') |
|---|
| 24 |
|
|---|
| 25 |
def get_declared_fields(bases, attrs, with_base_fields=True): |
|---|
| 26 |
""" |
|---|
| 27 |
Create a list of form field instances from the passed in 'attrs', plus any |
|---|
| 28 |
similar fields on the base classes (in 'bases'). This is used by both the |
|---|
| 29 |
Form and ModelForm metclasses. |
|---|
| 30 |
|
|---|
| 31 |
If 'with_base_fields' is True, all fields from the bases are used. |
|---|
| 32 |
Otherwise, only fields in the 'declared_fields' attribute on the bases are |
|---|
| 33 |
used. The distinction is useful in ModelForm subclassing. |
|---|
| 34 |
Also integrates any additional media definitions |
|---|
| 35 |
""" |
|---|
| 36 |
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] |
|---|
| 37 |
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) |
|---|
| 38 |
|
|---|
| 39 |
# If this class is subclassing another Form, add that Form's fields. |
|---|
| 40 |
# Note that we loop over the bases in *reverse*. This is necessary in |
|---|
| 41 |
# order to preserve the correct order of fields. |
|---|
| 42 |
if with_base_fields: |
|---|
| 43 |
for base in bases[::-1]: |
|---|
| 44 |
if hasattr(base, 'base_fields'): |
|---|
| 45 |
fields = base.base_fields.items() + fields |
|---|
| 46 |
else: |
|---|
| 47 |
for base in bases[::-1]: |
|---|
| 48 |
if hasattr(base, 'declared_fields'): |
|---|
| 49 |
fields = base.declared_fields.items() + fields |
|---|
| 50 |
|
|---|
| 51 |
return SortedDict(fields) |
|---|
| 52 |
|
|---|
| 53 |
class DeclarativeFieldsMetaclass(type): |
|---|
| 54 |
""" |
|---|
| 55 |
Metaclass that converts Field attributes to a dictionary called |
|---|
| 56 |
'base_fields', taking into account parent class 'base_fields' as well. |
|---|
| 57 |
""" |
|---|
| 58 |
def __new__(cls, name, bases, attrs): |
|---|
| 59 |
attrs['base_fields'] = get_declared_fields(bases, attrs) |
|---|
| 60 |
new_class = super(DeclarativeFieldsMetaclass, |
|---|
| 61 |
cls).__new__(cls, name, bases, attrs) |
|---|
| 62 |
if 'media' not in attrs: |
|---|
| 63 |
new_class.media = media_property(new_class) |
|---|
| 64 |
return new_class |
|---|
| 65 |
|
|---|
| 66 |
class BaseForm(StrAndUnicode): |
|---|
| 67 |
# This is the main implementation of all the Form logic. Note that this |
|---|
| 68 |
# class is different than Form. See the comments by the Form class for more |
|---|
| 69 |
# information. Any improvements to the form API should be made to *this* |
|---|
| 70 |
# class, not to the Form class. |
|---|
| 71 |
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, |
|---|
| 72 |
initial=None, error_class=ErrorList, label_suffix=':', |
|---|
| 73 |
empty_permitted=False): |
|---|
| 74 |
self.is_bound = data is not None or files is not None |
|---|
| 75 |
self.data = data or {} |
|---|
| 76 |
self.files = files or {} |
|---|
| 77 |
self.auto_id = auto_id |
|---|
| 78 |
self.prefix = prefix |
|---|
| 79 |
self.initial = initial or {} |
|---|
| 80 |
self.error_class = error_class |
|---|
| 81 |
self.label_suffix = label_suffix |
|---|
| 82 |
self.empty_permitted = empty_permitted |
|---|
| 83 |
self._errors = None # Stores the errors after clean() has been called. |
|---|
| 84 |
self._changed_data = None |
|---|
| 85 |
|
|---|
| 86 |
# The base_fields class attribute is the *class-wide* definition of |
|---|
| 87 |
# fields. Because a particular *instance* of the class might want to |
|---|
| 88 |
# alter self.fields, we create self.fields here by copying base_fields. |
|---|
| 89 |
# Instances should always modify self.fields; they should not modify |
|---|
| 90 |
# self.base_fields. |
|---|
| 91 |
self.fields = deepcopy(self.base_fields) |
|---|
| 92 |
|
|---|
| 93 |
def __unicode__(self): |
|---|
| 94 |
return self.as_table() |
|---|
| 95 |
|
|---|
| 96 |
def __iter__(self): |
|---|
| 97 |
for name, field in self.fields.items(): |
|---|
| 98 |
yield BoundField(self, field, name) |
|---|
| 99 |
|
|---|
| 100 |
def __getitem__(self, name): |
|---|
| 101 |
"Returns a BoundField with the given name." |
|---|
| 102 |
try: |
|---|
| 103 |
field = self.fields[name] |
|---|
| 104 |
except KeyError: |
|---|
| 105 |
raise KeyError('Key %r not found in Form' % name) |
|---|
| 106 |
return BoundField(self, field, name) |
|---|
| 107 |
|
|---|
| 108 |
def _get_errors(self): |
|---|
| 109 |
"Returns an ErrorDict for the data provided for the form" |
|---|
| 110 |
if self._errors is None: |
|---|
| 111 |
self.full_clean() |
|---|
| 112 |
return self._errors |
|---|
| 113 |
errors = property(_get_errors) |
|---|
| 114 |
|
|---|
| 115 |
def is_valid(self): |
|---|
| 116 |
""" |
|---|
| 117 |
Returns True if the form has no errors. Otherwise, False. If errors are |
|---|
| 118 |
being ignored, returns False. |
|---|
| 119 |
""" |
|---|
| 120 |
return self.is_bound and not bool(self.errors) |
|---|
| 121 |
|
|---|
| 122 |
def add_prefix(self, field_name): |
|---|
| 123 |
""" |
|---|
| 124 |
Returns the field name with a prefix appended, if this Form has a |
|---|
| 125 |
prefix set. |
|---|
| 126 |
|
|---|
| 127 |
Subclasses may wish to override. |
|---|
| 128 |
""" |
|---|
| 129 |
return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name |
|---|
| 130 |
|
|---|
| 131 |
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): |
|---|
| 132 |
"Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." |
|---|
| 133 |
top_errors = self.non_field_errors() # Errors that should be displayed above all fields. |
|---|
| 134 |
output, hidden_fields = [], [] |
|---|
| 135 |
for name, field in self.fields.items(): |
|---|
| 136 |
bf = BoundField(self, field, name) |
|---|
| 137 |
bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable. |
|---|
| 138 |
if bf.is_hidden: |
|---|
| 139 |
if bf_errors: |
|---|
| 140 |
top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors]) |
|---|
| 141 |
hidden_fields.append(unicode(bf)) |
|---|
| 142 |
else: |
|---|
| 143 |
if errors_on_separate_row and bf_errors: |
|---|
| 144 |
output.append(error_row % force_unicode(bf_errors)) |
|---|
| 145 |
if bf.label: |
|---|
| 146 |
label = escape(force_unicode(bf.label)) |
|---|
| 147 |
# Only add the suffix if the label does not end in |
|---|
| 148 |
# punctuation. |
|---|
| 149 |
if self.label_suffix: |
|---|
| 150 |
if label[-1] not in ':?.!': |
|---|
| 151 |
label += self.label_suffix |
|---|
| 152 |
label = bf.label_tag(label) or '' |
|---|
| 153 |
else: |
|---|
| 154 |
label = '' |
|---|
| 155 |
if field.help_text: |
|---|
| 156 |
help_text = help_text_html % force_unicode(field.help_text) |
|---|
| 157 |
else: |
|---|
| 158 |
help_text = u'' |
|---|
| 159 |
output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text}) |
|---|
| 160 |
if top_errors: |
|---|
| 161 |
output.insert(0, error_row % force_unicode(top_errors)) |
|---|
| 162 |
if hidden_fields: # Insert any hidden fields in the last row. |
|---|
| 163 |
str_hidden = u''.join(hidden_fields) |
|---|
| 164 |
if output: |
|---|
| 165 |
last_row = output[-1] |
|---|
| 166 |
# Chop off the trailing row_ender (e.g. '</td></tr>') and |
|---|
| 167 |
# insert the hidden fields. |
|---|
| 168 |
output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender |
|---|
| 169 |
else: |
|---|
| 170 |
# If there aren't any rows in the output, just append the |
|---|
| 171 |
# hidden fields. |
|---|
| 172 |
output.append(str_hidden) |
|---|
| 173 |
return mark_safe(u'\n'.join(output)) |
|---|
| 174 |
|
|---|
| 175 |
def as_table(self): |
|---|
| 176 |
"Returns this form rendered as HTML <tr>s -- excluding the <table></table>." |
|---|
| 177 |
return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', u'<br />%s', False) |
|---|
| 178 |
|
|---|
| 179 |
def as_ul(self): |
|---|
| 180 |
"Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." |
|---|
| 181 |
return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False) |
|---|
| 182 |
|
|---|
| 183 |
def as_p(self): |
|---|
| 184 |
"Returns this form rendered as HTML <p>s." |
|---|
| 185 |
return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True) |
|---|
| 186 |
|
|---|
| 187 |
def non_field_errors(self): |
|---|
| 188 |
""" |
|---|
| 189 |
Returns an ErrorList of errors that aren't associated with a particular |
|---|
| 190 |
field -- i.e., from Form.clean(). Returns an empty ErrorList if there |
|---|
| 191 |
are none. |
|---|
| 192 |
""" |
|---|
| 193 |
return self.errors.get(NON_FIELD_ERRORS, self.error_class()) |
|---|
| 194 |
|
|---|
| 195 |
def full_clean(self): |
|---|
| 196 |
""" |
|---|
| 197 |
Cleans all of self.data and populates self._errors and |
|---|
| 198 |
self.cleaned_data. |
|---|
| 199 |
""" |
|---|
| 200 |
self._errors = ErrorDict() |
|---|
| 201 |
if not self.is_bound: # Stop further processing. |
|---|
| 202 |
return |
|---|
| 203 |
self.cleaned_data = {} |
|---|
| 204 |
# If the form is permitted to be empty, and none of the form data has |
|---|
| 205 |
# changed from the initial data, short circuit any validation. |
|---|
| 206 |
if self.empty_permitted and not self.has_changed(): |
|---|
| 207 |
return |
|---|
| 208 |
for name, field in self.fields.items(): |
|---|
| 209 |
# value_from_datadict() gets the data from the data dictionaries. |
|---|
| 210 |
# Each widget type knows how to retrieve its own data, because some |
|---|
| 211 |
# widgets split data over several HTML fields. |
|---|
| 212 |
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) |
|---|
| 213 |
try: |
|---|
| 214 |
if isinstance(field, FileField): |
|---|
| 215 |
initial = self.initial.get(name, field.initial) |
|---|
| 216 |
value = field.clean(value, initial) |
|---|
| 217 |
else: |
|---|
| 218 |
value = field.clean(value) |
|---|
| 219 |
self.cleaned_data[name] = value |
|---|
| 220 |
if hasattr(self, 'clean_%s' % name): |
|---|
| 221 |
value = getattr(self, 'clean_%s' % name)() |
|---|
| 222 |
self.cleaned_data[name] = value |
|---|
| 223 |
except ValidationError, e: |
|---|
| 224 |
self._errors[name] = e.messages |
|---|
| 225 |
if name in self.cleaned_data: |
|---|
| 226 |
del self.cleaned_data[name] |
|---|
| 227 |
try: |
|---|
| 228 |
self.cleaned_data = self.clean() |
|---|
| 229 |
except ValidationError, e: |
|---|
| 230 |
self._errors[NON_FIELD_ERRORS] = e.messages |
|---|
| 231 |
if self._errors: |
|---|
| 232 |
delattr(self, 'cleaned_data') |
|---|
| 233 |
|
|---|
| 234 |
def clean(self): |
|---|
| 235 |
""" |
|---|
| 236 |
Hook for doing any extra form-wide cleaning after Field.clean() been |
|---|
| 237 |
called on every field. Any ValidationError raised by this method will |
|---|
| 238 |
not be associated with a particular field; it will have a special-case |
|---|
| 239 |
association with the field named '__all__'. |
|---|
| 240 |
""" |
|---|
| 241 |
return self.cleaned_data |
|---|
| 242 |
|
|---|
| 243 |
def has_changed(self): |
|---|
| 244 |
""" |
|---|
| 245 |
Returns True if data differs from initial. |
|---|
| 246 |
""" |
|---|
| 247 |
return bool(self.changed_data) |
|---|
| 248 |
|
|---|
| 249 |
def _get_changed_data(self): |
|---|
| 250 |
if self._changed_data is None: |
|---|
| 251 |
self._changed_data = [] |
|---|
| 252 |
# XXX: For now we're asking the individual widgets whether or not the |
|---|
| 253 |
# data has changed. It would probably be more efficient to hash the |
|---|
| 254 |
# initial data, store it in a hidden field, and compare a hash of the |
|---|
| 255 |
# submitted data, but we'd need a way to easily get the string value |
|---|
| 256 |
# for a given field. Right now, that logic is embedded in the render |
|---|
| 257 |
# method of each widget. |
|---|
| 258 |
for name, field in self.fields.items(): |
|---|
| 259 |
prefixed_name = self.add_prefix(name) |
|---|
| 260 |
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) |
|---|
| 261 |
initial_value = self.initial.get(name, field.initial) |
|---|
| 262 |
if field.widget._has_changed(initial_value, data_value): |
|---|
| 263 |
self._changed_data.append(name) |
|---|
| 264 |
return self._changed_data |
|---|
| 265 |
changed_data = property(_get_changed_data) |
|---|
| 266 |
|
|---|
| 267 |
def _get_media(self): |
|---|
| 268 |
""" |
|---|
| 269 |
Provide a description of all media required to render the widgets on this form |
|---|
| 270 |
""" |
|---|
| 271 |
media = Media() |
|---|
| 272 |
for field in self.fields.values(): |
|---|
| 273 |
media = media + field.widget.media |
|---|
| 274 |
return media |
|---|
| 275 |
media = property(_get_media) |
|---|
| 276 |
|
|---|
| 277 |
def is_multipart(self): |
|---|
| 278 |
""" |
|---|
| 279 |
Returns True if the form needs to be multipart-encrypted, i.e. it has |
|---|
| 280 |
FileInput. Otherwise, False. |
|---|
| 281 |
""" |
|---|
| 282 |
for field in self.fields.values(): |
|---|
| 283 |
if field.widget.needs_multipart_form: |
|---|
| 284 |
return True |
|---|
| 285 |
return False |
|---|
| 286 |
|
|---|
| 287 |
class Form(BaseForm): |
|---|
| 288 |
"A collection of Fields, plus their associated data." |
|---|
| 289 |
# This is a separate class from BaseForm in order to abstract the way |
|---|
| 290 |
# self.fields is specified. This class (Form) is the one that does the |
|---|
| 291 |
# fancy metaclass stuff purely for the semantic sugar -- it allows one |
|---|
| 292 |
# to define a form using declarative syntax. |
|---|
| 293 |
# BaseForm itself has no way of designating self.fields. |
|---|
| 294 |
__metaclass__ = DeclarativeFieldsMetaclass |
|---|
| 295 |
|
|---|
| 296 |
class BoundField(StrAndUnicode): |
|---|
| 297 |
"A Field plus data" |
|---|
| 298 |
def __init__(self, form, field, name): |
|---|
| 299 |
self.form = form |
|---|
| 300 |
self.field = field |
|---|
| 301 |
self.name = name |
|---|
| 302 |
self.html_name = form.add_prefix(name) |
|---|
| 303 |
if self.field.label is None: |
|---|
| 304 |
self.label = pretty_name(name) |
|---|
| 305 |
else: |
|---|
| 306 |
self.label = self.field.label |
|---|
| 307 |
self.help_text = field.help_text or '' |
|---|
| 308 |
|
|---|
| 309 |
def __unicode__(self): |
|---|
| 310 |
"""Renders this field as an HTML widget.""" |
|---|
| 311 |
return self.as_widget() |
|---|
| 312 |
|
|---|
| 313 |
def _errors(self): |
|---|
| 314 |
""" |
|---|
| 315 |
Returns an ErrorList for this field. Returns an empty ErrorList |
|---|
| 316 |
if there are none. |
|---|
| 317 |
""" |
|---|
| 318 |
return self.form.errors.get(self.name, self.form.error_class()) |
|---|
| 319 |
errors = property(_errors) |
|---|
| 320 |
|
|---|
| 321 |
def as_widget(self, widget=None, attrs=None): |
|---|
| 322 |
""" |
|---|
| 323 |
Renders the field by rendering the passed widget, adding any HTML |
|---|
| 324 |
attributes passed as attrs. If no widget is specified, then the |
|---|
| 325 |
field's default widget will be used. |
|---|
| 326 |
""" |
|---|
| 327 |
if not widget: |
|---|
| 328 |
widget = self.field.widget |
|---|
| 329 |
attrs = attrs or {} |
|---|
| 330 |
auto_id = self.auto_id |
|---|
| 331 |
if auto_id and 'id' not in attrs and 'id' not in widget.attrs: |
|---|
| 332 |
attrs['id'] = auto_id |
|---|
| 333 |
if not self.form.is_bound: |
|---|
| 334 |
data = self.form.initial.get(self.name, self.field.initial) |
|---|
| 335 |
if callable(data): |
|---|
| 336 |
data = data() |
|---|
| 337 |
else: |
|---|
| 338 |
data = self.data |
|---|
| 339 |
return widget.render(self.html_name, data, attrs=attrs) |
|---|
| 340 |
|
|---|
| 341 |
def as_text(self, attrs=None): |
|---|
| 342 |
""" |
|---|
| 343 |
Returns a string of HTML for representing this as an <input type="text">. |
|---|
| 344 |
""" |
|---|
| 345 |
return self.as_widget(TextInput(), attrs) |
|---|
| 346 |
|
|---|
| 347 |
def as_textarea(self, attrs=None): |
|---|
| 348 |
"Returns a string of HTML for representing this as a <textarea>." |
|---|
| 349 |
return self.as_widget(Textarea(), attrs) |
|---|
| 350 |
|
|---|
| 351 |
def as_hidden(self, attrs=None): |
|---|
| 352 |
""" |
|---|
| 353 |
Returns a string of HTML for representing this as an <input type="hidden">. |
|---|
| 354 |
""" |
|---|
| 355 |
return self.as_widget(self.field.hidden_widget(), attrs) |
|---|
| 356 |
|
|---|
| 357 |
def _data(self): |
|---|
| 358 |
""" |
|---|
| 359 |
Returns the data for this BoundField, or None if it wasn't given. |
|---|
| 360 |
""" |
|---|
| 361 |
return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name) |
|---|
| 362 |
data = property(_data) |
|---|
| 363 |
|
|---|
| 364 |
def label_tag(self, contents=None, attrs=None): |
|---|
| 365 |
""" |
|---|
| 366 |
Wraps the given contents in a <label>, if the field has an ID attribute. |
|---|
| 367 |
Does not HTML-escape the contents. If contents aren't given, uses the |
|---|
| 368 |
field's HTML-escaped label. |
|---|
| 369 |
|
|---|
| 370 |
If attrs are given, they're used as HTML attributes on the <label> tag. |
|---|
| 371 |
""" |
|---|
| 372 |
contents = contents or escape(self.label) |
|---|
| 373 |
widget = self.field.widget |
|---|
| 374 |
id_ = widget.attrs.get('id') or self.auto_id |
|---|
| 375 |
if id_: |
|---|
| 376 |
attrs = attrs and flatatt(attrs) or '' |
|---|
| 377 |
contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents) |
|---|
| 378 |
return mark_safe(contents) |
|---|
| 379 |
|
|---|
| 380 |
def _is_hidden(self): |
|---|
| 381 |
"Returns True if this BoundField's widget is hidden." |
|---|
| 382 |
return self.field.widget.is_hidden |
|---|
| 383 |
is_hidden = property(_is_hidden) |
|---|
| 384 |
|
|---|
| 385 |
def _auto_id(self): |
|---|
| 386 |
""" |
|---|
| 387 |
Calculates and returns the ID attribute for this BoundField, if the |
|---|
| 388 |
associated Form has specified auto_id. Returns an empty string otherwise. |
|---|
| 389 |
""" |
|---|
| 390 |
auto_id = self.form.auto_id |
|---|
| 391 |
if auto_id and '%s' in smart_unicode(auto_id): |
|---|
| 392 |
return smart_unicode(auto_id) % self.html_name |
|---|
| 393 |
elif auto_id: |
|---|
| 394 |
return self.html_name |
|---|
| 395 |
return '' |
|---|
| 396 |
auto_id = property(_auto_id) |
|---|