web/lib/django/forms/fields.py
changeset 29 cc9b7e14412b
parent 0 0d40e90630ef
--- a/web/lib/django/forms/fields.py	Wed May 19 17:43:59 2010 +0200
+++ b/web/lib/django/forms/fields.py	Tue May 25 02:43:45 2010 +0200
@@ -2,33 +2,33 @@
 Field classes.
 """
 
-import copy
 import datetime
 import os
 import re
 import time
 import urlparse
+import warnings
+from decimal import Decimal, DecimalException
 try:
     from cStringIO import StringIO
 except ImportError:
     from StringIO import StringIO
 
-# Python 2.3 fallbacks
-try:
-    from decimal import Decimal, DecimalException
-except ImportError:
-    from django.utils._decimal import Decimal, DecimalException
-try:
-    set
-except NameError:
-    from sets import Set as set
-
-import django.core.exceptions
+from django.core.exceptions import ValidationError
+from django.core import validators
+import django.utils.copycompat as copy
+from django.utils import formats
 from django.utils.translation import ugettext_lazy as _
 from django.utils.encoding import smart_unicode, smart_str
+from django.utils.functional import lazy
 
-from util import ErrorList, ValidationError
-from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
+# Provide this import for backwards compatibility.
+from django.core.validators import EMPTY_VALUES
+
+from util import ErrorList
+from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \
+        FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \
+        DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget
 
 __all__ = (
     'Field', 'CharField', 'IntegerField',
@@ -42,13 +42,25 @@
     'TypedChoiceField'
 )
 
-# These values, if given to to_python(), will trigger the self.required check.
-EMPTY_VALUES = (None, '')
+def en_format(name):
+    """
+    Helper function to stay backward compatible.
+    """
+    from django.conf.locale.en import formats
+    warnings.warn(
+        "`django.forms.fields.DEFAULT_%s` is deprecated; use `django.utils.formats.get_format('%s')` instead." % (name, name),
+        PendingDeprecationWarning
+    )
+    return getattr(formats, name)
 
+DEFAULT_DATE_INPUT_FORMATS = lazy(lambda: en_format('DATE_INPUT_FORMATS'), tuple, list)()
+DEFAULT_TIME_INPUT_FORMATS = lazy(lambda: en_format('TIME_INPUT_FORMATS'), tuple, list)()
+DEFAULT_DATETIME_INPUT_FORMATS = lazy(lambda: en_format('DATETIME_INPUT_FORMATS'), tuple, list)()
 
 class Field(object):
     widget = TextInput # Default widget to use when rendering this type of Field.
     hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
+    default_validators = [] # Default set of validators
     default_error_messages = {
         'required': _(u'This field is required.'),
         'invalid': _(u'Enter a valid value.'),
@@ -58,7 +70,8 @@
     creation_counter = 0
 
     def __init__(self, required=True, widget=None, label=None, initial=None,
-                 help_text=None, error_messages=None, show_hidden_initial=False):
+                 help_text=None, error_messages=None, show_hidden_initial=False,
+                 validators=[], localize=False):
         # required -- Boolean that specifies whether the field is required.
         #             True by default.
         # widget -- A Widget class, or instance of a Widget class, that should
@@ -72,8 +85,12 @@
         # initial -- A value to use in this Field's initial display. This value
         #            is *not* used as a fallback if data isn't given.
         # help_text -- An optional string to use as "help text" for this Field.
+        # error_messages -- An optional dictionary to override the default
+        #                   messages that the field will raise.
         # show_hidden_initial -- Boolean that specifies if it is needed to render a
         #                        hidden widget with initial value after widget.
+        # validators -- List of addtional validators to use
+        # localize -- Boolean that specifies if the field should be localized.
         if label is not None:
             label = smart_unicode(label)
         self.required, self.label, self.initial = required, label, initial
@@ -86,6 +103,9 @@
         if isinstance(widget, type):
             widget = widget()
 
+        # Trigger the localization machinery if needed.
+        self.localize = localize
+
         # Hook into self.widget_attrs() for any Field-specific HTML attributes.
         extra_attrs = self.widget_attrs(widget)
         if extra_attrs:
@@ -97,16 +117,42 @@
         self.creation_counter = Field.creation_counter
         Field.creation_counter += 1
 
-        def set_class_error_messages(messages, klass):
-            for base_class in klass.__bases__:
-                set_class_error_messages(messages, base_class)
-            messages.update(getattr(klass, 'default_error_messages', {}))
-
         messages = {}
-        set_class_error_messages(messages, self.__class__)
+        for c in reversed(self.__class__.__mro__):
+            messages.update(getattr(c, 'default_error_messages', {}))
         messages.update(error_messages or {})
         self.error_messages = messages
 
+        self.validators = self.default_validators + validators
+
+    def localize_value(self, value):
+        return formats.localize_input(value)
+
+    def to_python(self, value):
+        return value
+
+    def validate(self, value):
+        if value in validators.EMPTY_VALUES and self.required:
+            raise ValidationError(self.error_messages['required'])
+
+    def run_validators(self, value):
+        if value in validators.EMPTY_VALUES:
+            return
+        errors = []
+        for v in self.validators:
+            try:
+                v(value)
+            except ValidationError, e:
+                if hasattr(e, 'code') and e.code in self.error_messages:
+                    message = self.error_messages[e.code]
+                    if e.params:
+                        message = message % e.params
+                    errors.append(message)
+                else:
+                    errors.extend(e.messages)
+        if errors:
+            raise ValidationError(errors)
+
     def clean(self, value):
         """
         Validates the given value and returns its "cleaned" value as an
@@ -114,8 +160,9 @@
 
         Raises ValidationError for any errors.
         """
-        if self.required and value in EMPTY_VALUES:
-            raise ValidationError(self.error_messages['required'])
+        value = self.to_python(value)
+        self.validate(value)
+        self.run_validators(value)
         return value
 
     def widget_attrs(self, widget):
@@ -133,27 +180,19 @@
         return result
 
 class CharField(Field):
-    default_error_messages = {
-        'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'),
-        'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'),
-    }
-
     def __init__(self, max_length=None, min_length=None, *args, **kwargs):
         self.max_length, self.min_length = max_length, min_length
         super(CharField, self).__init__(*args, **kwargs)
+        if min_length is not None:
+            self.validators.append(validators.MinLengthValidator(min_length))
+        if max_length is not None:
+            self.validators.append(validators.MaxLengthValidator(max_length))
 
-    def clean(self, value):
-        "Validates max_length and min_length. Returns a Unicode object."
-        super(CharField, self).clean(value)
-        if value in EMPTY_VALUES:
+    def to_python(self, value):
+        "Returns a Unicode object."
+        if value in validators.EMPTY_VALUES:
             return u''
-        value = smart_unicode(value)
-        value_length = len(value)
-        if self.max_length is not None and value_length > self.max_length:
-            raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length})
-        if self.min_length is not None and value_length < self.min_length:
-            raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length})
-        return value
+        return smart_unicode(value)
 
     def widget_attrs(self, widget):
         if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
@@ -163,92 +202,101 @@
 class IntegerField(Field):
     default_error_messages = {
         'invalid': _(u'Enter a whole number.'),
-        'max_value': _(u'Ensure this value is less than or equal to %s.'),
-        'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+        'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
+        'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
     }
 
     def __init__(self, max_value=None, min_value=None, *args, **kwargs):
-        self.max_value, self.min_value = max_value, min_value
         super(IntegerField, self).__init__(*args, **kwargs)
 
-    def clean(self, value):
+        if max_value is not None:
+            self.validators.append(validators.MaxValueValidator(max_value))
+        if min_value is not None:
+            self.validators.append(validators.MinValueValidator(min_value))
+
+    def to_python(self, value):
         """
         Validates that int() can be called on the input. Returns the result
         of int(). Returns None for empty values.
         """
-        super(IntegerField, self).clean(value)
-        if value in EMPTY_VALUES:
+        value = super(IntegerField, self).to_python(value)
+        if value in validators.EMPTY_VALUES:
             return None
+        if self.localize:
+            value = formats.sanitize_separators(value)
         try:
             value = int(str(value))
         except (ValueError, TypeError):
             raise ValidationError(self.error_messages['invalid'])
-        if self.max_value is not None and value > self.max_value:
-            raise ValidationError(self.error_messages['max_value'] % self.max_value)
-        if self.min_value is not None and value < self.min_value:
-            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         return value
 
-class FloatField(Field):
+class FloatField(IntegerField):
     default_error_messages = {
         'invalid': _(u'Enter a number.'),
-        'max_value': _(u'Ensure this value is less than or equal to %s.'),
-        'min_value': _(u'Ensure this value is greater than or equal to %s.'),
     }
 
-    def __init__(self, max_value=None, min_value=None, *args, **kwargs):
-        self.max_value, self.min_value = max_value, min_value
-        Field.__init__(self, *args, **kwargs)
-
-    def clean(self, value):
+    def to_python(self, value):
+        """
+        Validates that float() can be called on the input. Returns the result
+        of float(). Returns None for empty values.
         """
-        Validates that float() can be called on the input. Returns a float.
-        Returns None for empty values.
-        """
-        super(FloatField, self).clean(value)
-        if not self.required and value in EMPTY_VALUES:
+        value = super(IntegerField, self).to_python(value)
+        if value in validators.EMPTY_VALUES:
             return None
+        if self.localize:
+            value = formats.sanitize_separators(value)
         try:
             value = float(value)
         except (ValueError, TypeError):
             raise ValidationError(self.error_messages['invalid'])
-        if self.max_value is not None and value > self.max_value:
-            raise ValidationError(self.error_messages['max_value'] % self.max_value)
-        if self.min_value is not None and value < self.min_value:
-            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         return value
 
 class DecimalField(Field):
     default_error_messages = {
         'invalid': _(u'Enter a number.'),
-        'max_value': _(u'Ensure this value is less than or equal to %s.'),
-        'min_value': _(u'Ensure this value is greater than or equal to %s.'),
+        'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
+        'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
         'max_digits': _('Ensure that there are no more than %s digits in total.'),
         'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
         'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
     }
 
     def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
-        self.max_value, self.min_value = max_value, min_value
         self.max_digits, self.decimal_places = max_digits, decimal_places
         Field.__init__(self, *args, **kwargs)
 
-    def clean(self, value):
+        if max_value is not None:
+            self.validators.append(validators.MaxValueValidator(max_value))
+        if min_value is not None:
+            self.validators.append(validators.MinValueValidator(min_value))
+
+    def to_python(self, value):
         """
         Validates that the input is a decimal number. Returns a Decimal
         instance. Returns None for empty values. Ensures that there are no more
         than max_digits in the number, and no more than decimal_places digits
         after the decimal point.
         """
-        super(DecimalField, self).clean(value)
-        if not self.required and value in EMPTY_VALUES:
+        if value in validators.EMPTY_VALUES:
             return None
+        if self.localize:
+            value = formats.sanitize_separators(value)
         value = smart_str(value).strip()
         try:
             value = Decimal(value)
         except DecimalException:
             raise ValidationError(self.error_messages['invalid'])
+        return value
 
+    def validate(self, value):
+        super(DecimalField, self).validate(value)
+        if value in validators.EMPTY_VALUES:
+            return
+        # Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
+        # since it is never equal to itself. However, NaN is the only value that
+        # isn't equal to itself, so we can use this to identify NaN
+        if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
+            raise ValidationError(self.error_messages['invalid'])
         sign, digittuple, exponent = value.as_tuple()
         decimals = abs(exponent)
         # digittuple doesn't include any leading zeros.
@@ -261,10 +309,6 @@
             digits = decimals
         whole_digits = digits - decimals
 
-        if self.max_value is not None and value > self.max_value:
-            raise ValidationError(self.error_messages['max_value'] % self.max_value)
-        if self.min_value is not None and value < self.min_value:
-            raise ValidationError(self.error_messages['min_value'] % self.min_value)
         if self.max_digits is not None and digits > self.max_digits:
             raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
         if self.decimal_places is not None and decimals > self.decimal_places:
@@ -273,14 +317,6 @@
             raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
         return value
 
-DEFAULT_DATE_INPUT_FORMATS = (
-    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
-    '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006'
-    '%d %b %Y', '%d %b, %Y',            # '25 Oct 2006', '25 Oct, 2006'
-    '%B %d %Y', '%B %d, %Y',            # 'October 25 2006', 'October 25, 2006'
-    '%d %B %Y', '%d %B, %Y',            # '25 October 2006', '25 October, 2006'
-)
-
 class DateField(Field):
     widget = DateInput
     default_error_messages = {
@@ -289,32 +325,26 @@
 
     def __init__(self, input_formats=None, *args, **kwargs):
         super(DateField, self).__init__(*args, **kwargs)
-        self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS
+        self.input_formats = input_formats
 
-    def clean(self, value):
+    def to_python(self, value):
         """
         Validates that the input can be converted to a date. Returns a Python
         datetime.date object.
         """
-        super(DateField, self).clean(value)
-        if value in EMPTY_VALUES:
+        if value in validators.EMPTY_VALUES:
             return None
         if isinstance(value, datetime.datetime):
             return value.date()
         if isinstance(value, datetime.date):
             return value
-        for format in self.input_formats:
+        for format in self.input_formats or formats.get_format('DATE_INPUT_FORMATS'):
             try:
                 return datetime.date(*time.strptime(value, format)[:3])
             except ValueError:
                 continue
         raise ValidationError(self.error_messages['invalid'])
 
-DEFAULT_TIME_INPUT_FORMATS = (
-    '%H:%M:%S',     # '14:30:59'
-    '%H:%M',        # '14:30'
-)
-
 class TimeField(Field):
     widget = TimeInput
     default_error_messages = {
@@ -323,37 +353,24 @@
 
     def __init__(self, input_formats=None, *args, **kwargs):
         super(TimeField, self).__init__(*args, **kwargs)
-        self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS
+        self.input_formats = input_formats
 
-    def clean(self, value):
+    def to_python(self, value):
         """
         Validates that the input can be converted to a time. Returns a Python
         datetime.time object.
         """
-        super(TimeField, self).clean(value)
-        if value in EMPTY_VALUES:
+        if value in validators.EMPTY_VALUES:
             return None
         if isinstance(value, datetime.time):
             return value
-        for format in self.input_formats:
+        for format in self.input_formats or formats.get_format('TIME_INPUT_FORMATS'):
             try:
                 return datetime.time(*time.strptime(value, format)[3:6])
             except ValueError:
                 continue
         raise ValidationError(self.error_messages['invalid'])
 
-DEFAULT_DATETIME_INPUT_FORMATS = (
-    '%Y-%m-%d %H:%M:%S',     # '2006-10-25 14:30:59'
-    '%Y-%m-%d %H:%M',        # '2006-10-25 14:30'
-    '%Y-%m-%d',              # '2006-10-25'
-    '%m/%d/%Y %H:%M:%S',     # '10/25/2006 14:30:59'
-    '%m/%d/%Y %H:%M',        # '10/25/2006 14:30'
-    '%m/%d/%Y',              # '10/25/2006'
-    '%m/%d/%y %H:%M:%S',     # '10/25/06 14:30:59'
-    '%m/%d/%y %H:%M',        # '10/25/06 14:30'
-    '%m/%d/%y',              # '10/25/06'
-)
-
 class DateTimeField(Field):
     widget = DateTimeInput
     default_error_messages = {
@@ -362,15 +379,14 @@
 
     def __init__(self, input_formats=None, *args, **kwargs):
         super(DateTimeField, self).__init__(*args, **kwargs)
-        self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS
+        self.input_formats = input_formats
 
-    def clean(self, value):
+    def to_python(self, value):
         """
         Validates that the input can be converted to a datetime. Returns a
         Python datetime.datetime object.
         """
-        super(DateTimeField, self).clean(value)
-        if value in EMPTY_VALUES:
+        if value in validators.EMPTY_VALUES:
             return None
         if isinstance(value, datetime.datetime):
             return value
@@ -382,7 +398,7 @@
             if len(value) != 2:
                 raise ValidationError(self.error_messages['invalid'])
             value = '%s %s' % tuple(value)
-        for format in self.input_formats:
+        for format in self.input_formats or formats.get_format('DATETIME_INPUT_FORMATS'):
             try:
                 return datetime.datetime(*time.strptime(value, format)[:6])
             except ValueError:
@@ -405,40 +421,13 @@
         if isinstance(regex, basestring):
             regex = re.compile(regex)
         self.regex = regex
+        self.validators.append(validators.RegexValidator(regex=regex))
 
-    def clean(self, value):
-        """
-        Validates that the input matches the regular expression. Returns a
-        Unicode object.
-        """
-        value = super(RegexField, self).clean(value)
-        if value == u'':
-            return value
-        if not self.regex.search(value):
-            raise ValidationError(self.error_messages['invalid'])
-        return value
-
-email_re = re.compile(
-    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
-    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
-    r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
-
-class EmailField(RegexField):
+class EmailField(CharField):
     default_error_messages = {
         'invalid': _(u'Enter a valid e-mail address.'),
     }
-
-    def __init__(self, max_length=None, min_length=None, *args, **kwargs):
-        RegexField.__init__(self, email_re, max_length, min_length, *args,
-                            **kwargs)
-
-try:
-    from django.conf import settings
-    URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
-except ImportError:
-    # It's OK if Django settings aren't configured.
-    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
-
+    default_validators = [validators.validate_email]
 
 class FileField(Field):
     widget = FileInput
@@ -453,12 +442,9 @@
         self.max_length = kwargs.pop('max_length', None)
         super(FileField, self).__init__(*args, **kwargs)
 
-    def clean(self, data, initial=None):
-        super(FileField, self).clean(initial or data)
-        if not self.required and data in EMPTY_VALUES:
+    def to_python(self, data):
+        if data in validators.EMPTY_VALUES:
             return None
-        elif not data and initial:
-            return initial
 
         # UploadedFile objects should have name and size attributes.
         try:
@@ -477,22 +463,30 @@
 
         return data
 
+    def clean(self, data, initial=None):
+        if not data and initial:
+            return initial
+        return super(FileField, self).clean(data)
+
 class ImageField(FileField):
     default_error_messages = {
         'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
     }
 
-    def clean(self, data, initial=None):
+    def to_python(self, data):
         """
         Checks that the file-upload field data contains a valid image (GIF, JPG,
         PNG, possibly others -- whatever the Python Imaging Library supports).
         """
-        f = super(ImageField, self).clean(data, initial)
+        f = super(ImageField, self).to_python(data)
         if f is None:
             return None
-        elif not data and initial:
-            return initial
-        from PIL import Image
+
+        # Try to import PIL in either of the two ways it can end up installed.
+        try:
+            from PIL import Image
+        except ImportError:
+            import Image
 
         # We need to get a file object for PIL. We might have a path or we might
         # have to read the data into memory.
@@ -530,59 +524,34 @@
             f.seek(0)
         return f
 
-url_re = re.compile(
-    r'^https?://' # http:// or https://
-    r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
-    r'localhost|' #localhost...
-    r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
-    r'(?::\d+)?' # optional port
-    r'(?:/?|/\S+)$', re.IGNORECASE)
-
-class URLField(RegexField):
+class URLField(CharField):
     default_error_messages = {
         'invalid': _(u'Enter a valid URL.'),
         'invalid_link': _(u'This URL appears to be a broken link.'),
     }
 
     def __init__(self, max_length=None, min_length=None, verify_exists=False,
-            validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs):
-        super(URLField, self).__init__(url_re, max_length, min_length, *args,
+            validator_user_agent=validators.URL_VALIDATOR_USER_AGENT, *args, **kwargs):
+        super(URLField, self).__init__(max_length, min_length, *args,
                                        **kwargs)
-        self.verify_exists = verify_exists
-        self.user_agent = validator_user_agent
+        self.validators.append(validators.URLValidator(verify_exists=verify_exists, validator_user_agent=validator_user_agent))
 
-    def clean(self, value):
-        # If no URL scheme given, assume http://
-        if value and '://' not in value:
-            value = u'http://%s' % value
-        # If no URL path given, assume /
-        if value and not urlparse.urlsplit(value)[2]:
-            value += '/'
-        value = super(URLField, self).clean(value)
-        if value == u'':
-            return value
-        if self.verify_exists:
-            import urllib2
-            headers = {
-                "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
-                "Accept-Language": "en-us,en;q=0.5",
-                "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
-                "Connection": "close",
-                "User-Agent": self.user_agent,
-            }
-            try:
-                req = urllib2.Request(value, None, headers)
-                u = urllib2.urlopen(req)
-            except ValueError:
-                raise ValidationError(self.error_messages['invalid'])
-            except: # urllib2.URLError, httplib.InvalidURL, etc.
-                raise ValidationError(self.error_messages['invalid_link'])
-        return value
+    def to_python(self, value):
+        if value:
+            if '://' not in value:
+                # If no URL scheme given, assume http://
+                value = u'http://%s' % value
+            url_fields = list(urlparse.urlsplit(value))
+            if not url_fields[2]:
+                # the path portion may need to be added before query params
+                url_fields[2] = '/'
+                value = urlparse.urlunsplit(url_fields)
+        return super(URLField, self).to_python(value)
 
 class BooleanField(Field):
     widget = CheckboxInput
 
-    def clean(self, value):
+    def to_python(self, value):
         """Returns a Python boolean object."""
         # Explicitly check for the string 'False', which is what a hidden field
         # will submit for False. Also check for '0', since this is what
@@ -592,7 +561,7 @@
             value = False
         else:
             value = bool(value)
-        super(BooleanField, self).clean(value)
+        value = super(BooleanField, self).to_python(value)
         if not value and self.required:
             raise ValidationError(self.error_messages['required'])
         return value
@@ -604,7 +573,7 @@
     """
     widget = NullBooleanSelect
 
-    def clean(self, value):
+    def to_python(self, value):
         """
         Explicitly checks for the string 'True' and 'False', which is what a
         hidden field will submit for True and False, and for '1' and '0', which
@@ -618,6 +587,9 @@
         else:
             return None
 
+    def validate(self, value):
+        pass
+
 class ChoiceField(Field):
     widget = Select
     default_error_messages = {
@@ -626,8 +598,8 @@
 
     def __init__(self, choices=(), required=True, widget=None, label=None,
                  initial=None, help_text=None, *args, **kwargs):
-        super(ChoiceField, self).__init__(required, widget, label, initial,
-                                          help_text, *args, **kwargs)
+        super(ChoiceField, self).__init__(required=required, widget=widget, label=label,
+                                        initial=initial, help_text=help_text, *args, **kwargs)
         self.choices = choices
 
     def _get_choices(self):
@@ -641,24 +613,24 @@
 
     choices = property(_get_choices, _set_choices)
 
-    def clean(self, value):
+    def to_python(self, value):
+        "Returns a Unicode object."
+        if value in validators.EMPTY_VALUES:
+            return u''
+        return smart_unicode(value)
+
+    def validate(self, value):
         """
         Validates that the input is in self.choices.
         """
-        value = super(ChoiceField, self).clean(value)
-        if value in EMPTY_VALUES:
-            value = u''
-        value = smart_unicode(value)
-        if value == u'':
-            return value
-        if not self.valid_value(value):
+        super(ChoiceField, self).validate(value)
+        if value and not self.valid_value(value):
             raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
-        return value
 
     def valid_value(self, value):
         "Check to see if the provided value is a valid choice"
         for k, v in self.choices:
-            if type(v) in (tuple, list):
+            if isinstance(v, (list, tuple)):
                 # This is an optgroup, so look inside the group for options
                 for k2, v2 in v:
                     if value == smart_unicode(k2):
@@ -674,27 +646,24 @@
         self.empty_value = kwargs.pop('empty_value', '')
         super(TypedChoiceField, self).__init__(*args, **kwargs)
 
-    def clean(self, value):
+    def to_python(self, value):
         """
         Validate that the value is in self.choices and can be coerced to the
         right type.
         """
-        value = super(TypedChoiceField, self).clean(value)
-        if value == self.empty_value or value in EMPTY_VALUES:
+        value = super(TypedChoiceField, self).to_python(value)
+        super(TypedChoiceField, self).validate(value)
+        if value == self.empty_value or value in validators.EMPTY_VALUES:
             return self.empty_value
-
-        # Hack alert: This field is purpose-made to use with Field.to_python as
-        # a coercion function so that ModelForms with choices work. However,
-        # Django's Field.to_python raises
-        # django.core.exceptions.ValidationError, which is a *different*
-        # exception than django.forms.util.ValidationError. So we need to catch
-        # both.
         try:
             value = self.coerce(value)
-        except (ValueError, TypeError, django.core.exceptions.ValidationError):
+        except (ValueError, TypeError, ValidationError):
             raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
         return value
 
+    def validate(self, value):
+        pass
+
 class MultipleChoiceField(ChoiceField):
     hidden_widget = MultipleHiddenInput
     widget = SelectMultiple
@@ -703,22 +672,23 @@
         'invalid_list': _(u'Enter a list of values.'),
     }
 
-    def clean(self, value):
+    def to_python(self, value):
+        if not value:
+            return []
+        elif not isinstance(value, (list, tuple)):
+            raise ValidationError(self.error_messages['invalid_list'])
+        return [smart_unicode(val) for val in value]
+
+    def validate(self, value):
         """
         Validates that the input is a list or tuple.
         """
         if self.required and not value:
             raise ValidationError(self.error_messages['required'])
-        elif not self.required and not value:
-            return []
-        if not isinstance(value, (list, tuple)):
-            raise ValidationError(self.error_messages['invalid_list'])
-        new_value = [smart_unicode(val) for val in value]
         # Validate that each value in the value list is in self.choices.
-        for val in new_value:
+        for val in value:
             if not self.valid_value(val):
                 raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
-        return new_value
 
 class ComboField(Field):
     """
@@ -773,6 +743,9 @@
             f.required = False
         self.fields = fields
 
+    def validate(self, value):
+        pass
+
     def clean(self, value):
         """
         Validates every value in the given list. A value is validated against
@@ -785,7 +758,7 @@
         clean_data = []
         errors = ErrorList()
         if not value or isinstance(value, (list, tuple)):
-            if not value or not [v for v in value if v not in EMPTY_VALUES]:
+            if not value or not [v for v in value if v not in validators.EMPTY_VALUES]:
                 if self.required:
                     raise ValidationError(self.error_messages['required'])
                 else:
@@ -797,7 +770,7 @@
                 field_value = value[i]
             except IndexError:
                 field_value = None
-            if self.required and field_value in EMPTY_VALUES:
+            if self.required and field_value in validators.EMPTY_VALUES:
                 raise ValidationError(self.error_messages['required'])
             try:
                 clean_data.append(field.clean(field_value))
@@ -808,7 +781,10 @@
                 errors.extend(e.messages)
         if errors:
             raise ValidationError(errors)
-        return self.compress(clean_data)
+
+        out = self.compress(clean_data)
+        self.validate(out)
+        return out
 
     def compress(self, data_list):
         """
@@ -877,30 +853,24 @@
         if data_list:
             # Raise a validation error if time or date is empty
             # (possible if SplitDateTimeField has required=False).
-            if data_list[0] in EMPTY_VALUES:
+            if data_list[0] in validators.EMPTY_VALUES:
                 raise ValidationError(self.error_messages['invalid_date'])
-            if data_list[1] in EMPTY_VALUES:
+            if data_list[1] in validators.EMPTY_VALUES:
                 raise ValidationError(self.error_messages['invalid_time'])
             return datetime.datetime.combine(*data_list)
         return None
 
-ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
 
-class IPAddressField(RegexField):
+class IPAddressField(CharField):
     default_error_messages = {
         'invalid': _(u'Enter a valid IPv4 address.'),
     }
-
-    def __init__(self, *args, **kwargs):
-        super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs)
+    default_validators = [validators.validate_ipv4_address]
 
-slug_re = re.compile(r'^[-\w]+$')
 
-class SlugField(RegexField):
+class SlugField(CharField):
     default_error_messages = {
         'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
                      u" underscores or hyphens."),
     }
-
-    def __init__(self, *args, **kwargs):
-        super(SlugField, self).__init__(slug_re, *args, **kwargs)
+    default_validators = [validators.validate_slug]