django.newforms: Implemented hook for validation not tied to a particular field. Renamed to_python() to clean() -- it's just...cleaner. Added Form.as_table(), Form.as_url(), Form.as_table_with_errors() and Form.as_ul_with_errors(). Added ComboField. Updated all unit tests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@3978 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2006-11-04 20:49:59 +00:00
parent 88029f7700
commit 46b0713315
4 changed files with 387 additions and 162 deletions

View File

@ -2,7 +2,6 @@
Django validation and HTML form handling. Django validation and HTML form handling.
TODO: TODO:
Validation not tied to a particular field
Default value for field Default value for field
Field labels Field labels
Nestable Forms Nestable Forms
@ -11,6 +10,7 @@ TODO:
"This form field requires foo.js" and form.js_includes() "This form field requires foo.js" and form.js_includes()
""" """
from util import ValidationError
from widgets import * from widgets import *
from fields import * from fields import *
from forms import Form from forms import Form

View File

@ -14,6 +14,7 @@ __all__ = (
'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
'RegexField', 'EmailField', 'URLField', 'BooleanField', 'RegexField', 'EmailField', 'URLField', 'BooleanField',
'ChoiceField', 'MultipleChoiceField', 'ChoiceField', 'MultipleChoiceField',
'ComboField',
) )
# These values, if given to to_python(), will trigger the self.required check. # These values, if given to to_python(), will trigger the self.required check.
@ -34,9 +35,9 @@ class Field(object):
widget = widget() widget = widget()
self.widget = widget self.widget = widget
def to_python(self, value): def clean(self, value):
""" """
Validates the given value and returns its "normalized" value as an Validates the given value and returns its "cleaned" value as an
appropriate Python object. appropriate Python object.
Raises ValidationError for any errors. Raises ValidationError for any errors.
@ -50,9 +51,9 @@ class CharField(Field):
Field.__init__(self, required, widget) Field.__init__(self, required, widget)
self.max_length, self.min_length = max_length, min_length self.max_length, self.min_length = max_length, min_length
def to_python(self, value): def clean(self, value):
"Validates max_length and min_length. Returns a Unicode object." "Validates max_length and min_length. Returns a Unicode object."
Field.to_python(self, value) Field.clean(self, value)
if value in EMPTY_VALUES: value = u'' if value in EMPTY_VALUES: value = u''
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = unicode(str(value), DEFAULT_ENCODING) value = unicode(str(value), DEFAULT_ENCODING)
@ -65,12 +66,12 @@ class CharField(Field):
return value return value
class IntegerField(Field): class IntegerField(Field):
def to_python(self, value): def clean(self, value):
""" """
Validates that int() can be called on the input. Returns the result Validates that int() can be called on the input. Returns the result
of int(). of int().
""" """
super(IntegerField, self).to_python(value) super(IntegerField, self).clean(value)
try: try:
return int(value) return int(value)
except (ValueError, TypeError): except (ValueError, TypeError):
@ -89,12 +90,12 @@ class DateField(Field):
Field.__init__(self, required, widget) Field.__init__(self, required, widget)
self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS
def to_python(self, value): def clean(self, value):
""" """
Validates that the input can be converted to a date. Returns a Python Validates that the input can be converted to a date. Returns a Python
datetime.date object. datetime.date object.
""" """
Field.to_python(self, value) Field.clean(self, value)
if value in EMPTY_VALUES: if value in EMPTY_VALUES:
return None return None
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
@ -125,12 +126,12 @@ class DateTimeField(Field):
Field.__init__(self, required, widget) Field.__init__(self, required, widget)
self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS
def to_python(self, value): def clean(self, value):
""" """
Validates that the input can be converted to a datetime. Returns a Validates that the input can be converted to a datetime. Returns a
Python datetime.datetime object. Python datetime.datetime object.
""" """
Field.to_python(self, value) Field.clean(self, value)
if value in EMPTY_VALUES: if value in EMPTY_VALUES:
return None return None
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
@ -157,12 +158,12 @@ class RegexField(Field):
self.regex = regex self.regex = regex
self.error_message = error_message or u'Enter a valid value.' self.error_message = error_message or u'Enter a valid value.'
def to_python(self, value): def clean(self, value):
""" """
Validates that the input matches the regular expression. Returns a Validates that the input matches the regular expression. Returns a
Unicode object. Unicode object.
""" """
Field.to_python(self, value) Field.clean(self, value)
if value in EMPTY_VALUES: value = u'' if value in EMPTY_VALUES: value = u''
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = unicode(str(value), DEFAULT_ENCODING) value = unicode(str(value), DEFAULT_ENCODING)
@ -192,8 +193,8 @@ class URLField(RegexField):
RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget)
self.verify_exists = verify_exists self.verify_exists = verify_exists
def to_python(self, value): def clean(self, value):
value = RegexField.to_python(self, value) value = RegexField.clean(self, value)
if self.verify_exists: if self.verify_exists:
import urllib2 import urllib2
try: try:
@ -207,9 +208,9 @@ class URLField(RegexField):
class BooleanField(Field): class BooleanField(Field):
widget = CheckboxInput widget = CheckboxInput
def to_python(self, value): def clean(self, value):
"Returns a Python boolean object." "Returns a Python boolean object."
Field.to_python(self, value) Field.clean(self, value)
return bool(value) return bool(value)
class ChoiceField(Field): class ChoiceField(Field):
@ -219,11 +220,11 @@ class ChoiceField(Field):
Field.__init__(self, required, widget) Field.__init__(self, required, widget)
self.choices = choices self.choices = choices
def to_python(self, value): def clean(self, value):
""" """
Validates that the input is in self.choices. Validates that the input is in self.choices.
""" """
value = Field.to_python(self, value) value = Field.clean(self, value)
if value in EMPTY_VALUES: value = u'' if value in EMPTY_VALUES: value = u''
if not isinstance(value, basestring): if not isinstance(value, basestring):
value = unicode(str(value), DEFAULT_ENCODING) value = unicode(str(value), DEFAULT_ENCODING)
@ -238,7 +239,7 @@ class MultipleChoiceField(ChoiceField):
def __init__(self, choices=(), required=True, widget=SelectMultiple): def __init__(self, choices=(), required=True, widget=SelectMultiple):
ChoiceField.__init__(self, choices, required, widget) ChoiceField.__init__(self, choices, required, widget)
def to_python(self, value): def clean(self, value):
""" """
Validates that the input is a list or tuple. Validates that the input is a list or tuple.
""" """
@ -259,3 +260,18 @@ class MultipleChoiceField(ChoiceField):
if val not in valid_values: if val not in valid_values:
raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % val) raise ValidationError(u'Select a valid choice. %s is not one of the available choices.' % val)
return new_value return new_value
class ComboField(Field):
def __init__(self, fields=(), required=True, widget=None):
Field.__init__(self, required, widget)
self.fields = fields
def clean(self, value):
"""
Validates the given value against all of self.fields, which is a
list of Field instances.
"""
Field.clean(self, value)
for field in self.fields:
value = field.clean(value)
return value

View File

@ -6,6 +6,13 @@ from fields import Field
from widgets import TextInput, Textarea from widgets import TextInput, Textarea
from util import ErrorDict, ErrorList, ValidationError from util import ErrorDict, ErrorList, ValidationError
NON_FIELD_ERRORS = '__all__'
def pretty_name(name):
"Converts 'first_name' to 'First name'"
name = name[0].upper() + name[1:]
return name.replace('_', ' ')
class DeclarativeFieldsMetaclass(type): class DeclarativeFieldsMetaclass(type):
"Metaclass that converts Field attributes to a dictionary called 'fields'." "Metaclass that converts Field attributes to a dictionary called 'fields'."
def __new__(cls, name, bases, attrs): def __new__(cls, name, bases, attrs):
@ -18,22 +25,33 @@ class Form(object):
def __init__(self, data=None): # TODO: prefix stuff def __init__(self, data=None): # TODO: prefix stuff
self.data = data or {} self.data = data or {}
self.__data_python = None # Stores the data after to_python() has been called. self.clean_data = None # Stores the data after clean() has been called.
self.__errors = None # Stores the errors after to_python() has been called. self.__errors = None # Stores the errors after clean() has been called.
def __str__(self):
return self.as_table()
def __iter__(self): def __iter__(self):
for name, field in self.fields.items(): for name, field in self.fields.items():
yield BoundField(self, field, name) yield BoundField(self, field, name)
def to_python(self): 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 clean(self):
if self.__errors is None: if self.__errors is None:
self._validate() self.full_clean()
return self.__data_python return self.clean_data
def errors(self): def errors(self):
"Returns an ErrorDict for self.data" "Returns an ErrorDict for self.data"
if self.__errors is None: if self.__errors is None:
self._validate() self.full_clean()
return self.__errors return self.__errors
def is_valid(self): def is_valid(self):
@ -44,27 +62,75 @@ class Form(object):
""" """
return not bool(self.errors()) return not bool(self.errors())
def __getitem__(self, name): def as_table(self):
"Returns a BoundField with the given name." "Returns this form rendered as an HTML <table>."
try: output = 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()])
field = self.fields[name] return '<table>\n%s\n</table>' % output
except KeyError:
raise KeyError('Key %r not found in Form' % name)
return BoundField(self, field, name)
def _validate(self): def as_ul(self):
data_python = {} "Returns this form rendered as an HTML <ul>."
output = u'\n'.join(['<li>%s: %s</li>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()])
return '<ul>\n%s\n</ul>' % output
def as_table_with_errors(self):
"Returns this form rendered as an HTML <table>, 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 '<table>\n%s\n</table>' % '\n'.join(output)
def as_ul_with_errors(self):
"Returns this form rendered as an HTML <ul>, 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 '<ul>\n%s\n</ul>' % '\n'.join(output)
def full_clean(self):
"""
Cleans all of self.data and populates self.__errors and self.clean_data.
"""
self.clean_data = {}
errors = ErrorDict() errors = ErrorDict()
for name, field in self.fields.items(): for name, field in self.fields.items():
value = self.data.get(name, None)
try: try:
value = field.to_python(self.data.get(name, None)) value = field.clean(value)
data_python[name] = value self.clean_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.clean_data[name] = value
except ValidationError, e: except ValidationError, e:
errors[name] = e.messages errors[name] = e.messages
if not errors: # Only set self.data_python if there weren't errors. try:
self.__data_python = data_python self.clean_data = self.clean()
except ValidationError, e:
errors[NON_FIELD_ERRORS] = e.messages
if errors:
self.clean_data = None
self.__errors = errors self.__errors = errors
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() been
called on every field.
"""
return self.clean_data
class BoundField(object): class BoundField(object):
"A Field plus data" "A Field plus data"
def __init__(self, form, field, name): def __init__(self, form, field, name):

View File

@ -343,63 +343,63 @@ If 'choices' is passed to both the constructor and render(), then they'll both b
# CharField ################################################################### # CharField ###################################################################
>>> f = CharField(required=False) >>> f = CharField(required=False)
>>> f.to_python(1) >>> f.clean(1)
u'1' u'1'
>>> f.to_python('hello') >>> f.clean('hello')
u'hello' u'hello'
>>> f.to_python(None) >>> f.clean(None)
u'' u''
>>> f.to_python([1, 2, 3]) >>> f.clean([1, 2, 3])
u'[1, 2, 3]' u'[1, 2, 3]'
CharField accepts an optional max_length parameter: CharField accepts an optional max_length parameter:
>>> f = CharField(max_length=10, required=False) >>> f = CharField(max_length=10, required=False)
>>> f.to_python('') >>> f.clean('')
u'' u''
>>> f.to_python('12345') >>> f.clean('12345')
u'12345' u'12345'
>>> f.to_python('1234567890') >>> f.clean('1234567890')
u'1234567890' u'1234567890'
>>> f.to_python('1234567890a') >>> f.clean('1234567890a')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at most 10 characters.'] ValidationError: [u'Ensure this value has at most 10 characters.']
CharField accepts an optional min_length parameter: CharField accepts an optional min_length parameter:
>>> f = CharField(min_length=10, required=False) >>> f = CharField(min_length=10, required=False)
>>> f.to_python('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 10 characters.'] ValidationError: [u'Ensure this value has at least 10 characters.']
>>> f.to_python('12345') >>> f.clean('12345')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Ensure this value has at least 10 characters.'] ValidationError: [u'Ensure this value has at least 10 characters.']
>>> f.to_python('1234567890') >>> f.clean('1234567890')
u'1234567890' u'1234567890'
>>> f.to_python('1234567890a') >>> f.clean('1234567890a')
u'1234567890a' u'1234567890a'
# IntegerField ################################################################ # IntegerField ################################################################
>>> f = IntegerField() >>> f = IntegerField()
>>> f.to_python('1') >>> f.clean('1')
1 1
>>> isinstance(f.to_python('1'), int) >>> isinstance(f.clean('1'), int)
True True
>>> f.to_python('23') >>> f.clean('23')
23 23
>>> f.to_python('a') >>> f.clean('a')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a whole number.'] ValidationError: [u'Enter a whole number.']
>>> f.to_python('1 ') >>> f.clean('1 ')
1 1
>>> f.to_python(' 1') >>> f.clean(' 1')
1 1
>>> f.to_python(' 1 ') >>> f.clean(' 1 ')
1 1
>>> f.to_python('1a') >>> f.clean('1a')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a whole number.'] ValidationError: [u'Enter a whole number.']
@ -408,75 +408,75 @@ ValidationError: [u'Enter a whole number.']
>>> import datetime >>> import datetime
>>> f = DateField() >>> f = DateField()
>>> f.to_python(datetime.date(2006, 10, 25)) >>> f.clean(datetime.date(2006, 10, 25))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('2006-10-25') >>> f.clean('2006-10-25')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('10/25/2006') >>> f.clean('10/25/2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('10/25/06') >>> f.clean('10/25/06')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('Oct 25 2006') >>> f.clean('Oct 25 2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('October 25 2006') >>> f.clean('October 25 2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('October 25, 2006') >>> f.clean('October 25, 2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('25 October 2006') >>> f.clean('25 October 2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('25 October, 2006') >>> f.clean('25 October, 2006')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('2006-4-31') >>> f.clean('2006-4-31')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
>>> f.to_python('200a-10-25') >>> f.clean('200a-10-25')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
>>> f.to_python('25/10/06') >>> f.clean('25/10/06')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
>>> f.to_python(None) >>> f.clean(None)
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This field is required.'] ValidationError: [u'This field is required.']
>>> f = DateField(required=False) >>> f = DateField(required=False)
>>> f.to_python(None) >>> f.clean(None)
>>> repr(f.to_python(None)) >>> repr(f.clean(None))
'None' 'None'
>>> f.to_python('') >>> f.clean('')
>>> repr(f.to_python('')) >>> repr(f.clean(''))
'None' 'None'
DateField accepts an optional input_formats parameter: DateField accepts an optional input_formats parameter:
>>> f = DateField(input_formats=['%Y %m %d']) >>> f = DateField(input_formats=['%Y %m %d'])
>>> f.to_python(datetime.date(2006, 10, 25)) >>> f.clean(datetime.date(2006, 10, 25))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30))
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
>>> f.to_python('2006 10 25') >>> f.clean('2006 10 25')
datetime.date(2006, 10, 25) datetime.date(2006, 10, 25)
The input_formats parameter overrides all default input formats, The input_formats parameter overrides all default input formats,
so the default formats won't work unless you specify them: so the default formats won't work unless you specify them:
>>> f.to_python('2006-10-25') >>> f.clean('2006-10-25')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
>>> f.to_python('10/25/2006') >>> f.clean('10/25/2006')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
>>> f.to_python('10/25/06') >>> f.clean('10/25/06')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date.'] ValidationError: [u'Enter a valid date.']
@ -485,63 +485,63 @@ ValidationError: [u'Enter a valid date.']
>>> import datetime >>> import datetime
>>> f = DateTimeField() >>> f = DateTimeField()
>>> f.to_python(datetime.date(2006, 10, 25)) >>> f.clean(datetime.date(2006, 10, 25))
datetime.datetime(2006, 10, 25, 0, 0) datetime.datetime(2006, 10, 25, 0, 0)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30))
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))
datetime.datetime(2006, 10, 25, 14, 30, 59) datetime.datetime(2006, 10, 25, 14, 30, 59)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))
datetime.datetime(2006, 10, 25, 14, 30, 59, 200) datetime.datetime(2006, 10, 25, 14, 30, 59, 200)
>>> f.to_python('2006-10-25 14:30:45') >>> f.clean('2006-10-25 14:30:45')
datetime.datetime(2006, 10, 25, 14, 30, 45) datetime.datetime(2006, 10, 25, 14, 30, 45)
>>> f.to_python('2006-10-25 14:30:00') >>> f.clean('2006-10-25 14:30:00')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('2006-10-25 14:30') >>> f.clean('2006-10-25 14:30')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('2006-10-25') >>> f.clean('2006-10-25')
datetime.datetime(2006, 10, 25, 0, 0) datetime.datetime(2006, 10, 25, 0, 0)
>>> f.to_python('10/25/2006 14:30:45') >>> f.clean('10/25/2006 14:30:45')
datetime.datetime(2006, 10, 25, 14, 30, 45) datetime.datetime(2006, 10, 25, 14, 30, 45)
>>> f.to_python('10/25/2006 14:30:00') >>> f.clean('10/25/2006 14:30:00')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('10/25/2006 14:30') >>> f.clean('10/25/2006 14:30')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('10/25/2006') >>> f.clean('10/25/2006')
datetime.datetime(2006, 10, 25, 0, 0) datetime.datetime(2006, 10, 25, 0, 0)
>>> f.to_python('10/25/06 14:30:45') >>> f.clean('10/25/06 14:30:45')
datetime.datetime(2006, 10, 25, 14, 30, 45) datetime.datetime(2006, 10, 25, 14, 30, 45)
>>> f.to_python('10/25/06 14:30:00') >>> f.clean('10/25/06 14:30:00')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('10/25/06 14:30') >>> f.clean('10/25/06 14:30')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python('10/25/06') >>> f.clean('10/25/06')
datetime.datetime(2006, 10, 25, 0, 0) datetime.datetime(2006, 10, 25, 0, 0)
>>> f.to_python('hello') >>> f.clean('hello')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date/time.'] ValidationError: [u'Enter a valid date/time.']
>>> f.to_python('2006-10-25 4:30 p.m.') >>> f.clean('2006-10-25 4:30 p.m.')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date/time.'] ValidationError: [u'Enter a valid date/time.']
DateField accepts an optional input_formats parameter: DateField accepts an optional input_formats parameter:
>>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) >>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p'])
>>> f.to_python(datetime.date(2006, 10, 25)) >>> f.clean(datetime.date(2006, 10, 25))
datetime.datetime(2006, 10, 25, 0, 0) datetime.datetime(2006, 10, 25, 0, 0)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30))
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))
datetime.datetime(2006, 10, 25, 14, 30, 59) datetime.datetime(2006, 10, 25, 14, 30, 59)
>>> f.to_python(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) >>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))
datetime.datetime(2006, 10, 25, 14, 30, 59, 200) datetime.datetime(2006, 10, 25, 14, 30, 59, 200)
>>> f.to_python('2006 10 25 2:30 PM') >>> f.clean('2006 10 25 2:30 PM')
datetime.datetime(2006, 10, 25, 14, 30) datetime.datetime(2006, 10, 25, 14, 30)
The input_formats parameter overrides all default input formats, The input_formats parameter overrides all default input formats,
so the default formats won't work unless you specify them: so the default formats won't work unless you specify them:
>>> f.to_python('2006-10-25 14:30:45') >>> f.clean('2006-10-25 14:30:45')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid date/time.'] ValidationError: [u'Enter a valid date/time.']
@ -549,51 +549,51 @@ ValidationError: [u'Enter a valid date/time.']
# RegexField ################################################################## # RegexField ##################################################################
>>> f = RegexField('^\d[A-F]\d$') >>> f = RegexField('^\d[A-F]\d$')
>>> f.to_python('2A2') >>> f.clean('2A2')
u'2A2' u'2A2'
>>> f.to_python('3F3') >>> f.clean('3F3')
u'3F3' u'3F3'
>>> f.to_python('3G3') >>> f.clean('3G3')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
>>> f.to_python(' 2A2') >>> f.clean(' 2A2')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
>>> f.to_python('2A2 ') >>> f.clean('2A2 ')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
Alternatively, RegexField can take a compiled regular expression: Alternatively, RegexField can take a compiled regular expression:
>>> f = RegexField(re.compile('^\d[A-F]\d$')) >>> f = RegexField(re.compile('^\d[A-F]\d$'))
>>> f.to_python('2A2') >>> f.clean('2A2')
u'2A2' u'2A2'
>>> f.to_python('3F3') >>> f.clean('3F3')
u'3F3' u'3F3'
>>> f.to_python('3G3') >>> f.clean('3G3')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
>>> f.to_python(' 2A2') >>> f.clean(' 2A2')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
>>> f.to_python('2A2 ') >>> f.clean('2A2 ')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid value.'] ValidationError: [u'Enter a valid value.']
RegexField takes an optional error_message argument: RegexField takes an optional error_message argument:
>>> f = RegexField('^\d\d\d\d$', 'Enter a four-digit number.') >>> f = RegexField('^\d\d\d\d$', 'Enter a four-digit number.')
>>> f.to_python('1234') >>> f.clean('1234')
u'1234' u'1234'
>>> f.to_python('123') >>> f.clean('123')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a four-digit number.'] ValidationError: [u'Enter a four-digit number.']
>>> f.to_python('abcd') >>> f.clean('abcd')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a four-digit number.'] ValidationError: [u'Enter a four-digit number.']
@ -601,17 +601,17 @@ ValidationError: [u'Enter a four-digit number.']
# EmailField ################################################################## # EmailField ##################################################################
>>> f = EmailField() >>> f = EmailField()
>>> f.to_python('person@example.com') >>> f.clean('person@example.com')
u'person@example.com' u'person@example.com'
>>> f.to_python('foo') >>> f.clean('foo')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid e-mail address.'] ValidationError: [u'Enter a valid e-mail address.']
>>> f.to_python('foo@') >>> f.clean('foo@')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid e-mail address.'] ValidationError: [u'Enter a valid e-mail address.']
>>> f.to_python('foo@bar') >>> f.clean('foo@bar')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid e-mail address.'] ValidationError: [u'Enter a valid e-mail address.']
@ -619,31 +619,31 @@ ValidationError: [u'Enter a valid e-mail address.']
# URLField ################################################################## # URLField ##################################################################
>>> f = URLField() >>> f = URLField()
>>> f.to_python('http://example.com') >>> f.clean('http://example.com')
u'http://example.com' u'http://example.com'
>>> f.to_python('http://www.example.com') >>> f.clean('http://www.example.com')
u'http://www.example.com' u'http://www.example.com'
>>> f.to_python('foo') >>> f.clean('foo')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('example.com') >>> f.clean('example.com')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('http://') >>> f.clean('http://')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('http://example') >>> f.clean('http://example')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('http://example.') >>> f.clean('http://example.')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('http://.com') >>> f.clean('http://.com')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
@ -651,17 +651,17 @@ ValidationError: [u'Enter a valid URL.']
URLField takes an optional verify_exists parameter, which is False by default. URLField takes an optional verify_exists parameter, which is False by default.
This verifies that the URL is live on the Internet and doesn't return a 404 or 500: This verifies that the URL is live on the Internet and doesn't return a 404 or 500:
>>> f = URLField(verify_exists=True) >>> f = URLField(verify_exists=True)
>>> f.to_python('http://www.google.com') >>> f.clean('http://www.google.com') # This will fail if there's no Internet connection
u'http://www.google.com' u'http://www.google.com'
>>> f.to_python('http://example') >>> f.clean('http://example')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid URL.'] ValidationError: [u'Enter a valid URL.']
>>> f.to_python('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') # bad domain >>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') # bad domain
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This URL appears to be a broken link.'] ValidationError: [u'This URL appears to be a broken link.']
>>> f.to_python('http://google.com/we-love-microsoft.html') # good domain, bad page >>> f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This URL appears to be a broken link.'] ValidationError: [u'This URL appears to be a broken link.']
@ -669,41 +669,41 @@ ValidationError: [u'This URL appears to be a broken link.']
# BooleanField ################################################################ # BooleanField ################################################################
>>> f = BooleanField() >>> f = BooleanField()
>>> f.to_python(True) >>> f.clean(True)
True True
>>> f.to_python(False) >>> f.clean(False)
False False
>>> f.to_python(1) >>> f.clean(1)
True True
>>> f.to_python(0) >>> f.clean(0)
False False
>>> f.to_python('Django rocks') >>> f.clean('Django rocks')
True True
# ChoiceField ################################################################# # ChoiceField #################################################################
>>> f = ChoiceField(choices=[('1', '1'), ('2', '2')]) >>> f = ChoiceField(choices=[('1', '1'), ('2', '2')])
>>> f.to_python(1) >>> f.clean(1)
u'1' u'1'
>>> f.to_python('1') >>> f.clean('1')
u'1' u'1'
>>> f.to_python(None) >>> f.clean(None)
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This field is required.'] ValidationError: [u'This field is required.']
>>> f.to_python('') >>> f.clean('')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This field is required.'] ValidationError: [u'This field is required.']
>>> f.to_python('3') >>> f.clean('3')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ValidationError: [u'Select a valid choice. 3 is not one of the available choices.']
>>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) >>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')])
>>> f.to_python('J') >>> f.clean('J')
u'J' u'J'
>>> f.to_python('John') >>> f.clean('John')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. John is not one of the available choices.'] ValidationError: [u'Select a valid choice. John is not one of the available choices.']
@ -711,39 +711,98 @@ ValidationError: [u'Select a valid choice. John is not one of the available choi
# MultipleChoiceField ######################################################### # MultipleChoiceField #########################################################
>>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) >>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')])
>>> f.to_python([1]) >>> f.clean([1])
[u'1'] [u'1']
>>> f.to_python(['1']) >>> f.clean(['1'])
[u'1'] [u'1']
>>> f.to_python(['1', '2']) >>> f.clean(['1', '2'])
[u'1', u'2'] [u'1', u'2']
>>> f.to_python([1, '2']) >>> f.clean([1, '2'])
[u'1', u'2'] [u'1', u'2']
>>> f.to_python((1, '2')) >>> f.clean((1, '2'))
[u'1', u'2'] [u'1', u'2']
>>> f.to_python('hello') >>> f.clean('hello')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a list of values.'] ValidationError: [u'Enter a list of values.']
>>> f.to_python([]) >>> f.clean([])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This field is required.'] ValidationError: [u'This field is required.']
>>> f.to_python(()) >>> f.clean(())
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'This field is required.'] ValidationError: [u'This field is required.']
>>> f.to_python(['3']) >>> f.clean(['3'])
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ValidationError: [u'Select a valid choice. 3 is not one of the available choices.']
# ComboField ##################################################################
ComboField takes a list of fields that should be used to validate a value,
in that order:
>>> f = ComboField(fields=[CharField(max_length=20), EmailField()])
>>> f.clean('test@example.com')
u'test@example.com'
>>> f.clean('longemailaddress@example.com')
Traceback (most recent call last):
...
ValidationError: [u'Ensure this value has at most 20 characters.']
>>> f.clean('not an e-mail')
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid e-mail address.']
>>> f.clean('')
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
>>> f.clean(None)
Traceback (most recent call last):
...
ValidationError: [u'This field is required.']
# Form ######################################################################## # Form ########################################################################
>>> class Person(Form): >>> class Person(Form):
... first_name = CharField() ... first_name = CharField()
... last_name = CharField() ... last_name = CharField()
... birthday = DateField() ... birthday = DateField()
>>> p = Person()
>>> print p
<table>
<tr><td>First name:</td><td><input type="text" name="first_name" /></td></tr>
<tr><td>Last name:</td><td><input type="text" name="last_name" /></td></tr>
<tr><td>Birthday:</td><td><input type="text" name="birthday" /></td></tr>
</table>
>>> print p.as_table()
<table>
<tr><td>First name:</td><td><input type="text" name="first_name" /></td></tr>
<tr><td>Last name:</td><td><input type="text" name="last_name" /></td></tr>
<tr><td>Birthday:</td><td><input type="text" name="birthday" /></td></tr>
</table>
>>> print p.as_ul()
<ul>
<li>First name: <input type="text" name="first_name" /></li>
<li>Last name: <input type="text" name="last_name" /></li>
<li>Birthday: <input type="text" name="birthday" /></li>
</ul>
>>> print p.as_table_with_errors()
<table>
<tr><td colspan="2"><ul><li>This field is required.</li></ul></td></tr>
<tr><td>First name:</td><td><input type="text" name="first_name" /></td></tr>
<tr><td colspan="2"><ul><li>This field is required.</li></ul></td></tr>
<tr><td>Last name:</td><td><input type="text" name="last_name" /></td></tr>
<tr><td colspan="2"><ul><li>This field is required.</li></ul></td></tr>
<tr><td>Birthday:</td><td><input type="text" name="birthday" /></td></tr>
</table>
>>> print p.as_ul_with_errors()
<ul>
<li><ul><li>This field is required.</li></ul>First name: <input type="text" name="first_name" /></li>
<li><ul><li>This field is required.</li></ul>Last name: <input type="text" name="last_name" /></li>
<li><ul><li>This field is required.</li></ul>Birthday: <input type="text" name="birthday" /></li>
</ul>
>>> p = Person({'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9'}) >>> p = Person({'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9'})
>>> p.errors() >>> p.errors()
{} {}
@ -753,7 +812,7 @@ True
u'' u''
>>> p.errors().as_text() >>> p.errors().as_text()
u'' u''
>>> p.to_python() >>> p.clean()
{'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)} {'first_name': u'John', 'last_name': u'Lennon', 'birthday': datetime.date(1940, 10, 9)}
>>> print p['first_name'] >>> print p['first_name']
<input type="text" name="first_name" value="John" /> <input type="text" name="first_name" value="John" />
@ -766,6 +825,12 @@ u''
<input type="text" name="first_name" value="John" /> <input type="text" name="first_name" value="John" />
<input type="text" name="last_name" value="Lennon" /> <input type="text" name="last_name" value="Lennon" />
<input type="text" name="birthday" value="1940-10-9" /> <input type="text" name="birthday" value="1940-10-9" />
>>> print p
<table>
<tr><td>First name:</td><td><input type="text" name="first_name" value="John" /></td></tr>
<tr><td>Last name:</td><td><input type="text" name="last_name" value="Lennon" /></td></tr>
<tr><td>Birthday:</td><td><input type="text" name="birthday" value="1940-10-9" /></td></tr>
</table>
>>> p = Person({'last_name': u'Lennon'}) >>> p = Person({'last_name': u'Lennon'})
>>> p.errors() >>> p.errors()
@ -779,8 +844,8 @@ u'<ul class="errorlist"><li>first_name<ul class="errorlist"><li>This field is re
* This field is required. * This field is required.
* birthday * birthday
* This field is required. * This field is required.
>>> p.to_python() >>> p.clean()
>>> repr(p.to_python()) >>> repr(p.clean())
'None' 'None'
>>> p['first_name'].errors >>> p['first_name'].errors
[u'This field is required.'] [u'This field is required.']
@ -887,6 +952,84 @@ MultipleChoiceField is a special case, as its data is required to be a list:
<option value="J">John Lennon</option> <option value="J">John Lennon</option>
<option value="P" selected="selected">Paul McCartney</option> <option value="P" selected="selected">Paul McCartney</option>
</select> </select>
There are a couple of ways to do multiple-field validation. If you want the
validation message to be associated with a particular field, implement the
clean_XXX() method on the Form, where XXX is the field name. As in
Field.clean(), the clean_XXX() method should return the cleaned value:
>>> class UserRegistration(Form):
... username = CharField(max_length=10)
... password1 = CharField(widget=PasswordInput)
... password2 = CharField(widget=PasswordInput)
... def clean_password2(self):
... if self.clean_data.get('password1') and self.clean_data.get('password2') and self.clean_data['password1'] != self.clean_data['password2']:
... raise ValidationError(u'Please make sure your passwords match.')
... return self.clean_data['password2']
>>> f = UserRegistration()
>>> f.errors()
{'username': [u'This field is required.'], 'password1': [u'This field is required.'], 'password2': [u'This field is required.']}
>>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'})
>>> f.errors()
{'password2': [u'Please make sure your passwords match.']}
>>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'})
>>> f.errors()
{}
>>> f.clean()
{'username': u'adrian', 'password1': u'foo', 'password2': u'foo'}
Another way of doing multiple-field validation is by implementing the
Form's clean() method. If you do this, any ValidationError raised by that
method will not be associated with a particular field; it will have a
special-case association with the field named '__all__'. Note that
Form.clean() still needs to return a dictionary of all clean data:
>>> class UserRegistration(Form):
... username = CharField(max_length=10)
... password1 = CharField(widget=PasswordInput)
... password2 = CharField(widget=PasswordInput)
... def clean(self):
... if self.clean_data.get('password1') and self.clean_data.get('password2') and self.clean_data['password1'] != self.clean_data['password2']:
... raise ValidationError(u'Please make sure your passwords match.')
... return self.clean_data
>>> f = UserRegistration()
>>> print f.as_table()
<table>
<tr><td>Username:</td><td><input type="text" name="username" /></td></tr>
<tr><td>Password1:</td><td><input type="password" name="password1" /></td></tr>
<tr><td>Password2:</td><td><input type="password" name="password2" /></td></tr>
</table>
>>> f.errors()
{'username': [u'This field is required.'], 'password1': [u'This field is required.'], 'password2': [u'This field is required.']}
>>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'})
>>> f.errors()
{'__all__': [u'Please make sure your passwords match.']}
>>> print f.as_table()
<table>
<tr><td>Username:</td><td><input type="text" name="username" value="adrian" /></td></tr>
<tr><td>Password1:</td><td><input type="password" name="password1" value="foo" /></td></tr>
<tr><td>Password2:</td><td><input type="password" name="password2" value="bar" /></td></tr>
</table>
>>> print f.as_table_with_errors()
<table>
<tr><td colspan="2"><ul><li>Please make sure your passwords match.</li></ul></td></tr>
<tr><td>Username:</td><td><input type="text" name="username" value="adrian" /></td></tr>
<tr><td>Password1:</td><td><input type="password" name="password1" value="foo" /></td></tr>
<tr><td>Password2:</td><td><input type="password" name="password2" value="bar" /></td></tr>
</table>
>>> print f.as_ul_with_errors()
<ul>
<li><ul><li>Please make sure your passwords match.</li></ul></li>
<li>Username: <input type="text" name="username" value="adrian" /></li>
<li>Password1: <input type="password" name="password1" value="foo" /></li>
<li>Password2: <input type="password" name="password2" value="bar" /></li>
</ul>
>>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'})
>>> f.errors()
{}
>>> f.clean()
{'username': u'adrian', 'password1': u'foo', 'password2': u'foo'}
""" """
if __name__ == "__main__": if __name__ == "__main__":