From 7268643b2587eb6f2b3bd58e064972201f82ee0e Mon Sep 17 00:00:00 2001 From: Noam Date: Tue, 24 Apr 2018 10:24:05 +0300 Subject: [PATCH] min_value/max_value support in DurationField (#5643) * Added min_value/max_value field arguments to DurationField. * Made field mapping use mix/max kwargs for DurationField validators. --- docs/api-guide/fields.md | 5 ++++- rest_framework/fields.py | 19 +++++++++++++++++++ rest_framework/utils/field_mapping.py | 2 +- tests/test_fields.py | 17 +++++++++++++++++ tests/test_model_serializer.py | 25 +++++++++++++++++++++++-- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 5cb096f1c..8d25d6c78 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -360,7 +360,10 @@ Corresponds to `django.db.models.fields.DurationField` The `validated_data` for these fields will contain a `datetime.timedelta` instance. The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`. -**Signature:** `DurationField()` +**Signature:** `DurationField(max_value=None, min_value=None)` + +- `max_value` Validate that the duration provided is no greater than this value. +- `min_value` Validate that the duration provided is no less than this value. --- diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c13279675..d6e363339 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1354,8 +1354,27 @@ class TimeField(Field): class DurationField(Field): default_error_messages = { '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}.'), } + def __init__(self, **kwargs): + self.max_value = kwargs.pop('max_value', None) + self.min_value = kwargs.pop('min_value', None) + super(DurationField, self).__init__(**kwargs) + if self.max_value is not None: + message = lazy( + self.error_messages['max_value'].format, + six.text_type)(max_value=self.max_value) + self.validators.append( + MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = lazy( + self.error_messages['min_value'].format, + six.text_type)(min_value=self.min_value) + self.validators.append( + MinValueValidator(self.min_value, message=message)) + def to_internal_value(self, value): if isinstance(value, datetime.timedelta): return value diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 722981b20..50de3f125 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -12,7 +12,7 @@ from rest_framework.compat import postgres_fields from rest_framework.validators import UniqueValidator NUMERIC_FIELD_TYPES = ( - models.IntegerField, models.FloatField, models.DecimalField + models.IntegerField, models.FloatField, models.DecimalField, models.DurationField, ) diff --git a/tests/test_fields.py b/tests/test_fields.py index 0ee49e9c1..7227c2f5a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1459,6 +1459,23 @@ class TestNoOutputFormatTimeField(FieldValues): field = serializers.TimeField(format=None) +class TestMinMaxDurationField(FieldValues): + """ + Valid and invalid values for `DurationField` with min and max limits. + """ + 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.'], + '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.'], + } + outputs = {} + field = serializers.DurationField(min_value=datetime.timedelta(days=1), max_value=datetime.timedelta(days=4)) + + class TestDurationField(FieldValues): """ Valid and invalid values for `DurationField`. diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index e4fc8b37f..d865350fb 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -7,6 +7,7 @@ an appropriate set of serializer fields for each case. """ from __future__ import unicode_literals +import datetime import decimal from collections import OrderedDict @@ -16,7 +17,6 @@ from django.core.validators import ( MaxValueValidator, MinLengthValidator, MinValueValidator ) from django.db import models -from django.db.models import DurationField as ModelDurationField from django.test import TestCase from django.utils import six @@ -349,7 +349,7 @@ class TestDurationFieldMapping(TestCase): """ A model that defines DurationField. """ - duration_field = ModelDurationField() + duration_field = models.DurationField() class TestSerializer(serializers.ModelSerializer): class Meta: @@ -363,6 +363,27 @@ class TestDurationFieldMapping(TestCase): """) self.assertEqual(unicode_repr(TestSerializer()), expected) + def test_duration_field_with_validators(self): + class ValidatedDurationFieldModel(models.Model): + """ + A model that defines DurationField with validators. + """ + duration_field = models.DurationField( + validators=[MinValueValidator(datetime.timedelta(days=1)), MaxValueValidator(datetime.timedelta(days=3))] + ) + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = ValidatedDurationFieldModel + fields = '__all__' + + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + duration_field = DurationField(max_value=datetime.timedelta(3), min_value=datetime.timedelta(1)) + """) + self.assertEqual(unicode_repr(TestSerializer()), expected) + class TestGenericIPAddressFieldValidation(TestCase): def test_ip_address_validation(self):