-
-
Notifications
You must be signed in to change notification settings - Fork 73
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
Conversation
…ffset-aware UTC-based objects
This fixes #253 |
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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.
docs/source/serialization.rst
Outdated
@@ -24,6 +24,10 @@ Python JSON | |||
``DateTime('2015/11/23 19:45:55')`` ``'2015-11-23T19:45:55'`` | |||
======================================= ====================================== | |||
|
|||
.. warning:: |
There was a problem hiding this comment.
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.
1 similar comment
2 similar comments
@thet any chance you have time to have a look at this? |
There was a problem hiding this 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() |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. :)
2 similar comments
@loechel Please check again the current implementation. It takes into account your comments. |
docs/source/deserialization.rst
Outdated
|
||
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. |
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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:
- The event's timezone (not implemented yet)
- The user's timezone (probably most often not set)
- 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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
2 similar comments
1 similar comment
There was a problem hiding this 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.
docs/source/deserialization.rst
Outdated
Deserialization | ||
=============== | ||
|
||
It's worth to note an special case when deserializing Datetimes objects, and how plone.restapi will handle them. |
There was a problem hiding this comment.
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"
docs/source/deserialization.rst
Outdated
|
||
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. |
There was a problem hiding this comment.
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"
docs/source/deserialization.rst
Outdated
|
||
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. |
There was a problem hiding this comment.
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
docs/source/deserialization.rst
Outdated
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:: |
There was a problem hiding this comment.
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"
src/plone/restapi/__init__.py
Outdated
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
1 similar comment
1 similar comment
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:
Event information shown in event view:
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:
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:
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. |
This is superseeded by #415 , closing. |
…ffset-aware UTC-based objects