""" Form classes """ import copy from django.utils.datastructures import SortedDict from django.utils.html import escape from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode from fields import Field from widgets import 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('_', ' ') class SortedDictFromList(SortedDict): "A dictionary that keeps its keys in the order in which they're inserted." # This is different than django.utils.datastructures.SortedDict, because # this takes a list/tuple as the argument to __init__(). def __init__(self, data=None): if data is None: data = [] self.keyOrder = [d[0] for d in data] dict.__init__(self, dict(data)) def copy(self): return SortedDictFromList([(k, copy.copy(v)) for k, v in self.items()]) 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): 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. for base in bases[::-1]: if hasattr(base, 'base_fields'): fields = base.base_fields.items() + fields attrs['base_fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) 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, auto_id='id_%s', prefix=None, initial=None): self.is_bound = data is not None self.data = data or {} self.auto_id = auto_id self.prefix = prefix self.initial = initial or {} self._errors = None # Stores the errors after clean() has been called. # 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 = self.base_fields.copy() 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 self.data" 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 = ErrorList([escape(error) for error in bf.errors]) # Escape and cache in local variable. if bf.is_hidden: if bf_errors: top_errors.extend(['(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 a colon if the label does not end in punctuation. if label[-1] not in ':?.!': label += ':' 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 % 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. '') 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 u'\n'.join(output) def as_table(self): "Returns this form rendered as HTML s -- excluding the
." return self._html_output(u'%(label)s%(errors)s%(field)s%(help_text)s', u'%s', '', u'
%s', False) def as_ul(self): "Returns this form rendered as HTML
  • s -- excluding the ." return self._html_output(u'
  • %(errors)s%(label)s %(field)s%(help_text)s
  • ', u'
  • %s
  • ', '', u' %s', False) def as_p(self): "Returns this form rendered as HTML

    s." return self._html_output(u'

    %(label)s %(field)s%(help_text)s

    ', u'%s', '

    ', 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, ErrorList()) 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 = {} for name, field in self.fields.items(): # value_from_datadict() gets the data from the dictionary. # 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.add_prefix(name)) try: 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 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." # Use the 'widget' attribute on the field to determine which type # of HTML widget to use. value = self.as_widget(self.field.widget) if not isinstance(value, basestring): # Some Widget render() methods -- notably RadioSelect -- return a # "special" object rather than a string. Call __unicode__() on that # object to get its rendered value. value = unicode(value) return value def _errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ return self.form.errors.get(self.name, ErrorList()) errors = property(_errors) def as_widget(self, widget, attrs=None): 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 . """ return self.as_widget(TextInput(), attrs) def as_textarea(self, attrs=None): "Returns a string of HTML for representing this as a