Skip to content

Commit c94ddc3

Browse files
committed
Add DurationField
1 parent a0f66ff commit c94ddc3

File tree

6 files changed

+130
-4
lines changed

6 files changed

+130
-4
lines changed

docs/api-guide/fields.md

+9
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ 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` for Django>=1.8
309+
otherwise to `django.db.models.fields.BigIntegerField`.
310+
311+
312+
**Signature:** `DurationField()`
313+
305314
---
306315

307316
# Choice selection fields

rest_framework/compat.py

+74
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
# flake8: noqa
77
from __future__ import unicode_literals
8+
import datetime
9+
import re
810
from django.core.exceptions import ImproperlyConfigured
911
from django.conf import settings
1012
from django.utils.encoding import force_text
@@ -258,3 +260,75 @@ def apply_markdown(text):
258260
SHORT_SEPARATORS = (b',', b':')
259261
LONG_SEPARATORS = (b', ', b': ')
260262
INDENT_SEPARATORS = (b',', b': ')
263+
264+
265+
if django.VERSION >= (1, 8):
266+
from django.utils.dateparse import parse_duration
267+
from django.utils.duration import duration_string
268+
from django.db.models import DurationField
269+
else:
270+
from django.db.models import BigIntegerField
271+
272+
class DurationField(BigIntegerField):
273+
pass
274+
275+
276+
# Backported from django 1.8
277+
standard_duration_re = re.compile(
278+
r'^'
279+
r'(?:(?P<days>-?\d+) )?'
280+
r'((?:(?P<hours>\d+):)(?=\d+:\d+))?'
281+
r'(?:(?P<minutes>\d+):)?'
282+
r'(?P<seconds>\d+)'
283+
r'(?:\.(?P<microseconds>\d{1,6})\d{0,6})?'
284+
r'$'
285+
)
286+
287+
# Support the sections of ISO 8601 date representation that are accepted by
288+
# timedelta
289+
iso8601_duration_re = re.compile(
290+
r'^P'
291+
r'(?:(?P<days>\d+(.\d+)?)D)?'
292+
r'(?:T'
293+
r'(?:(?P<hours>\d+(.\d+)?)H)?'
294+
r'(?:(?P<minutes>\d+(.\d+)?)M)?'
295+
r'(?:(?P<seconds>\d+(.\d+)?)S)?'
296+
r')?'
297+
r'$'
298+
)
299+
300+
def parse_duration(value):
301+
"""Parses a duration string and returns a datetime.timedelta.
302+
303+
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
304+
305+
Also supports ISO 8601 representation.
306+
"""
307+
match = standard_duration_re.match(value)
308+
if not match:
309+
match = iso8601_duration_re.match(value)
310+
if match:
311+
kw = match.groupdict()
312+
if kw.get('microseconds'):
313+
kw['microseconds'] = kw['microseconds'].ljust(6, unicode_to_repr('0'))
314+
kw = dict((k, float(v)) for k, v in six.iteritems(kw) if v is not None)
315+
return datetime.timedelta(**kw)
316+
317+
def duration_string(duration):
318+
days = duration.days
319+
seconds = duration.seconds
320+
microseconds = duration.microseconds
321+
322+
minutes = seconds // 60
323+
seconds = seconds % 60
324+
325+
hours = minutes // 60
326+
minutes = minutes % 60
327+
328+
string = '{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds)
329+
if days:
330+
string = '{0} '.format(days) + string
331+
if microseconds:
332+
string += '.{0:06d}'.format(microseconds)
333+
334+
return string

rest_framework/fields.py

+18-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,23 @@ 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 to_internal_value(self, value):
1012+
if isinstance(value, datetime.timedelta):
1013+
return value
1014+
parsed = parse_duration(value)
1015+
if parsed is not None:
1016+
return parsed
1017+
self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]')
1018+
1019+
def to_representation(self, value):
1020+
return duration_string(value)
1021+
1022+
10061023
# Choice types...
10071024

10081025
class ChoiceField(Field):

rest_framework/serializers.py

+6-2
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,
@@ -42,7 +46,6 @@
4246
from rest_framework.relations import * # NOQA
4347
from rest_framework.fields import * # NOQA
4448

45-
4649
# We assume that 'validators' are intended for the child serializer,
4750
# rather than the parent serializer.
4851
LIST_SERIALIZER_KWARGS = (
@@ -716,6 +719,7 @@ class ModelSerializer(Serializer):
716719
models.DateField: DateField,
717720
models.DateTimeField: DateTimeField,
718721
models.DecimalField: DecimalField,
722+
ModelDurationField: DurationField,
719723
models.EmailField: EmailField,
720724
models.Field: ModelField,
721725
models.FileField: FileField,

tests/test_fields.py

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

907907

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

910930
class TestChoiceField(FieldValues):

tests/test_model_serializer.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.test import TestCase
1313
from django.utils import six
1414
from rest_framework import serializers
15-
from rest_framework.compat import unicode_repr
15+
from rest_framework.compat import unicode_repr, DurationField as ModelDurationField
1616

1717

1818
def dedent(blocktext):
@@ -45,6 +45,7 @@ class RegularFieldsModel(models.Model):
4545
date_field = models.DateField()
4646
datetime_field = models.DateTimeField()
4747
decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
48+
duration_field = ModelDurationField()
4849
email_field = models.EmailField(max_length=100)
4950
float_field = models.FloatField()
5051
integer_field = models.IntegerField()
@@ -138,6 +139,7 @@ class Meta:
138139
date_field = DateField()
139140
datetime_field = DateTimeField()
140141
decimal_field = DecimalField(decimal_places=1, max_digits=3)
142+
duration_field = DurationField()
141143
email_field = EmailField(max_length=100)
142144
float_field = FloatField()
143145
integer_field = IntegerField()

0 commit comments

Comments
 (0)