mirror of
https://github.com/django/django.git
synced 2025-07-06 18:59:13 +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.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.related import OneToOne, ManyToOne
|
||||
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.loading import register_models
|
||||
from django.dispatch import dispatcher
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import curry
|
||||
from django.conf import settings
|
||||
@ -38,6 +39,7 @@ class ModelBase(type):
|
||||
|
||||
# Build complete list of parents
|
||||
for base in bases:
|
||||
# TODO: Checking for the presence of '_meta' is hackish.
|
||||
if '_meta' in dir(base):
|
||||
new_class._meta.parents.append(base)
|
||||
new_class._meta.parents.extend(base._meta.parents)
|
||||
@ -196,6 +198,28 @@ class Model(object):
|
||||
|
||||
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):
|
||||
"""
|
||||
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.utils.functional import curry, lazy
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext_lazy, ngettext
|
||||
import datetime, os
|
||||
from django.utils.translation import gettext, gettext_lazy, ngettext
|
||||
import datetime, os, time
|
||||
|
||||
class NOT_PROVIDED:
|
||||
pass
|
||||
@ -37,7 +37,7 @@ def manipulator_validator_unique(f, opts, self, field_data, all_data):
|
||||
return
|
||||
if getattr(self, 'original_object', None) and self.original_object._get_pk_val() == old_obj._get_pk_val():
|
||||
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:
|
||||
#
|
||||
@ -96,6 +96,37 @@ class Field(object):
|
||||
# This is needed because bisect does not take a comparison function.
|
||||
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):
|
||||
self.name = name
|
||||
self.attname, self.column = self.get_attname_column()
|
||||
@ -299,8 +330,17 @@ class AutoField(Field):
|
||||
empty_strings_allowed = False
|
||||
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
|
||||
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):
|
||||
if not rel:
|
||||
return [] # Don't add a FormField unless it's in a related context.
|
||||
@ -327,6 +367,12 @@ class BooleanField(Field):
|
||||
kwargs['blank'] = True
|
||||
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):
|
||||
return [forms.CheckboxField]
|
||||
|
||||
@ -334,6 +380,17 @@ class CharField(Field):
|
||||
def get_manipulator_field_objs(self):
|
||||
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):
|
||||
def get_manipulator_field_objs(self):
|
||||
return [forms.CommaSeparatedIntegerField]
|
||||
@ -348,6 +405,14 @@ class DateField(Field):
|
||||
kwargs['blank'] = True
|
||||
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):
|
||||
if lookup_type == 'range':
|
||||
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 '')}
|
||||
|
||||
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):
|
||||
# Casts dates into string format for entry into database.
|
||||
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 ''),
|
||||
time_field: (val is not None and val.strftime("%H:%M:%S") or '')}
|
||||
|
||||
class EmailField(Field):
|
||||
class EmailField(CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['maxlength'] = 75
|
||||
Field.__init__(self, *args, **kwargs)
|
||||
CharField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
@ -443,6 +524,9 @@ class EmailField(Field):
|
||||
def get_manipulator_field_objs(self):
|
||||
return [forms.EmailField]
|
||||
|
||||
def validate(self, field_data, all_data):
|
||||
validators.isValidEmail(field_data, all_data)
|
||||
|
||||
class FileField(Field):
|
||||
def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
|
||||
self.upload_to = upload_to
|
||||
@ -583,6 +667,9 @@ class IPAddressField(Field):
|
||||
def get_manipulator_field_objs(self):
|
||||
return [forms.IPAddressField]
|
||||
|
||||
def validate(self, field_data, all_data):
|
||||
validators.isValidIPAddress4(field_data, None)
|
||||
|
||||
class NullBooleanField(Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['null'] = True
|
||||
@ -595,6 +682,9 @@ class PhoneNumberField(IntegerField):
|
||||
def get_manipulator_field_objs(self):
|
||||
return [forms.PhoneNumberField]
|
||||
|
||||
def validate(self, field_data, all_data):
|
||||
validators.isValidPhone(field_data, all_data)
|
||||
|
||||
class PositiveIntegerField(IntegerField):
|
||||
def get_manipulator_field_objs(self):
|
||||
return [forms.PositiveIntegerField]
|
||||
@ -626,7 +716,7 @@ class TextField(Field):
|
||||
class TimeField(Field):
|
||||
empty_strings_allowed = False
|
||||
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:
|
||||
kwargs['editable'] = False
|
||||
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