summaryrefslogtreecommitdiff
path: root/django/db/models/fields/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/db/models/fields/__init__.py')
-rw-r--r--django/db/models/fields/__init__.py243
1 files changed, 174 insertions, 69 deletions
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index b70f320df3..25f44a4d1c 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -13,12 +13,12 @@ from django.db.models.query_utils import QueryWrapper
from django.dispatch import dispatcher
from django.conf import settings
from django import forms
-from django.core import exceptions
+from django.core import exceptions, validators
from django.utils.datastructures import DictWrapper
from django.utils.functional import curry
from django.utils.itercompat import tee
from django.utils.text import capfirst
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import ugettext_lazy as _, ugettext
from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils import datetime_safe
@@ -60,6 +60,12 @@ class Field(object):
# creates, creation_counter is used for all user-specified fields.
creation_counter = 0
auto_creation_counter = -1
+ default_validators = [] # Default set of validators
+ default_error_messages = {
+ 'invalid_choice': _(u'Value %r is not a valid choice.'),
+ 'null': _(u'This field cannot be null.'),
+ 'blank': _(u'This field cannot be blank.'),
+ }
# Generic field type description, usually overriden by subclasses
def _description(self):
@@ -73,7 +79,8 @@ class Field(object):
db_index=False, rel=None, default=NOT_PROVIDED, editable=True,
serialize=True, unique_for_date=None, unique_for_month=None,
unique_for_year=None, choices=None, help_text='', db_column=None,
- db_tablespace=None, auto_created=False):
+ db_tablespace=None, auto_created=False, validators=[],
+ error_messages=None):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@@ -106,6 +113,42 @@ class Field(object):
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
+ self.validators = self.default_validators + validators
+
+ messages = {}
+ for c in reversed(self.__class__.__mro__):
+ messages.update(getattr(c, 'default_error_messages', {}))
+ messages.update(error_messages or {})
+ self.error_messages = messages
+
+ def __getstate__(self):
+ """
+ Pickling support.
+ """
+ from django.utils.functional import Promise
+ obj_dict = self.__dict__.copy()
+ items = []
+ translated_keys = []
+ for k, v in self.error_messages.items():
+ if isinstance(v, Promise):
+ args = getattr(v, '_proxy____args', None)
+ if args:
+ translated_keys.append(k)
+ v = args[0]
+ items.append((k,v))
+ obj_dict['_translated_keys'] = translated_keys
+ obj_dict['error_messages'] = dict(items)
+ return obj_dict
+
+ def __setstate__(self, obj_dict):
+ """
+ Unpickling support.
+ """
+ translated_keys = obj_dict.pop('_translated_keys')
+ self.__dict__.update(obj_dict)
+ for k in translated_keys:
+ self.error_messages[k] = _(self.error_messages[k])
+
def __cmp__(self, other):
# This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter)
@@ -127,6 +170,54 @@ class Field(object):
"""
return value
+ def run_validators(self, value):
+ if value in validators.EMPTY_VALUES:
+ return
+
+ errors = []
+ for v in self.validators:
+ try:
+ v(value)
+ except exceptions.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 exceptions.ValidationError(errors)
+
+ def validate(self, value, model_instance):
+ """
+ Validates value and throws ValidationError. Subclasses should override
+ this to provide validation logic.
+ """
+ if not self.editable:
+ # Skip validation for non-editable fields.
+ return
+ if self._choices and value:
+ if not value in dict(self.choices):
+ raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
+
+ if value is None and not self.null:
+ raise exceptions.ValidationError(self.error_messages['null'])
+
+ if not self.blank and value in validators.EMPTY_VALUES:
+ raise exceptions.ValidationError(self.error_messages['blank'])
+
+ def clean(self, value, model_instance):
+ """
+ Convert the value's type and run validation. Validation errors from to_python
+ and validate are propagated. The correct value is returned if no error is
+ raised.
+ """
+ value = self.to_python(value)
+ self.validate(value, model_instance)
+ self.run_validators(value)
+ return value
+
def db_type(self, connection):
"""
Returns the database column data type for this field, for the provided
@@ -377,9 +468,12 @@ class Field(object):
return getattr(obj, self.attname)
class AutoField(Field):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _(u'This value must be an integer.'),
+ }
def __init__(self, *args, **kwargs):
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
kwargs['blank'] = True
@@ -391,8 +485,10 @@ class AutoField(Field):
try:
return int(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be an integer."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
+
+ def validate(self, value, model_instance):
+ pass
def get_prep_value(self, value):
if value is None:
@@ -410,7 +506,10 @@ class AutoField(Field):
class BooleanField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Boolean (Either True or False)")
+ default_error_messages = {
+ 'invalid': _(u'This value must be either True or False.'),
+ }
+ description = _("Boolean (Either True or False)")
def __init__(self, *args, **kwargs):
kwargs['blank'] = True
if 'default' not in kwargs and not kwargs.get('null'):
@@ -424,8 +523,7 @@ class BooleanField(Field):
if value in (True, False): return value
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
- raise exceptions.ValidationError(
- _("This value must be either True or False."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@@ -453,36 +551,35 @@ class BooleanField(Field):
return super(BooleanField, self).formfield(**defaults)
class CharField(Field):
- description = ugettext_lazy("String (up to %(max_length)s)")
+ description = _("String (up to %(max_length)s)")
+
+ def __init__(self, *args, **kwargs):
+ super(CharField, self).__init__(*args, **kwargs)
+ self.validators.append(validators.MaxLengthValidator(self.max_length))
def get_internal_type(self):
return "CharField"
def to_python(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, basestring) or value is None:
return value
- if value is None:
- if self.null:
- return value
- else:
- raise exceptions.ValidationError(
- ugettext_lazy("This field cannot be null."))
return smart_unicode(value)
def formfield(self, **kwargs):
+ # Passing max_length to forms.CharField means that the value's length
+ # will be validated twice. This is considered acceptable since we want
+ # the value in the form field (to pass into widget for example).
defaults = {'max_length': self.max_length}
defaults.update(kwargs)
return super(CharField, self).formfield(**defaults)
# TODO: Maybe move this into contrib, because it's specialized.
class CommaSeparatedIntegerField(CharField):
- description = ugettext_lazy("Comma-separated integers")
+ default_validators = [validators.validate_comma_separated_integer_list]
+ description = _("Comma-separated integers")
def formfield(self, **kwargs):
defaults = {
- 'form_class': forms.RegexField,
- 'regex': '^[\d,]+$',
- 'max_length': self.max_length,
'error_messages': {
'invalid': _(u'Enter only digits separated by commas.'),
}
@@ -493,9 +590,13 @@ class CommaSeparatedIntegerField(CharField):
ansi_date_re = re.compile(r'^\d{4}-\d{1,2}-\d{1,2}$')
class DateField(Field):
- description = ugettext_lazy("Date (without time)")
+ description = _("Date (without time)")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _('Enter a valid date in YYYY-MM-DD format.'),
+ 'invalid_date': _('Invalid date: %s'),
+ }
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
#HACKs : auto_now_add/auto_now should be done as a default or a pre_save.
@@ -516,8 +617,7 @@ class DateField(Field):
return value
if not ansi_date_re.search(value):
- raise exceptions.ValidationError(
- _('Enter a valid date in YYYY-MM-DD format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
# Now that we have the date string in YYYY-MM-DD format, check to make
# sure it's a valid date.
# We could use time.strptime here and catch errors, but datetime.date
@@ -526,7 +626,7 @@ class DateField(Field):
try:
return datetime.date(year, month, day)
except ValueError, e:
- msg = _('Invalid date: %s') % _(str(e))
+ msg = self.error_messages['invalid_date'] % _(str(e))
raise exceptions.ValidationError(msg)
def pre_save(self, model_instance, add):
@@ -575,7 +675,10 @@ class DateField(Field):
return super(DateField, self).formfield(**defaults)
class DateTimeField(DateField):
- description = ugettext_lazy("Date (with time)")
+ default_error_messages = {
+ 'invalid': _(u'Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'),
+ }
+ description = _("Date (with time)")
def get_internal_type(self):
return "DateTimeField"
@@ -596,8 +699,7 @@ class DateTimeField(DateField):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@@ -614,8 +716,7 @@ class DateTimeField(DateField):
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3],
**kwargs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_value(self, value):
return self.to_python(value)
@@ -642,7 +743,11 @@ class DateTimeField(DateField):
class DecimalField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Decimal number")
+ default_error_messages = {
+ 'invalid': _(u'This value must be a decimal number.'),
+ }
+ description = _("Decimal number")
+
def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
self.max_digits, self.decimal_places = max_digits, decimal_places
Field.__init__(self, verbose_name, name, **kwargs)
@@ -656,8 +761,7 @@ class DecimalField(Field):
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
- raise exceptions.ValidationError(
- _("This value must be a decimal number."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def _format(self, value):
if isinstance(value, basestring) or value is None:
@@ -696,18 +800,15 @@ class DecimalField(Field):
return super(DecimalField, self).formfield(**defaults)
class EmailField(CharField):
- description = ugettext_lazy("E-mail address")
+ default_validators = [validators.validate_email]
+ description = _("E-mail address")
+
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 75)
CharField.__init__(self, *args, **kwargs)
- def formfield(self, **kwargs):
- defaults = {'form_class': forms.EmailField}
- defaults.update(kwargs)
- return super(EmailField, self).formfield(**defaults)
-
class FilePathField(Field):
- description = ugettext_lazy("File path")
+ description = _("File path")
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
@@ -729,7 +830,10 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Floating point number")
+ default_error_messages = {
+ 'invalid': _("This value must be a float."),
+ }
+ description = _("Floating point number")
def get_prep_value(self, value):
if value is None:
@@ -745,8 +849,7 @@ class FloatField(Field):
try:
return float(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be a float."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField}
@@ -755,7 +858,10 @@ class FloatField(Field):
class IntegerField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Integer")
+ default_error_messages = {
+ 'invalid': _("This value must be a float."),
+ }
+ description = _("Integer")
def get_prep_value(self, value):
if value is None:
@@ -771,8 +877,7 @@ class IntegerField(Field):
try:
return int(value)
except (TypeError, ValueError):
- raise exceptions.ValidationError(
- _("This value must be an integer."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField}
@@ -781,7 +886,7 @@ class IntegerField(Field):
class BigIntegerField(IntegerField):
empty_strings_allowed = False
- description = ugettext_lazy("Big (8 byte) integer")
+ description = _("Big (8 byte) integer")
MAX_BIGINT = 9223372036854775807
def get_internal_type(self):
return "BigIntegerField"
@@ -794,7 +899,7 @@ class BigIntegerField(IntegerField):
class IPAddressField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("IP address")
+ description = _("IP address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15
Field.__init__(self, *args, **kwargs)
@@ -809,7 +914,11 @@ class IPAddressField(Field):
class NullBooleanField(Field):
empty_strings_allowed = False
- description = ugettext_lazy("Boolean (Either True, False or None)")
+ default_error_messages = {
+ 'invalid': _("This value must be either None, True or False."),
+ }
+ description = _("Boolean (Either True, False or None)")
+
def __init__(self, *args, **kwargs):
kwargs['null'] = True
Field.__init__(self, *args, **kwargs)
@@ -822,8 +931,7 @@ class NullBooleanField(Field):
if value in ('None',): return None
if value in ('t', 'True', '1'): return True
if value in ('f', 'False', '0'): return False
- raise exceptions.ValidationError(
- _("This value must be either None, True or False."))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a web request (e.g. the
@@ -849,7 +957,7 @@ class NullBooleanField(Field):
return super(NullBooleanField, self).formfield(**defaults)
class PositiveIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "PositiveIntegerField"
@@ -860,7 +968,7 @@ class PositiveIntegerField(IntegerField):
return super(PositiveIntegerField, self).formfield(**defaults)
class PositiveSmallIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "PositiveSmallIntegerField"
@@ -870,7 +978,7 @@ class PositiveSmallIntegerField(IntegerField):
return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField):
- description = ugettext_lazy("String (up to %(max_length)s)")
+ description = _("String (up to %(max_length)s)")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50)
# Set db_index=True unless it's been set manually.
@@ -887,13 +995,13 @@ class SlugField(CharField):
return super(SlugField, self).formfield(**defaults)
class SmallIntegerField(IntegerField):
- description = ugettext_lazy("Integer")
+ description = _("Integer")
def get_internal_type(self):
return "SmallIntegerField"
class TextField(Field):
- description = ugettext_lazy("Text")
+ description = _("Text")
def get_internal_type(self):
return "TextField"
@@ -904,9 +1012,12 @@ class TextField(Field):
return super(TextField, self).formfield(**defaults)
class TimeField(Field):
- description = ugettext_lazy("Time")
+ description = _("Time")
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'),
+ }
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
self.auto_now, self.auto_now_add = auto_now, auto_now_add
if auto_now or auto_now_add:
@@ -935,8 +1046,7 @@ class TimeField(Field):
value, usecs = value.split('.')
usecs = int(usecs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
else:
usecs = 0
kwargs = {'microsecond': usecs}
@@ -949,8 +1059,7 @@ class TimeField(Field):
return datetime.time(*time.strptime(value, '%H:%M')[3:5],
**kwargs)
except ValueError:
- raise exceptions.ValidationError(
- _('Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'))
+ raise exceptions.ValidationError(self.error_messages['invalid'])
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -983,21 +1092,17 @@ class TimeField(Field):
return super(TimeField, self).formfield(**defaults)
class URLField(CharField):
- description = ugettext_lazy("URL")
+ description = _("URL")
def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
- self.verify_exists = verify_exists
CharField.__init__(self, verbose_name, name, **kwargs)
-
- def formfield(self, **kwargs):
- defaults = {'form_class': forms.URLField, 'verify_exists': self.verify_exists}
- defaults.update(kwargs)
- return super(URLField, self).formfield(**defaults)
+ self.validators.append(validators.URLValidator(verify_exists=verify_exists))
class XMLField(TextField):
- description = ugettext_lazy("XML text")
+ description = _("XML text")
def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
self.schema_path = schema_path
Field.__init__(self, verbose_name, name, **kwargs)
+