summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Hardy <Will.Hardy@thermondo.de>2016-05-26 14:48:36 +0200
committerTim Graham <timograham@gmail.com>2016-07-14 13:34:15 -0400
commit8ef78b81654ebcf19a1fc241e2b1ede35100096b (patch)
tree67baaa077b5ed4530330a2202b5d3906708690b3
parenta7b5dfd1703a8fbed70b7aca5e6dda092af51154 (diff)
Fixed #26656 -- Added duration (timedelta) support to DjangoJSONEncoder.
-rw-r--r--django/core/serializers/json.py3
-rw-r--r--django/utils/dateparse.py6
-rw-r--r--django/utils/duration.py23
-rw-r--r--docs/releases/1.11.txt4
-rw-r--r--docs/topics/serialization.txt9
-rw-r--r--tests/serializers/test_json.py13
-rw-r--r--tests/utils_tests/test_duration.py40
7 files changed, 93 insertions, 5 deletions
diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py
index a94a207ad6..b66b3697b3 100644
--- a/django/core/serializers/json.py
+++ b/django/core/serializers/json.py
@@ -16,6 +16,7 @@ from django.core.serializers.python import (
Deserializer as PythonDeserializer, Serializer as PythonSerializer,
)
from django.utils import six
+from django.utils.duration import duration_iso_string
from django.utils.functional import Promise
from django.utils.timezone import is_aware
@@ -108,6 +109,8 @@ class DjangoJSONEncoder(json.JSONEncoder):
if o.microsecond:
r = r[:12]
return r
+ elif isinstance(o, datetime.timedelta):
+ return duration_iso_string(o)
elif isinstance(o, decimal.Decimal):
return str(o)
elif isinstance(o, uuid.UUID):
diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py
index 30a96f818e..c3d7eb06b9 100644
--- a/django/utils/dateparse.py
+++ b/django/utils/dateparse.py
@@ -40,7 +40,8 @@ standard_duration_re = re.compile(
# Support the sections of ISO 8601 date representation that are accepted by
# timedelta
iso8601_duration_re = re.compile(
- r'^P'
+ r'^(?P<sign>[-+]?)'
+ r'P'
r'(?:(?P<days>\d+(.\d+)?)D)?'
r'(?:T'
r'(?:(?P<hours>\d+(.\d+)?)H)?'
@@ -121,7 +122,8 @@ def parse_duration(value):
match = iso8601_duration_re.match(value)
if match:
kw = match.groupdict()
+ sign = -1 if kw.pop('sign', '+') == '-' else 1
if kw.get('microseconds'):
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
kw = {k: float(v) for k, v in six.iteritems(kw) if v is not None}
- return datetime.timedelta(**kw)
+ return sign * datetime.timedelta(**kw)
diff --git a/django/utils/duration.py b/django/utils/duration.py
index c37c885b91..53322b51d2 100644
--- a/django/utils/duration.py
+++ b/django/utils/duration.py
@@ -1,7 +1,7 @@
-"""Version of str(timedelta) which is not English specific."""
+import datetime
-def duration_string(duration):
+def _get_duration_components(duration):
days = duration.days
seconds = duration.seconds
microseconds = duration.microseconds
@@ -12,6 +12,13 @@ def duration_string(duration):
hours = minutes // 60
minutes = minutes % 60
+ return days, hours, minutes, seconds, microseconds
+
+
+def duration_string(duration):
+ """Version of str(timedelta) which is not English specific."""
+ days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
+
string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
if days:
string = '{} '.format(days) + string
@@ -19,3 +26,15 @@ def duration_string(duration):
string += '.{:06d}'.format(microseconds)
return string
+
+
+def duration_iso_string(duration):
+ if duration < datetime.timedelta(0):
+ sign = '-'
+ duration *= -1
+ else:
+ sign = ''
+
+ days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
+ ms = '.{:06d}'.format(microseconds) if microseconds else ""
+ return '{}P{}DT{:02d}H{:02d}M{:02d}{}S'.format(sign, days, hours, minutes, seconds, ms)
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index dc01a824e8..2056e54921 100644
--- a/docs/releases/1.11.txt
+++ b/docs/releases/1.11.txt
@@ -223,6 +223,10 @@ Serialization
can now be customized by passing a ``cls`` keyword argument to the
``serializers.serialize()`` function.
+* :class:`~django.core.serializers.json.DjangoJSONEncoder` now serializes
+ :class:`~datetime.timedelta` objects (used by
+ :class:`~django.db.models.DurationField`).
+
Signals
~~~~~~~
diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt
index c08ea27977..5db5e656ec 100644
--- a/docs/topics/serialization.txt
+++ b/docs/topics/serialization.txt
@@ -297,6 +297,11 @@ The JSON serializer uses ``DjangoJSONEncoder`` for encoding. A subclass of
:class:`~datetime.time`
A string of the form ``HH:MM:ss.sss`` as defined in `ECMA-262`_.
+:class:`~datetime.timedelta`
+ A string representing a duration as defined in ISO-8601. For example,
+ ``timedelta(days=1, hours=2, seconds=3.4)`` is represented as
+ ``'P1DT02H00M03.400000S'``.
+
:class:`~decimal.Decimal`, ``Promise`` (``django.utils.functional.lazy()`` objects), :class:`~uuid.UUID`
A string representation of the object.
@@ -304,6 +309,10 @@ The JSON serializer uses ``DjangoJSONEncoder`` for encoding. A subclass of
Support for ``Promise`` was added.
+.. versionchanged:: 1.11
+
+ Support for :class:`~datetime.timedelta` was added.
+
.. _ecma-262: http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
YAML
diff --git a/tests/serializers/test_json.py b/tests/serializers/test_json.py
index d0c9656643..b191ce6644 100644
--- a/tests/serializers/test_json.py
+++ b/tests/serializers/test_json.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+import datetime
import decimal
import json
import re
@@ -302,3 +303,15 @@ class DjangoJSONEncoderTests(SimpleTestCase):
json.dumps({'lang': ugettext_lazy("French")}, cls=DjangoJSONEncoder),
'{"lang": "Fran\\u00e7ais"}'
)
+
+ def test_timedelta(self):
+ duration = datetime.timedelta(days=1, hours=2, seconds=3)
+ self.assertEqual(
+ json.dumps({'duration': duration}, cls=DjangoJSONEncoder),
+ '{"duration": "P1DT02H00M03S"}'
+ )
+ duration = datetime.timedelta(0)
+ self.assertEqual(
+ json.dumps({'duration': duration}, cls=DjangoJSONEncoder),
+ '{"duration": "P0DT00H00M00S"}'
+ )
diff --git a/tests/utils_tests/test_duration.py b/tests/utils_tests/test_duration.py
index 559d2ef16f..d0564f396f 100644
--- a/tests/utils_tests/test_duration.py
+++ b/tests/utils_tests/test_duration.py
@@ -2,7 +2,7 @@ import datetime
import unittest
from django.utils.dateparse import parse_duration
-from django.utils.duration import duration_string
+from django.utils.duration import duration_iso_string, duration_string
class TestDurationString(unittest.TestCase):
@@ -41,3 +41,41 @@ class TestParseDurationRoundtrip(unittest.TestCase):
def test_negative(self):
duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
self.assertEqual(parse_duration(duration_string(duration)), duration)
+
+
+class TestISODurationString(unittest.TestCase):
+
+ def test_simple(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_iso_string(duration), 'P0DT01H03M05S')
+
+ def test_days(self):
+ duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_iso_string(duration), 'P1DT01H03M05S')
+
+ def test_microseconds(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
+ self.assertEqual(duration_iso_string(duration), 'P0DT01H03M05.012345S')
+
+ def test_negative(self):
+ duration = -1 * datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_iso_string(duration), '-P1DT01H03M05S')
+
+
+class TestParseISODurationRoundtrip(unittest.TestCase):
+
+ def test_simple(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_iso_string(duration)), duration)
+
+ def test_days(self):
+ duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_iso_string(duration)), duration)
+
+ def test_microseconds(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
+ self.assertEqual(parse_duration(duration_iso_string(duration)), duration)
+
+ def test_negative(self):
+ duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_iso_string(duration)).total_seconds(), duration.total_seconds())