From 93d83df61195ea598bc0a2a5cdce1df8e88fcb6d Mon Sep 17 00:00:00 2001
From: Boulder Sprinters
Date: Fri, 15 Dec 2006 18:32:47 +0000
Subject: boulder-oracle-sprint: Merged to trunk [4210].
git-svn-id: http://code.djangoproject.com/svn/django/branches/boulder-oracle-sprint@4212 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
django/newforms/forms.py | 214 +++++++++++++++++++++++++++++++++--------------
1 file changed, 149 insertions(+), 65 deletions(-)
(limited to 'django/newforms/forms.py')
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index 2730bfe4e4..96948264e4 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -2,9 +2,11 @@
Form classes
"""
+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
NON_FIELD_ERRORS = '__all__'
@@ -13,23 +15,37 @@ def pretty_name(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))
+
class DeclarativeFieldsMetaclass(type):
"Metaclass that converts Field attributes to a dictionary called 'fields'."
def __new__(cls, name, bases, attrs):
- attrs['fields'] = dict([(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)])
+ fields = [(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)]
+ fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
+ 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):
@@ -44,60 +60,75 @@ 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 bool(self.errors())
+ 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 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)
+ output.append(normal_row % {'errors': bf_errors, 'label': bf.label_tag(escape(bf.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. '') 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 u'\n'.join(['
| %s: | %s |
' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()])
+ return self._html_output(u'| %(label)s | %(field)s |
', u'| %s |
', '', True)
def as_ul(self):
"Returns this form rendered as HTML s -- excluding the ."
- return u'\n'.join(['%s: %s' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()])
-
- def as_table_with_errors(self):
- "Returns this form rendered as HTML 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('
|
' % '\n'.join(['%s' % 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(' |
' % '\n'.join(['%s' % e for e in bf.errors]))
- output.append('| %s: | %s |
' % (pretty_name(name), bf))
- return u'\n'.join(output)
+ return self._html_output(u'%(errors)s%(label)s %(field)s', u'%s', '', False)
- def as_ul_with_errors(self):
- "Returns this form rendered as HTML 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('' % '\n'.join(['%s' % e for e in self.errors()[NON_FIELD_ERRORS]]))
- for name, field in self.fields.items():
- bf = BoundField(self, field, name)
- line = ''
- if bf.errors:
- line += '' % '\n'.join(['%s' % e for e in bf.errors])
- line += '%s: %s' % (pretty_name(name), bf)
- output.append(line)
- return u'\n'.join(output)
+ def as_p(self):
+ "Returns this form rendered as HTML s."
+ return self._html_output(u'
%(label)s %(field)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):
"""
@@ -105,8 +136,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
@@ -126,40 +163,56 @@ 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)
+ self.label = self.field.label or pretty_name(name)
- 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):
"""
@@ -171,15 +224,46 @@ class BoundField(object):
"Returns a string of HTML for representing this as a