| """ |
| Form classes |
| """ |
| |
| from copy import deepcopy |
| |
| from django.utils.datastructures import SortedDict |
| from django.utils.html import escape |
| from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode |
| from django.utils.safestring import mark_safe |
| |
| from fields import Field, FileField |
| from widgets import Media, media_property, TextInput, Textarea |
| from util import flatatt, ErrorDict, ErrorList, ValidationError |
| |
| __all__ = ('BaseForm', 'Form') |
| |
| NON_FIELD_ERRORS = '__all__' |
| |
| def pretty_name(name): |
| "Converts 'first_name' to 'First name'" |
| name = name[0].upper() + name[1:] |
| return name.replace('_', ' ') |
| |
| def get_declared_fields(bases, attrs, with_base_fields=True): |
| """ |
| Create a list of form field instances from the passed in 'attrs', plus any |
| similar fields on the base classes (in 'bases'). This is used by both the |
| Form and ModelForm metclasses. |
| |
| If 'with_base_fields' is True, all fields from the bases are used. |
| Otherwise, only fields in the 'declared_fields' attribute on the bases are |
| used. The distinction is useful in ModelForm subclassing. |
| Also integrates any additional media definitions |
| """ |
| fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] |
| fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) |
| |
| # If this class is subclassing another Form, add that Form's fields. |
| # Note that we loop over the bases in *reverse*. This is necessary in |
| # order to preserve the correct order of fields. |
| if with_base_fields: |
| for base in bases[::-1]: |
| if hasattr(base, 'base_fields'): |
| fields = base.base_fields.items() + fields |
| else: |
| for base in bases[::-1]: |
| if hasattr(base, 'declared_fields'): |
| fields = base.declared_fields.items() + fields |
| |
| return SortedDict(fields) |
| |
| class DeclarativeFieldsMetaclass(type): |
| """ |
| Metaclass that converts Field attributes to a dictionary called |
| 'base_fields', taking into account parent class 'base_fields' as well. |
| """ |
| def __new__(cls, name, bases, attrs): |
| attrs['base_fields'] = get_declared_fields(bases, attrs) |
| new_class = super(DeclarativeFieldsMetaclass, |
| cls).__new__(cls, name, bases, attrs) |
| if 'media' not in attrs: |
| new_class.media = media_property(new_class) |
| return new_class |
| |
| class BaseForm(StrAndUnicode): |
| # This is the main implementation of all the Form logic. Note that this |
| # class is different than Form. See the comments by the Form class for more |
| # information. Any improvements to the form API should be made to *this* |
| # class, not to the Form class. |
| def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, |
| initial=None, error_class=ErrorList, label_suffix=':', |
| empty_permitted=False): |
| self.is_bound = data is not None or files is not None |
| self.data = data or {} |
| self.files = files or {} |
| self.auto_id = auto_id |
| self.prefix = prefix |
| self.initial = initial or {} |
| self.error_class = error_class |
| self.label_suffix = label_suffix |
| self.empty_permitted = empty_permitted |
| self._errors = None # Stores the errors after clean() has been called. |
| self._changed_data = None |
| |
| # The base_fields class attribute is the *class-wide* definition of |
| # fields. Because a particular *instance* of the class might want to |
| # alter self.fields, we create self.fields here by copying base_fields. |
| # Instances should always modify self.fields; they should not modify |
| # self.base_fields. |
| self.fields = deepcopy(self.base_fields) |
| |
| def __unicode__(self): |
| return self.as_table() |
| |
| def __iter__(self): |
| for name, field in self.fields.items(): |
| yield BoundField(self, field, name) |
| |
| def __getitem__(self, name): |
| "Returns a BoundField with the given name." |
| try: |
| field = self.fields[name] |
| except KeyError: |
| raise KeyError('Key %r not found in Form' % name) |
| return BoundField(self, field, name) |
| |
| def _get_errors(self): |
| "Returns an ErrorDict for the data provided for the form" |
| if self._errors is None: |
| self.full_clean() |
| return self._errors |
| errors = property(_get_errors) |
| |
| def is_valid(self): |
| """ |
| Returns True if the form has no errors. Otherwise, False. If errors are |
| being ignored, returns False. |
| """ |
| return self.is_bound and not bool(self.errors) |
| |
| def add_prefix(self, field_name): |
| """ |
| Returns the field name with a prefix appended, if this Form has a |
| prefix set. |
| |
| Subclasses may wish to override. |
| """ |
| return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name |
| |
| def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): |
| "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." |
| top_errors = self.non_field_errors() # Errors that should be displayed above all fields. |
| output, hidden_fields = [], [] |
| for name, field in self.fields.items(): |
| bf = BoundField(self, field, name) |
| bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable. |
| if bf.is_hidden: |
| if bf_errors: |
| top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors]) |
| hidden_fields.append(unicode(bf)) |
| else: |
| if errors_on_separate_row and bf_errors: |
| output.append(error_row % force_unicode(bf_errors)) |
| if bf.label: |
| label = escape(force_unicode(bf.label)) |
| # Only add the suffix if the label does not end in |
| # punctuation. |
| if self.label_suffix: |
| if label[-1] not in ':?.!': |
| label += self.label_suffix |
| label = bf.label_tag(label) or '' |
| else: |
| label = '' |
| if field.help_text: |
| help_text = help_text_html % force_unicode(field.help_text) |
| else: |
| help_text = u'' |
| output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text}) |
| if top_errors: |
| output.insert(0, error_row % force_unicode(top_errors)) |
| if hidden_fields: # Insert any hidden fields in the last row. |
| str_hidden = u''.join(hidden_fields) |
| if output: |
| last_row = output[-1] |
| # Chop off the trailing row_ender (e.g. '</td></tr>') and |
| # insert the hidden fields. |
| output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender |
| else: |
| # If there aren't any rows in the output, just append the |
| # hidden fields. |
| output.append(str_hidden) |
| return mark_safe(u'\n'.join(output)) |
| |
| def as_table(self): |
| "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." |
| 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) |
| |
| def as_ul(self): |
| "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." |
| return self._html_output(u'<li>%(errors)s%(label)s %(field)s%(help_text)s</li>', u'<li>%s</li>', '</li>', u' %s', False) |
| |
| def as_p(self): |
| "Returns this form rendered as HTML <p>s." |
| return self._html_output(u'<p>%(label)s %(field)s%(help_text)s</p>', u'%s', '</p>', u' %s', True) |
| |
| def non_field_errors(self): |
| """ |
| Returns an ErrorList of errors that aren't associated with a particular |
| field -- i.e., from Form.clean(). Returns an empty ErrorList if there |
| are none. |
| """ |
| return self.errors.get(NON_FIELD_ERRORS, self.error_class()) |
| |
| def full_clean(self): |
| """ |
| Cleans all of self.data and populates self._errors and |
| self.cleaned_data. |
| """ |
| self._errors = ErrorDict() |
| if not self.is_bound: # Stop further processing. |
| return |
| self.cleaned_data = {} |
| # If the form is permitted to be empty, and none of the form data has |
| # changed from the initial data, short circuit any validation. |
| if self.empty_permitted and not self.has_changed(): |
| return |
| for name, field in self.fields.items(): |
| # value_from_datadict() gets the data from the data dictionaries. |
| # Each widget type knows how to retrieve its own data, because some |
| # widgets split data over several HTML fields. |
| value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) |
| try: |
| if isinstance(field, FileField): |
| initial = self.initial.get(name, field.initial) |
| value = field.clean(value, initial) |
| else: |
| value = field.clean(value) |
| self.cleaned_data[name] = value |
| if hasattr(self, 'clean_%s' % name): |
| value = getattr(self, 'clean_%s' % name)() |
| self.cleaned_data[name] = value |
| except ValidationError, e: |
| self._errors[name] = e.messages |
| if name in self.cleaned_data: |
| del self.cleaned_data[name] |
| try: |
| self.cleaned_data = self.clean() |
| except ValidationError, e: |
| self._errors[NON_FIELD_ERRORS] = e.messages |
| if self._errors: |
| delattr(self, 'cleaned_data') |
| |
| def clean(self): |
| """ |
| Hook for doing any extra form-wide cleaning after Field.clean() been |
| called on every field. Any ValidationError raised by this method will |
| not be associated with a particular field; it will have a special-case |
| association with the field named '__all__'. |
| """ |
| return self.cleaned_data |
| |
| def has_changed(self): |
| """ |
| Returns True if data differs from initial. |
| """ |
| return bool(self.changed_data) |
| |
| def _get_changed_data(self): |
| if self._changed_data is None: |
| self._changed_data = [] |
| # XXX: For now we're asking the individual widgets whether or not the |
| # data has changed. It would probably be more efficient to hash the |
| # initial data, store it in a hidden field, and compare a hash of the |
| # submitted data, but we'd need a way to easily get the string value |
| # for a given field. Right now, that logic is embedded in the render |
| # method of each widget. |
| for name, field in self.fields.items(): |
| prefixed_name = self.add_prefix(name) |
| data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) |
| initial_value = self.initial.get(name, field.initial) |
| if field.widget._has_changed(initial_value, data_value): |
| self._changed_data.append(name) |
| return self._changed_data |
| changed_data = property(_get_changed_data) |
| |
| def _get_media(self): |
| """ |
| Provide a description of all media required to render the widgets on this form |
| """ |
| media = Media() |
| for field in self.fields.values(): |
| media = media + field.widget.media |
| return media |
| media = property(_get_media) |
| |
| def is_multipart(self): |
| """ |
| Returns True if the form needs to be multipart-encrypted, i.e. it has |
| FileInput. Otherwise, False. |
| """ |
| for field in self.fields.values(): |
| if field.widget.needs_multipart_form: |
| return True |
| return False |
| |
| class Form(BaseForm): |
| "A collection of Fields, plus their associated data." |
| # This is a separate class from BaseForm in order to abstract the way |
| # self.fields is specified. This class (Form) is the one that does the |
| # fancy metaclass stuff purely for the semantic sugar -- it allows one |
| # to define a form using declarative syntax. |
| # BaseForm itself has no way of designating self.fields. |
| __metaclass__ = DeclarativeFieldsMetaclass |
| |
| class BoundField(StrAndUnicode): |
| "A Field plus data" |
| def __init__(self, form, field, name): |
| self.form = form |
| self.field = field |
| self.name = name |
| self.html_name = form.add_prefix(name) |
| if self.field.label is None: |
| self.label = pretty_name(name) |
| else: |
| self.label = self.field.label |
| self.help_text = field.help_text or '' |
| |
| def __unicode__(self): |
| """Renders this field as an HTML widget.""" |
| return self.as_widget() |
| |
| def _errors(self): |
| """ |
| Returns an ErrorList for this field. Returns an empty ErrorList |
| if there are none. |
| """ |
| return self.form.errors.get(self.name, self.form.error_class()) |
| errors = property(_errors) |
| |
| def as_widget(self, widget=None, attrs=None): |
| """ |
| Renders the field by rendering the passed widget, adding any HTML |
| attributes passed as attrs. If no widget is specified, then the |
| field's default widget will be used. |
| """ |
| if not widget: |
| widget = self.field.widget |
| attrs = attrs or {} |
| auto_id = self.auto_id |
| if auto_id and 'id' not in attrs and 'id' not in widget.attrs: |
| attrs['id'] = auto_id |
| if not self.form.is_bound: |
| data = self.form.initial.get(self.name, self.field.initial) |
| if callable(data): |
| data = data() |
| else: |
| data = self.data |
| return widget.render(self.html_name, data, attrs=attrs) |
| |
| def as_text(self, attrs=None): |
| """ |
| Returns a string of HTML for representing this as an <input type="text">. |
| """ |
| return self.as_widget(TextInput(), attrs) |
| |
| def as_textarea(self, attrs=None): |
| "Returns a string of HTML for representing this as a <textarea>." |
| return self.as_widget(Textarea(), attrs) |
| |
| def as_hidden(self, attrs=None): |
| """ |
| Returns a string of HTML for representing this as an <input type="hidden">. |
| """ |
| return self.as_widget(self.field.hidden_widget(), attrs) |
| |
| def _data(self): |
| """ |
| Returns the data for this BoundField, or None if it wasn't given. |
| """ |
| return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name) |
| data = property(_data) |
| |
| def label_tag(self, contents=None, attrs=None): |
| """ |
| Wraps the given contents in a <label>, if the field has an ID attribute. |
| Does not HTML-escape the contents. If contents aren't given, uses the |
| field's HTML-escaped label. |
| |
| If attrs are given, they're used as HTML attributes on the <label> tag. |
| """ |
| contents = contents or escape(self.label) |
| widget = self.field.widget |
| id_ = widget.attrs.get('id') or self.auto_id |
| if id_: |
| attrs = attrs and flatatt(attrs) or '' |
| contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents) |
| return mark_safe(contents) |
| |
| def _is_hidden(self): |
| "Returns True if this BoundField's widget is hidden." |
| return self.field.widget.is_hidden |
| is_hidden = property(_is_hidden) |
| |
| def _auto_id(self): |
| """ |
| Calculates and returns the ID attribute for this BoundField, if the |
| associated Form has specified auto_id. Returns an empty string otherwise. |
| """ |
| auto_id = self.form.auto_id |
| if auto_id and '%s' in smart_unicode(auto_id): |
| return smart_unicode(auto_id) % self.html_name |
| elif auto_id: |
| return self.html_name |
| return '' |
| auto_id = property(_auto_id) |