Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the serialization of the datetime objects, and store them as o… #394

Closed
wants to merge 13 commits into from

Conversation

sneridagh
Copy link
Member

…ffset-aware UTC-based objects

@sneridagh
Copy link
Member Author

This fixes #253

@csenger
Copy link
Contributor

csenger commented Aug 11, 2017

Looks good to me.

@@ -46,8 +46,7 @@ def __call__(self, value):
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)
value = DateTime(value).toZone('UTC').asdatetime()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tisto @buchi @lukasgraf

I just tried a new approach and change the code to store the datetime objects as datetime offset-aware and referenced always to UTC. It felt bad to convert it to the local time of the server each time, and it caused a bad offset when the API waas used by people in other timezones than the server's one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is perfect, It should always be in an isoformat with UTC timezone.

@@ -24,6 +24,10 @@ Python JSON
``DateTime('2015/11/23 19:45:55')`` ``'2015-11-23T19:45:55'``
======================================= ======================================

.. warning::
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing to keep in mind by using this approach is when used in combination with zope.schema date validators. If that's the case, you should also use a datetime object that also contains offset-aware object as the validator value.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 96.004% when pulling d6f3edd on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 96.004% when pulling f7c4dec on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 96.004% when pulling 776ff65 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.004%) to 96.438% when pulling 8ee4b32 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

1 similar comment
@coveralls
Copy link

coveralls commented Aug 24, 2017

Coverage Status

Coverage increased (+0.004%) to 96.438% when pulling 8ee4b32 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

coveralls commented Aug 24, 2017

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling d578263 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@sneridagh sneridagh requested a review from csenger August 25, 2017 09:10
@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 4dd3ff0 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

2 similar comments
@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 4dd3ff0 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 4dd3ff0 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@tisto
Copy link
Sponsor Member

tisto commented Aug 29, 2017

@thet any chance you have time to have a look at this?

@sneridagh sneridagh requested a review from loechel August 29, 2017 08:33
Copy link
Member

@loechel loechel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue with doing it on try except base, I suggested an alternative that should be more explicite and solves it too.

@@ -46,8 +46,7 @@ def __call__(self, value):
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)
value = DateTime(value).toZone('UTC').asdatetime()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is perfect, It should always be in an isoformat with UTC timezone.

if value != dm.get():
dm.set(value)
modified = True
except TypeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, but I did not like the usage of an Error Handler to apply a value

I would suggest something like:

dm_value = dm.get()
if value.tzinfo is None:
    value = value.replace(tzinfo=pytz.UTC)
else:
     value = value.asdatetime(pytz.UTC)
 if dm_value.tzinfo is None:
    dm_value = dm_value.replace(tzinfo=pytz.UTC)
else:
     dm_value = dm_value.asdatetime(pytz.UTC)

if value != dm_value:
    dm.set(value)
    modified = True

instead of the try except clause.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but you have to detect first if dm.get() returns a datetime right? if not, value.tzinfo will fail. I wanted to prevent to infer more conditional clauses here, which in turn, increases the complexity ratio of the method (and then plone.recipe.codeanalysis complains about it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are right, there needs to be an if isinstance(value, datetime) and isinstance(dm_value, datetime): that wraps both if else clause.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the complexity of an aditional if level is better than an try except. It could also be an idea to split this test aus into a separate function.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sneridagh @loechel For me the main objective of this code snippet is readability. I don't mind a try/except if it makes the code more readable. Though, without a few more comments I don't really understand what goes on here anyways. :)

@coveralls
Copy link

coveralls commented Aug 30, 2017

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 7f5dd84 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 7f5dd84 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

2 similar comments
@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 7f5dd84 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.007%) to 96.441% when pulling 7f5dd84 on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@sneridagh
Copy link
Member Author

@loechel Please check again the current implementation. It takes into account your comments.


and builds the `start` and `end` fields with the proper timezone, depending on the `timezone` field. It also returns the datetime object with the proper timezone offset.

If using Plone 4 and p.a.event 1.x you should construct the Event type using this approach, otherwise the Event object will be created with a wrong timezones.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: -> "with a wrong timezone."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if value.tzinfo is None:
value = value.replace(tzinfo=pytz.UTC)
if dm_value.tzinfo is None:
dm_value = dm_value.replace(tzinfo=pytz.UTC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to localize the datetime values to the portal's timezone?

Something like:

from plone.app.event.base import default_timezone
dtz = default_timezone(self.context, as_tzinfo=True)
dtz.localize(value)

Or if you have to avoid a dependency on plone.app.event here:

value = DateTime(
                value.year(),
                value.month(),
                value.day(),
                value.hour(),
                value.minute()
).asdatetime()

That uses the default timezone of DateTime, which is the servers timezone (which should actually be set to UTC but often is set in the local zone).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would always go for UTC as convention.

value = DateTime(value).toZone(DateTime().localZone()).asdatetime(
).replace(tzinfo=None)
# and convert to a UTC based timezone as datetime
value = DateTime(value).toZone('UTC').asdatetime()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again like above, I wouldn't use 'UTC' but the portal's default timezone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the concept of having a "portal" timezone is wrong. In which case is p.a.event using it? It's ok if you always have users in the same timezone, but this is hardly the case in most user cases.

All datetimes should be referenced using a common baseline. Then the UI convert it to the user's timezone. If not, a user in India will see always the dates offsetted with the portal timezone, which is pretty useless for him. This is what happened before and that's the reason of this change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thet I thought you agree on that too when you said: #253 (comment)

Copy link
Member

@thet thet Aug 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, true. I was suggesting that because I thought you do not have a timezone information in the JSON object available - besides the offset.
But if you have timezone-naive datetimes (UTC is not timezone-naive), like in the code above - and you convert those to UTC, then you make a decision which could be wrong. Unless you always exchange dates in UTC per convention.

Fact is, plone.app.event 2.x does not convert it's datetimes to UTC but stores them in the default timezone - which is:

  1. The event's timezone (not implemented yet)
  2. The user's timezone (probably most often not set)
  3. The portal's timezone (You have to set this on portal creation time)

I agree that users from anywhere want to have the option to show the datetimes in their local zone. But that's a UI thing and should be done via JS with the option to show the datetimes in the original zone.

So: converting to UTC and storing in UTC is OK, if you have no timezone identifier (offsets are a bit useless) and exchange in UTC anyways.
IMO it's not OK if the datetimes are "naive" - without any timezone information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sneridagh If you want, we can have a short hangout if you think I can help here...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thet sure, whenever you are available, we can have a talk about it. We should find a common agreement on this.

Agreed that the implementation makes an assumption that p.a.event is not (yet) doing but IMHO should do and we should fix it upstream in Plone. That is, save the internal dates with timezone information under a common frame of reference (say UTC).

Clients (and existing UI) should provide always datetimes depending on the user's timezone (we are already doing that in a client's project all Angular UI. Current libraries always transform to UTC on serializing and it's easy to be user's timezone aware.

Setting a portal default timezone (or even a user timezone) it's useless and error prone. The current implementation now it's broken, as you can see, the provided tests are not passing using the current version of plone.restapi.

The more sensible solution (as agreed in #253 seems to store always the timezone information) but then, use a common reference for that (using server's timezone will also be error prone and dates could not be the same if you migrate one server from timezone).

@tisto @lukasgraf @buchi any insights on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say, if the convention in plone.restapi is to always export dates as UTC that is an convention. If other modules for other export formats uses another convetion that might not be consistent, but possible.

I would prefer to always export datetime objects as timezone aware objects, and that transformed to UTC, but also to always import all native datetime input as new timezone aware datetime objects. Presentation in the UI should be bound to user settings.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling ba84c0f on fix-253-add-time-zone-to-serializer into 5b4435e on master.

2 similar comments
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling ba84c0f on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling ba84c0f on fix-253-add-time-zone-to-serializer into 5b4435e on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling 7169bee on fix-253-add-time-zone-to-serializer into 5b4435e on master.

1 similar comment
@coveralls
Copy link

coveralls commented Aug 30, 2017

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling 7169bee on fix-253-add-time-zone-to-serializer into 5b4435e on master.

Copy link
Member

@loechel loechel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still have a few typos found, and one potential critical case on init.py --> if an pkg_resources.DistributionNotFound Error occure following import errors will occure as then HAS_PLONE_APP_EVENT_20 is undefined.

But overall it now looks very good.

Deserialization
===============

It's worth to note an special case when deserializing Datetimes objects, and how plone.restapi will handle them.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still a typo: "to note an special" --> to note a special"


It's worth to note an special case when deserializing Datetimes objects, and how plone.restapi will handle them.

Although not supported by Plone itself yet, plone.restapi will store all the Datetimes that will be handling along with its timezone converted to UTC.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"all the Datetimes" --> "datetime objects"


If using Plone 4 and p.a.event 1.x you should construct the Event type using this approach, otherwise the Event object will be created with a wrong timezone.

This approach was counterintuitive, and thus, it was changed it Plone 5 version of p.a.event.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better: it was changed for Plone 5 by p.a.event version 2

p.a.event 2.x in Plone 5
------------------------

The implementation of p.a.event in 2.x no longer requires to provide a `timezone` schema property, because the timezone is computed taking the timezone already existent in dates supplied::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"p.a.event in 2.x no longer" --> p.a.event 2.x no longer"

HAS_PLONE_APP_EVENT_20 = True
else:
HAS_PLONE_APP_EVENT_20 = False

except pkg_resources.DistributionNotFound: # pragma: no cover
HAS_PLONE_APP_CONTENTTYPES = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't in the except case also HAS_PLONE_APP_EVENT_20 = False be added, as else you do not have that value later on.

if value.tzinfo is None:
value = value.replace(tzinfo=pytz.UTC)
if dm_value.tzinfo is None:
dm_value = dm_value.replace(tzinfo=pytz.UTC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would always go for UTC as convention.

if value.tzinfo is None:
value = value.replace(tzinfo=pytz.UTC)
if dm_value.tzinfo is None:
dm_value = dm_value.replace(tzinfo=pytz.UTC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch that the second case in my proposal is unnessesary, two datetime objects that have the same time in utc are equal even if the contain different timezone infos.

@coveralls
Copy link

coveralls commented Sep 5, 2017

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling 66d3e22 on fix-253-add-time-zone-to-serializer into 5c34f4d on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.01%) to 96.42% when pulling 66d3e22 on fix-253-add-time-zone-to-serializer into 5c34f4d on master.

@loechel
Copy link
Member

loechel commented Oct 3, 2017

@tisto could you please make a last check and merge, and also close #396

Thanks

@coveralls
Copy link

coveralls commented Oct 3, 2017

Coverage Status

Coverage decreased (-0.1%) to 96.436% when pulling 00e5871 on fix-253-add-time-zone-to-serializer into 88a6779 on master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.1%) to 96.436% when pulling 00e5871 on fix-253-add-time-zone-to-serializer into 88a6779 on master.

@buchi
Copy link
Member

buchi commented Oct 22, 2017

This doesn't work for me. I'v done the following test with Plone 5.0 with p.a.event 2.0:

Create an event TTW with:

start: 2017-10-01T10:00
end: 2017-10-31T10:00
effective: 2017-10-02T10:00
expires: 2017-10-30T10:00

Event information shown in event view:

01.10.2017 10:00 to 31.10.2017 10:00 (Europe/Berlin / UTC200)
Effective: 2 Oktober, 2017 10:00
Expires: 30 Oktober, 2017, 10:00

Create the "same" event with REST API:

{
    "@type": "Event",
    "title": "A REST API event",
    "effective": "2017-10-02T10:00:00",
    "end": "2017-10-31T10:00:00+01:00",
    "expires": "2017-10-30T10:00:00",
    "start": "2017-10-01T10:00:00+02:00"
}

Event information shown in event view:

01.10.2017 08:00 to 31.10.2017 09:00 (UTC / UTC0)
Effective: 2 Oktober, 2017 10:00
Expires: 30 Oktober, 2017, 10:00

Both events represent the same dates but timezone information is wrong when the event is created using the REST API.

Editing the REST API event TTW by adding e description results in:

01.10.2017 08:00 to 31.10.2017 09:00 (Europe/Berlin / UTC200)
Effective: 2 Oktober, 2017 10:00
Expires: 30 Oktober, 2017, 10:00

Now the timezone information gets added, but no timezone conversion occurs. Thus the dates are now wrong. Even worse, they get modified though the user didn't change them.

@tisto tisto added this to the 1.0b1 milestone Nov 4, 2017
@sneridagh
Copy link
Member Author

This is superseeded by #415 , closing.

@sneridagh sneridagh closed this Nov 7, 2017
@jensens jensens deleted the fix-253-add-time-zone-to-serializer branch April 7, 2020 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants