Ticket #12498: complex-validators.diff

File complex-validators.diff, 15.4 KB (added by jkocherhans, 9 years ago)

Patch to restore what was removed in [12078].

  • django/core/validators.py

    diff --git a/django/core/validators.py b/django/core/validators.py
    index 6cd290f..4515ca7 100644
    a b class MaxLengthValidator(BaseValidator): 
    135135    message = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).')
    136136    code = 'max_length'
    137137
     138
     139class ComplexValidator(object):
     140    def get_value(self, name, all_values, obj):
     141        assert all_values or obj, "Either all_values or obj must be supplied"
     142
     143        if all_values:
     144            return all_values.get(name, None)
     145        if obj:
     146            return getattr(obj, name, None)
     147       
     148
     149    def __call__(self, value, all_values={}, obj=None):
     150        raise NotImplementedError()
     151
     152class RequiredIfOtherFieldBlank(ComplexValidator):
     153    def __init__(self, other_field):
     154        self.other_field = other_field
     155
     156    def __call__(self, value, all_values={}, obj=None):
     157        if self.get_value(self.other_field, all_values, obj) in EMPTY_VALUES:
     158            if value in EMPTY_VALUES:
     159                raise ValidationError('This field is required if %s is blank.' % self.other_field)
     160
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index 660c570..eace314 100644
    a b class Model(object): 
    799799            except ValidationError, e:
    800800                errors[f.name] = e.messages
    801801
     802        # run complex validators after the fields have been cleaned since they
     803        # need access to model_instance.
     804        for f in self._meta.fields:
     805            if f.name in errors:
     806                continue
     807
     808            value = getattr(self, f.attname)
     809            for v in f.validators:
     810                if isinstance(v, validators.ComplexValidator):
     811                    try:
     812                        v(value, obj=self)
     813                    except ValidationError, e:
     814                        error_list = errors.setdefault(f.name, [])
     815                        if hasattr(e, 'code') and e.code in f.error_messages:
     816                            message = f.error_messages[e.code]
     817                            if e.params:
     818                                message = message % e.params
     819                            error_list.append(message)
     820                        else:
     821                            error_list.extend(e.messages)
    802822        # Form.clean() is run even if other validation fails, so do the
    803823        # same with Model.validate() for consistency.
    804824        try:
  • django/db/models/fields/__init__.py

    diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
    index 5c36437..96fc36f 100644
    a b class Field(object): 
    176176
    177177        errors = []
    178178        for v in self.validators:
    179             try:
    180                 v(value)
    181             except exceptions.ValidationError, e:
    182                 if hasattr(e, 'code') and e.code in self.error_messages:
    183                     message = self.error_messages[e.code]
    184                     if e.params:
    185                         message = message % e.params
    186                     errors.append(message)
    187                 else:
    188                     errors.extend(e.messages)
     179            # Don't run complex validators since they need the model instance
     180            # and must therefore be run on the model level.
     181            if not isinstance(v, validators.ComplexValidator):
     182                try:
     183                    v(value)
     184                except exceptions.ValidationError, e:
     185                    if hasattr(e, 'code') and e.code in self.error_messages:
     186                        message = self.error_messages[e.code]
     187                        if e.params:
     188                            message = message % e.params
     189                        errors.append(message)
     190                    else:
     191                        errors.extend(e.messages)
    189192        if errors:
    190193            raise exceptions.ValidationError(errors)
    191194
  • django/forms/fields.py

    diff --git a/django/forms/fields.py b/django/forms/fields.py
    index 23a88d6..40e3d90 100644
    a b class Field(object): 
    130130            return
    131131        errors = []
    132132        for v in self.validators:
    133             try:
    134                 v(value)
    135             except ValidationError, e:
    136                 if hasattr(e, 'code') and e.code in self.error_messages:
    137                     message = self.error_messages[e.code]
    138                     if e.params:
    139                         message = message % e.params
    140                     errors.append(message)
    141                 else:
    142                     errors.extend(e.messages)
     133            # don't run complex validators since they need all_values
     134            # and must therefore be run on the form level
     135            if not isinstance(v, validators.ComplexValidator):
     136                try:
     137                    v(value)
     138                except ValidationError, e:
     139                    if hasattr(e, 'code') and e.code in self.error_messages:
     140                        message = self.error_messages[e.code]
     141                        if e.params:
     142                            message = message % e.params
     143                        errors.append(message)
     144                    else:
     145                        errors.extend(e.messages)
    143146        if errors:
    144147            raise ValidationError(errors)
    145148
  • django/forms/forms.py

    diff --git a/django/forms/forms.py b/django/forms/forms.py
    index d484300..5b8a4d0 100644
    a b Form classes 
    33"""
    44
    55from django.core.exceptions import ValidationError
     6from django.core.validators import ComplexValidator
    67from django.utils.copycompat import deepcopy
    78from django.utils.datastructures import SortedDict
    89from django.utils.html import conditional_escape
    class BaseForm(StrAndUnicode): 
    282283                self._errors[name] = self.error_class(e.messages)
    283284                if name in self.cleaned_data:
    284285                    del self.cleaned_data[name]
     286
     287        # Run complex validators after the fields have been cleaned since they
     288        # need access to all_values.
     289        for name, field in self.fields.items():
     290            if not name in self.cleaned_data:
     291                continue
     292            failed = False
     293            for v in field.validators:
     294                # Skip noncomplex validators, they have already been run on the field.
     295                if not isinstance(v, ComplexValidator):
     296                    continue
     297                try:
     298                    v(self.cleaned_data[name], all_values=self.cleaned_data)
     299                except ValidationError, e:
     300                    failed = True
     301                    error_list = self._errors.setdefault(name, self.error_class())
     302                    if hasattr(e, 'code') and e.code in field.error_messages:
     303                        message = field.error_messages[e.code]
     304                        if e.params:
     305                            message = message % e.params
     306                        error_list.append(message)
     307                    else:
     308                        error_list.extend(e.messages)
     309            if failed:
     310                del self.cleaned_data[name]
    285311        try:
    286312            self.cleaned_data = self.clean()
    287313        except ValidationError, e:
  • tests/modeltests/validation/models.py

    diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py
    index db5100b..1260417 100644
    a b  
    11from datetime import datetime
    22
    33from django.core.exceptions import ValidationError
     4from django.core.validators import ComplexValidator
    45from django.db import models
    56from django.test import TestCase
    67
    def validate_answer_to_universe(value): 
    89    if value != 42:
    910        raise ValidationError('This is not the answer to life, universe and everything!', code='not42')
    1011
     12class ValidateFieldNotEqualsOtherField(ComplexValidator):
     13    def __init__(self, other_field):
     14        self.other_field = other_field
     15
     16    def __call__(self, value, all_values={}, obj=None):
     17        if value == self.get_value(self.other_field, all_values, obj):
     18            raise ValidationError("Must not equal to %r's value" % self.other_field, code='not_equal', params=(self.other_field,))
     19
    1120class ModelToValidate(models.Model):
    1221    name = models.CharField(max_length=100)
    1322    created = models.DateTimeField(default=datetime.now)
    class ModelToValidate(models.Model): 
    1524    parent = models.ForeignKey('self', blank=True, null=True)
    1625    email = models.EmailField(blank=True)
    1726    url = models.URLField(blank=True)
    18     f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe])
     27    f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe, ValidateFieldNotEqualsOtherField('number')])
    1928
    2029    def validate(self):
    2130        super(ModelToValidate, self).validate()
    class CustomMessagesModel(models.Model): 
    4958    other  = models.IntegerField(blank=True, null=True)
    5059    number = models.IntegerField(
    5160            error_messages={'null': 'NULL', 'not42': 'AAARGH', 'not_equal': '%s != me'},
    52             validators=[validate_answer_to_universe]
     61            validators=[validate_answer_to_universe, ValidateFieldNotEqualsOtherField('other')]
    5362        )
    5463
    5564
  • tests/modeltests/validation/test_custom_messages.py

    diff --git a/tests/modeltests/validation/test_custom_messages.py b/tests/modeltests/validation/test_custom_messages.py
    index 9a958a0..e543dbd 100644
    a b  
    11from modeltests.validation import ValidationTestCase
    2 from models import CustomMessagesModel
    3 
    42
     3from models import CustomMessagesModel
    54class CustomMessagesTest(ValidationTestCase):
     5    def test_custom_complex_validator_message(self):
     6        cmm = CustomMessagesModel(number=42, other=42)
     7        self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['other != me'])
     8
    69    def test_custom_simple_validator_message(self):
    710        cmm = CustomMessagesModel(number=12)
    811        self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['AAARGH'])
  • tests/modeltests/validation/validators.py

    diff --git a/tests/modeltests/validation/validators.py b/tests/modeltests/validation/validators.py
    index d46d0c5..d54aab6 100644
    a b class TestModelsWithValidators(ValidationTestCase): 
    1818                [u'This is not the answer to life, universe and everything!']
    1919            )
    2020
     21    def test_custom_complex_validator_raises_error_for_incorrect_value(self):
     22        mtv = ModelToValidate(number=42, name='Some Name', f_with_custom_validator=42)
     23        self.assertFailsValidation(mtv.full_validate, ['f_with_custom_validator'])
     24        self.assertFieldFailsValidationWithMessage(
     25                mtv.full_validate,
     26                'f_with_custom_validator',
     27                [u"Must not equal to 'number''s value"]
     28            )
     29
     30    def test_complex_validator_isnt_run_if_field_doesnt_validate(self):
     31        mtv = ModelToValidate(number=32, name='Some Name', f_with_custom_validator=32)
     32        self.assertFieldFailsValidationWithMessage(
     33                mtv.full_validate,
     34                'f_with_custom_validator',
     35                [u'This is not the answer to life, universe and everything!']
     36            )
  • tests/modeltests/validators/tests.py

    diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py
    index 1108ee8..331f6f0 100644
    a b from django.core.validators import ( 
    99        validate_integer, validate_email, validate_slug, validate_ipv4_address,
    1010        validate_comma_separated_integer_list, MaxValueValidator,
    1111        MinValueValidator, MaxLengthValidator, MinLengthValidator,
    12         URLValidator, BaseValidator, RegexValidator,
     12        RequiredIfOtherFieldBlank, URLValidator, BaseValidator,
     13        RegexValidator,
    1314)
    1415
    1516now = datetime.now()
    for validator, value, expected in SIMPLE_VALIDATORS_VALUES: 
    152153    setattr(TestSimpleValidators, *get_simple_test_func(validator, expected, value, test_counter))
    153154    test_counter += 1
    154155
     156class TestComplexValidators(TestCase):
     157    pass
     158
     159COMPLEX_VALIDATORS_VALUES = (
     160    #(validator, value, all_values, obj, expected),
     161    (RequiredIfOtherFieldBlank('other'), 'given', {'other': 'given'}, None, None),
     162    (RequiredIfOtherFieldBlank('other'), '', {'other': 'given'}, None, None),
     163    (RequiredIfOtherFieldBlank('other'), 'given', {}, None, AssertionError),
     164    (RequiredIfOtherFieldBlank('other'), '', {}, None, AssertionError),
     165    (RequiredIfOtherFieldBlank('other'), '', {'other': ''}, None, ValidationError),
     166)
     167
     168def get_complex_test_func(validator, expected, value, all_values, obj, num):
     169    if isinstance(expected, type) and issubclass(expected, Exception):
     170        test_mask = 'test_%s_raises_error_%d'
     171        def test_func(self):
     172            self.assertRaises(expected, validator, value, all_values=all_values, obj=obj)
     173    else:
     174        test_mask = 'test_%s_%d'
     175        def test_func(self):
     176            self.assertEqual(expected, validator(value, all_values=all_values, obj=obj))
     177    test_name = test_mask % (validator.__class__.__name__, num)
     178    return test_name, test_func
     179
     180test_counter = {}
     181for validator, value, all_values, obj, expected in COMPLEX_VALIDATORS_VALUES:
     182    num = test_counter[validator.__class__.__name__] = test_counter.setdefault(validator.__class__.__name__, 0) + 1
     183    setattr(TestComplexValidators, *get_complex_test_func(validator, expected, value, all_values, obj, num))
     184
  • tests/regressiontests/forms/tests.py

    diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
    index db70500..e9ae948 100644
    a b from formsets import tests as formset_tests 
    3838from media import media_tests
    3939
    4040from fields import FieldsTests
    41 from validators import TestFieldWithValidators
     41from validators import TestFormWithValidators, TestFieldWithValidators
    4242
    4343__test__ = {
    4444    'extra_tests': extra_tests,
  • tests/regressiontests/forms/validators.py

    diff --git a/tests/regressiontests/forms/validators.py b/tests/regressiontests/forms/validators.py
    index b75a14e..becf4e2 100644
    a b from django import forms 
    44from django.core import validators
    55from django.core.exceptions import ValidationError
    66
     7class AlwaysFailingValidator(validators.ComplexValidator):
     8    def __call__(self, value, all_values={}, obj=None):
     9        raise ValidationError('AlwaysFailingValidator')
    710
    811class TestFieldWithValidators(TestCase):
    912    def test_all_errors_get_reported(self):
    class TestFieldWithValidators(TestCase): 
    1619        except ValidationError, e:
    1720            self.assertEqual(2, len(e.messages))
    1821
     22class TestFormWithValidators(TestCase):
     23    def test_all_complex_validators_get_run_even_if_they_fail(self):
     24        class MyForm(forms.Form):
     25            validator_field = forms.CharField(
     26                    validators=[
     27                        AlwaysFailingValidator(),
     28                        AlwaysFailingValidator(),
     29                    ]
     30                )
     31        form = MyForm({'validator_field': 'some value'})
     32        self.assertFalse(form.is_valid())
     33        self.assertEqual(['validator_field'], form.errors.keys())
     34        self.assertEqual(['AlwaysFailingValidator', 'AlwaysFailingValidator'], form.errors['validator_field'])
Back to Top