From 60bd1e54cd5c471e7976ef4005ef13390e1ace68 Mon Sep 17 00:00:00 2001 From: Gil Forcada Codinachs Date: Mon, 13 Jul 2026 18:37:25 +0200 Subject: [PATCH 1/3] feat(export content): allow content export customization --- .../exportimport/exporters/configure.zcml | 5 +++ src/plone/exportimport/exporters/content.py | 32 ++++++++++++------- src/plone/exportimport/interfaces.py | 4 +++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/plone/exportimport/exporters/configure.zcml b/src/plone/exportimport/exporters/configure.zcml index ba9c8bb..05fa2d3 100644 --- a/src/plone/exportimport/exporters/configure.zcml +++ b/src/plone/exportimport/exporters/configure.zcml @@ -15,6 +15,11 @@ for="plone.base.interfaces.siteroot.IPloneSiteRoot" name="plone.exporter.content" /> + Generator: + def __init__(self, obj) -> None: + self.obj = obj + + def get_objects(self, query, errors) -> Generator: """Return all objects to be serialized.""" - query = self.query catalog = api.portal.get_tool("portal_catalog") if "object_provides" not in query: query["object_provides"] = "plone.dexterity.interfaces.IDexterityContent" @@ -39,7 +36,7 @@ def all_objects(self) -> Generator: except Exception: brain_path = brain.getPath() msg = f"Error getting object {brain_path} from brain" - self.errors.append({"path": brain_path, "message": msg}) + errors.append({"path": brain_path, "message": msg}) logger.exception(msg, exc_info=True) else: yield obj @@ -47,6 +44,20 @@ def all_objects(self) -> Generator: if not index % 100: logger.info(f"Content Exporter: Handled {index} items...") + +@implementer(interfaces.INamedExporter) +class ContentExporter(BaseExporter): + name: str = "content" + query: dict = None + filename_fmt: str = settings.EXPORT_CONTENT_FILEPATH + metadata: types.ExportImportMetadata = None + default_site_language: str = "en" + + def all_objects(self) -> Generator: + """Return all objects to be serialized.""" + adapter = getAdapter(self.site, interfaces.IObjectsExporter) + yield from adapter.get_objects(self.query, self.errors) + def serialize(self, obj: DexterityContent) -> dict: """Serialize object.""" obj_uid = content_utils.get_uid(obj) @@ -152,7 +163,6 @@ def export_data( ) -> list[Path]: # Content in a subpath of base_path base_path = base_path / self.name - query = query if query else {} site = self.site self.query = query if query else {"path": content_utils.get_obj_path(site)} metadata = types.ExportImportMetadata() diff --git a/src/plone/exportimport/interfaces.py b/src/plone/exportimport/interfaces.py index fd2b3fb..a98ec34 100644 --- a/src/plone/exportimport/interfaces.py +++ b/src/plone/exportimport/interfaces.py @@ -21,3 +21,7 @@ class INamedExporter(Interface): class INamedImporter(Interface): """Component to import content from a Plone Site.""" + + +class IObjectsExporter(Interface): + """Component to decide which objects are exported from a Plone Site.""" From 6bd346005d1baee4195b1145f8024803bb28a16f Mon Sep 17 00:00:00 2001 From: Gil Forcada Codinachs Date: Thu, 20 Aug 2026 16:11:20 +0200 Subject: [PATCH 2/3] chore: add tests --- tests/exporters/test_exporters_content.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/exporters/test_exporters_content.py b/tests/exporters/test_exporters_content.py index b90b59a..df25430 100644 --- a/tests/exporters/test_exporters_content.py +++ b/tests/exporters/test_exporters_content.py @@ -4,6 +4,7 @@ from zope.component import getAdapter from zope.component.hooks import setSite +import inspect import json import pytest @@ -100,6 +101,29 @@ def test_content_is_exported(self, export_path, paths_as_relative, path): assert "parent" not in keys +class TestExporterObjects: + + @pytest.fixture(autouse=True) + def _init(self, portal): + self.src_portal = portal + self.exporter = content.ObjectsExporter(portal) + + def test_adapter_is_registered(self): + adapter = getAdapter(self.src_portal, interfaces.IObjectsExporter) + assert isinstance(adapter, content.ObjectsExporter) + + def test_output_is_generator(self): + exporter = self.exporter + result = exporter.get_objects({}, []) + assert inspect.isgenerator(result) + + def test_output_objects(self): + exporter = self.exporter + result = exporter.get_objects({}, []) + objects = [x for x in result] + assert len(objects) == 9 + + class TestExporterContentMetadata: @pytest.fixture(autouse=True) def _init(self, portal, export_path): From 15e9d21bc54c6bde8c7edf061921306fe6112d52 Mon Sep 17 00:00:00 2001 From: Gil Forcada Codinachs Date: Tue, 21 Jul 2026 09:38:03 +0200 Subject: [PATCH 3/3] Add news entry --- news/86.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/86.feature diff --git a/news/86.feature b/news/86.feature new file mode 100644 index 0000000..7cb673f --- /dev/null +++ b/news/86.feature @@ -0,0 +1 @@ +Make it easy to customize which content gets exported @gforcada