Skip to content

Commit

Permalink
Fix datetime deserialization for timezone aware fields
Browse files Browse the repository at this point in the history
  • Loading branch information
buchi committed Nov 6, 2017
1 parent 3aaf76e commit f7a7f21
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 6 deletions.
6 changes: 5 additions & 1 deletion CHANGES.rst
Expand Up @@ -4,7 +4,11 @@ Changelog
1.0a23 (unreleased)
-------------------

- Nothing changed yet.
Bugfixes:

- Fix datetime deserialization for timezone aware fields.
This fixes https://github.com/plone/plone.restapi/issues/253
[buchi]


1.0a22 (2017-11-04)
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -43,6 +43,7 @@
'setuptools',
'plone.rest >= 1.0a6', # json renderer moved to plone.restapi
'PyJWT',
'pytz',
],
extras_require={'test': [
'Products.Archetypes',
Expand Down
40 changes: 35 additions & 5 deletions src/plone/restapi/deserializer/dxfields.py
Expand Up @@ -8,18 +8,22 @@
from plone.namedfile.interfaces import INamedField
from plone.restapi.interfaces import IFieldDeserializer
from plone.restapi.services.content.tus import TUSUpload
from pytz import timezone
from pytz import utc
from z3c.form.interfaces import IDataManager
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.component import queryMultiAdapter
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.schema.interfaces import ICollection
from zope.schema.interfaces import IDatetime
from zope.schema.interfaces import IDict
from zope.schema.interfaces import IField
from zope.schema.interfaces import IFromUnicode
from zope.schema.interfaces import ITextLine
from zope.schema.interfaces import ITime
from zope.schema.interfaces import ITimedelta
from zope.schema.interfaces import ITextLine


@implementer(IFieldDeserializer)
Expand Down Expand Up @@ -62,14 +66,40 @@ def __call__(self, value):
class DatetimeFieldDeserializer(DefaultFieldDeserializer):

def __call__(self, value):
# Datetime fields may contain timezone naive or timezone aware
# objects. Unfortunately the zope.schema.Datetime field does not
# contain any information if the field value should be timezone naive
# or timezone aware. While some fields (start, end) store timezone
# aware objects others (effective, expires) store timezone naive
# objects.
# We try to guess the correct deserialization from the current field
# value.
dm = queryMultiAdapter((self.context, self.field), IDataManager)
current = dm.get()
if current is not None:
tzinfo = current.tzinfo
else:
tzinfo = None

# Parse ISO 8601 string with Zope's DateTime module
try:
# Parse ISO 8601 string with Zope's DateTime module
# and convert to a timezone naive datetime in local time
value = DateTime(value).toZone(DateTime().localZone()).asdatetime(
).replace(tzinfo=None)
dt = DateTime(value).asdatetime()
except (SyntaxError, DateTimeError) as e:
raise ValueError(e.message)

# Convert to TZ aware in UTC
if dt.tzinfo is not None:
dt = dt.astimezone(utc)
else:
dt = utc.localize(dt)

# Convert to local TZ aware or naive UTC
if tzinfo is not None:
tz = timezone(tzinfo.zone)
value = tz.normalize(dt.astimezone(tz))
else:
value = utc.normalize(dt.astimezone(utc)).replace(tzinfo=None)

self.field.validate(value)
return value

Expand Down
5 changes: 5 additions & 0 deletions src/plone/restapi/tests/dxtypes.py
Expand Up @@ -11,6 +11,7 @@
from plone.dexterity.content import Item
from plone.namedfile import field as namedfile
from plone.supermodel import model
from pytz import timezone
from z3c.relationfield.schema import RelationChoice
from z3c.relationfield.schema import RelationList
from zope import schema
Expand Down Expand Up @@ -47,6 +48,10 @@ class IDXTestDocumentSchema(model.Schema):
test_choice_field = schema.Choice(values=[u'foo', u'bar'], required=False)
test_date_field = schema.Date(required=False)
test_datetime_field = schema.Datetime(required=False)
test_datetime_tz_field = schema.Datetime(
required=False,
defaultFactory=lambda: timezone("Europe/Zurich").localize(
datetime(2017, 10, 31, 10, 0)))
test_decimal_field = schema.Decimal(required=False)
test_dict_field = schema.Dict(required=False)
test_float_field = schema.Float(required=False)
Expand Down
25 changes: 25 additions & 0 deletions src/plone/restapi/tests/test_dxfield_deserializer.py
Expand Up @@ -9,6 +9,7 @@
from plone.dexterity.utils import iterSchemata
from plone.restapi.interfaces import IFieldDeserializer
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from pytz import timezone
from zope.component import getMultiAdapter
from zope.schema.interfaces import ValidationError

Expand Down Expand Up @@ -86,6 +87,30 @@ def test_datetime_deserialization_handles_timezone(self):
u'2015-12-20T10:39:54.361+01')
self.assertEqual(datetime(2015, 12, 20, 9, 39, 54, 361000), value)

def test_datetime_with_tz_deserialization_keeps_timezone(self):
value = self.deserialize('test_datetime_tz_field',
u'2015-12-20T10:39:54.361+01')
self.assertEqual(timezone("Europe/Zurich").localize(
datetime(2015, 12, 20, 10, 39, 54, 361000)), value)

def test_datetime_with_tz_deserialization_converts_timezone(self):
value = self.deserialize('test_datetime_tz_field',
u'2015-12-20T10:39:54.361-04')
self.assertEqual(timezone("Europe/Zurich").localize(
datetime(2015, 12, 20, 15, 39, 54, 361000)), value)

def test_datetime_with_tz_deserialization_adds_timezone(self):
value = self.deserialize('test_datetime_tz_field',
u'2015-12-20T10:39:54.361')
self.assertEqual(timezone("Europe/Zurich").localize(
datetime(2015, 12, 20, 11, 39, 54, 361000)), value)

def test_datetime_with_tz_deserialization_handles_dst(self):
value = self.deserialize('test_datetime_tz_field',
u'2015-05-20T10:39:54.361+02')
self.assertEqual(timezone("Europe/Zurich").localize(
datetime(2015, 05, 20, 10, 39, 54, 361000)), value)

def test_decimal_deserialization_returns_decimal(self):
value = self.deserialize('test_decimal_field', u'1.1')
self.assertTrue(isinstance(value, Decimal), 'Not a <Decimal>')
Expand Down

0 comments on commit f7a7f21

Please sign in to comment.