diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc4f5c2..37dbaa7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,12 +12,12 @@ jobs: max-parallel: 4 matrix: python: ["3.8", "3.9", "3.10", "3.11"] - plone: ["52", "60"] - exclude: - - python: "3.10" - plone: "52" - - python: "3.11" - plone: "52" + plone: ["60"] + # exclude: + # - python: "3.10" + # plone: "52" + # - python: "3.11" + # plone: "52" steps: - uses: actions/checkout@v3 - name: Cache eggs diff --git a/CHANGES.rst b/CHANGES.rst index 79f7903..7733574 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,8 @@ Changelog 1.3.3 (unreleased) ------------------ -- Nothing changed yet. +- Fix serializer/deserializer for footerTop blocks: use blocks handlers to fix data. + [cekk] 1.3.2 (2024-03-14) diff --git a/base.cfg b/base.cfg index e6ac592..e11e89f 100644 --- a/base.cfg +++ b/base.cfg @@ -50,7 +50,7 @@ recipe = plone.recipe.codeanalysis directory = ${buildout:directory}/src flake8-exclude=bootstrap.py,bootstrap-buildout.py,docs,bin,*.egg,setup.py,overrides,omelette flake8-max-complexity = 25 -flake8-ignore = E203, E266, E501, W503, E999 +flake8-ignore = E203, E266, E501, W503, E999, C101 flake8-max-line-length = 200 # flake8-select = B,C,E,F,W,T4,B9 flake8-extensions = diff --git a/setup.cfg b/setup.cfg index bd9250c..7f3f5c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,13 +6,8 @@ ignore = .gitattributes [isort] -# for details see -# http://docs.plone.org/develop/styleguide/python.html#grouping-and-sorting -force_alphabetical_sort = True -force_single_line = True -lines_after_imports = 2 -line_length = 200 -not_skip = __init__.py +# black compatible isort rules: +profile = plone [flake8] # black compatible flake8 rules: @@ -22,10 +17,11 @@ ignore = E501 T001 C813 + C101 # E203, E266 exclude = bootstrap.py,docs,*.egg.,omelette max-line-length = 88 max-complexity = 18 select = B,C,E,F,W,T4,B9 -builtins = unicode,basestring +builtins = unicode,basestring diff --git a/setup.py b/setup.py index e255fe7..6b0bedb 100644 --- a/setup.py +++ b/setup.py @@ -52,8 +52,7 @@ install_requires=[ "setuptools", "plone.api >= 1.8.4", - "plone.restapi", - "plone.app.dexterity", + "plone.restapi>=9.5.0", "plone.volto", ], extras_require={ diff --git a/src/redturtle/voltoplugin/editablefooter/browser/controlpanel.py b/src/redturtle/voltoplugin/editablefooter/browser/controlpanel.py index 7e886a3..d43bff6 100644 --- a/src/redturtle/voltoplugin/editablefooter/browser/controlpanel.py +++ b/src/redturtle/voltoplugin/editablefooter/browser/controlpanel.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- from plone.app.registry.browser import controlpanel -from redturtle.voltoplugin.editablefooter.interfaces import ( - IEditableFooterSettings, -) from redturtle.voltoplugin.editablefooter import _ +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings class EditableFooterForm(controlpanel.RegistryEditForm): diff --git a/src/redturtle/voltoplugin/editablefooter/restapi/__init__.py b/src/redturtle/voltoplugin/editablefooter/restapi/__init__.py index e69de29..35449c7 100644 --- a/src/redturtle/voltoplugin/editablefooter/restapi/__init__.py +++ b/src/redturtle/voltoplugin/editablefooter/restapi/__init__.py @@ -0,0 +1,14 @@ +from plone.restapi.blocks import iter_block_transform_handlers +from plone.restapi.blocks import visit_blocks + + +def fix_footer_top_blocks(context, blocks, transformer): + if not blocks: + return blocks + for block in visit_blocks(context, blocks): + new_block = block.copy() + for handler in iter_block_transform_handlers(context, block, transformer): + new_block = handler(new_block) + block.clear() + block.update(new_block) + return blocks diff --git a/src/redturtle/voltoplugin/editablefooter/restapi/controlpanel.py b/src/redturtle/voltoplugin/editablefooter/restapi/controlpanel.py index 97bd516..b18a6a8 100644 --- a/src/redturtle/voltoplugin/editablefooter/restapi/controlpanel.py +++ b/src/redturtle/voltoplugin/editablefooter/restapi/controlpanel.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from plone.restapi.controlpanels import RegistryConfigletPanel +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings from redturtle.voltoplugin.editablefooter.interfaces import ( IRedturtleVoltoEditablefooterLayer, - IEditableFooterSettings, ) from zope.component import adapter from zope.interface import implementer diff --git a/src/redturtle/voltoplugin/editablefooter/restapi/deserializer.py b/src/redturtle/voltoplugin/editablefooter/restapi/deserializer.py index 48ee160..92aa3c2 100644 --- a/src/redturtle/voltoplugin/editablefooter/restapi/deserializer.py +++ b/src/redturtle/voltoplugin/editablefooter/restapi/deserializer.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- +from plone import api from plone.restapi.deserializer import json_body -from plone.restapi.deserializer.controlpanels import ( - ControlpanelDeserializeFromJson, -) +from plone.restapi.deserializer.controlpanels import ControlpanelDeserializeFromJson +from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson -from redturtle.voltoplugin.editablefooter.interfaces import ( - IEditableFooterSettings, -) +from redturtle.voltoplugin.editablefooter import _ +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings +from redturtle.voltoplugin.editablefooter.restapi import fix_footer_top_blocks from zExceptions import BadRequest from zope.component import adapter from zope.interface import implementer @@ -21,10 +21,39 @@ def __call__(self): req = json_body(self.controlpanel.request) proxy = self.registry.forInterface(self.schema, prefix=self.schema_prefix) errors = [] - data = req.get("footer_columns", {}) + data = req.get("footer_columns", []) if not data: - errors.append({"message": "Missing data", "field": "footer_columns"}) + errors.append( + { + "message": api.portal.translate( + _("missing_data_label", default="Missing data") + ), + "field": "footer_columns", + } + ) raise BadRequest(errors) + if not isinstance(data, list): + errors.append( + { + "message": api.portal.translate( + _( + "wrong_type_data_label", + default="Wrong type: need to be a list of values", + ) + ), + "field": "footer_columns", + } + ) + raise BadRequest(errors) + for path_setting in data: + footer_top = path_setting.get("footerTop", {}).get("blocks", {}) + if footer_top: + path_setting["footerTop"]["blocks"] = fix_footer_top_blocks( + context=self.context, + blocks=footer_top, + transformer=IBlockFieldDeserializationTransformer, + ) + try: # later we need to do some validations setattr(proxy, "footer_columns", json.dumps(data)) diff --git a/src/redturtle/voltoplugin/editablefooter/restapi/get.py b/src/redturtle/voltoplugin/editablefooter/restapi/get.py index 097a635..b0ec14f 100644 --- a/src/redturtle/voltoplugin/editablefooter/restapi/get.py +++ b/src/redturtle/voltoplugin/editablefooter/restapi/get.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- -from redturtle.voltoplugin.editablefooter.interfaces import ( - IEditableFooterSettings, -) from plone import api from plone.registry.interfaces import IRegistry +from plone.restapi.interfaces import IBlockFieldSerializationTransformer +from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings +from redturtle.voltoplugin.editablefooter.restapi import fix_footer_top_blocks from zope.component import getUtility from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse + try: from plone.volto.interfaces import IVoltoSettings @@ -21,9 +23,6 @@ @implementer(IPublishTraverse) class FooterColumns(Service): - def __init__(self, context, request): - super(FooterColumns, self).__init__(context, request) - def reply(self): record = api.portal.get_registry_record( "footer_columns", interface=IEditableFooterSettings, default="" @@ -34,6 +33,14 @@ def reply(self): portal_url = self.get_portal_url() for el in data or []: if isinstance(el, dict): + footer_top = el.get("footerTop", {}).get("blocks", {}) + if footer_top: + el["footerTop"]["blocks"] = fix_footer_top_blocks( + context=self.context, + blocks=footer_top, + transformer=IBlockFieldSerializationTransformer, + ) + for item in el.get("items") or []: if ( isinstance(item, dict) @@ -44,7 +51,7 @@ def reply(self): item["text"]["data"] = item["text"]["data"].replace( 'href="/', f'href="{portal_url}/' ) - return data + return json_compatible(data) def get_portal_url(self): portal_url = api.portal.get().absolute_url() diff --git a/src/redturtle/voltoplugin/editablefooter/restapi/serializer.py b/src/redturtle/voltoplugin/editablefooter/restapi/serializer.py index 0428c16..d0a2149 100644 --- a/src/redturtle/voltoplugin/editablefooter/restapi/serializer.py +++ b/src/redturtle/voltoplugin/editablefooter/restapi/serializer.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -from redturtle.voltoplugin.editablefooter.interfaces import ( - IEditableFooterSettings, -) +from plone import api +from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.serializer.controlpanels import ControlpanelSerializeToJson +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings +from redturtle.voltoplugin.editablefooter.restapi import fix_footer_top_blocks from zope.component import adapter from zope.interface import implementer @@ -14,8 +15,20 @@ @adapter(IEditableFooterSettings) class EditableFooterControlpanelSerializeToJson(ControlpanelSerializeToJson): def __call__(self): - json_data = super(EditableFooterControlpanelSerializeToJson, self).__call__() + json_data = super().__call__() conf = json_data["data"].get("footer_columns", "") - if conf: - json_data["data"]["footer_columns"] = json.loads(conf) + if not conf: + return json_data + footer_columns = json.loads(conf) + + for path_setting in footer_columns: + footer_top = path_setting.get("footerTop", {}).get("blocks", {}) + if footer_top: + path_setting["footerTop"]["blocks"] = fix_footer_top_blocks( + context=api.portal.get(), + blocks=footer_top, + transformer=IBlockFieldSerializationTransformer, + ) + + json_data["data"]["footer_columns"] = footer_columns return json_data diff --git a/src/redturtle/voltoplugin/editablefooter/testing.py b/src/redturtle/voltoplugin/editablefooter/testing.py index 2e74cee..592619e 100644 --- a/src/redturtle/voltoplugin/editablefooter/testing.py +++ b/src/redturtle/voltoplugin/editablefooter/testing.py @@ -7,9 +7,9 @@ from plone.restapi.testing import PloneRestApiDXLayer from plone.testing import z2 -import redturtle.voltoplugin.editablefooter import plone.restapi import plone.volto +import redturtle.voltoplugin.editablefooter class VoltoEditableFooterLayer(PloneSandboxLayer): diff --git a/src/redturtle/voltoplugin/editablefooter/tests/test_controlpanel_api.py b/src/redturtle/voltoplugin/editablefooter/tests/test_controlpanel_api.py index d3ca613..ec3b725 100644 --- a/src/redturtle/voltoplugin/editablefooter/tests/test_controlpanel_api.py +++ b/src/redturtle/voltoplugin/editablefooter/tests/test_controlpanel_api.py @@ -5,12 +5,10 @@ from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID from plone.restapi.testing import RelativeSession +from redturtle.voltoplugin.editablefooter.interfaces import IEditableFooterSettings from redturtle.voltoplugin.editablefooter.testing import ( VOLTO_EDITABLEFOOTER_API_FUNCTIONAL_TESTING, ) -from redturtle.voltoplugin.editablefooter.interfaces import ( - IEditableFooterSettings, -) from transaction import commit import json @@ -63,6 +61,11 @@ def setUp(self): self.api_session.headers.update({"Accept": "application/json"}) self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + self.document = api.content.create( + container=self.portal, type="Document", title="document" + ) + commit() + def tearDown(self): self.api_session.close() @@ -82,16 +85,114 @@ def test_set_wrong_data(self): self.assertEqual(response.status_code, 400) def test_deserializer_convert_dict_into_json_string(self): - data = {"foo": "", "bar": 2} + data = [{"foo": "", "bar": 2}] self.api_session.patch(self.controlpanel_url, json={"footer_columns": data}) commit() self.assertEqual( self.get_record_value(field="footer_columns"), json.dumps(data) ) + def test_deserializer_raise_error_if_footer_columns_not_a_list(self): + data = {"foo": "", "bar": 2} + res = self.api_session.patch( + self.controlpanel_url, json={"footer_columns": data} + ) + self.assertEqual(res.status_code, 400) + + def test_deserializer_save_resolveuid_in_footerTop_internal_links(self): + data = [ + { + "footerTop": { + "blocks": { + "2955de0f-ea5e-475f-8efd-34c7060b99b9": { + "@type": "slate", + "plaintext": " link ", + "value": [ + { + "children": [ + { + "type": "link", + "data": { + "url": self.document.absolute_url(), + "dataElement": "", + }, + "children": [{"text": "link"}], + }, + ], + "type": "p", + } + ], + } + }, + "blocks_layout": { + "items": ["2955de0f-ea5e-475f-8efd-34c7060b99b9"] + }, + }, + "rootPath": "/", + } + ] + res = self.api_session.patch( + self.controlpanel_url, json={"footer_columns": data} + ) + commit() + + stored_value = json.loads(self.get_record_value(field="footer_columns")) + + self.assertEqual(res.status_code, 204) + self.assertEqual( + stored_value[0]["footerTop"]["blocks"][ + "2955de0f-ea5e-475f-8efd-34c7060b99b9" + ]["value"][0]["children"][0]["data"]["url"], + f"resolveuid/{self.document.UID()}", + ) + + def test_serializer_return_expanded_resolveuid_in_footerTop_internal_links(self): + data = [ + { + "footerTop": { + "blocks": { + "2955de0f-ea5e-475f-8efd-34c7060b99b9": { + "@type": "slate", + "plaintext": " link ", + "value": [ + { + "children": [ + { + "type": "link", + "data": { + "url": self.document.absolute_url(), + "dataElement": "", + }, + "children": [{"text": "link"}], + }, + ], + "type": "p", + } + ], + } + }, + "blocks_layout": { + "items": ["2955de0f-ea5e-475f-8efd-34c7060b99b9"] + }, + }, + "rootPath": "/", + } + ] + self.api_session.patch(self.controlpanel_url, json={"footer_columns": data}) + commit() + + res = self.api_session.get(self.controlpanel_url).json() + + self.assertEqual( + res["data"]["footer_columns"][0]["footerTop"]["blocks"][ + "2955de0f-ea5e-475f-8efd-34c7060b99b9" + ]["value"][0]["children"][0]["data"]["url"], + self.document.absolute_url(), + ) + def test_serializer_convert_string_into_json_object(self): self.assertEqual(self.get_record_value(field="footer_columns"), "") - value = {"foo": "bar"} + value = [{"foo": "bar"}] self.set_record_value(field="footer_columns", value=json.dumps(value)) response = self.api_session.get(self.controlpanel_url) diff --git a/src/redturtle/voltoplugin/editablefooter/tests/test_footer_columns_route.py b/src/redturtle/voltoplugin/editablefooter/tests/test_footer_columns_route.py index d2e236d..45b0506 100644 --- a/src/redturtle/voltoplugin/editablefooter/tests/test_footer_columns_route.py +++ b/src/redturtle/voltoplugin/editablefooter/tests/test_footer_columns_route.py @@ -14,7 +14,6 @@ from transaction import commit from zope.component import getUtility - import json import unittest @@ -44,6 +43,11 @@ def setUp(self): ] self.set_record_value(field="footer_columns", value=json.dumps(self.value)) + self.document = api.content.create( + container=self.portal, type="Document", title="document" + ) + commit() + def tearDown(self): self.api_session.close() @@ -67,6 +71,49 @@ def test_return_json_data(self): json.dumps(self.value).replace('href=\\"/', f'href=\\"{self.portal_url}/'), ) + def test_return_expanded_resolveuid_in_footerTop_internal_links(self): + data = [ + { + "footerTop": { + "blocks": { + "2955de0f-ea5e-475f-8efd-34c7060b99b9": { + "@type": "slate", + "plaintext": " link ", + "value": [ + { + "children": [ + { + "type": "link", + "data": { + "url": self.document.absolute_url(), + "dataElement": "", + }, + "children": [{"text": "link"}], + }, + ], + "type": "p", + } + ], + } + }, + "blocks_layout": { + "items": ["2955de0f-ea5e-475f-8efd-34c7060b99b9"] + }, + }, + "rootPath": "/", + } + ] + self.set_record_value(field="footer_columns", value=json.dumps(data)) + + response = self.api_session.get("/@footer-columns").json() + + self.assertEqual( + response[0]["footerTop"]["blocks"]["2955de0f-ea5e-475f-8efd-34c7060b99b9"][ + "value" + ][0]["children"][0]["data"]["url"], + self.document.absolute_url(), + ) + class FooterColumnsEndpointTestWithPloneVolto(FooterColumnsEndpointTest): layer = VOLTO_EDITABLEFOOTER_API_FUNCTIONAL_TESTING diff --git a/test_plone60.cfg b/test_plone60.cfg index 5081fdc..cf2d6de 100644 --- a/test_plone60.cfg +++ b/test_plone60.cfg @@ -8,6 +8,7 @@ extends = update-versions-file = test_plone60.cfg [versions] +plone.restapi = 9.5.0 # Added by buildout at 2024-01-17 12:29:47.135659 build = 1.0.3