diff options
Diffstat (limited to 'django/newforms/forms.py')
| -rw-r--r-- | django/newforms/forms.py | 204 |
1 files changed, 140 insertions, 64 deletions
diff --git a/django/newforms/forms.py b/django/newforms/forms.py index b8264fb691..201cce3868 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -2,10 +2,13 @@ Form classes """ -from django.utils.datastructures import SortedDict +from django.utils.datastructures import SortedDict, MultiValueDict +from django.utils.html import escape from fields import Field -from widgets import TextInput, Textarea -from util import ErrorDict, ErrorList, ValidationError +from widgets import TextInput, Textarea, HiddenInput +from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError + +__all__ = ('BaseForm', 'Form') NON_FIELD_ERRORS = '__all__' @@ -31,17 +34,20 @@ class DeclarativeFieldsMetaclass(type): attrs['fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) -class Form(object): - "A collection of Fields, plus their associated data." - __metaclass__ = DeclarativeFieldsMetaclass - - def __init__(self, data=None, auto_id=False): # TODO: prefix stuff +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): + self.ignore_errors = data is None self.data = data or {} self.auto_id = auto_id + self.prefix = prefix self.clean_data = None # Stores the data after clean() has been called. self.__errors = None # Stores the errors after clean() has been called. - def __str__(self): + def __unicode__(self): return self.as_table() def __iter__(self): @@ -56,60 +62,76 @@ class Form(object): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def clean(self): - if self.__errors is None: - self.full_clean() - return self.clean_data - - def errors(self): + def _errors(self): "Returns an ErrorDict for self.data" if self.__errors is None: self.full_clean() return self.__errors + errors = property(_errors) def is_valid(self): """ - Returns True if the form has no errors. Otherwise, False. This exists - solely for convenience, so client code can use positive logic rather - than confusing negative logic ("if not form.errors()"). + Returns True if the form has no errors. Otherwise, False. If errors are + being ignored, returns False. + """ + return not self.ignore_errors 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 not bool(self.errors()) + return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name + + def _html_output(self, normal_row, error_row, row_ender, 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 = bf.errors # Cache in local variable. + if bf.is_hidden: + if bf_errors: + top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) + hidden_fields.append(unicode(bf)) + else: + if errors_on_separate_row and bf_errors: + output.append(error_row % bf_errors) + label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' + output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': bf}) + 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. '</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 u'\n'.join(output) def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." - return u'\n'.join(['<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<tr><th>%(label)s</th><td>%(errors)s%(field)s</td></tr>', u'<tr><td colspan="2">%s</td></tr>', '</td></tr>', False) def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." - return u'\n'.join(['<li>%s: %s</li>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()]) + return self._html_output(u'<li>%(errors)s%(label)s %(field)s</li>', u'<li>%s</li>', '</li>', False) - def as_table_with_errors(self): - "Returns this form rendered as HTML <tr>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - if bf.errors: - output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors])) - output.append('<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), bf)) - return u'\n'.join(output) + def as_p(self): + "Returns this form rendered as HTML <p>s." + return self._html_output(u'<p>%(label)s %(field)s</p>', u'<p>%s</p>', '</p>', True) - def as_ul_with_errors(self): - "Returns this form rendered as HTML <li>s, with errors." - output = [] - if self.errors().get(NON_FIELD_ERRORS): - # Errors not corresponding to a particular field are displayed at the top. - output.append('<li><ul>%s</ul></li>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]])) - for name, field in self.fields.items(): - bf = BoundField(self, field, name) - line = '<li>' - if bf.errors: - line += '<ul>%s</ul>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors]) - line += '%s: %s</li>' % (pretty_name(name), bf) - output.append(line) - return u'\n'.join(output) + 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): """ @@ -117,8 +139,14 @@ class Form(object): """ self.clean_data = {} errors = ErrorDict() + if self.ignore_errors: # Stop further processing. + self.__errors = errors + return for name, field in self.fields.items(): - value = self.data.get(name, None) + # 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.clean_data[name] = value @@ -138,40 +166,59 @@ class Form(object): def clean(self): """ Hook for doing any extra form-wide cleaning after Field.clean() been - called on every field. + 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.clean_data -class BoundField(object): +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.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 - def __str__(self): + 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. - return self.as_widget(self._field.widget) + 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 the __str__() on that + # object to get its rendered value. + value = value.__str__() + return value def _errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ - try: - return self._form.errors()[self._name] - except KeyError: - return ErrorList() + 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 not attrs.has_key('id') and not widget.attrs.has_key('id') and auto_id: + if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): attrs['id'] = auto_id - return widget.render(self._name, self._form.data.get(self._name, None), attrs=attrs) + return widget.render(self.html_name, self.data, attrs=attrs) def as_text(self, attrs=None): """ @@ -183,15 +230,44 @@ class BoundField(object): "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(HiddenInput(), 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.html_name) + data = property(_data) + + def label_tag(self, contents=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. + """ + contents = contents or escape(self.label) + widget = self.field.widget + id_ = widget.attrs.get('id') or self.auto_id + if id_: + contents = '<label for="%s">%s</label>' % (widget.id_for_label(id_), contents) + return 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 + auto_id = self.form.auto_id if auto_id and '%s' in str(auto_id): - return str(auto_id) % self._name + return str(auto_id) % self.html_name elif auto_id: - return self._name + return self.html_name return '' auto_id = property(_auto_id) |
