1
0
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:
Adrian Holovaty 2006-03-13 01:33:45 +00:00
parent ce41a3e736
commit bbc62cdccc
4 changed files with 268 additions and 7 deletions

View File

@ -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.

View File

@ -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)

View File

View 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.']}
"""