From 3b4f617a2e2b472c7fecd64cd844778dc419a3b8 Mon Sep 17 00:00:00 2001 From: Sergey Piskunov Date: Fri, 5 Mar 2021 11:04:36 +0200 Subject: [PATCH] Adding more verbose validation error messages in case of both "min_value" and "max_value" limits are set. --- rest_framework/fields.py | 88 +++++++++++++---- tests/test_fields.py | 202 +++++++++++++++++++++++++++++++++++---- 2 files changed, 253 insertions(+), 37 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fdfba13f2..7e3e68fb2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -931,6 +931,7 @@ class IntegerField(Field): 'invalid': _('A valid integer 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}.'), + 'range': _('Ensure this value is in the range between {min_value} and {max_value}.'), 'max_string_length': _('String value too large.') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. @@ -940,14 +941,22 @@ class IntegerField(Field): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) super().__init__(**kwargs) + + if self.min_value is not None and self.max_value is not None: + message = lazy_format( + self.error_messages['range'], min_value=self.min_value, max_value=self.max_value) + else: + message = None + if self.max_value is not None: - message = lazy_format(self.error_messages['max_value'], max_value=self.max_value) self.validators.append( - MaxValueValidator(self.max_value, message=message)) + MaxValueValidator(self.max_value, message=message or lazy_format( + self.error_messages['max_value'], max_value=self.max_value))) + if self.min_value is not None: - message = lazy_format(self.error_messages['min_value'], min_value=self.min_value) self.validators.append( - MinValueValidator(self.min_value, message=message)) + MinValueValidator(self.min_value, message=message or lazy_format( + self.error_messages['min_value'], min_value=self.min_value))) def to_internal_value(self, data): if isinstance(data, str) and len(data) > self.MAX_STRING_LENGTH: @@ -968,6 +977,7 @@ class FloatField(Field): '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}.'), + 'range': _('Ensure this value is in the range between {min_value} and {max_value}.'), 'max_string_length': _('String value too large.') } MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. @@ -976,14 +986,22 @@ class FloatField(Field): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) super().__init__(**kwargs) + + if self.min_value is not None and self.max_value is not None: + message = lazy_format( + self.error_messages['range'], min_value=self.min_value, max_value=self.max_value) + else: + message = None + if self.max_value is not None: - message = lazy_format(self.error_messages['max_value'], max_value=self.max_value) self.validators.append( - MaxValueValidator(self.max_value, message=message)) + MaxValueValidator(self.max_value, message=message or lazy_format( + self.error_messages['max_value'], max_value=self.max_value))) + if self.min_value is not None: - message = lazy_format(self.error_messages['min_value'], min_value=self.min_value) self.validators.append( - MinValueValidator(self.min_value, message=message)) + MinValueValidator(self.min_value, message=message or lazy_format( + self.error_messages['min_value'], min_value=self.min_value))) def to_internal_value(self, data): @@ -1004,6 +1022,7 @@ class DecimalField(Field): '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}.'), + 'range': _('Ensure this value is in the range between {min_value} and {max_value}.'), '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.'), @@ -1031,14 +1050,21 @@ class DecimalField(Field): super().__init__(**kwargs) + if self.min_value is not None and self.max_value is not None: + message = lazy_format( + self.error_messages['range'], min_value=self.min_value, max_value=self.max_value) + else: + message = None + if self.max_value is not None: - message = lazy_format(self.error_messages['max_value'], max_value=self.max_value) self.validators.append( - MaxValueValidator(self.max_value, message=message)) + MaxValueValidator(self.max_value, message=message or lazy_format( + self.error_messages['max_value'], max_value=self.max_value))) + if self.min_value is not None: - message = lazy_format(self.error_messages['min_value'], min_value=self.min_value) self.validators.append( - MinValueValidator(self.min_value, message=message)) + MinValueValidator(self.min_value, message=message or lazy_format( + self.error_messages['min_value'], min_value=self.min_value))) if rounding is not None: valid_roundings = [v for k, v in vars(decimal).items() if k.startswith('ROUND_')] @@ -1365,20 +1391,29 @@ class DurationField(Field): 'invalid': _('Duration has wrong format. Use one of these formats instead: {format}.'), '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}.'), + 'range': _('Ensure this value is in the range between {min_value} and {max_value}.'), } def __init__(self, **kwargs): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) super().__init__(**kwargs) + + if self.min_value is not None and self.max_value is not None: + message = lazy_format( + self.error_messages['range'], min_value=self.min_value, max_value=self.max_value) + else: + message = None + if self.max_value is not None: - message = lazy_format(self.error_messages['max_value'], max_value=self.max_value) self.validators.append( - MaxValueValidator(self.max_value, message=message)) + MaxValueValidator(self.max_value, message=message or lazy_format( + self.error_messages['max_value'], max_value=self.max_value))) + if self.min_value is not None: - message = lazy_format(self.error_messages['min_value'], min_value=self.min_value) self.validators.append( - MinValueValidator(self.min_value, message=message)) + MinValueValidator(self.min_value, message=message or lazy_format( + self.error_messages['min_value'], min_value=self.min_value))) def to_internal_value(self, value): if isinstance(value, datetime.timedelta): @@ -1603,7 +1638,8 @@ class ListField(Field): 'not_a_list': _('Expected a list of items but got type "{input_type}".'), 'empty': _('This list may not be empty.'), 'min_length': _('Ensure this field has at least {min_length} elements.'), - 'max_length': _('Ensure this field has no more than {max_length} elements.') + 'max_length': _('Ensure this field has no more than {max_length} elements.'), + 'range': _('Ensure this field has at least {min_length} and no more than {max_length} elements.'), } def __init__(self, *args, **kwargs): @@ -1620,12 +1656,22 @@ class ListField(Field): super().__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) + + if self.min_length is not None and self.max_length is not None: + message = lazy_format( + self.error_messages['range'], min_length=self.min_length, max_length=self.max_length) + else: + message = None + if self.max_length is not None: - message = lazy_format(self.error_messages['max_length'], max_length=self.max_length) - self.validators.append(MaxLengthValidator(self.max_length, message=message)) + self.validators.append( + MaxLengthValidator(self.max_length, message=message or lazy_format( + self.error_messages['max_length'], max_length=self.max_length))) + if self.min_length is not None: - message = lazy_format(self.error_messages['min_length'], min_length=self.min_length) - self.validators.append(MinLengthValidator(self.min_length, message=message)) + self.validators.append( + MinLengthValidator(self.min_length, message=message or lazy_format( + self.error_messages['min_length'], min_length=self.min_length))) def get_value(self, dictionary): if self.field_name not in dictionary: diff --git a/tests/test_fields.py b/tests/test_fields.py index fdd570d8a..bcebc1db6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1019,15 +1019,51 @@ class TestMinMaxIntegerField(FieldValues): 3: 3, } invalid_inputs = { - 0: ['Ensure this value is greater than or equal to 1.'], - 4: ['Ensure this value is less than or equal to 3.'], - '0': ['Ensure this value is greater than or equal to 1.'], - '4': ['Ensure this value is less than or equal to 3.'], + 0: ['Ensure this value is in the range between 1 and 3.'], + 4: ['Ensure this value is in the range between 1 and 3.'], + '0': ['Ensure this value is in the range between 1 and 3.'], + '4': ['Ensure this value is in the range between 1 and 3.'], } outputs = {} field = serializers.IntegerField(min_value=1, max_value=3) +class TestMinIntegerField(FieldValues): + """ + Valid and invalid values for `IntegerField` with min limit. + """ + valid_inputs = { + '1': 1, + '3': 3, + 1: 1, + 3: 3, + } + invalid_inputs = { + 0: ['Ensure this value is greater than or equal to 1.'], + '0': ['Ensure this value is greater than or equal to 1.'], + } + outputs = {} + field = serializers.IntegerField(min_value=1) + + +class TestMaxIntegerField(FieldValues): + """ + Valid and invalid values for `IntegerField` with max limit. + """ + valid_inputs = { + '1': 1, + '3': 3, + 1: 1, + 3: 3, + } + invalid_inputs = { + 4: ['Ensure this value is less than or equal to 3.'], + '4': ['Ensure this value is less than or equal to 3.'], + } + outputs = {} + field = serializers.IntegerField(max_value=3) + + class TestFloatField(FieldValues): """ Valid and invalid values for `FloatField`. @@ -1067,15 +1103,55 @@ class TestMinMaxFloatField(FieldValues): 3.0: 3.0, } invalid_inputs = { - 0.9: ['Ensure this value is greater than or equal to 1.'], - 3.1: ['Ensure this value is less than or equal to 3.'], - '0.0': ['Ensure this value is greater than or equal to 1.'], - '3.1': ['Ensure this value is less than or equal to 3.'], + 0.9: ['Ensure this value is in the range between 1 and 3.'], + 3.1: ['Ensure this value is in the range between 1 and 3.'], + '0.0': ['Ensure this value is in the range between 1 and 3.'], + '3.1': ['Ensure this value is in the range between 1 and 3.'], } outputs = {} field = serializers.FloatField(min_value=1, max_value=3) +class TestMinFloatField(FieldValues): + """ + Valid and invalid values for `FloatField` with min limit. + """ + valid_inputs = { + '1': 1, + '3': 3, + 1: 1, + 3: 3, + 1.0: 1.0, + 3.0: 3.0, + } + invalid_inputs = { + 0.9: ['Ensure this value is greater than or equal to 1.'], + '0.0': ['Ensure this value is greater than or equal to 1.'], + } + outputs = {} + field = serializers.FloatField(min_value=1) + + +class TestMaxFloatField(FieldValues): + """ + Valid and invalid values for `FloatField` with max limit. + """ + valid_inputs = { + '1': 1, + '3': 3, + 1: 1, + 3: 3, + 1.0: 1.0, + 3.0: 3.0, + } + invalid_inputs = { + 3.1: ['Ensure this value is less than or equal to 3.'], + '3.1': ['Ensure this value is less than or equal to 3.'], + } + outputs = {} + field = serializers.FloatField(max_value=3) + + class TestDecimalField(FieldValues): """ Valid and invalid values for `DecimalField`. @@ -1124,8 +1200,8 @@ class TestMinMaxDecimalField(FieldValues): '20.0': Decimal('20.0'), } invalid_inputs = { - '9.9': ['Ensure this value is greater than or equal to 10.'], - '20.1': ['Ensure this value is less than or equal to 20.'], + '9.9': ['Ensure this value is in the range between 10 and 20.'], + '20.1': ['Ensure this value is in the range between 10 and 20.'], } outputs = {} field = serializers.DecimalField( @@ -1134,6 +1210,42 @@ class TestMinMaxDecimalField(FieldValues): ) +class TestMinDecimalField(FieldValues): + """ + Valid and invalid values for `DecimalField` with min limit. + """ + valid_inputs = { + '10.0': Decimal('10.0'), + '20.0': Decimal('20.0'), + } + invalid_inputs = { + '9.9': ['Ensure this value is greater than or equal to 10.'], + } + outputs = {} + field = serializers.DecimalField( + max_digits=3, decimal_places=1, + min_value=10 + ) + + +class TestMaxDecimalField(FieldValues): + """ + Valid and invalid values for `DecimalField` with max limit. + """ + valid_inputs = { + '10.0': Decimal('10.0'), + '20.0': Decimal('20.0'), + } + invalid_inputs = { + '20.1': ['Ensure this value is less than or equal to 20.'], + } + outputs = {} + field = serializers.DecimalField( + max_digits=3, decimal_places=1, + max_value=20 + ) + + class TestNoMaxDigitsDecimalField(FieldValues): field = serializers.DecimalField( max_value=100, min_value=0, @@ -1536,14 +1648,45 @@ class TestMinMaxDurationField(FieldValues): 86401: datetime.timedelta(days=1, seconds=1), } invalid_inputs = { - 3600: ['Ensure this value is greater than or equal to 1 day, 0:00:00.'], - '4 08:32:01.000123': ['Ensure this value is less than or equal to 4 days, 0:00:00.'], - '3600': ['Ensure this value is greater than or equal to 1 day, 0:00:00.'], + 3600: ['Ensure this value is in the range between 1 day, 0:00:00 and 4 days, 0:00:00.'], + '4 08:32:01.000123': ['Ensure this value is in the range between 1 day, 0:00:00 and 4 days, 0:00:00.'], + '3600': ['Ensure this value is in the range between 1 day, 0:00:00 and 4 days, 0:00:00.'], } outputs = {} field = serializers.DurationField(min_value=datetime.timedelta(days=1), max_value=datetime.timedelta(days=4)) +class TestMinDurationField(FieldValues): + """ + Valid and invalid values for `DurationField` with min limit. + """ + valid_inputs = { + '3 08:32:01.000123': datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123), + 86401: datetime.timedelta(days=1, seconds=1), + } + invalid_inputs = { + 3600: ['Ensure this value is greater than or equal to 1 day, 0:00:00.'], + '3600': ['Ensure this value is greater than or equal to 1 day, 0:00:00.'], + } + outputs = {} + field = serializers.DurationField(min_value=datetime.timedelta(days=1)) + + +class TestMaxDurationField(FieldValues): + """ + Valid and invalid values for `DurationField` with max limit. + """ + valid_inputs = { + '3 08:32:01.000123': datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123), + 86401: datetime.timedelta(days=1, seconds=1), + } + invalid_inputs = { + '4 08:32:01.000123': ['Ensure this value is less than or equal to 4 days, 0:00:00.'], + } + outputs = {} + field = serializers.DurationField(max_value=datetime.timedelta(days=4)) + + class TestDurationField(FieldValues): """ Valid and invalid values for `DurationField`. @@ -1989,16 +2132,43 @@ class TestEmptyListField(FieldValues): field = serializers.ListField(child=serializers.IntegerField(), allow_empty=False) -class TestListFieldLengthLimit(FieldValues): +class TestListFieldMinMaxLengthLimit(FieldValues): + """ + Valid and invalid values for `ListField` with min and max limits. + """ valid_inputs = () invalid_inputs = [ - ((0, 1), ['Ensure this field has at least 3 elements.']), - ((0, 1, 2, 3, 4, 5), ['Ensure this field has no more than 4 elements.']), + ((0, 1), ['Ensure this field has at least 3 and no more than 4 elements.']), + ((0, 1, 2, 3, 4, 5), ['Ensure this field has at least 3 and no more than 4 elements.']), ] outputs = () field = serializers.ListField(child=serializers.IntegerField(), min_length=3, max_length=4) +class TestListFieldMinLengthLimit(FieldValues): + """ + Valid and invalid values for `ListField` with min limit. + """ + valid_inputs = () + invalid_inputs = [ + ((0, 1), ['Ensure this field has at least 3 elements.']), + ] + outputs = () + field = serializers.ListField(child=serializers.IntegerField(), min_length=3) + + +class TestListFieldMaxLengthLimit(FieldValues): + """ + Valid and invalid values for `ListField` with max limit. + """ + valid_inputs = () + invalid_inputs = [ + ((0, 1, 2, 3, 4, 5), ['Ensure this field has no more than 4 elements.']), + ] + outputs = () + field = serializers.ListField(child=serializers.IntegerField(), max_length=4) + + class TestUnvalidatedListField(FieldValues): """ Values for `ListField` with no `child` argument.