From 6152164303b6b8ac7705873fbebac72177efcb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Honza=20Kr=C3=A1l?= Date: Mon, 1 Jun 2009 15:38:58 +0000 Subject: [PATCH] [soc2009/model-validation] Added clean() and validate() methods to Model. This also called for ValidationError to also accept dicts as message. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/model-validation@10869 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/exceptions.py | 12 +++++++++- django/db/models/base.py | 32 ++++++++++++++++++++++++- tests/modeltests/validation/__init__.py | 0 tests/modeltests/validation/models.py | 28 ++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/modeltests/validation/__init__.py create mode 100644 tests/modeltests/validation/models.py diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 2fe384e6dd..c0bcbae3ab 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -32,23 +32,33 @@ class FieldError(Exception): """Some kind of problem with a model field.""" pass +NON_FIELD_ERRORS = '__all__' class ValidationError(Exception): """An error while validating data.""" def __init__(self, message): + import operator from django.utils.encoding import force_unicode """ ValidationError can be passed any object that can be printed (usually - a string) or a list of objects. + a string), a list of objects or a dictionary. """ + if isinstance(message, dict): + self.message_dict = message + message = reduce(operator.add, message.values()) + if isinstance(message, list): self.messages = [force_unicode(msg) for msg in message] else: message = force_unicode(message) self.messages = [message] + + def __str__(self): # This is needed because, without a __str__(), printing an exception # instance would result in this: # AttributeError: ValidationError instance has no attribute 'args' # See http://www.python.org/doc/current/tut/node10.html#handling + if hasattr(self, 'message_dict'): + return repr(self.message_dict) return repr(self.messages) diff --git a/django/db/models/base.py b/django/db/models/base.py index 13ff7e8f35..b581656f8d 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -9,7 +9,7 @@ except NameError: from sets import Set as set # Python 2.3 fallback. import django.db.models.manager # Imported to register signal handler. -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField from django.db.models.query import delete_objects, Q @@ -597,6 +597,36 @@ class Model(object): def prepare_database_save(self, unused): return self.pk + def validate(self): + """ + Hook for doing any extra model-wide validation after Model.clean() been + called on every field. Any ValidationError raised by this method will + not be associated with a particular field; it will have a special-case + association with the field named '__all__'. + """ + pass + + def clean(self): + """ + Cleans all fields and raises ValidationError containing message_dict of + all validation errors if any occur. + """ + errors = {} + for f in self._meta.fields: + try: + # TODO: is the [sg]etattr correct? + setattr(self, f.attname, f.clean(getattr(self, f.attname), self)) + except ValidationError, e: + errors[f.name] = e.messages + try: + # TODO: run this only if not errors?? + self.validate() + except ValidationError, e: + errors[NON_FIELD_ERRORS] = e.messages + + if errors: + raise ValidationError(errors) + ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # diff --git a/tests/modeltests/validation/__init__.py b/tests/modeltests/validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py new file mode 100644 index 0000000000..d536495da7 --- /dev/null +++ b/tests/modeltests/validation/models.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from django.core.exceptions import ValidationError +from django.db import models +from django.test import TestCase + +class ModelToValidate(models.Model): + name = models.CharField(max_length=100) + created = models.DateTimeField(default=datetime.now) + number = models.IntegerField() + + def validate(self): + super(ModelToValidate, self).validate() + if self.number == 11: + raise ValidationError('Invalid number supplied!') + +class BaseModelValidationTests(TestCase): + def test_missing_required_field_raises_error(self): + mtv = ModelToValidate() + self.assertRaises(ValidationError, mtv.clean) + + def test_with_correct_value_model_validates(self): + mtv = ModelToValidate(number=10) + self.assertEqual(None, mtv.clean()) + + def test_custom_validate_method_is_called(self): + mtv = ModelToValidate(number=11) + self.assertRaises(ValidationError, mtv.clean)