Skip to content

Commit

Permalink
dateutil parser, UTC conversion
Browse files Browse the repository at this point in the history
Convert all datetime, DateTime and time instances to UTC before serializing.
Use python-dateutil instead of DateTime to parse date strings when de-serializing.
  • Loading branch information
thet committed Apr 4, 2018
1 parent c92802a commit d0df851
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 21 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -64,6 +64,12 @@ New Features:

New Features:

- Convert all datetime, DateTime and time instances to UTC before serializing.
[thet]

- Use python-dateutil instead of DateTime to parse date strings when de-serializing.
[thet]

- Allow users to get their own user information.
[erral]

Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -41,6 +41,7 @@
zip_safe=False,
install_requires=[
'setuptools',
'python-dateutil',
'plone.rest >= 1.0a6', # json renderer moved to plone.restapi
'PyJWT',
'pytz',
Expand Down
19 changes: 10 additions & 9 deletions src/plone/restapi/deserializer/dxfields.py
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
from DateTime import DateTime
from DateTime.interfaces import DateTimeError
from datetime import timedelta
from plone.app.textfield.interfaces import IRichText
from plone.app.textfield.value import RichTextValue
Expand All @@ -25,6 +23,8 @@
from zope.schema.interfaces import ITime
from zope.schema.interfaces import ITimedelta

import dateutil


@implementer(IFieldDeserializer)
@adapter(IField, IDexterityContent, IBrowserRequest)
Expand Down Expand Up @@ -86,11 +86,11 @@ def __call__(self, value):
self.field.validate(value)
return

# Parse ISO 8601 string with Zope's DateTime module
# Parse ISO 8601 string with dateutil
try:
dt = DateTime(value).asdatetime()
except (SyntaxError, DateTimeError) as e:
raise ValueError(e.message)
dt = dateutil.parser.parse(value)
except ValueError:
raise ValueError(u'Invalid date: {}'.format(value))

# Convert to TZ aware in UTC
if dt.tzinfo is not None:
Expand Down Expand Up @@ -169,9 +169,10 @@ def __call__(self, value):
# Create an ISO 8601 datetime string and parse it with Zope's
# DateTime module and then convert it to a timezone naive time
# in local time
value = DateTime(u'2000-01-01T' + value).toZone(DateTime(
).localZone()).asdatetime().replace(tzinfo=None).time()
except (SyntaxError, DateTimeError):
# TODO: should really a timezone naive time be returned?
# using ``timetz()`` would be timezone aware.
value = dateutil.parser.parse(value).time()
except ValueError:
raise ValueError(u'Invalid time: {}'.format(value))

self.field.validate(value)
Expand Down
39 changes: 32 additions & 7 deletions src/plone/restapi/serializer/converters.py
@@ -1,24 +1,49 @@
# -*- coding: utf-8 -*-
from datetime import date
from DateTime import DateTime
from datetime import date
from datetime import datetime
from datetime import time
from datetime import timedelta
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
from plone.app.textfield.interfaces import IRichTextValue
from plone.dexterity.interfaces import IDexterityContent
from plone.restapi.interfaces import IJsonCompatible
from plone.restapi.interfaces import IContextawareJsonCompatible
from plone.restapi.interfaces import IJsonCompatible
from Products.CMFPlone.utils import safe_unicode
from zope.component import adapter, queryMultiAdapter
from zope.component import adapter
from zope.component import queryMultiAdapter
from zope.globalrequest import getRequest
from zope.i18n import translate
from zope.i18nmessageid.message import Message
from zope.interface import implementer
from zope.interface import Interface

import Missing
import pytz
# import re


def datetimelike_to_iso(value):
if isinstance(value, DateTime):
value = value.asdatetime()

if getattr(value, 'tzinfo', None):
# timezone aware date/time objects are converted to UTC first.
utc = pytz.timezone('UTC')
value = value.astimezone(utc)
value.replace(microsecond=0) # Remove microsecond part, we don't need it.
iso = value.isoformat()
# if value.tzinfo:

This comment has been minimized.

Copy link
@gotcha

gotcha Sep 16, 2021

Member

@thet Do you remember the reason for keeping this tz code commented instead of not commiting it ?

# # Use "Z" instead of a timezone offset of "+00:00" to indicate UTC.
# regex = None
# if isinstance(value, datetime):
# regex = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
# if isinstance(value, time):
# regex = re.compile(r'\d{2}:\d{2}:\d{2}')
# match = regex.match(iso)
# iso = match.group(0) + 'Z'
return iso


def json_compatible(value, context=None):
Expand Down Expand Up @@ -113,25 +138,25 @@ def persistent_mapping_converter(value):
@adapter(datetime)
@implementer(IJsonCompatible)
def python_datetime_converter(value):
return json_compatible(DateTime(value))
return json_compatible(datetimelike_to_iso(value))


@adapter(DateTime)
@implementer(IJsonCompatible)
def zope_DateTime_converter(value):
return json_compatible(value.ISO8601())
return json_compatible(datetimelike_to_iso(value))


@adapter(date)
@implementer(IJsonCompatible)
def date_converter(value):
return json_compatible(value.isoformat())
return json_compatible(datetimelike_to_iso(value))


@adapter(time)
@implementer(IJsonCompatible)
def time_converter(value):
return json_compatible(value.isoformat())
return json_compatible(datetimelike_to_iso(value))


@adapter(timedelta)
Expand Down
40 changes: 35 additions & 5 deletions src/plone/restapi/testing.py
@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
from Products.CMFCore.utils import getToolByName

# pylint: disable=E1002
# E1002: Use of super on an old style class

from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.i18n.locales.interfaces import IContentLanguages
from plone.app.i18n.locales.interfaces import IMetadataLanguages
Expand All @@ -22,17 +19,20 @@
from plone.restapi.tests.helpers import add_catalog_indexes
from plone.testing import z2
from plone.uuid.interfaces import IUUIDGenerator
from Products.CMFCore.utils import getToolByName
from urlparse import urljoin
from urlparse import urlparse
from zope.component import getGlobalSiteManager
from zope.component import getUtility
from zope.configuration import xmlconfig
from zope.interface import implements
import re

import requests
import collective.MockMailHost
import os
import pkg_resources
import re
import requests
import time


try:
Expand All @@ -54,11 +54,21 @@ def set_available_languages():
getUtility(IMetadataLanguages).setAvailableLanguages(enabled_languages)


def set_timezone(tz='UTC'):
# Set the OS timezone for predictable test results
os.environ['TZ'] = tz
time.tzset()


class PloneRestApiDXLayer(PloneSandboxLayer):

defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)

def setUpZope(self, app, configurationContext):
# Set the OS timezone for predictable test results
self.ostz = os.environ.get('TZ', None)
set_timezone()

import plone.restapi
xmlconfig.file(
'configure.zcml',
Expand Down Expand Up @@ -88,6 +98,10 @@ def setUpPloneSite(self, portal):
states = portal.portal_workflow['simple_publication_workflow'].states
states['published'].title = u'Published with accent é'.encode('utf8')

def tearDownZope(self, app):
if self.ostz:
os.environ['TZ'] = self.ostz


PLONE_RESTAPI_DX_FIXTURE = PloneRestApiDXLayer()
PLONE_RESTAPI_DX_INTEGRATION_TESTING = IntegrationTesting(
Expand All @@ -104,6 +118,10 @@ class PloneRestApiDXPAMLayer(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)

def setUpZope(self, app, configurationContext):
# Set the OS timezone for predictable test results
self.ostz = os.environ.get('TZ', None)
set_timezone()

import plone.restapi
xmlconfig.file(
'configure.zcml',
Expand Down Expand Up @@ -135,6 +153,10 @@ def setUpPloneSite(self, portal):
states = portal.portal_workflow['simple_publication_workflow'].states
states['published'].title = u'Published with accent é'.encode('utf8')

def tearDownZope(self, app):
if self.ostz:
os.environ['TZ'] = self.ostz


PLONE_RESTAPI_DX_PAM_FIXTURE = PloneRestApiDXPAMLayer()
PLONE_RESTAPI_DX_PAM_INTEGRATION_TESTING = IntegrationTesting(
Expand All @@ -152,6 +174,10 @@ class PloneRestApiATLayer(PloneSandboxLayer):
defaultBases = (PLONE_FIXTURE,)

def setUpZope(self, app, configurationContext):
# Set the OS timezone for predictable test results
self.ostz = os.environ.get('TZ', None)
set_timezone()

import Products.ATContentTypes
self.loadZCML(package=Products.ATContentTypes)
import plone.app.dexterity
Expand Down Expand Up @@ -186,6 +212,10 @@ def setUpPloneSite(self, portal):
states = portal.portal_workflow['simple_publication_workflow'].states
states['published'].title = u'Published with accent é'.encode('utf8')

def tearDownZope(self, app):
if self.ostz:
os.environ['TZ'] = self.ostz


PLONE_RESTAPI_AT_FIXTURE = PloneRestApiATLayer()
PLONE_RESTAPI_AT_INTEGRATION_TESTING = IntegrationTesting(
Expand Down

0 comments on commit d0df851

Please sign in to comment.