Browse Source

Fixed #26656 -- Added duration (timedelta) support to DjangoJSONEncoder.

Will Hardy 8 years ago
parent
commit
8ef78b8165

+ 3 - 0
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):

+ 4 - 2
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)

+ 21 - 2
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)

+ 4 - 0
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
 ~~~~~~~
 

+ 9 - 0
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

+ 13 - 0
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"}'
+        )

+ 39 - 1
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())