Fixed #26656 -- Added duration (timedelta) support to DjangoJSONEncoder.
This commit is contained in:
parent
a7b5dfd170
commit
8ef78b8165
|
@ -16,6 +16,7 @@ from django.core.serializers.python import (
|
||||||
Deserializer as PythonDeserializer, Serializer as PythonSerializer,
|
Deserializer as PythonDeserializer, Serializer as PythonSerializer,
|
||||||
)
|
)
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
from django.utils.duration import duration_iso_string
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.timezone import is_aware
|
from django.utils.timezone import is_aware
|
||||||
|
|
||||||
|
@ -108,6 +109,8 @@ class DjangoJSONEncoder(json.JSONEncoder):
|
||||||
if o.microsecond:
|
if o.microsecond:
|
||||||
r = r[:12]
|
r = r[:12]
|
||||||
return r
|
return r
|
||||||
|
elif isinstance(o, datetime.timedelta):
|
||||||
|
return duration_iso_string(o)
|
||||||
elif isinstance(o, decimal.Decimal):
|
elif isinstance(o, decimal.Decimal):
|
||||||
return str(o)
|
return str(o)
|
||||||
elif isinstance(o, uuid.UUID):
|
elif isinstance(o, uuid.UUID):
|
||||||
|
|
|
@ -40,7 +40,8 @@ standard_duration_re = re.compile(
|
||||||
# Support the sections of ISO 8601 date representation that are accepted by
|
# Support the sections of ISO 8601 date representation that are accepted by
|
||||||
# timedelta
|
# timedelta
|
||||||
iso8601_duration_re = re.compile(
|
iso8601_duration_re = re.compile(
|
||||||
r'^P'
|
r'^(?P<sign>[-+]?)'
|
||||||
|
r'P'
|
||||||
r'(?:(?P<days>\d+(.\d+)?)D)?'
|
r'(?:(?P<days>\d+(.\d+)?)D)?'
|
||||||
r'(?:T'
|
r'(?:T'
|
||||||
r'(?:(?P<hours>\d+(.\d+)?)H)?'
|
r'(?:(?P<hours>\d+(.\d+)?)H)?'
|
||||||
|
@ -121,7 +122,8 @@ def parse_duration(value):
|
||||||
match = iso8601_duration_re.match(value)
|
match = iso8601_duration_re.match(value)
|
||||||
if match:
|
if match:
|
||||||
kw = match.groupdict()
|
kw = match.groupdict()
|
||||||
|
sign = -1 if kw.pop('sign', '+') == '-' else 1
|
||||||
if kw.get('microseconds'):
|
if kw.get('microseconds'):
|
||||||
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
|
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
|
||||||
kw = {k: float(v) for k, v in six.iteritems(kw) if v is not None}
|
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)
|
||||||
|
|
|
@ -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
|
days = duration.days
|
||||||
seconds = duration.seconds
|
seconds = duration.seconds
|
||||||
microseconds = duration.microseconds
|
microseconds = duration.microseconds
|
||||||
|
@ -12,6 +12,13 @@ def duration_string(duration):
|
||||||
hours = minutes // 60
|
hours = minutes // 60
|
||||||
minutes = 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)
|
string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
|
||||||
if days:
|
if days:
|
||||||
string = '{} '.format(days) + string
|
string = '{} '.format(days) + string
|
||||||
|
@ -19,3 +26,15 @@ def duration_string(duration):
|
||||||
string += '.{:06d}'.format(microseconds)
|
string += '.{:06d}'.format(microseconds)
|
||||||
|
|
||||||
return string
|
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)
|
||||||
|
|
|
@ -223,6 +223,10 @@ Serialization
|
||||||
can now be customized by passing a ``cls`` keyword argument to the
|
can now be customized by passing a ``cls`` keyword argument to the
|
||||||
``serializers.serialize()`` function.
|
``serializers.serialize()`` function.
|
||||||
|
|
||||||
|
* :class:`~django.core.serializers.json.DjangoJSONEncoder` now serializes
|
||||||
|
:class:`~datetime.timedelta` objects (used by
|
||||||
|
:class:`~django.db.models.DurationField`).
|
||||||
|
|
||||||
Signals
|
Signals
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -297,6 +297,11 @@ The JSON serializer uses ``DjangoJSONEncoder`` for encoding. A subclass of
|
||||||
:class:`~datetime.time`
|
:class:`~datetime.time`
|
||||||
A string of the form ``HH:MM:ss.sss`` as defined in `ECMA-262`_.
|
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`
|
:class:`~decimal.Decimal`, ``Promise`` (``django.utils.functional.lazy()`` objects), :class:`~uuid.UUID`
|
||||||
A string representation of the object.
|
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.
|
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
|
.. _ecma-262: http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||||
|
|
||||||
YAML
|
YAML
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
@ -302,3 +303,15 @@ class DjangoJSONEncoderTests(SimpleTestCase):
|
||||||
json.dumps({'lang': ugettext_lazy("French")}, cls=DjangoJSONEncoder),
|
json.dumps({'lang': ugettext_lazy("French")}, cls=DjangoJSONEncoder),
|
||||||
'{"lang": "Fran\\u00e7ais"}'
|
'{"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"}'
|
||||||
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.utils.dateparse import parse_duration
|
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):
|
class TestDurationString(unittest.TestCase):
|
||||||
|
@ -41,3 +41,41 @@ class TestParseDurationRoundtrip(unittest.TestCase):
|
||||||
def test_negative(self):
|
def test_negative(self):
|
||||||
duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
|
duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
|
||||||
self.assertEqual(parse_duration(duration_string(duration)), duration)
|
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())
|
||||||
|
|
Loading…
Reference in New Issue