diff -r 6641e941ef1e -r ff1a9aa48cfd app/django/forms/forms.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/django/forms/forms.py Tue Oct 14 16:00:59 2008 +0000 @@ -0,0 +1,422 @@ +""" +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 add_initial_prefix(self, field_name): + """ + Add a 'initial' prefix for checking dynamic initial values + """ + return u'initial-%s' % self.add_prefix(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. '') and + # insert the hidden fields. + if not last_row.endswith(row_ender): + # This can happen in the as_p() case (and possibly others + # that users write): if there are only top errors, we may + # not be able to conscript the last row for our purposes, + # so insert a new, empty row. + last_row = normal_row % {'errors': '', 'label': '', 'field': '', 'help_text': ''} + output.append(last_row) + 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
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, 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) + if not field.show_hidden_initial: + initial_value = self.initial.get(name, field.initial) + else: + initial_prefixed_name = self.add_initial_prefix(name) + hidden_widget = field.hidden_widget() + initial_value = hidden_widget.value_from_datadict( + self.data, self.files, initial_prefixed_name) + 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) + self.html_initial_name = form.add_initial_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.""" + if self.field.show_hidden_initial: + return self.as_widget() + self.as_hidden(only_initial=True) + 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, only_initial=False): + """ + 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 + if not only_initial: + name = self.html_name + else: + name = self.html_initial_name + return widget.render(name, data, attrs=attrs) + + def as_text(self, attrs=None, **kwargs): + """ + Returns a string of HTML for representing this as an . + """ + return self.as_widget(TextInput(), attrs, **kwargs) + + def as_textarea(self, attrs=None, **kwargs): + "Returns a string of HTML for representing this as a