Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DurationField #2989

Merged
merged 1 commit into from
Jun 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/api-guide/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions rest_framework/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,11 @@ def apply_markdown(text):
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
25 changes: 24 additions & 1 deletion rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
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
Expand Down Expand Up @@ -1003,6 +1003,29 @@ def to_representation(self, value):
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):
Expand Down
8 changes: 7 additions & 1 deletion rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
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,
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
26 changes: 25 additions & 1 deletion tests/test_model_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
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):
Expand Down Expand Up @@ -284,6 +286,28 @@ class Meta:
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.
# ------------------------------------

Expand Down