From f701ecceb74ba8bfc0e270b64460626bd4544766 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Mon, 1 Jun 2015 18:20:53 +0200 Subject: [PATCH] Add DurationField --- docs/api-guide/fields.md | 12 ++++++++++++ rest_framework/compat.py | 8 ++++++++ rest_framework/fields.py | 25 ++++++++++++++++++++++++- rest_framework/serializers.py | 8 +++++++- tests/test_fields.py | 23 +++++++++++++++++++++++ tests/test_model_serializer.py | 26 +++++++++++++++++++++++++- 6 files changed, 99 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index c87db7854..aad188511 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -302,6 +302,18 @@ Corresponds to `django.db.models.fields.TimeField` Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`) +## DurationField + +A Duration representation. +Corresponds to `django.db.models.fields.Duration` + +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]'`. + +**Note:** This field is only available with Django versions >= 1.8. + +**Signature:** `DurationField()` + --- # Choice selection fields diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 1ba907314..8d6151fa2 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -258,3 +258,11 @@ else: SHORT_SEPARATORS = (b',', b':') LONG_SEPARATORS = (b', ', b': ') INDENT_SEPARATORS = (b',', b': ') + + +if django.VERSION >= (1, 8): + from django.db.models import DurationField + from django.utils.dateparse import parse_duration + from django.utils.duration import duration_string +else: + DurationField = duration_string = parse_duration = None diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d8bb0a017..85c451078 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -12,7 +12,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( EmailValidator, MinValueValidator, MaxValueValidator, MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict, - unicode_repr, unicode_to_repr + unicode_repr, unicode_to_repr, parse_duration, duration_string, ) from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings @@ -1003,6 +1003,29 @@ class TimeField(Field): return value.strftime(self.format) +class DurationField(Field): + default_error_messages = { + 'invalid': _('Duration has wrong format. Use one of these formats instead: {format}.'), + } + + def __init__(self, *args, **kwargs): + if parse_duration is None: + raise NotImplementedError( + 'DurationField not supported for django versions prior to 1.8') + return super(DurationField, self).__init__(*args, **kwargs) + + def to_internal_value(self, value): + if isinstance(value, datetime.timedelta): + return value + parsed = parse_duration(value) + if parsed is not None: + return parsed + self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]') + + def to_representation(self, value): + return duration_string(value) + + # Choice types... class ChoiceField(Field): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 73ac6bc2a..55f571db9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,7 +15,11 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField from django.db.models import query from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import postgres_fields, unicode_to_repr +from rest_framework.compat import ( + postgres_fields, + unicode_to_repr, + DurationField as ModelDurationField, +) from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, @@ -731,6 +735,8 @@ class ModelSerializer(Serializer): models.TimeField: TimeField, models.URLField: URLField, } + if ModelDurationField is not None: + serializer_field_mapping[ModelDurationField] = DurationField serializer_related_field = PrimaryKeyRelatedField serializer_url_field = HyperlinkedIdentityField serializer_choice_field = ChoiceField diff --git a/tests/test_fields.py b/tests/test_fields.py index 568e8d5e7..ae1920d9f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -905,6 +905,29 @@ class TestNoOutputFormatTimeField(FieldValues): field = serializers.TimeField(format=None) +@pytest.mark.skipif(django.VERSION < (1, 8), + reason='DurationField is only available for django1.8+') +class TestDurationField(FieldValues): + """ + Valid and invalid values for `DurationField`. + """ + valid_inputs = { + '13': datetime.timedelta(seconds=13), + '3 08:32:01.000123': datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123), + '08:01': datetime.timedelta(minutes=8, seconds=1), + datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123), + } + invalid_inputs = { + 'abc': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'], + '3 08:32 01.123': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'], + } + outputs = { + datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): '3 08:32:01.000123', + } + if django.VERSION >= (1, 8): + field = serializers.DurationField() + + # Choice types... class TestChoiceField(FieldValues): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index dc34649ea..a94133823 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -6,13 +6,15 @@ These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ from __future__ import unicode_literals +import django from django.core.exceptions import ImproperlyConfigured from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator from django.db import models from django.test import TestCase from django.utils import six +import pytest from rest_framework import serializers -from rest_framework.compat import unicode_repr +from rest_framework.compat import unicode_repr, DurationField as ModelDurationField def dedent(blocktext): @@ -284,6 +286,28 @@ class TestRegularFieldMappings(TestCase): ChildSerializer().fields +@pytest.mark.skipif(django.VERSION < (1, 8), + reason='DurationField is only available for django1.8+') +class TestDurationFieldMapping(TestCase): + def test_duration_field(self): + class DurationFieldModel(models.Model): + """ + A model that defines DurationField. + """ + duration_field = ModelDurationField() + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = DurationFieldModel + + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + duration_field = DurationField() + """) + self.assertEqual(unicode_repr(TestSerializer()), expected) + + # Tests for relational field mappings. # ------------------------------------