plone.exportimport 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. plone/exportimport/__init__.py +8 -0
  2. plone/exportimport/cli/__init__.py +78 -0
  3. plone/exportimport/configure.zcml +21 -0
  4. plone/exportimport/deserializers/__init__.py +0 -0
  5. plone/exportimport/deserializers/blob.py +44 -0
  6. plone/exportimport/deserializers/blocks.py +15 -0
  7. plone/exportimport/deserializers/configure.zcml +4 -0
  8. plone/exportimport/exporters/__init__.py +85 -0
  9. plone/exportimport/exporters/base.py +80 -0
  10. plone/exportimport/exporters/configure.zcml +61 -0
  11. plone/exportimport/exporters/content.py +165 -0
  12. plone/exportimport/exporters/discussions.py +34 -0
  13. plone/exportimport/exporters/portlets.py +19 -0
  14. plone/exportimport/exporters/principals.py +24 -0
  15. plone/exportimport/exporters/redirects.py +19 -0
  16. plone/exportimport/exporters/relations.py +19 -0
  17. plone/exportimport/exporters/translations.py +21 -0
  18. plone/exportimport/importers/__init__.py +66 -0
  19. plone/exportimport/importers/base.py +93 -0
  20. plone/exportimport/importers/configure.zcml +67 -0
  21. plone/exportimport/importers/content.py +166 -0
  22. plone/exportimport/importers/discussions.py +24 -0
  23. plone/exportimport/importers/final.py +44 -0
  24. plone/exportimport/importers/portlets.py +16 -0
  25. plone/exportimport/importers/principals.py +34 -0
  26. plone/exportimport/importers/redirects.py +16 -0
  27. plone/exportimport/importers/relations.py +23 -0
  28. plone/exportimport/importers/translations.py +16 -0
  29. plone/exportimport/interfaces.py +23 -0
  30. plone/exportimport/serializers/__init__.py +0 -0
  31. plone/exportimport/serializers/blob.py +100 -0
  32. plone/exportimport/serializers/blocks.py +15 -0
  33. plone/exportimport/serializers/configure.zcml +10 -0
  34. plone/exportimport/serializers/fields.py +93 -0
  35. plone/exportimport/settings.py +19 -0
  36. plone/exportimport/testing/__init__.py +27 -0
  37. plone/exportimport/types.py +71 -0
  38. plone/exportimport/utils/__init__.py +1 -0
  39. plone/exportimport/utils/cli.py +63 -0
  40. plone/exportimport/utils/content/__init__.py +19 -0
  41. plone/exportimport/utils/content/blocks.py +47 -0
  42. plone/exportimport/utils/content/core.py +81 -0
  43. plone/exportimport/utils/content/export_helpers.py +365 -0
  44. plone/exportimport/utils/content/import_helpers.py +382 -0
  45. plone/exportimport/utils/content/revisions.py +45 -0
  46. plone/exportimport/utils/dates.py +21 -0
  47. plone/exportimport/utils/discussions.py +137 -0
  48. plone/exportimport/utils/path.py +21 -0
  49. plone/exportimport/utils/permissions.py +49 -0
  50. plone/exportimport/utils/portlets.py +312 -0
  51. plone/exportimport/utils/principals/__init__.py +4 -0
  52. plone/exportimport/utils/principals/groups.py +50 -0
  53. plone/exportimport/utils/principals/helpers.py +41 -0
  54. plone/exportimport/utils/principals/members.py +152 -0
  55. plone/exportimport/utils/redirects.py +30 -0
  56. plone/exportimport/utils/relations.py +102 -0
  57. plone/exportimport/utils/translations.py +143 -0
  58. plone/exportimport/utils/zca.py +13 -0
  59. plone.exportimport-1.0.0-py3.13-nspkg.pth +1 -0
  60. plone.exportimport-1.0.0.dist-info/LICENSE +339 -0
  61. plone.exportimport-1.0.0.dist-info/METADATA +316 -0
  62. plone.exportimport-1.0.0.dist-info/RECORD +66 -0
  63. plone.exportimport-1.0.0.dist-info/WHEEL +5 -0
  64. plone.exportimport-1.0.0.dist-info/entry_points.txt +6 -0
  65. plone.exportimport-1.0.0.dist-info/namespace_packages.txt +1 -0
  66. plone.exportimport-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """Plone Distribution support."""
2
+
3
+ import logging
4
+
5
+
6
+ PACKAGE_NAME = "plone.exportimport"
7
+
8
+ logger = logging.getLogger(PACKAGE_NAME)
@@ -0,0 +1,78 @@
1
+ from plone import api
2
+ from plone.exportimport.exporters import get_exporter
3
+ from plone.exportimport.importers import get_importer
4
+ from plone.exportimport.utils import cli as cli_helpers
5
+
6
+ import argparse
7
+ import sys
8
+ import transaction
9
+
10
+
11
+ CLI_SPEC = {
12
+ "exporter": {
13
+ "description": "Export Plone Site content",
14
+ "options": {
15
+ "zopeconf": "Path to zope.conf",
16
+ "site": "Plone site ID to export the content from",
17
+ "path": "Path to export the content",
18
+ "--include-revisions": "Include revision history",
19
+ },
20
+ },
21
+ "importer": {
22
+ "description": "Import content into a Plone Site",
23
+ "options": {
24
+ "zopeconf": "Path to zope.conf",
25
+ "site": "Plone site ID to import the content to",
26
+ "path": "Path to import the content from",
27
+ },
28
+ },
29
+ }
30
+
31
+
32
+ def _parse_args(description: str, options: dict, args: list):
33
+ parser = argparse.ArgumentParser(description=description)
34
+ for key, help in options.items():
35
+ if key.startswith("-"):
36
+ parser.add_argument(key, action="store_true", help=help)
37
+ else:
38
+ parser.add_argument(key, help=help)
39
+ namespace, _ = parser.parse_known_args(args[1:])
40
+ return namespace
41
+
42
+
43
+ def exporter_cli(args=sys.argv):
44
+ """Export a Plone site."""
45
+ logger = cli_helpers.get_logger("Exporter")
46
+ exporter_cli = CLI_SPEC["exporter"]
47
+ # We get an argparse.Namespace instance.
48
+ namespace = _parse_args(exporter_cli["description"], exporter_cli["options"], args)
49
+ app = cli_helpers.get_app(namespace.zopeconf)
50
+ path = cli_helpers._process_path(namespace.path)
51
+ if not path:
52
+ logger.error(f"{namespace.path} does not exist, please create it first.")
53
+ sys.exit(1)
54
+ site = cli_helpers.get_site(app, namespace.site, logger)
55
+ with api.env.adopt_roles(["Manager"]):
56
+ results = get_exporter(site).export_site(path, options=namespace)
57
+ logger.info(f" Using path {path} to export content from Plone site at /{site.id}")
58
+ for item in results[1:]:
59
+ logger.info(f" Wrote {item}")
60
+
61
+
62
+ def importer_cli(args=sys.argv):
63
+ """Import content to a Plone site."""
64
+ logger = cli_helpers.get_logger("Importer")
65
+ importer_cli = CLI_SPEC["importer"]
66
+ namespace = _parse_args(importer_cli["description"], importer_cli["options"], args)
67
+ app = cli_helpers.get_app(namespace.zopeconf)
68
+ path = cli_helpers._process_path(namespace.path)
69
+ if not path:
70
+ logger.error(f"{namespace.path} does not exist, aborting import.")
71
+ sys.exit(1)
72
+ site = cli_helpers.get_site(app, namespace.site, logger)
73
+ with api.env.adopt_roles(["Manager"]):
74
+ results = get_importer(site).import_site(path)
75
+ logger.info(f" Using path {path} to import content to Plone site at /{site.id}")
76
+ for item in results:
77
+ logger.info(f" - {item}")
78
+ transaction.commit()
@@ -0,0 +1,21 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:i18n="http://namespaces.zope.org/i18n"
4
+ xmlns:zcml="http://namespaces.zope.org/zcml"
5
+ i18n_domain="plone"
6
+ >
7
+
8
+ <!-- Dependencies -->
9
+ <include package="Products.CMFCore" />
10
+ <include package="Products.CMFPlone" />
11
+ <include package="plone.namedfile" />
12
+ <include package="plone.app.dexterity" />
13
+ <include package="plone.restapi" />
14
+
15
+ <include package=".serializers" />
16
+ <include package=".deserializers" />
17
+ <include package=".exporters" />
18
+ <include package=".importers" />
19
+
20
+
21
+ </configure>
File without changes
@@ -0,0 +1,44 @@
1
+ from pathlib import Path
2
+ from plone.dexterity.interfaces import IDexterityContent
3
+ from plone.exportimport import settings
4
+ from plone.exportimport.interfaces import IExportImportRequestMarker
5
+ from plone.exportimport.utils import path as path_utils
6
+ from plone.namedfile.interfaces import INamedField
7
+ from plone.restapi.deserializer.dxfields import DefaultFieldDeserializer
8
+ from plone.restapi.interfaces import IFieldDeserializer
9
+ from zope.component import adapter
10
+ from zope.globalrequest import getRequest
11
+ from zope.interface import implementer
12
+
13
+ import codecs
14
+
15
+
16
+ def load_blob(path: str) -> bytes:
17
+ """Load blob from fs and encode it as base64."""
18
+ request = getRequest()
19
+ content_import_path = Path(request[settings.IMPORT_PATH_KEY])
20
+ path = content_import_path / path
21
+ if not path.exists():
22
+ raise ValueError(f"Blob not found at {path}")
23
+ data = path_utils.encode_file_contents(path)
24
+ return codecs.decode(data, "base64")
25
+
26
+
27
+ @adapter(INamedField, IDexterityContent, IExportImportRequestMarker)
28
+ @implementer(IFieldDeserializer)
29
+ class ExportImportNamedFieldDeserializer(DefaultFieldDeserializer):
30
+ def __call__(self, value):
31
+ result = None
32
+ blob_path = value.pop("blob_path", "")
33
+ content_type = value.get("content-type", "application/octet-stream")
34
+ filename = value.get("filename", None)
35
+ data = load_blob(blob_path)
36
+ # Convert if we have data
37
+ if data:
38
+ result = self.field._type(
39
+ data=data, contentType=content_type, filename=filename
40
+ )
41
+
42
+ # Always validate to check for required fields
43
+ self.field.validate(result)
44
+ return result
@@ -0,0 +1,15 @@
1
+ from plone.exportimport.interfaces import IExportImportRequestMarker
2
+ from plone.restapi.behaviors import IBlocks
3
+ from plone.restapi.deserializer.dxfields import DefaultFieldDeserializer
4
+ from plone.restapi.interfaces import IFieldDeserializer
5
+ from plone.schema import IJSONField
6
+ from zope.component import adapter
7
+ from zope.interface import implementer
8
+
9
+
10
+ @implementer(IFieldDeserializer)
11
+ @adapter(IJSONField, IBlocks, IExportImportRequestMarker)
12
+ class ExportImportBlocksDeserializer(DefaultFieldDeserializer):
13
+ """We skip the subscribers that deserialize the blocks from the frontend.
14
+ We only need the raw data.
15
+ """
@@ -0,0 +1,4 @@
1
+ <configure xmlns="http://namespaces.zope.org/zope">
2
+ <adapter factory=".blob.ExportImportNamedFieldDeserializer" />
3
+ <adapter factory=".blocks.ExportImportBlocksDeserializer" />
4
+ </configure>
@@ -0,0 +1,85 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone import api
4
+ from plone.exportimport import interfaces
5
+ from plone.exportimport import logger
6
+ from plone.exportimport import PACKAGE_NAME
7
+ from Products.CMFPlone.Portal import PloneSite
8
+ from tempfile import mkdtemp
9
+ from typing import Dict
10
+ from typing import List
11
+ from typing import Optional
12
+ from zope.component import getAdapter
13
+ from zope.component import hooks
14
+ from zope.component import queryAdapter
15
+ from zope.interface import implementer
16
+
17
+ import argparse
18
+
19
+
20
+ EXPORTER_NAMES = [
21
+ "plone.exporter.content",
22
+ "plone.exporter.principals",
23
+ "plone.exporter.redirects",
24
+ "plone.exporter.relations",
25
+ "plone.exporter.translations",
26
+ "plone.exporter.discussions",
27
+ "plone.exporter.portlets",
28
+ ]
29
+
30
+
31
+ ExporterMapping = Dict[str, BaseExporter]
32
+
33
+
34
+ @implementer(interfaces.IExporter)
35
+ class Exporter:
36
+ """Export content from a Plone Site."""
37
+
38
+ exporters: ExporterMapping
39
+
40
+ def __init__(self, site):
41
+ self.site = site
42
+ self.exporters = self.all_exporters()
43
+
44
+ def all_exporters(self) -> ExporterMapping:
45
+ """Return all exporters."""
46
+ exporters = {}
47
+ for exporter_name in EXPORTER_NAMES:
48
+ exporter = queryAdapter(
49
+ self.site, interfaces.INamedExporter, name=exporter_name
50
+ )
51
+ if exporter:
52
+ exporters[exporter_name] = exporter
53
+ return exporters
54
+
55
+ @staticmethod
56
+ def _prepare_path(path: Optional[Path] = None) -> Path:
57
+ """Return a valid path to use for the export.
58
+
59
+ If base_path is not given, create a temporary directory to export
60
+ the content.
61
+ """
62
+ valid_path = path.is_dir() if path else False
63
+ if not valid_path:
64
+ path = Path(mkdtemp(prefix=PACKAGE_NAME))
65
+ return path
66
+
67
+ def export_site(
68
+ self, path: Optional[Path] = None, options: Optional[argparse.Namespace] = None
69
+ ) -> List[Path]:
70
+ """Export the given site to the filesystem."""
71
+ path = self._prepare_path(path)
72
+ paths: List[Path] = [path]
73
+ with hooks.site(self.site):
74
+ for exporter_name, exporter in self.exporters.items():
75
+ logger.debug(f"Exporting {self.site} with {exporter_name} to {path}")
76
+ new_paths = exporter.export_data(path, options=options)
77
+ paths.extend(new_paths)
78
+ return paths
79
+
80
+
81
+ def get_exporter(site: PloneSite = None) -> Exporter:
82
+ """Get the exporter."""
83
+ if site is None:
84
+ site = api.portal.get()
85
+ return getAdapter(site, interfaces.IExporter)
@@ -0,0 +1,80 @@
1
+ from pathlib import Path
2
+ from plone.exportimport import types
3
+ from plone.exportimport.utils import content as utils
4
+ from plone.exportimport.utils import path as path_utils
5
+ from Products.CMFPlone.Portal import PloneSite
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import List
9
+ from typing import Optional
10
+ from zope.globalrequest import getRequest
11
+
12
+ import argparse
13
+ import json
14
+
15
+
16
+ class BaseExporter:
17
+ name: str
18
+ base_path: Path
19
+ errors: list = None
20
+ request: types.HTTPRequest = None
21
+ data_hooks: List[Callable] = None
22
+ obj_hooks: List[Callable] = None
23
+ options: Optional[argparse.Namespace] = None
24
+
25
+ def __init__(
26
+ self,
27
+ site: PloneSite = None,
28
+ ):
29
+ self.site = site
30
+ self.errors = []
31
+ self.request = getRequest()
32
+
33
+ def get_option(self, name, default=None):
34
+ return getattr(self.options, name, default)
35
+
36
+ @property
37
+ def filepath(self) -> Path:
38
+ """Filepath to be used during export."""
39
+ filename = f"{self.name}.json"
40
+ return self.base_path / filename
41
+
42
+ def _serializer(self, obj: Any) -> Callable:
43
+ """Serializer for object."""
44
+ return utils.get_serializer(obj, self.request)
45
+
46
+ def _dump(self, data: dict, filepath: Path = None) -> Path:
47
+ """Dump serialized data to disk."""
48
+ filepath = filepath if filepath else self.filepath
49
+ # Create container, if it does not exist
50
+ path_utils.get_parent_folder(filepath)
51
+ with open(filepath, "w") as fh:
52
+ json.dump(data, fh, indent=2, sort_keys=True)
53
+ # json.dump does not add a newline at the end of the file, so we
54
+ # explicitly do it. Otherwise when you manually edit a file and
55
+ # you use an editor that respects the standard `.editorconfig`
56
+ # that we have in most Plone packages, you always get a diff because
57
+ # your editor has automatically added a newline at the end.
58
+ fh.write("\n")
59
+ return filepath
60
+
61
+ def dump(self) -> List[Path]:
62
+ """Serialize objects."""
63
+ return []
64
+
65
+ def export_data(
66
+ self,
67
+ base_path: Path,
68
+ data_hooks: List[Callable] = None,
69
+ obj_hooks: List[Callable] = None,
70
+ options: Optional[argparse.Namespace] = None,
71
+ ) -> List[Path]:
72
+ """Write data to filesystem."""
73
+ if not base_path.exists():
74
+ base_path.mkdir(parents=True)
75
+ self.base_path = base_path
76
+ self.data_hooks = self.data_hooks or data_hooks or []
77
+ self.obj_hooks = self.obj_hooks or obj_hooks or []
78
+ self.options = options
79
+ paths = self.dump()
80
+ return paths
@@ -0,0 +1,61 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:zcml="http://namespaces.zope.org/zcml"
4
+ >
5
+ <!-- Main exporter -->
6
+ <adapter
7
+ factory=".Exporter"
8
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
9
+ />
10
+
11
+ <!-- Exporters -->
12
+ <adapter
13
+ factory=".content.ContentExporter"
14
+ provides="plone.exportimport.interfaces.INamedExporter"
15
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
16
+ name="plone.exporter.content"
17
+ />
18
+ <adapter
19
+ factory=".principals.PrincipalsExporter"
20
+ provides="plone.exportimport.interfaces.INamedExporter"
21
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
22
+ name="plone.exporter.principals"
23
+ />
24
+ <adapter
25
+ factory=".redirects.RedirectsExporter"
26
+ provides="plone.exportimport.interfaces.INamedExporter"
27
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
28
+ name="plone.exporter.redirects"
29
+ />
30
+ <adapter
31
+ factory=".relations.RelationsExporter"
32
+ provides="plone.exportimport.interfaces.INamedExporter"
33
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
34
+ name="plone.exporter.relations"
35
+ />
36
+ <configure zcml:condition="installed plone.app.multilingual">
37
+ <adapter
38
+ factory=".translations.TranslationsExporter"
39
+ provides="plone.exportimport.interfaces.INamedExporter"
40
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
41
+ name="plone.exporter.translations"
42
+ />
43
+ </configure>
44
+ <configure zcml:condition="installed plone.app.discussion">
45
+ <adapter
46
+ factory=".discussions.DiscussionsExporter"
47
+ provides="plone.exportimport.interfaces.INamedExporter"
48
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
49
+ name="plone.exporter.discussions"
50
+ />
51
+ </configure>
52
+ <configure zcml:condition="installed plone.app.portlets">
53
+ <adapter
54
+ factory=".portlets.PortletsExporter"
55
+ provides="plone.exportimport.interfaces.INamedExporter"
56
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
57
+ name="plone.exporter.portlets"
58
+ />
59
+ </configure>
60
+
61
+ </configure>
@@ -0,0 +1,165 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone import api
4
+ from plone.base.interfaces import IPloneSiteRoot
5
+ from plone.dexterity.content import DexterityContent
6
+ from plone.exportimport import interfaces
7
+ from plone.exportimport import logger
8
+ from plone.exportimport import settings
9
+ from plone.exportimport import types
10
+ from plone.exportimport.interfaces import IExportImportRequestMarker
11
+ from plone.exportimport.utils import content as content_utils
12
+ from plone.exportimport.utils import request_provides
13
+ from typing import Callable
14
+ from typing import Generator
15
+ from typing import List
16
+ from typing import Optional
17
+ from zope.interface import implementer
18
+
19
+ import argparse
20
+
21
+
22
+ @implementer(interfaces.INamedExporter)
23
+ class ContentExporter(BaseExporter):
24
+ name: str = "content"
25
+ query: dict = None
26
+ filename_fmt: str = settings.EXPORT_CONTENT_FILEPATH
27
+ metadata: types.ExportImportMetadata = None
28
+ default_site_language: str = "en"
29
+
30
+ def all_objects(self) -> Generator:
31
+ """Return all objects to be serialized."""
32
+ query = self.query
33
+ catalog = api.portal.get_tool("portal_catalog")
34
+ if "object_provides" not in query:
35
+ query["object_provides"] = "plone.dexterity.interfaces.IDexterityContent"
36
+ brains = catalog.unrestrictedSearchResults(**query)
37
+ logger.info(f"Exporting {len(brains)}")
38
+ for index, brain in enumerate(brains, start=1):
39
+ try:
40
+ obj = brain.getObject()
41
+ except Exception:
42
+ brain_path = brain.getPath()
43
+ msg = f"Error getting object {brain_path} from brain"
44
+ self.errors.append({"path": brain_path, "message": msg})
45
+ logger.exception(msg, exc_info=True)
46
+ else:
47
+ yield obj
48
+
49
+ if not index % 100:
50
+ logger.info(f"Handled {index} items...")
51
+
52
+ def serialize(self, obj: DexterityContent) -> dict:
53
+ """Serialize object."""
54
+ obj_uid = content_utils.get_uid(obj)
55
+ obj_path = content_utils.get_obj_path(obj)
56
+ config = types.ExporterConfig(
57
+ site=self.site,
58
+ site_root_uid=self.site.UID(),
59
+ request=self.request,
60
+ serializer=self._serializer(obj),
61
+ logger_prefix=f"- {obj_path}:",
62
+ )
63
+ kwargs = {}
64
+ is_site_root = IPloneSiteRoot.providedBy(obj)
65
+ is_folderish = content_utils.is_folderish(obj)
66
+ if is_folderish:
67
+ if not is_site_root:
68
+ kwargs["include_items"] = False
69
+ # Process metadata
70
+ for helper in content_utils.metadata_helpers():
71
+ logger.debug(f"{config.logger_prefix} Updating metadata {helper.name}")
72
+ value = helper.func(obj, config)
73
+ if value is not None:
74
+ getattr(self.metadata, helper.name)[obj_uid] = value
75
+
76
+ # Apply object hooks
77
+ for func in self.obj_hooks:
78
+ logger.debug(
79
+ f"{config.logger_prefix} Running object hook {func.__name__} on object"
80
+ )
81
+ obj = func(obj, config)
82
+
83
+ # Serialize
84
+ data = config.serializer(**kwargs)
85
+
86
+ # Fix serialized data
87
+ for fixer in content_utils.fixers():
88
+ logger.debug(
89
+ f"{config.logger_prefix} Running {fixer.name} on serialized data"
90
+ )
91
+ data = fixer.func(data, obj, config)
92
+
93
+ # Enrich
94
+ for enricher in content_utils.enrichers(
95
+ include_revisions=self.get_option("include_revisions")
96
+ ):
97
+ logger.debug(f"{config.logger_prefix} Running {enricher.name}")
98
+ additional = enricher.func(obj, config)
99
+ if additional:
100
+ data.update(additional)
101
+
102
+ # Apply data hooks
103
+ for func in self.data_hooks:
104
+ logger.debug(
105
+ f"{config.logger_prefix} Running data hook {func.__name__} on payload"
106
+ )
107
+ data = func(data, obj, config)
108
+
109
+ # Cleanup data
110
+ for cleaner in content_utils.cleaners():
111
+ logger.debug(
112
+ f"{config.logger_prefix} Running {cleaner.name} on serialized data"
113
+ )
114
+ data = cleaner.func(data, config)
115
+ return data
116
+
117
+ def dump_one(self, obj: DexterityContent) -> Path:
118
+ """Serialize object and dump it to disk."""
119
+ obj_path = content_utils.get_obj_path(obj)
120
+ # Serialize object
121
+ data = self.serialize(obj)
122
+ base_path = self.base_path
123
+ filepath = base_path / self.filename_fmt.format(**data)
124
+ filepath = self._dump(data, filepath)
125
+ logger.debug(f"- {obj_path}: Wrote serialized data to {filepath}")
126
+ # Add to list of files
127
+ self.metadata._all_[obj_path] = str(filepath.relative_to(base_path))
128
+ return filepath
129
+
130
+ def dump_metadata(self) -> Path:
131
+ metadata = self.metadata.export()
132
+ filepath = self.base_path / "__metadata__.json"
133
+ return self._dump(metadata, filepath)
134
+
135
+ def dump(self) -> List[Path]:
136
+ """Serialize contents and dump them to disk."""
137
+ paths = []
138
+ with request_provides(self.request, IExportImportRequestMarker):
139
+ for obj in self.all_objects():
140
+ path = self.dump_one(obj)
141
+ if path:
142
+ paths.append(path)
143
+ # Add list of blobs to serialization
144
+ paths.insert(0, self.dump_metadata())
145
+ return paths
146
+
147
+ def export_data(
148
+ self,
149
+ base_path: Path,
150
+ data_hooks: List[Callable] = None,
151
+ obj_hooks: List[Callable] = None,
152
+ query: Optional[dict] = None,
153
+ options: Optional[argparse.Namespace] = None,
154
+ ) -> List[Path]:
155
+ # Content in a subpath of base_path
156
+ base_path = base_path / self.name
157
+ query = query if query else {}
158
+ site = self.site
159
+ self.query = query if query else {"path": content_utils.get_obj_path(site)}
160
+ metadata = types.ExportImportMetadata()
161
+ self.metadata = metadata
162
+ self.request[settings.EXPORT_CONTENT_METADATA_KEY] = metadata
163
+ self.request[settings.EXPORT_PATH_KEY] = base_path
164
+ self.default_site_language = site.language
165
+ return super().export_data(base_path, data_hooks, obj_hooks, options=options)
@@ -0,0 +1,34 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone.exportimport import interfaces
4
+ from plone.exportimport import logger
5
+ from typing import List
6
+ from zope.interface import implementer
7
+
8
+
9
+ try:
10
+ import plone.app.discussion # noqa
11
+ except ImportError:
12
+ HAS_DISCUSSION = False
13
+ else:
14
+ HAS_DISCUSSION = True
15
+
16
+
17
+ @implementer(interfaces.INamedExporter)
18
+ class DiscussionsExporter(BaseExporter):
19
+ name: str = "discussions"
20
+
21
+ def dump(self) -> List[Path]:
22
+ """Serialize object and dump it to disk."""
23
+ if not HAS_DISCUSSION:
24
+ logger.debug("- Discussions: Skipping (plone.app.discussion not installed)")
25
+ return []
26
+
27
+ from plone.exportimport.utils import discussions as utils
28
+
29
+ discussions = utils.get_discussions()
30
+ filepath = self._dump(discussions, self.filepath)
31
+ logger.debug(
32
+ f"- Discussions: Wrote {len(discussions)} discussions to {filepath}"
33
+ )
34
+ return [filepath]
@@ -0,0 +1,19 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone.exportimport import interfaces
4
+ from plone.exportimport import logger
5
+ from plone.exportimport.utils import portlets as utils
6
+ from typing import List
7
+ from zope.interface import implementer
8
+
9
+
10
+ @implementer(interfaces.INamedExporter)
11
+ class PortletsExporter(BaseExporter):
12
+ name: str = "portlets"
13
+
14
+ def dump(self) -> List[Path]:
15
+ """Serialize object and dump it to disk."""
16
+ portlets = utils.get_portlets()
17
+ filepath = self._dump(portlets, self.filepath)
18
+ logger.debug(f"- Portlets: Wrote {len(portlets)} portlets to {filepath}")
19
+ return [filepath]
@@ -0,0 +1,24 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone.exportimport import interfaces
4
+ from plone.exportimport import logger
5
+ from plone.exportimport.utils import principals as principals_utils
6
+ from typing import List
7
+ from zope.interface import implementer
8
+
9
+
10
+ @implementer(interfaces.INamedExporter)
11
+ class PrincipalsExporter(BaseExporter):
12
+ name: str = "principals"
13
+
14
+ def dump(self) -> List[Path]:
15
+ """Serialize object and dump it to disk."""
16
+ data = {
17
+ "groups": principals_utils.export_groups(),
18
+ "members": principals_utils.export_members(),
19
+ }
20
+ filepath = self._dump(data, self.filepath)
21
+ logger.debug(
22
+ f"- Principals: Wrote {len(data['groups'])} groups and {len(data['members'])} members to {filepath}"
23
+ )
24
+ return [filepath]
@@ -0,0 +1,19 @@
1
+ from .base import BaseExporter
2
+ from pathlib import Path
3
+ from plone.exportimport import interfaces
4
+ from plone.exportimport import logger
5
+ from plone.exportimport.utils import redirects as utils
6
+ from typing import List
7
+ from zope.interface import implementer
8
+
9
+
10
+ @implementer(interfaces.INamedExporter)
11
+ class RedirectsExporter(BaseExporter):
12
+ name: str = "redirects"
13
+
14
+ def dump(self) -> List[Path]:
15
+ """Serialize object and dump it to disk."""
16
+ redirects = utils.get_redirects()
17
+ filepath = self._dump(redirects, self.filepath)
18
+ logger.debug(f"- Redirects: Wrote {len(redirects)} redirects to {filepath}")
19
+ return [filepath]