Skip to content

Commit

Permalink
[fc] Repository: plone.restapi
Browse files Browse the repository at this point in the history
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 &lt;[email protected]&gt;
Co-authored-by: David Glick &lt;[email protected]&gt;

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
davisagli committed Feb 13, 2025
1 parent 2a85db4 commit 3663fa6
Showing 1 changed file with 20 additions and 11 deletions.
31 changes: 20 additions & 11 deletions last_commit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 &lt;[email protected]&gt;
* add changelog

* Make it work

* Fix DateTime patch

---------

Co-authored-by: Steve Piercy &lt;[email protected]&gt;
Co-authored-by: Steve Piercy &lt;[email protected]&gt;
Co-authored-by: David Glick &lt;[email protected]&gt;

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'

0 comments on commit 3663fa6

Please sign in to comment.