diff options
Diffstat (limited to 'django/db/models/fields/__init__.py')
| -rw-r--r-- | django/db/models/fields/__init__.py | 243 |
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) + |
