-
-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Branch: refs/heads/main Date: 2025-02-12T21:34:28-08:00 Author: Andrea Cecchi (cekk) <[email protected]> Commit: plone/plone.restapi@6e2ab6b Handle timezone in publication fields (effective and expires) (#1192) * Fix deserializer/serializer to handle timezone in IPublication fields * fix code-style * add changelog * Make it work * Fix DateTime patch --------- Co-authored-by: Steve Piercy <[email protected]> Co-authored-by: David Glick <[email protected]> Files changed: A news/1192.bugfix A src/plone/restapi/tests/test_dxfield_publication.py M src/plone/restapi/deserializer/dxfields.py M src/plone/restapi/serializer/configure.zcml M src/plone/restapi/serializer/converters.py M src/plone/restapi/serializer/dxfields.py
- Loading branch information
Showing
1 changed file
with
20 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,25 +2,34 @@ Repository: plone.restapi | |
|
||
|
||
Branch: refs/heads/main | ||
Date: 2025-02-12T21:23:24-08:00 | ||
Author: Guido Stevens (gyst) <[email protected]> | ||
Commit: https://github.com/plone/plone.restapi/commit/90931f4f979cbf95ec8e1b54f026b75dbaf6694e | ||
Date: 2025-02-12T21:34:28-08:00 | ||
Author: Andrea Cecchi (cekk) <[email protected]> | ||
Commit: https://github.com/plone/plone.restapi/commit/6e2ab6b2315a0b942ba6e1943eec9bed6a3f1a9d | ||
|
||
Do not hardcode show_inactive in search (#1880) | ||
Handle timezone in publication fields (effective and expires) (#1192) | ||
|
||
* Do not hardcode show_inactive in search | ||
* Fix deserializer/serializer to handle timezone in IPublication fields | ||
|
||
* Update 1879.bugfix | ||
* fix code-style | ||
|
||
Co-authored-by: Steve Piercy <[email protected]> | ||
* add changelog | ||
|
||
* Make it work | ||
|
||
* Fix DateTime patch | ||
|
||
--------- | ||
|
||
Co-authored-by: Steve Piercy <[email protected]> | ||
Co-authored-by: Steve Piercy <[email protected]> | ||
Co-authored-by: David Glick <[email protected]> | ||
|
||
Files changed: | ||
A news/1879.bugfix | ||
M src/plone/restapi/search/handler.py | ||
A news/1192.bugfix | ||
A src/plone/restapi/tests/test_dxfield_publication.py | ||
M src/plone/restapi/deserializer/dxfields.py | ||
M src/plone/restapi/serializer/configure.zcml | ||
M src/plone/restapi/serializer/converters.py | ||
M src/plone/restapi/serializer/dxfields.py | ||
|
||
b'diff --git a/news/1879.bugfix b/news/1879.bugfix\nnew file mode 100644\nindex 000000000..8acdf1199\n--- /dev/null\n+++ b/news/1879.bugfix\n@@ -0,0 +1 @@\n+Do not hardcode ``show_inactive`` in search; let ``Products.CMFPlone`` handle that. @gyst\ndiff --git a/src/plone/restapi/search/handler.py b/src/plone/restapi/search/handler.py\nindex 22dda01fb..87be9f56e 100644\n--- a/src/plone/restapi/search/handler.py\n+++ b/src/plone/restapi/search/handler.py\n@@ -120,9 +120,6 @@ def filter_query(self, query):\n types = types["query"]\n query["portal_type"] = self.filter_types(types)\n \n- # respect effective/expiration date\n- query["show_inactive"] = False\n-\n # respect navigation root\n if "path" not in query:\n query["path"] = {"query": get_navigation_root(self.context)}\n' | ||
b'diff --git a/news/1192.bugfix b/news/1192.bugfix\nnew file mode 100644\nindex 000000000..52d60e8f5\n--- /dev/null\n+++ b/news/1192.bugfix\n@@ -0,0 +1 @@\n+Save effective and expires date into Plone with right hours (according to current timezone) [cekk]\ndiff --git a/src/plone/restapi/deserializer/dxfields.py b/src/plone/restapi/deserializer/dxfields.py\nindex 534291b87..736a7bfe5 100644\n--- a/src/plone/restapi/deserializer/dxfields.py\n+++ b/src/plone/restapi/deserializer/dxfields.py\n@@ -1,6 +1,7 @@\n from datetime import timedelta\n from decimal import Decimal\n from plone.app.contenttypes.interfaces import ILink\n+from plone.app.dexterity.behaviors.metadata import IPublication\n from plone.app.textfield.interfaces import IRichText\n from plone.app.textfield.value import RichTextValue\n from plone.dexterity.interfaces import IDexterityContent\n@@ -30,6 +31,7 @@\n \n import codecs\n import dateutil\n+\n import html as html_parser\n \n \n@@ -89,21 +91,6 @@ def __call__(self, value):\n @adapter(IDatetime, IDexterityContent, IBrowserRequest)\n class DatetimeFieldDeserializer(DefaultFieldDeserializer):\n def __call__(self, value):\n- # Datetime fields may contain timezone naive or timezone aware\n- # objects. Unfortunately the zope.schema.Datetime field does not\n- # contain any information if the field value should be timezone naive\n- # or timezone aware. While some fields (start, end) store timezone\n- # aware objects others (effective, expires) store timezone naive\n- # objects.\n- # We try to guess the correct deserialization from the current field\n- # value.\n- dm = queryMultiAdapter((self.context, self.field), IDataManager)\n- current = dm.get()\n- if current is not None:\n- tzinfo = current.tzinfo\n- else:\n- tzinfo = None\n-\n # This happens when a \'null\' is posted for a non-required field.\n if value is None:\n self.field.validate(value)\n@@ -121,12 +108,29 @@ def __call__(self, value):\n else:\n dt = utc.localize(dt)\n \n- # Convert to local TZ aware or naive UTC\n- if tzinfo is not None:\n- tz = timezone(tzinfo.zone)\n- value = tz.normalize(dt.astimezone(tz))\n+ # Datetime fields may contain timezone naive or timezone aware\n+ # objects. Unfortunately the zope.schema.Datetime field does not\n+ # contain any information if the field value should be timezone naive\n+ # or timezone aware. While some fields (start, end) store timezone\n+ # aware objects others (effective, expires) store timezone naive\n+ # objects.\n+ # We try to guess the correct deserialization from the current field\n+ # value.\n+ if self.field.interface == IPublication:\n+ # The IPublication adapter is a special case that expects\n+ # a timezone-naive local datetime\n+ value = dt.astimezone().replace(tzinfo=None)\n else:\n- value = utc.normalize(dt.astimezone(utc)).replace(tzinfo=None)\n+ # Otherwise let\'s check what is currently stored.\n+ dm = queryMultiAdapter((self.context, self.field), IDataManager)\n+ current = dm.get()\n+ if current is not None:\n+ # Timezone-aware. Convert to the same timezone.\n+ tz = timezone(current.tzinfo.zone)\n+ value = tz.normalize(dt.astimezone(tz))\n+ else:\n+ # Timezone-naive. Convert to UTC and remove the tzinfo.\n+ value = utc.normalize(dt.astimezone(utc)).replace(tzinfo=None)\n \n self.field.validate(value)\n return value\ndiff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml\nindex 32c63b2d7..271da018a 100644\n--- a/src/plone/restapi/serializer/configure.zcml\n+++ b/src/plone/restapi/serializer/configure.zcml\n@@ -27,6 +27,7 @@\n <adapter factory=".dxfields.DefaultPrimaryFieldTarget" />\n <adapter factory=".dxfields.PrimaryFileFieldTarget" />\n <adapter factory=".dxfields.TextLineFieldSerializer" />\n+ <adapter factory=".dxfields.DateTimeFieldSerializer" />\n \n <adapter factory=".blocks.BlocksJSONFieldSerializer" />\n <subscriber\ndiff --git a/src/plone/restapi/serializer/converters.py b/src/plone/restapi/serializer/converters.py\nindex 6da75b754..256418dac 100644\n--- a/src/plone/restapi/serializer/converters.py\n+++ b/src/plone/restapi/serializer/converters.py\n@@ -28,7 +28,10 @@\n \n def datetimelike_to_iso(value):\n if isinstance(value, DateTime):\n- value = value.asdatetime()\n+ if value.timezoneNaive():\n+ value = value.asdatetime()\n+ else:\n+ value = pytz.timezone("UTC").localize(value.utcdatetime())\n \n if getattr(value, "tzinfo", None):\n # timezone aware date/time objects are converted to UTC first.\ndiff --git a/src/plone/restapi/serializer/dxfields.py b/src/plone/restapi/serializer/dxfields.py\nindex c63c7fe80..9966cb882 100644\n--- a/src/plone/restapi/serializer/dxfields.py\n+++ b/src/plone/restapi/serializer/dxfields.py\n@@ -1,6 +1,7 @@\n from AccessControl import getSecurityManager\n from plone.app.contenttypes.interfaces import ILink\n from plone.app.contenttypes.utils import replace_link_variables_by_paths\n+from plone.app.dexterity.behaviors.metadata import IPublication\n from plone.app.textfield.interfaces import IRichText\n from plone.dexterity.interfaces import IDexterityContent\n from plone.namedfile.interfaces import INamedFileField\n@@ -18,6 +19,7 @@\n from zope.interface import Interface\n from zope.schema.interfaces import IChoice\n from zope.schema.interfaces import ICollection\n+from zope.schema.interfaces import IDatetime\n from zope.schema.interfaces import IField\n from zope.schema.interfaces import ITextLine\n from zope.schema.interfaces import IVocabularyTokenized\n@@ -206,3 +208,17 @@ def __call__(self):\n return "/".join(\n (self.context.absolute_url(), "@@download", self.field.__name__)\n )\n+\n+\n+@adapter(IDatetime, IDexterityContent, Interface)\n+@implementer(IFieldSerializer)\n+class DateTimeFieldSerializer(DefaultFieldSerializer):\n+ def get_value(self, default=None):\n+ value = super(DateTimeFieldSerializer, self).get_value(default=default)\n+ if value and self.field.interface == IPublication:\n+ # We want the dates with full tz infos\n+ # default value is taken from\n+ # plone.app.dexterity.behaviors.metadata.Publication that escape\n+ # timezone\n+ return getattr(self.context, self.field.__name__)()\n+ return value\ndiff --git a/src/plone/restapi/tests/test_dxfield_publication.py b/src/plone/restapi/tests/test_dxfield_publication.py\nnew file mode 100644\nindex 000000000..01bd864c2\n--- /dev/null\n+++ b/src/plone/restapi/tests/test_dxfield_publication.py\n@@ -0,0 +1,86 @@\n+# -*- coding: utf-8 -*-\n+from DateTime import DateTime\n+from plone.registry.interfaces import IRegistry\n+from plone.restapi.interfaces import IDeserializeFromJson\n+from plone.restapi.interfaces import ISerializeToJson\n+from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING\n+from transaction import commit\n+from zope.component import getMultiAdapter\n+from zope.component import getUtility\n+\n+import unittest\n+import os\n+import time\n+\n+\n+class TestPublicationFields(unittest.TestCase):\n+\n+ layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING\n+\n+ def setUp(self):\n+\n+ self.portal = self.layer["portal"]\n+ self.request = self.layer["request"]\n+\n+ tz = "Europe/Rome"\n+ os.environ["TZ"] = tz\n+ time.tzset()\n+\n+ # Patch DateTime\'s timezone for deterministic behavior.\n+ self.DT_orig_localZone = DateTime.localZone\n+ self.DT_orig_calcTimezoneName = DateTime._calcTimezoneName\n+ DateTime.localZone = lambda cls=None, ltm=None: tz\n+ DateTime._calcTimezoneName = lambda self, x, ms: tz\n+\n+ from plone.dexterity import content\n+\n+ content.FLOOR_DATE = DateTime(1970, 0)\n+ content.CEILING_DATE = DateTime(2500, 0)\n+ self._orig_content_zone = content._zone\n+ content._zone = "GMT+2"\n+\n+ registry = getUtility(IRegistry)\n+ registry["plone.portal_timezone"] = tz\n+ registry["plone.available_timezones"] = [tz]\n+\n+ self.app = self.layer["app"]\n+ self.portal = self.layer["portal"]\n+\n+ commit()\n+\n+ def tearDown(self):\n+ os.environ["TZ"] = "UTC"\n+ time.tzset()\n+\n+ from DateTime import DateTime\n+\n+ DateTime.localZone = self.DT_orig_localZone\n+ DateTime._calcTimezoneName = self.DT_orig_calcTimezoneName\n+\n+ from plone.dexterity import content\n+\n+ content._zone = self._orig_content_zone\n+ content.FLOOR_DATE = DateTime(1970, 0)\n+ content.CEILING_DATE = DateTime(2500, 0)\n+\n+ registry = getUtility(IRegistry)\n+ registry["plone.portal_timezone"] = "UTC"\n+ registry["plone.available_timezones"] = ["UTC"]\n+\n+ def test_effective_date_deserialization_localized(self):\n+ self.portal.invokeFactory("Document", id="doc-test", title="Test Document")\n+ doc = self.portal["doc-test"]\n+ deserializer = getMultiAdapter(\n+ (self.portal["doc-test"], self.request), IDeserializeFromJson\n+ )\n+ deserializer(data={"effective": "2015-05-20T10:39:54.361+00"})\n+ self.assertEqual(str(doc.effective_date), "2015/05/20 12:39:00 Europe/Rome")\n+\n+ def test_effective_date_serialization_localized(self):\n+ self.portal.invokeFactory("Document", id="doc-test", title="Test Document")\n+ doc = self.portal["doc-test"]\n+ doc.effective_date = DateTime("2015/05/20 12:39:00 Europe/Rome")\n+\n+ serializer = getMultiAdapter((doc, self.request), ISerializeToJson)\n+ data = serializer()\n+ self.assertEqual(data["effective"], "2015-05-20T10:39:00+00:00")\n' | ||
|