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 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.""" 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):