Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include revisions only when passing --include-revisions. #44

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ Export content from a Plone site to a directory in the file system.

Usage:

- `plone-exporter` <path-to-zopeconf> <site-id> <path-to-export-data>
- `plone-exporter` [--include-revisions] <path-to-zopeconf> <site-id> <path-to-export-data>

Example:

```shell
plone-exporter instance/etc/zope.conf Plone /tmp/plone_data/
```

By default, the revisions history (older versions of each content item) are not exported.
If you do want them, add `--include-revisions` on the command line.

## `plone-importer`

Import content from a file system directory into an existing Plone site.
Expand Down
1 change: 1 addition & 0 deletions news/39.feature.4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Include revisions only when passing `--include-revisions`. @mauritsvanrees
9 changes: 7 additions & 2 deletions src/plone/exportimport/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"zopeconf": "Path to zope.conf",
"site": "Plone site ID to export the content from",
"path": "Path to export the content",
"--include-revisions": "Include revision history",
},
},
"importer": {
Expand All @@ -31,7 +32,10 @@
def _parse_args(description: str, options: dict, args: list):
parser = argparse.ArgumentParser(description=description)
for key, help in options.items():
parser.add_argument(key, help=help)
if key.startswith("-"):
parser.add_argument(key, action="store_true", help=help)
else:
parser.add_argument(key, help=help)
namespace, _ = parser.parse_known_args(args[1:])
return namespace

Expand All @@ -40,6 +44,7 @@ def exporter_cli(args=sys.argv):
"""Export a Plone site."""
logger = cli_helpers.get_logger("Exporter")
exporter_cli = CLI_SPEC["exporter"]
# We get an argparse.Namespace instance.
namespace = _parse_args(exporter_cli["description"], exporter_cli["options"], args)
app = cli_helpers.get_app(namespace.zopeconf)
path = cli_helpers._process_path(namespace.path)
Expand All @@ -48,7 +53,7 @@ def exporter_cli(args=sys.argv):
sys.exit(1)
site = cli_helpers.get_site(app, namespace.site, logger)
with api.env.adopt_roles(["Manager"]):
results = get_exporter(site).export_site(path)
results = get_exporter(site).export_site(path, options=namespace)
logger.info(f" Using path {path} to export content from Plone site at /{site.id}")
for item in results[1:]:
logger.info(f" Wrote {item}")
Expand Down
8 changes: 6 additions & 2 deletions src/plone/exportimport/exporters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from zope.component import queryAdapter
from zope.interface import implementer

import argparse


EXPORTER_NAMES = [
"plone.exporter.content",
Expand Down Expand Up @@ -62,14 +64,16 @@ def _prepare_path(path: Optional[Path] = None) -> Path:
path = Path(mkdtemp(prefix=PACKAGE_NAME))
return path

def export_site(self, path: Optional[Path] = None) -> List[Path]:
def export_site(
self, path: Optional[Path] = None, options: Optional[argparse.Namespace] = None
) -> List[Path]:
"""Export the given site to the filesystem."""
path = self._prepare_path(path)
paths: List[Path] = [path]
with hooks.site(self.site):
for exporter_name, exporter in self.exporters.items():
logger.debug(f"Exporting {self.site} with {exporter_name} to {path}")
new_paths = exporter.export_data(path)
new_paths = exporter.export_data(path, options=options)
paths.extend(new_paths)
return paths

Expand Down
8 changes: 8 additions & 0 deletions src/plone/exportimport/exporters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from zope.globalrequest import getRequest

import argparse
import json


Expand All @@ -18,6 +20,7 @@ class BaseExporter:
request: types.HTTPRequest = None
data_hooks: List[Callable] = None
obj_hooks: List[Callable] = None
options: Optional[argparse.Namespace] = None

def __init__(
self,
Expand All @@ -27,6 +30,9 @@ def __init__(
self.errors = []
self.request = getRequest()

def get_option(self, name, default=None):
return getattr(self.options, name, default)

@property
def filepath(self) -> Path:
"""Filepath to be used during export."""
Expand Down Expand Up @@ -61,12 +67,14 @@ def export_data(
base_path: Path,
data_hooks: List[Callable] = None,
obj_hooks: List[Callable] = None,
options: Optional[argparse.Namespace] = None,
) -> List[Path]:
"""Write data to filesystem."""
if not base_path.exists():
base_path.mkdir(parents=True)
self.base_path = base_path
self.data_hooks = self.data_hooks or data_hooks or []
self.obj_hooks = self.obj_hooks or obj_hooks or []
self.options = options
paths = self.dump()
return paths
10 changes: 8 additions & 2 deletions src/plone/exportimport/exporters/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from typing import Optional
from zope.interface import implementer

import argparse


@implementer(interfaces.INamedExporter)
class ContentExporter(BaseExporter):
Expand Down Expand Up @@ -87,8 +89,11 @@ def serialize(self, obj: DexterityContent) -> dict:
f"{config.logger_prefix} Running {fixer.name} on serialized data"
)
data = fixer.func(data, obj, config)

# Enrich
for enricher in content_utils.enrichers():
for enricher in content_utils.enrichers(
include_revisions=self.get_option("include_revisions")
):
logger.debug(f"{config.logger_prefix} Running {enricher.name}")
additional = enricher.func(obj, config)
if additional:
Expand Down Expand Up @@ -145,6 +150,7 @@ def export_data(
data_hooks: List[Callable] = None,
obj_hooks: List[Callable] = None,
query: Optional[dict] = None,
options: Optional[argparse.Namespace] = None,
) -> List[Path]:
# Content in a subpath of base_path
base_path = base_path / self.name
Expand All @@ -156,4 +162,4 @@ def export_data(
self.request[settings.EXPORT_CONTENT_METADATA_KEY] = metadata
self.request[settings.EXPORT_PATH_KEY] = base_path
self.default_site_language = site.language
return super().export_data(base_path, data_hooks, obj_hooks)
return super().export_data(base_path, data_hooks, obj_hooks, options=options)
5 changes: 3 additions & 2 deletions src/plone/exportimport/utils/content/export_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,14 @@ def fixers() -> List[types.ExportImportHelper]:
return fixers


def enrichers() -> List[types.ExportImportHelper]:
def enrichers(include_revisions: bool = False) -> List[types.ExportImportHelper]:
enrichers = []
funcs = [
add_constraints_info,
add_workflow_history,
add_revisions_history,
]
if include_revisions:
funcs.append(add_revisions_history)
if IConversation is not None:
funcs.append(add_conversation)
for func in funcs:
Expand Down
19 changes: 16 additions & 3 deletions src/plone/exportimport/utils/content/import_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
set_local_permissions as _set_local_permissions,
)
from plone.restapi.interfaces import IDeserializeFromJson
from Products.CMFEditions.CopyModifyMergeRepositoryTool import (
CopyModifyMergeRepositoryTool,
)
from typing import Callable
from typing import List
from unittest.mock import patch
from urllib.parse import unquote
from zope.component import getMultiAdapter

Expand Down Expand Up @@ -107,6 +111,10 @@ def processors() -> List[types.ExportImportHelper]:
return processors


def _mock_isVersionable(*args, **kwargs):
return False


def get_obj_instance(item: dict, config: types.ImporterConfig) -> DexterityContent:
# Get container
container = get_parent_from_item(item)
Expand All @@ -117,9 +125,14 @@ def get_obj_instance(item: dict, config: types.ImporterConfig) -> DexterityConte
logger.debug(f"{config.logger_prefix} Will update {new}")
else:
factory_kwargs = item.get("factory_kwargs", {})
new = unrestricted_construct_instance(
item["@type"], container, item["id"], **factory_kwargs
)
# Temporarily disable versioning, otherwise the first version
# is basically nothing, it does not even have a title.
with patch.object(
CopyModifyMergeRepositoryTool, "isVersionable", _mock_isVersionable
):
new = unrestricted_construct_instance(
item["@type"], container, item["id"], **factory_kwargs
)
logger.debug(f"{config.logger_prefix} Created {new}")
return new

Expand Down
Loading