mirror of
https://github.com/django/django.git
synced 2025-07-07 03:09:22 +00:00
magic-removal: Added first bit of validation-aware models. Model objects now have a validate() method. See docstrings in db.models.base and db.models.fields.__init__ for information. Also added unit tests for all the currently supported validation.
git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2518 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
ce41a3e736
commit
bbc62cdccc
@ -1,5 +1,7 @@
|
|||||||
import django.db.models.manipulators
|
import django.db.models.manipulators
|
||||||
import django.db.models.manager
|
import django.db.models.manager
|
||||||
|
from django.core import validators
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
|
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
|
||||||
from django.db.models.fields.related import OneToOne, ManyToOne
|
from django.db.models.fields.related import OneToOne, ManyToOne
|
||||||
from django.db.models.related import RelatedObject
|
from django.db.models.related import RelatedObject
|
||||||
@ -9,7 +11,6 @@ from django.db import connection, backend, transaction
|
|||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.loading import register_models
|
from django.db.models.loading import register_models
|
||||||
from django.dispatch import dispatcher
|
from django.dispatch import dispatcher
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -38,6 +39,7 @@ class ModelBase(type):
|
|||||||
|
|
||||||
# Build complete list of parents
|
# Build complete list of parents
|
||||||
for base in bases:
|
for base in bases:
|
||||||
|
# TODO: Checking for the presence of '_meta' is hackish.
|
||||||
if '_meta' in dir(base):
|
if '_meta' in dir(base):
|
||||||
new_class._meta.parents.append(base)
|
new_class._meta.parents.append(base)
|
||||||
new_class._meta.parents.extend(base._meta.parents)
|
new_class._meta.parents.extend(base._meta.parents)
|
||||||
@ -196,6 +198,28 @@ class Model(object):
|
|||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
"""
|
||||||
|
First coerces all fields on this instance to their proper Python types.
|
||||||
|
Then runs validation on every field. Returns a dictionary of
|
||||||
|
field_name -> error_list.
|
||||||
|
"""
|
||||||
|
error_dict = {}
|
||||||
|
invalid_python = {}
|
||||||
|
for f in self._meta.fields:
|
||||||
|
try:
|
||||||
|
setattr(self, f.attname, f.to_python(getattr(self, f.attname, f.get_default())))
|
||||||
|
except validators.ValidationError, e:
|
||||||
|
error_dict[f.name] = e.messages
|
||||||
|
invalid_python[f.name] = 1
|
||||||
|
for f in self._meta.fields:
|
||||||
|
if f.name in invalid_python:
|
||||||
|
continue
|
||||||
|
errors = f.validate_full(getattr(self, f.attname, f.get_default()), self.__dict__)
|
||||||
|
if errors:
|
||||||
|
error_dict[f.name] = errors
|
||||||
|
return error_dict
|
||||||
|
|
||||||
def _collect_sub_objects(self, seen_objs):
|
def _collect_sub_objects(self, seen_objs):
|
||||||
"""
|
"""
|
||||||
Recursively populates seen_objs with all objects related to this object.
|
Recursively populates seen_objs with all objects related to this object.
|
||||||
|
@ -6,8 +6,8 @@ from django import forms
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.functional import curry, lazy
|
from django.utils.functional import curry, lazy
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import gettext_lazy, ngettext
|
from django.utils.translation import gettext, gettext_lazy, ngettext
|
||||||
import datetime, os
|
import datetime, os, time
|
||||||
|
|
||||||
class NOT_PROVIDED:
|
class NOT_PROVIDED:
|
||||||
pass
|
pass
|
||||||
@ -37,7 +37,7 @@ def manipulator_validator_unique(f, opts, self, field_data, all_data):
|
|||||||
return
|
return
|
||||||
if getattr(self, 'original_object', None) and self.original_object._get_pk_val() == old_obj._get_pk_val():
|
if getattr(self, 'original_object', None) and self.original_object._get_pk_val() == old_obj._get_pk_val():
|
||||||
return
|
return
|
||||||
raise validators.ValidationError, _("%(optname)s with this %(fieldname)s already exists.") % {'optname': capfirst(opts.verbose_name), 'fieldname': f.verbose_name}
|
raise validators.ValidationError, gettext("%(optname)s with this %(fieldname)s already exists.") % {'optname': capfirst(opts.verbose_name), 'fieldname': f.verbose_name}
|
||||||
|
|
||||||
# A guide to Field parameters:
|
# A guide to Field parameters:
|
||||||
#
|
#
|
||||||
@ -96,6 +96,37 @@ class Field(object):
|
|||||||
# This is needed because bisect does not take a comparison function.
|
# This is needed because bisect does not take a comparison function.
|
||||||
return cmp(self.creation_counter, other.creation_counter)
|
return cmp(self.creation_counter, other.creation_counter)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""
|
||||||
|
Converts the input value into the expected Python data type, raising
|
||||||
|
validators.ValidationError if the data can't be converted. Returns the
|
||||||
|
converted value. Subclasses should override this.
|
||||||
|
"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_full(self, field_data, all_data):
|
||||||
|
"""
|
||||||
|
Returns a list of errors for this field. This is the main interface,
|
||||||
|
as it encapsulates some basic validation logic used by all fields.
|
||||||
|
Subclasses should implement validate(), not validate_full().
|
||||||
|
"""
|
||||||
|
if not self.blank and not field_data:
|
||||||
|
return [gettext_lazy('This field is required.')]
|
||||||
|
try:
|
||||||
|
self.validate(field_data, all_data)
|
||||||
|
except validators.ValidationError, e:
|
||||||
|
return e.messages
|
||||||
|
return []
|
||||||
|
|
||||||
|
def validate(self, field_data, all_data):
|
||||||
|
"""
|
||||||
|
Raises validators.ValidationError if field_data has any errors.
|
||||||
|
Subclasses should override this to specify field-specific validation
|
||||||
|
logic. This method should assume field_data has already been converted
|
||||||
|
into the appropriate data type by Field.to_python().
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def set_attributes_from_name(self, name):
|
def set_attributes_from_name(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.attname, self.column = self.get_attname_column()
|
self.attname, self.column = self.get_attname_column()
|
||||||
@ -299,8 +330,17 @@ class AutoField(Field):
|
|||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
|
assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
|
||||||
|
kwargs['blank'] = True
|
||||||
Field.__init__(self, *args, **kwargs)
|
Field.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise validators.ValidationError, gettext("This value must be an integer.")
|
||||||
|
|
||||||
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
|
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
|
||||||
if not rel:
|
if not rel:
|
||||||
return [] # Don't add a FormField unless it's in a related context.
|
return [] # Don't add a FormField unless it's in a related context.
|
||||||
@ -327,6 +367,12 @@ class BooleanField(Field):
|
|||||||
kwargs['blank'] = True
|
kwargs['blank'] = True
|
||||||
Field.__init__(self, *args, **kwargs)
|
Field.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if value in (True, False): return value
|
||||||
|
if value is 't': return True
|
||||||
|
if value is 'f': return False
|
||||||
|
raise validators.ValidationError, gettext("This value must be either True or False.")
|
||||||
|
|
||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.CheckboxField]
|
return [forms.CheckboxField]
|
||||||
|
|
||||||
@ -334,6 +380,17 @@ class CharField(Field):
|
|||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.TextField]
|
return [forms.TextField]
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
return value
|
||||||
|
if value is None:
|
||||||
|
if self.null:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise validators.ValidationError, gettext_lazy("This field cannot be null.")
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
# TODO: Maybe move this into contrib, because it's specialized.
|
||||||
class CommaSeparatedIntegerField(CharField):
|
class CommaSeparatedIntegerField(CharField):
|
||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.CommaSeparatedIntegerField]
|
return [forms.CommaSeparatedIntegerField]
|
||||||
@ -348,6 +405,14 @@ class DateField(Field):
|
|||||||
kwargs['blank'] = True
|
kwargs['blank'] = True
|
||||||
Field.__init__(self, verbose_name, name, **kwargs)
|
Field.__init__(self, verbose_name, name, **kwargs)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
return value.date()
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
return value
|
||||||
|
validators.isValidANSIDate(value, None)
|
||||||
|
return datetime.date(*time.strptime(value, '%Y-%m-%d')[:3])
|
||||||
|
|
||||||
def get_db_prep_lookup(self, lookup_type, value):
|
def get_db_prep_lookup(self, lookup_type, value):
|
||||||
if lookup_type == 'range':
|
if lookup_type == 'range':
|
||||||
value = [str(v) for v in value]
|
value = [str(v) for v in value]
|
||||||
@ -391,6 +456,22 @@ class DateField(Field):
|
|||||||
return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')}
|
return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')}
|
||||||
|
|
||||||
class DateTimeField(DateField):
|
class DateTimeField(DateField):
|
||||||
|
def to_python(self, value):
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
return value
|
||||||
|
if isinstance(value, datetime.date):
|
||||||
|
return datetime.datetime(value.year, value.month, value.day)
|
||||||
|
try: # Seconds are optional, so try converting seconds first.
|
||||||
|
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6])
|
||||||
|
except ValueError:
|
||||||
|
try: # Try without seconds.
|
||||||
|
return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M')[:5])
|
||||||
|
except ValueError: # Try without hour/minutes/seconds.
|
||||||
|
try:
|
||||||
|
return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3])
|
||||||
|
except ValueError:
|
||||||
|
raise validators.ValidationError, gettext('Enter a valid date/time in YYYY-MM-DD HH:MM format.')
|
||||||
|
|
||||||
def get_db_prep_save(self, value):
|
def get_db_prep_save(self, value):
|
||||||
# Casts dates into string format for entry into database.
|
# Casts dates into string format for entry into database.
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@ -432,10 +513,10 @@ class DateTimeField(DateField):
|
|||||||
return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''),
|
return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''),
|
||||||
time_field: (val is not None and val.strftime("%H:%M:%S") or '')}
|
time_field: (val is not None and val.strftime("%H:%M:%S") or '')}
|
||||||
|
|
||||||
class EmailField(Field):
|
class EmailField(CharField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['maxlength'] = 75
|
kwargs['maxlength'] = 75
|
||||||
Field.__init__(self, *args, **kwargs)
|
CharField.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
def get_internal_type(self):
|
def get_internal_type(self):
|
||||||
return "CharField"
|
return "CharField"
|
||||||
@ -443,6 +524,9 @@ class EmailField(Field):
|
|||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.EmailField]
|
return [forms.EmailField]
|
||||||
|
|
||||||
|
def validate(self, field_data, all_data):
|
||||||
|
validators.isValidEmail(field_data, all_data)
|
||||||
|
|
||||||
class FileField(Field):
|
class FileField(Field):
|
||||||
def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
|
def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
|
||||||
self.upload_to = upload_to
|
self.upload_to = upload_to
|
||||||
@ -583,6 +667,9 @@ class IPAddressField(Field):
|
|||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.IPAddressField]
|
return [forms.IPAddressField]
|
||||||
|
|
||||||
|
def validate(self, field_data, all_data):
|
||||||
|
validators.isValidIPAddress4(field_data, None)
|
||||||
|
|
||||||
class NullBooleanField(Field):
|
class NullBooleanField(Field):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['null'] = True
|
kwargs['null'] = True
|
||||||
@ -595,6 +682,9 @@ class PhoneNumberField(IntegerField):
|
|||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.PhoneNumberField]
|
return [forms.PhoneNumberField]
|
||||||
|
|
||||||
|
def validate(self, field_data, all_data):
|
||||||
|
validators.isValidPhone(field_data, all_data)
|
||||||
|
|
||||||
class PositiveIntegerField(IntegerField):
|
class PositiveIntegerField(IntegerField):
|
||||||
def get_manipulator_field_objs(self):
|
def get_manipulator_field_objs(self):
|
||||||
return [forms.PositiveIntegerField]
|
return [forms.PositiveIntegerField]
|
||||||
@ -626,7 +716,7 @@ class TextField(Field):
|
|||||||
class TimeField(Field):
|
class TimeField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs):
|
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
|
self.auto_now, self.auto_now_add = auto_now, auto_now_add
|
||||||
if auto_now or auto_now_add:
|
if auto_now or auto_now_add:
|
||||||
kwargs['editable'] = False
|
kwargs['editable'] = False
|
||||||
Field.__init__(self, verbose_name, name, **kwargs)
|
Field.__init__(self, verbose_name, name, **kwargs)
|
||||||
|
0
tests/modeltests/validation/__init__.py
Normal file
0
tests/modeltests/validation/__init__.py
Normal file
147
tests/modeltests/validation/models.py
Normal file
147
tests/modeltests/validation/models.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
XX. Validation
|
||||||
|
|
||||||
|
Each model instance has a validate() method that returns a dictionary of
|
||||||
|
validation errors in the instance's fields. This method has a side effect
|
||||||
|
of converting each field to its appropriate Python data type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Person(models.Model):
|
||||||
|
is_child = models.BooleanField()
|
||||||
|
name = models.CharField(maxlength=20)
|
||||||
|
birthdate = models.DateField()
|
||||||
|
favorite_moment = models.DateTimeField()
|
||||||
|
email = models.EmailField()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
API_TESTS = """
|
||||||
|
|
||||||
|
>>> import datetime
|
||||||
|
>>> valid_params = {
|
||||||
|
... 'is_child': True,
|
||||||
|
... 'name': 'John',
|
||||||
|
... 'birthdate': datetime.date(2000, 5, 3),
|
||||||
|
... 'favorite_moment': datetime.datetime(2002, 4, 3, 13, 23),
|
||||||
|
... 'email': 'john@example.com'
|
||||||
|
... }
|
||||||
|
>>> p = Person(**valid_params)
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, id='23'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.id
|
||||||
|
23
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, id='foo'))
|
||||||
|
>>> p.validate()
|
||||||
|
{'id': ['This value must be an integer.']}
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, id=None))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> repr(p.id)
|
||||||
|
'None'
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, is_child='t'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.is_child
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, is_child='f'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.is_child
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, is_child=True))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.is_child
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, is_child=False))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.is_child
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, is_child='foo'))
|
||||||
|
>>> p.validate()
|
||||||
|
{'is_child': ['This value must be either True or False.']}
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, name=u'Jose'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.name
|
||||||
|
u'Jose'
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, name=227))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.name
|
||||||
|
'227'
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, birthdate=datetime.date(2000, 5, 3)))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.birthdate
|
||||||
|
datetime.date(2000, 5, 3)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, birthdate=datetime.datetime(2000, 5, 3)))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.birthdate
|
||||||
|
datetime.date(2000, 5, 3)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, birthdate='2000-05-03'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.birthdate
|
||||||
|
datetime.date(2000, 5, 3)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, birthdate='2000-5-3'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.birthdate
|
||||||
|
datetime.date(2000, 5, 3)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, birthdate='foo'))
|
||||||
|
>>> p.validate()
|
||||||
|
{'birthdate': ['Enter a valid date in YYYY-MM-DD format.']}
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, favorite_moment=datetime.datetime(2002, 4, 3, 13, 23)))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.favorite_moment
|
||||||
|
datetime.datetime(2002, 4, 3, 13, 23)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, favorite_moment=datetime.datetime(2002, 4, 3)))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.favorite_moment
|
||||||
|
datetime.datetime(2002, 4, 3, 0, 0)
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, email='john@example.com'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.email
|
||||||
|
'john@example.com'
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, email=u'john@example.com'))
|
||||||
|
>>> p.validate()
|
||||||
|
{}
|
||||||
|
>>> p.email
|
||||||
|
u'john@example.com'
|
||||||
|
|
||||||
|
>>> p = Person(**dict(valid_params, email=22))
|
||||||
|
>>> p.validate()
|
||||||
|
{'email': ['Enter a valid e-mail address.']}
|
||||||
|
|
||||||
|
"""
|
Loading…
x
Reference in New Issue
Block a user