diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 55c2fe48e..56fd543d8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -987,8 +987,8 @@ class FloatField(Field): class DecimalField(Field): default_error_messages = { 'invalid': _('A valid number is required.'), - 'max_value': _('Ensure this value is less than or equal to {max_value}.'), - 'min_value': _('Ensure this value is greater than or equal to {min_value}.'), + 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'), + 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'), 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'), 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'), 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'), diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 722981b20..89e9a6675 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -3,6 +3,7 @@ Helper functions for mapping model fields to a dictionary of default keyword arguments that should be used for their equivalent serializer fields. """ import inspect +from collections import OrderedDict from django.core import validators from django.db import models @@ -127,12 +128,14 @@ def get_field_kwargs(field_name, model_field): else: # Ensure that max_value is passed explicitly as a keyword arg, # rather than as a validator. - max_value = next(( - validator.limit_value for validator in validator_kwarg + max_value, message = next(( + (validator.limit_value, validator.message) for validator in validator_kwarg if isinstance(validator, validators.MaxValueValidator) - ), None) + ), (None, '')) if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['max_value'] = max_value + if message != '': + kwargs.setdefault('error_messages', OrderedDict()).update(max_value=message) validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MaxValueValidator) @@ -140,12 +143,14 @@ def get_field_kwargs(field_name, model_field): # Ensure that min_value is passed explicitly as a keyword arg, # rather than as a validator. - min_value = next(( - validator.limit_value for validator in validator_kwarg + min_value, message = next(( + (validator.limit_value, validator.message) for validator in validator_kwarg if isinstance(validator, validators.MinValueValidator) - ), None) + ), (None, '')) if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['min_value'] = min_value + if message != '': + kwargs.setdefault('error_messages', OrderedDict()).update(min_value=message) validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MinValueValidator) @@ -154,6 +159,9 @@ def get_field_kwargs(field_name, model_field): # URLField does not need to include the URLValidator argument, # as it is explicitly added in. if isinstance(model_field, models.URLField): + custom_message = model_field.error_messages.get("invalid", None) + if custom_message is not None: + kwargs.setdefault('error_messages', {}).update(invalid=custom_message) validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.URLValidator) @@ -162,6 +170,9 @@ def get_field_kwargs(field_name, model_field): # EmailField does not need to include the validate_email argument, # as it is explicitly added in. if isinstance(model_field, models.EmailField): + custom_message = model_field.error_messages.get("invalid", None) + if custom_message is not None: + kwargs.setdefault('error_messages', {}).update(invalid=custom_message) validator_kwarg = [ validator for validator in validator_kwarg if validator is not validators.validate_email @@ -194,6 +205,9 @@ def get_field_kwargs(field_name, model_field): isinstance(model_field, models.TextField) or isinstance(model_field, models.FileField)): kwargs['max_length'] = max_length + custom_message = model_field.error_messages.get("max_length", '') + if custom_message != '': + kwargs.setdefault('error_messages', OrderedDict()).update(max_length=custom_message) validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MaxLengthValidator) @@ -201,12 +215,14 @@ def get_field_kwargs(field_name, model_field): # Ensure that min_length is passed explicitly as a keyword arg, # rather than as a validator. - min_length = next(( - validator.limit_value for validator in validator_kwarg + min_length, message = next(( + (validator.limit_value, validator.message) for validator in validator_kwarg if isinstance(validator, validators.MinLengthValidator) - ), None) + ), (None, '')) if min_length is not None and isinstance(model_field, models.CharField): kwargs['min_length'] = min_length + if message != '': + kwargs.setdefault('error_messages', OrderedDict()).update(min_length=message) validator_kwarg = [ validator for validator in validator_kwarg if not isinstance(validator, validators.MinLengthValidator) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 203e1fe7f..f77b47a72 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -200,7 +200,7 @@ class TestRegularFieldMappings(TestCase): expected = dedent(""" TestSerializer(): id = IntegerField(label='ID', read_only=True) - value_limit_field = IntegerField(max_value=10, min_value=1) + value_limit_field = IntegerField(error_messages=OrderedDict([('max_value', 'Ensure this value is less than or equal to %(limit_value)s.'), ('min_value', 'Ensure this value is greater than or equal to %(limit_value)s.')]), max_value=10, min_value=1) length_limit_field = CharField(max_length=12, min_length=3) blank_field = CharField(allow_blank=True, max_length=10, required=False) null_field = IntegerField(allow_null=True, required=False) @@ -214,7 +214,11 @@ class TestRegularFieldMappings(TestCase): expected = expected.replace( "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')", "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')" + ).replace( + "{'max_value': 'Ensure this value is less than or equal to %(limit_value)s.', 'min_value': 'Ensure this value is greater than or equal to %(limit_value)s.'}", + "{'max_value': u'Ensure this value is less than or equal to %(limit_value)s.', 'min_value': u'Ensure this value is greater than or equal to %(limit_value)s.'}" ) + self.maxDiff = None self.assertEqual(unicode_repr(TestSerializer()), expected) def test_method_field(self): diff --git a/tests/test_validators.py b/tests/test_validators.py index 62126ddb3..a71436b4f 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,6 +1,9 @@ import datetime import pytest +from django.core.validators import ( + MaxValueValidator, MinLengthValidator, MinValueValidator +) from django.db import DataError, models from django.test import TestCase @@ -563,3 +566,99 @@ class ValidatorsTests(TestCase): date_field='bar') with pytest.raises(NotImplementedError): validator.filter_queryset(attrs=None, queryset=None) + + +class ItemModel(models.Model): + price = models.DecimalField(decimal_places=2, max_digits=10, validators=[MinValueValidator(limit_value=0, message='Price has to be >= 0.'), + MaxValueValidator(limit_value=10, message='Price has to be <= 10.')]) + + +class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = ItemModel + fields = '__all__' + + +class ValidatorMessageTests(TestCase): + def test_min_value_validator_message_is_copied_from_model(self): + data = {'price': -1} + s = ItemSerializer(data=data, partial=True) + s.is_valid() + + assert s.errors['price'] == ['Price has to be >= 0.'] + + def test_max_value_validator_message_is_copied_from_model(self): + data = {'price': 11} + s = ItemSerializer(data=data, partial=True) + s.is_valid() + + assert s.errors['price'] == ['Price has to be <= 10.'] + + def test_url_validator_message_is_copied_from_model(self): + class BlogModel(models.Model): + url = models.URLField( + error_messages={"invalid": "This URL is not valid."} + ) + + class BlogSerializer(serializers.ModelSerializer): + class Meta: + model = BlogModel + fields = '__all__' + + data = {'url': 'broken url'} + s = BlogSerializer(data=data) + s.is_valid() + + assert s.errors['url'] == ['This URL is not valid.'] + + def test_email_validator_message_is_copied_from_model(self): + class UserModel(models.Model): + email = models.EmailField( + error_messages={"invalid": "Please enter a valid email."} + ) + + class UserSerializer(serializers.ModelSerializer): + class Meta: + model = UserModel + fields = '__all__' + + data = {'email': 'invalid email'} + s = UserSerializer(data=data) + s.is_valid() + + assert s.errors['email'] == ['Please enter a valid email.'] + + def test_min_length_validator_message_is_copied_from_model(self): + class Review(models.Model): + text = models.CharField( + max_length=100, + validators=[MinLengthValidator(limit_value=5, message='This is too short.')] + ) + + class ReviewSerializer(serializers.ModelSerializer): + class Meta: + model = Review + fields = '__all__' + + data = {'text': 'Hi'} + s = ReviewSerializer(data=data) + s.is_valid() + + assert s.errors['text'] == ['This is too short.'] + + def test_max_length_validator_message_is_copied_from_model(self): + class Post(models.Model): + text = models.CharField( + max_length=1, + error_messages={"max_length": "This is too long"} + ) + + class PostSerializer(serializers.ModelSerializer): + class Meta: + model = Post + fields = '__all__' + + data = {'text': 'A very long text'} + s = PostSerializer(data=data) + assert not s.is_valid() + assert s.errors['text'] == ['This is too long']