Skip to content

Commit d60dcf4

Browse files
committed
Add DurationField
1 parent a0f66ff commit d60dcf4

File tree

6 files changed

+98
-3
lines changed

6 files changed

+98
-3
lines changed

docs/api-guide/fields.md

+12
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,18 @@ Corresponds to `django.db.models.fields.TimeField`
302302

303303
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'`)
304304

305+
## DurationField
306+
307+
A Duration representation.
308+
Corresponds to `django.db.models.fields.Duration`
309+
310+
The `validated_data` for these fields will contain a `datetime.timedelta` instance.
311+
The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`.
312+
313+
**Note:** This field is only available with Django versions >= 1.8.
314+
315+
**Signature:** `DurationField()`
316+
305317
---
306318

307319
# Choice selection fields

rest_framework/compat.py

+8
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,11 @@ def apply_markdown(text):
258258
SHORT_SEPARATORS = (b',', b':')
259259
LONG_SEPARATORS = (b', ', b': ')
260260
INDENT_SEPARATORS = (b',', b': ')
261+
262+
263+
if django.VERSION >= (1, 8):
264+
from django.db.models import DurationField
265+
from django.utils.dateparse import parse_duration
266+
from django.utils.duration import duration_string
267+
else:
268+
DurationField = duration_string = parse_duration = None

rest_framework/fields.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.compat import (
1313
EmailValidator, MinValueValidator, MaxValueValidator,
1414
MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict,
15-
unicode_repr, unicode_to_repr
15+
unicode_repr, unicode_to_repr, parse_duration, duration_string,
1616
)
1717
from rest_framework.exceptions import ValidationError
1818
from rest_framework.settings import api_settings
@@ -1003,6 +1003,29 @@ def to_representation(self, value):
10031003
return value.strftime(self.format)
10041004

10051005

1006+
class DurationField(Field):
1007+
default_error_messages = {
1008+
'invalid': _('Duration has wrong format. Use one of these formats instead: {format}.'),
1009+
}
1010+
1011+
def __init__(self, *args, **kwargs):
1012+
if parse_duration is None:
1013+
raise NotImplementedError(
1014+
'DurationField not supported for django versions prior to 1.8')
1015+
return super(DurationField, self).__init__(*args, **kwargs)
1016+
1017+
def to_internal_value(self, value):
1018+
if isinstance(value, datetime.timedelta):
1019+
return value
1020+
parsed = parse_duration(value)
1021+
if parsed is not None:
1022+
return parsed
1023+
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
1024+
1025+
def to_representation(self, value):
1026+
return duration_string(value)
1027+
1028+
10061029
# Choice types...
10071030

10081031
class ChoiceField(Field):

rest_framework/serializers.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
1616
from django.db.models import query
1717
from django.utils.translation import ugettext_lazy as _
18-
from rest_framework.compat import postgres_fields, unicode_to_repr
18+
from rest_framework.compat import (
19+
postgres_fields,
20+
unicode_to_repr,
21+
DurationField as ModelDurationField,
22+
)
1923
from rest_framework.utils import model_meta
2024
from rest_framework.utils.field_mapping import (
2125
get_url_kwargs, get_field_kwargs,
@@ -731,6 +735,8 @@ class ModelSerializer(Serializer):
731735
models.TimeField: TimeField,
732736
models.URLField: URLField,
733737
}
738+
if ModelDurationField is not None:
739+
serializer_field_mapping[ModelDurationField] = DurationField
734740
serializer_related_field = PrimaryKeyRelatedField
735741
serializer_url_field = HyperlinkedIdentityField
736742
serializer_choice_field = ChoiceField

tests/test_fields.py

+22
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,28 @@ class TestNoOutputFormatTimeField(FieldValues):
905905
field = serializers.TimeField(format=None)
906906

907907

908+
@pytest.mark.skipif(django.VERSION < (1, 8),
909+
reason='DurationField is only available for django1.8+')
910+
class TestDurationField(FieldValues):
911+
"""
912+
Valid and invalid values for `DurationField`.
913+
"""
914+
valid_inputs = {
915+
'13': datetime.timedelta(seconds=13),
916+
'3 08:32:01.000123': datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123),
917+
'08:01': datetime.timedelta(minutes=8, seconds=1),
918+
datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123),
919+
}
920+
invalid_inputs = {
921+
'abc': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'],
922+
'3 08:32 01.123': ['Duration has wrong format. Use one of these formats instead: [DD] [HH:[MM:]]ss[.uuuuuu].'],
923+
}
924+
outputs = {
925+
datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): '3 08:32:01.000123',
926+
}
927+
field = serializers.DurationField()
928+
929+
908930
# Choice types...
909931

910932
class TestChoiceField(FieldValues):

tests/test_model_serializer.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
an appropriate set of serializer fields for each case.
77
"""
88
from __future__ import unicode_literals
9+
import django
910
from django.core.exceptions import ImproperlyConfigured
1011
from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
1112
from django.db import models
1213
from django.test import TestCase
1314
from django.utils import six
15+
import pytest
1416
from rest_framework import serializers
15-
from rest_framework.compat import unicode_repr
17+
from rest_framework.compat import unicode_repr, DurationField as ModelDurationField
1618

1719

1820
def dedent(blocktext):
@@ -284,6 +286,28 @@ class Meta:
284286
ChildSerializer().fields
285287

286288

289+
@pytest.mark.skipif(django.VERSION < (1, 8),
290+
reason='DurationField is only available for django1.8+')
291+
class TestDurationFieldMapping(TestCase):
292+
def test_duration_field(self):
293+
class DurationFieldModel(models.Model):
294+
"""
295+
A model that defines DurationField.
296+
"""
297+
duration_field = ModelDurationField()
298+
299+
class TestSerializer(serializers.ModelSerializer):
300+
class Meta:
301+
model = DurationFieldModel
302+
303+
expected = dedent("""
304+
TestSerializer():
305+
id = IntegerField(label='ID', read_only=True)
306+
duration_field = DurationField()
307+
""")
308+
self.assertEqual(unicode_repr(TestSerializer()), expected)
309+
310+
287311
# Tests for relational field mappings.
288312
# ------------------------------------
289313

0 commit comments

Comments
 (0)