plone.exportimport 2.0.2__tar.gz → 2.2.0__tar.gz

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 (192) hide show
  1. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/CHANGES.md +33 -0
  2. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/PKG-INFO +38 -2
  3. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/setup.py +5 -2
  4. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/cli/__init__.py +45 -17
  5. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/deserializers/blob.py +18 -5
  6. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/base.py +19 -6
  7. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/configure.zcml +5 -0
  8. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/content.py +57 -18
  9. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/discussions.py +3 -1
  10. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/portlets.py +3 -1
  11. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/redirects.py +3 -1
  12. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/relations.py +3 -1
  13. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/translations.py +3 -1
  14. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/__init__.py +8 -0
  15. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/content.py +39 -1
  16. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/interfaces.py +4 -0
  17. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/serializers/blob.py +6 -1
  18. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/testing/__init__.py +3 -0
  19. plone_exportimport-2.2.0/src/plone/exportimport/testing/configure.zcml +43 -0
  20. plone_exportimport-2.2.0/src/plone/exportimport/testing/content/dummy.py +43 -0
  21. plone_exportimport-2.2.0/src/plone/exportimport/testing/profiles/testing/metadata.xml +5 -0
  22. plone_exportimport-2.2.0/src/plone/exportimport/testing/profiles/testing/rolemap.xml +24 -0
  23. plone_exportimport-2.2.0/src/plone/exportimport/testing/profiles/testing/types/DummyContent.xml +53 -0
  24. plone_exportimport-2.2.0/src/plone/exportimport/testing/profiles/testing/types/Plone_Site.xml +11 -0
  25. plone_exportimport-2.2.0/src/plone/exportimport/testing/profiles/testing/types.xml +11 -0
  26. plone_exportimport-2.2.0/src/plone/exportimport/utils/cli.py +132 -0
  27. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/__init__.py +2 -0
  28. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/blocks.py +4 -2
  29. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/core.py +26 -0
  30. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/export_helpers.py +7 -3
  31. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/discussions.py +12 -4
  32. plone_exportimport-2.2.0/src/plone/exportimport/utils/path.py +85 -0
  33. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/portlets.py +7 -3
  34. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/redirects.py +9 -2
  35. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/relations.py +15 -1
  36. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/translations.py +12 -2
  37. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/PKG-INFO +38 -2
  38. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/SOURCES.txt +18 -0
  39. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/requires.txt +4 -0
  40. plone_exportimport-2.2.0/tests/_resources/cli_import/content/0e1d9d4a2c3b4f5e8a7b6c5d4e3f2a1b/data.json +34 -0
  41. plone_exportimport-2.2.0/tests/_resources/cli_import/content/__metadata__.json +14 -0
  42. plone_exportimport-2.2.0/tests/_resources/cli_import/content/plone_site_root/data.json +28 -0
  43. plone_exportimport-2.2.0/tests/cli/conftest.py +94 -0
  44. plone_exportimport-2.2.0/tests/cli/test_cli_exporter.py +51 -0
  45. plone_exportimport-2.2.0/tests/cli/test_cli_exporter_partial.py +82 -0
  46. plone_exportimport-2.2.0/tests/cli/test_cli_importer.py +81 -0
  47. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/conftest.py +12 -0
  48. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/conftest.py +1 -3
  49. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_content.py +96 -0
  50. plone_exportimport-2.2.0/tests/exporters/test_exporters_partial.py +137 -0
  51. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/conftest.py +1 -3
  52. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_content.py +106 -0
  53. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/services/test_services.py +10 -1
  54. plone_exportimport-2.2.0/tests/utils/conftest.py +0 -0
  55. plone_exportimport-2.2.0/tests/utils/test_utils_cli.py +89 -0
  56. plone_exportimport-2.2.0/tests/utils/test_utils_content_blocks.py +66 -0
  57. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_content_export_helpers.py +1 -0
  58. plone_exportimport-2.2.0/tests/utils/test_utils_path.py +105 -0
  59. plone_exportimport-2.0.2/src/plone/exportimport/utils/cli.py +0 -68
  60. plone_exportimport-2.0.2/src/plone/exportimport/utils/path.py +0 -21
  61. plone_exportimport-2.0.2/tests/utils/test_utils_path.py +0 -48
  62. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/.editorconfig +0 -0
  63. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/.flake8 +0 -0
  64. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/.gitignore +0 -0
  65. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/.meta.toml +0 -0
  66. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/.pre-commit-config.yaml +0 -0
  67. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/CONTRIBUTING.md +0 -0
  68. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/LICENSE +0 -0
  69. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/MANIFEST.in +0 -0
  70. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/Makefile +0 -0
  71. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/README.md +0 -0
  72. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/constraints.txt +0 -0
  73. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/instance.yaml +0 -0
  74. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/mx.ini +0 -0
  75. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/pyproject.toml +0 -0
  76. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/requirements.txt +0 -0
  77. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/setup.cfg +0 -0
  78. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/__init__.py +0 -0
  79. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/configure.zcml +0 -0
  80. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/deserializers/__init__.py +0 -0
  81. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/deserializers/blocks.py +0 -0
  82. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/deserializers/configure.zcml +0 -0
  83. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/__init__.py +0 -0
  84. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/exporters/principals.py +0 -0
  85. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/base.py +0 -0
  86. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/configure.zcml +0 -0
  87. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/discussions.py +0 -0
  88. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/final.py +0 -0
  89. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/portlets.py +0 -0
  90. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/principals.py +0 -0
  91. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/redirects.py +0 -0
  92. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/relations.py +0 -0
  93. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/importers/translations.py +0 -0
  94. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/permissions.zcml +0 -0
  95. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/serializers/__init__.py +0 -0
  96. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/serializers/blocks.py +0 -0
  97. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/serializers/configure.zcml +0 -0
  98. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/serializers/fields.py +0 -0
  99. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/services/__init__.py +0 -0
  100. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/services/configure.zcml +0 -0
  101. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/settings.py +0 -0
  102. /plone_exportimport-2.0.2/tests/_resources/empty_import/.gitkeep → /plone_exportimport-2.2.0/src/plone/exportimport/testing/content/__init__.py +0 -0
  103. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/types.py +0 -0
  104. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/__init__.py +0 -0
  105. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/import_helpers.py +0 -0
  106. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/content/revisions.py +0 -0
  107. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/dates.py +0 -0
  108. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/permissions.py +0 -0
  109. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/principals/__init__.py +0 -0
  110. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/principals/groups.py +0 -0
  111. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/principals/helpers.py +0 -0
  112. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/principals/members.py +0 -0
  113. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/zca.py +0 -0
  114. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone/exportimport/utils/zip_utils.py +0 -0
  115. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/dependency_links.txt +0 -0
  116. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/entry_points.txt +0 -0
  117. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/not-zip-safe +0 -0
  118. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/src/plone.exportimport.egg-info/top_level.txt +0 -0
  119. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/35661c9bb5be42c68f665aa1ed291418/data.json +0 -0
  120. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/3e0dd7c4b2714eafa1d6fc6a1493f953/data.json +0 -0
  121. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/3e0dd7c4b2714eafa1d6fc6a1493f953/file/plone.pdf +0 -0
  122. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/45b0b46f17104a7b8fa7bb94d3dd5bd9/data.json +0 -0
  123. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/5d77cd5686184ec49ab077017b1fbc3a/data.json +0 -0
  124. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/70844f7bec1843b8ab2796c972c9ebfe/data.json +0 -0
  125. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/7c1393f615c4447c80db0d784390c5b7/data.json +0 -0
  126. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/7c1393f615c4447c80db0d784390c5b7/image/brazil-coins.png +0 -0
  127. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/90b11c863598495ba699b22ca76b1041/data.json +0 -0
  128. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/90b11c863598495ba699b22ca76b1041/image/2025.png +0 -0
  129. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/__metadata__.json +0 -0
  130. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/e7359727ace64e609b79c4091c38822a/data.json +0 -0
  131. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/content/plone_site_root/data.json +0 -0
  132. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/discussions.json +0 -0
  133. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/portlets.json +0 -0
  134. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/principals.json +0 -0
  135. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/redirects.json +0 -0
  136. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/relations.json +0 -0
  137. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/base_import/translations.json +0 -0
  138. /plone_exportimport-2.0.2/tests/utils/conftest.py → /plone_exportimport-2.2.0/tests/_resources/empty_import/.gitkeep +0 -0
  139. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/01fbca4cccd448d880959ba93aa8176e/data.json +0 -0
  140. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/1cbf4ae73c74459485d3fd6cb9714e43/data.json +0 -0
  141. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/20737423549c43a88488242ec629e087/data.json +0 -0
  142. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/2be023c76b3e4e0aa9908c70f19a14df/data.json +0 -0
  143. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/2d099ac593344be7aaf2e4aafbba9065/data.json +0 -0
  144. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/3e3d4bf2cac4482f9c03e34b52092f02/data.json +0 -0
  145. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/496cbe4f17904d6bb9321b23f91dc8cc/data.json +0 -0
  146. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/59e06b23024a4c4b9d93672fe440e019/data.json +0 -0
  147. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/5ffe1d0c89c745eaab26df0e71ac82de/data.json +0 -0
  148. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/6d8a2b38d445462aa19f604b0801a13c/data.json +0 -0
  149. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/85a60611c2744e7abe53e3f356df236a/data.json +0 -0
  150. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/__metadata__.json +0 -0
  151. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/b69412b6d0b0438d85b340c13a39fc8c/data.json +0 -0
  152. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/cecc6f7007344d3cbac68f1da4bd084a/data.json +0 -0
  153. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/ddb27456033d4b86a78f3ec783874b63/data.json +0 -0
  154. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/f40d3a236b464aa487b715adf3c0be92/data.json +0 -0
  155. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/f9767fbdadf041bd892010d21b290cb5/data.json +0 -0
  156. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/f98b033fa8ce46f487ca8923ddae7480/data.json +0 -0
  157. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/fbcd98e2d1a741e2a90b8f0389203530/data.json +0 -0
  158. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/content/plone_site_root/data.json +0 -0
  159. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/discussions.json +0 -0
  160. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/principals.json +0 -0
  161. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/redirects.json +0 -0
  162. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/relations.json +0 -0
  163. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/multilingual_import/translations.json +0 -0
  164. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/portlets_import/portlets.json +0 -0
  165. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/_resources/principals_import/principals.json +0 -0
  166. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters.py +0 -0
  167. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_discussions.py +0 -0
  168. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_portlets.py +0 -0
  169. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_principals.py +0 -0
  170. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_redirects.py +0 -0
  171. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_relations.py +0 -0
  172. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/exporters/test_exporters_translations.py +0 -0
  173. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers.py +0 -0
  174. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_discussions.py +0 -0
  175. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_final.py +0 -0
  176. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_portlets.py +0 -0
  177. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_principals.py +0 -0
  178. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_redirects.py +0 -0
  179. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_relations.py +0 -0
  180. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/importers/test_importers_translations.py +0 -0
  181. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/services/__init__.py +0 -0
  182. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/services/conftest.py +0 -0
  183. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_content_core.py +0 -0
  184. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_content_revisions.py +0 -0
  185. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_dates.py +0 -0
  186. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_discussions.py +0 -0
  187. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_principals_groups.py +0 -0
  188. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_principals_members.py +0 -0
  189. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_redirects.py +0 -0
  190. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_relations.py +0 -0
  191. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tests/utils/test_utils_translations.py +0 -0
  192. {plone_exportimport-2.0.2 → plone_exportimport-2.2.0}/tox.ini +0 -0
@@ -9,6 +9,39 @@
9
9
 
10
10
  <!-- towncrier release notes start -->
11
11
 
12
+ ## 2.2.0 (2026-10-01)
13
+
14
+
15
+ ### New features
16
+
17
+ - Add `--path` and `--paths` options to `plone-exporter` to export only parts of a site. Relations, translations, discussions, portlets and redirects are limited to the exported content @ericof #86
18
+
19
+
20
+ ### Bug fixes
21
+
22
+ - Do not fail exporting content whose ``language`` is ``None``. Such an item serializes to ``"language": null``, and ``fix_language`` raised ``AttributeError: 'NoneType' object has no attribute 'get'``. It is now treated like a missing language and gets the default-language placeholder, which the importer resolves. @kunalKumar-13 #95
23
+ - Fixed export and import of grid blocks whose images have no ``image_scales`` information. @ericof #109
24
+ - Fixed ``plone-exporter`` silently skipping fields protected by a read permission not granted to the ``Manager`` role, such as behavior fields on the Plone Site root. Both command line tools now run with the site hook set and without overriding the roles of the system user. @ericof #110
25
+ - Do not silently drop file and image fields on import when a blob's file name has another Unicode normalization form on disk. Blob files are now exported under a normalized file name (NFC, without path separators or characters invalid on Windows), the import also finds the NFC form of a recorded blob path, and items that could not be fully deserialized are listed as imported with errors, making `plone-importer` exit with status 1 @ericof #114
26
+
27
+
28
+ ### Internal
29
+
30
+ - Declared ``plone.autoform``, ``plone.behavior``, ``plone.supermodel`` and ``Products.GenericSetup`` as dependencies. @ericof
31
+
32
+
33
+ ### Tests
34
+
35
+ - Fix intermittent failure of the services export/import roundtrip test, which compared ZIP archives including file timestamps @ericof
36
+ - Fixed test isolation with pytest-plone 1.1.0 by keeping importers and fixtures from committing transactions during tests. @ericof
37
+
38
+ ## 2.1.0 (2026-09-10)
39
+
40
+
41
+ ### New features
42
+
43
+ - Make it easy to customize which content gets exported @gforcada #86
44
+
12
45
  ## 2.0.2 (2026-08-21)
13
46
 
14
47
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plone.exportimport
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: Plone content export / import support
5
5
  Home-page: https://plone.org
6
6
  Author: Plone Foundation
@@ -17,7 +17,6 @@ Classifier: Framework :: Plone
17
17
  Classifier: Framework :: Plone :: 6.2
18
18
  Classifier: Framework :: Plone :: Core
19
19
  Classifier: Framework :: Zope :: 5
20
- Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
21
20
  Classifier: Operating System :: OS Independent
22
21
  Classifier: Programming Language :: Python
23
22
  Classifier: Programming Language :: Python :: 3.10
@@ -35,14 +34,18 @@ Requires-Dist: plone.app.dexterity
35
34
  Requires-Dist: plone.app.redirector
36
35
  Requires-Dist: plone.app.textfield
37
36
  Requires-Dist: plone.app.users
37
+ Requires-Dist: plone.autoform
38
38
  Requires-Dist: plone.base
39
+ Requires-Dist: plone.behavior
39
40
  Requires-Dist: plone.dexterity
40
41
  Requires-Dist: plone.namedfile
41
42
  Requires-Dist: plone.restapi
43
+ Requires-Dist: plone.supermodel
42
44
  Requires-Dist: plone.uuid
43
45
  Requires-Dist: Products.CMFEditions
44
46
  Requires-Dist: Products.CMFPlone
45
47
  Requires-Dist: Products.DCWorkflow
48
+ Requires-Dist: Products.GenericSetup
46
49
  Requires-Dist: Products.PlonePAS
47
50
  Requires-Dist: Products.PortalTransforms
48
51
  Requires-Dist: z3c.relationfield
@@ -241,6 +244,39 @@ The project is licensed under the GPLv2.
241
244
 
242
245
  <!-- towncrier release notes start -->
243
246
 
247
+ ## 2.2.0 (2026-10-01)
248
+
249
+
250
+ ### New features
251
+
252
+ - Add `--path` and `--paths` options to `plone-exporter` to export only parts of a site. Relations, translations, discussions, portlets and redirects are limited to the exported content @ericof #86
253
+
254
+
255
+ ### Bug fixes
256
+
257
+ - Do not fail exporting content whose ``language`` is ``None``. Such an item serializes to ``"language": null``, and ``fix_language`` raised ``AttributeError: 'NoneType' object has no attribute 'get'``. It is now treated like a missing language and gets the default-language placeholder, which the importer resolves. @kunalKumar-13 #95
258
+ - Fixed export and import of grid blocks whose images have no ``image_scales`` information. @ericof #109
259
+ - Fixed ``plone-exporter`` silently skipping fields protected by a read permission not granted to the ``Manager`` role, such as behavior fields on the Plone Site root. Both command line tools now run with the site hook set and without overriding the roles of the system user. @ericof #110
260
+ - Do not silently drop file and image fields on import when a blob's file name has another Unicode normalization form on disk. Blob files are now exported under a normalized file name (NFC, without path separators or characters invalid on Windows), the import also finds the NFC form of a recorded blob path, and items that could not be fully deserialized are listed as imported with errors, making `plone-importer` exit with status 1 @ericof #114
261
+
262
+
263
+ ### Internal
264
+
265
+ - Declared ``plone.autoform``, ``plone.behavior``, ``plone.supermodel`` and ``Products.GenericSetup`` as dependencies. @ericof
266
+
267
+
268
+ ### Tests
269
+
270
+ - Fix intermittent failure of the services export/import roundtrip test, which compared ZIP archives including file timestamps @ericof
271
+ - Fixed test isolation with pytest-plone 1.1.0 by keeping importers and fixtures from committing transactions during tests. @ericof
272
+
273
+ ## 2.1.0 (2026-09-10)
274
+
275
+
276
+ ### New features
277
+
278
+ - Make it easy to customize which content gets exported @gforcada #86
279
+
244
280
  ## 2.0.2 (2026-08-21)
245
281
 
246
282
 
@@ -8,7 +8,7 @@ long_description = f"""
8
8
 
9
9
  setup(
10
10
  name="plone.exportimport",
11
- version="2.0.2",
11
+ version="2.2.0",
12
12
  description="Plone content export / import support",
13
13
  long_description=long_description,
14
14
  long_description_content_type="text/markdown",
@@ -19,7 +19,6 @@ setup(
19
19
  "Framework :: Plone :: 6.2",
20
20
  "Framework :: Plone :: Core",
21
21
  "Framework :: Zope :: 5",
22
- "License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
23
22
  "Operating System :: OS Independent",
24
23
  "Programming Language :: Python",
25
24
  "Programming Language :: Python :: 3.10",
@@ -50,14 +49,18 @@ setup(
50
49
  "plone.app.redirector",
51
50
  "plone.app.textfield",
52
51
  "plone.app.users",
52
+ "plone.autoform",
53
53
  "plone.base",
54
+ "plone.behavior",
54
55
  "plone.dexterity",
55
56
  "plone.namedfile",
56
57
  "plone.restapi",
58
+ "plone.supermodel",
57
59
  "plone.uuid",
58
60
  "Products.CMFEditions",
59
61
  "Products.CMFPlone",
60
62
  "Products.DCWorkflow",
63
+ "Products.GenericSetup",
61
64
  "Products.PlonePAS",
62
65
  "Products.PortalTransforms",
63
66
  "z3c.relationfield",
@@ -1,4 +1,3 @@
1
- from plone import api
2
1
  from plone.exportimport import logger as package_logger
3
2
  from plone.exportimport.exporters import get_exporter
4
3
  from plone.exportimport.importers import get_importer
@@ -13,19 +12,44 @@ CLI_SPEC = {
13
12
  "exporter": {
14
13
  "description": "Export Plone Site content",
15
14
  "options": {
16
- "zopeconf": "Path to zope.conf",
17
- "site": "Plone site ID or path to site to export the content from",
18
- "path": "Path to export the content",
19
- "--include-revisions": "Include revision history",
15
+ "zopeconf": {"help": "Path to zope.conf"},
16
+ "site": {
17
+ "help": "Plone site ID or path to site to export the content from"
18
+ },
19
+ "path": {"help": "Path to export the content"},
20
+ "--include-revisions": {
21
+ "action": "store_true",
22
+ "help": "Include revision history",
23
+ },
24
+ "--path": {
25
+ "action": "append",
26
+ "dest": "include_paths",
27
+ "metavar": "CONTENT_PATH",
28
+ "help": (
29
+ "Export only this content, and everything inside it. "
30
+ "Relative to the site root. Can be repeated"
31
+ ),
32
+ },
33
+ "--paths": {
34
+ "dest": "paths_file",
35
+ "metavar": "FILE",
36
+ "help": (
37
+ "File with the content paths to export, one per line. "
38
+ "Empty lines and lines starting with # are ignored"
39
+ ),
40
+ },
20
41
  },
21
42
  },
22
43
  "importer": {
23
44
  "description": "Import content into a Plone Site",
24
45
  "options": {
25
- "zopeconf": "Path to zope.conf",
26
- "site": "Plone site ID or path to site to import the content to",
27
- "path": "Path to import the content from",
28
- "--quiet": "Do not report items being imported",
46
+ "zopeconf": {"help": "Path to zope.conf"},
47
+ "site": {"help": "Plone site ID or path to site to import the content to"},
48
+ "path": {"help": "Path to import the content from"},
49
+ "--quiet": {
50
+ "action": "store_true",
51
+ "help": "Do not report items being imported",
52
+ },
29
53
  },
30
54
  },
31
55
  }
@@ -33,11 +57,8 @@ CLI_SPEC = {
33
57
 
34
58
  def _parse_args(description: str, options: dict, args: list):
35
59
  parser = argparse.ArgumentParser(description=description)
36
- for key, help in options.items():
37
- if key.startswith("-"):
38
- parser.add_argument(key, action="store_true", help=help)
39
- else:
40
- parser.add_argument(key, help=help)
60
+ for key, kwargs in options.items():
61
+ parser.add_argument(key, **kwargs)
41
62
  namespace, _ = parser.parse_known_args(args[1:])
42
63
  return namespace
43
64
 
@@ -54,7 +75,10 @@ def exporter_cli(args=sys.argv):
54
75
  logger.error(f"{namespace.path} does not exist, please create it first.")
55
76
  sys.exit(1)
56
77
  site = cli_helpers.get_site(app, namespace.site, logger)
57
- with api.env.adopt_roles(["Manager"]):
78
+ namespace.query = cli_helpers.get_export_query(
79
+ site, namespace.include_paths, namespace.paths_file, logger
80
+ )
81
+ with hooks.site(site):
58
82
  results = get_exporter(site).export_site(path, options=namespace)
59
83
  logger.info(f" Using path {path} to export content from Plone site at /{site.id}")
60
84
  for item in results[1:]:
@@ -75,9 +99,13 @@ def importer_cli(args=sys.argv):
75
99
  if not namespace.quiet:
76
100
  cli_helpers.setup_logger_console(package_logger)
77
101
  site = cli_helpers.get_site(app, namespace.site, logger)
78
- with hooks.site(site), api.env.adopt_roles(["Manager"]):
102
+ with hooks.site(site):
79
103
  logger.info(f" Using path {path} to import content to Plone site at /{site.id}")
80
- results = get_importer(site).import_site(path)
104
+ importer = get_importer(site)
105
+ results = importer.import_site(path)
81
106
  for item in results:
82
107
  logger.info(f" - {item}")
83
108
  transaction.commit()
109
+ if incomplete := importer.incomplete:
110
+ logger.error(f" {len(incomplete)} items were imported with errors")
111
+ sys.exit(1)
@@ -11,16 +11,29 @@ from zope.globalrequest import getRequest
11
11
  from zope.interface import implementer
12
12
 
13
13
  import codecs
14
+ import unicodedata
14
15
 
15
16
 
16
17
  def load_blob(path: str) -> bytes:
17
- """Load blob from fs and encode it as base64."""
18
+ """Load blob from fs and encode it as base64.
19
+
20
+ If no file exists at ``path``, the NFC form of ``path`` is tried as well:
21
+ exports may record the name in NFD while the file on disk is NFC (for
22
+ instance, after a commit with git's ``core.precomposeunicode``), and only
23
+ some filesystems treat both forms as the same name.
24
+
25
+ :param path: Blob path, relative to the content import directory.
26
+ :returns: Contents of the blob file.
27
+ :raises ValueError: If the blob file does not exist.
28
+ """
18
29
  request = getRequest()
19
30
  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)
31
+ blob_path = content_import_path / path
32
+ if not blob_path.exists():
33
+ blob_path = content_import_path / unicodedata.normalize("NFC", path)
34
+ if not blob_path.exists():
35
+ raise ValueError(f"Blob not found at {content_import_path / path}")
36
+ data = path_utils.encode_file_contents(blob_path)
24
37
  return codecs.decode(data, "base64")
25
38
 
26
39
 
@@ -14,10 +14,10 @@ import json
14
14
  class BaseExporter:
15
15
  name: str
16
16
  base_path: Path
17
- errors: list = None
18
- request: types.HTTPRequest = None
19
- data_hooks: list[Callable] = None
20
- obj_hooks: list[Callable] = None
17
+ errors: list
18
+ request: types.HTTPRequest
19
+ data_hooks: list[Callable] | None = None
20
+ obj_hooks: list[Callable] | None = None
21
21
  options: argparse.Namespace | None = None
22
22
 
23
23
  def __init__(
@@ -31,6 +31,18 @@ class BaseExporter:
31
31
  def get_option(self, name, default=None):
32
32
  return getattr(self.options, name, default)
33
33
 
34
+ def exported_content(self) -> dict[str, str] | None:
35
+ """Return the content selected for a partial export.
36
+
37
+ A partial export is requested by setting ``options.query``.
38
+ Exporters use this to keep only data related to the exported content.
39
+
40
+ :returns: Mapping of UID to physical path of the exported content, or
41
+ ``None`` when the whole site is exported.
42
+ """
43
+ query = self.get_option("query")
44
+ return utils.get_content_paths(query) if query else None
45
+
34
46
  @property
35
47
  def filepath(self) -> Path:
36
48
  """Filepath to be used during export."""
@@ -63,9 +75,10 @@ class BaseExporter:
63
75
  def export_data(
64
76
  self,
65
77
  base_path: Path,
66
- data_hooks: list[Callable] = None,
67
- obj_hooks: list[Callable] = None,
78
+ data_hooks: list[Callable] | None = None,
79
+ obj_hooks: list[Callable] | None = None,
68
80
  options: argparse.Namespace | None = None,
81
+ **kwargs: Any,
69
82
  ) -> list[Path]:
70
83
  """Write data to filesystem."""
71
84
  if not base_path.exists():
@@ -15,6 +15,11 @@
15
15
  for="plone.base.interfaces.siteroot.IPloneSiteRoot"
16
16
  name="plone.exporter.content"
17
17
  />
18
+ <adapter
19
+ factory=".content.ObjectsExporter"
20
+ provides="plone.exportimport.interfaces.IObjectsExporter"
21
+ for="plone.base.interfaces.siteroot.IPloneSiteRoot"
22
+ />
18
23
  <adapter
19
24
  factory=".principals.PrincipalsExporter"
20
25
  provides="plone.exportimport.interfaces.INamedExporter"
@@ -12,26 +12,22 @@ from plone.exportimport import types
12
12
  from plone.exportimport.interfaces import IExportImportRequestMarker
13
13
  from plone.exportimport.utils import content as content_utils
14
14
  from plone.exportimport.utils import request_provides
15
+ from typing import Any
16
+ from zope.component import getAdapter
15
17
  from zope.interface import implementer
16
18
 
17
19
  import argparse
18
20
 
19
21
 
20
- @implementer(interfaces.INamedExporter)
21
- class ContentExporter(BaseExporter):
22
- name: str = "content"
23
- query: dict = None
24
- filename_fmt: str = settings.EXPORT_CONTENT_FILEPATH
25
- metadata: types.ExportImportMetadata = None
26
- default_site_language: str = "en"
22
+ class ObjectsExporter:
27
23
 
28
- def all_objects(self) -> Generator:
24
+ def __init__(self, obj) -> None:
25
+ self.obj = obj
26
+
27
+ def get_objects(self, query, errors) -> Generator:
29
28
  """Return all objects to be serialized."""
30
- query = self.query
31
29
  catalog = api.portal.get_tool("portal_catalog")
32
- if "object_provides" not in query:
33
- query["object_provides"] = "plone.dexterity.interfaces.IDexterityContent"
34
- brains = catalog.unrestrictedSearchResults(**query)
30
+ brains = catalog.unrestrictedSearchResults(**content_utils.content_query(query))
35
31
  logger.info(f"Exporting {len(brains)}")
36
32
  for index, brain in enumerate(brains, start=1):
37
33
  try:
@@ -39,7 +35,7 @@ class ContentExporter(BaseExporter):
39
35
  except Exception:
40
36
  brain_path = brain.getPath()
41
37
  msg = f"Error getting object {brain_path} from brain"
42
- self.errors.append({"path": brain_path, "message": msg})
38
+ errors.append({"path": brain_path, "message": msg})
43
39
  logger.exception(msg, exc_info=True)
44
40
  else:
45
41
  yield obj
@@ -47,6 +43,20 @@ class ContentExporter(BaseExporter):
47
43
  if not index % 100:
48
44
  logger.info(f"Content Exporter: Handled {index} items...")
49
45
 
46
+
47
+ @implementer(interfaces.INamedExporter)
48
+ class ContentExporter(BaseExporter):
49
+ name: str = "content"
50
+ query: dict = None
51
+ filename_fmt: str = settings.EXPORT_CONTENT_FILEPATH
52
+ metadata: types.ExportImportMetadata = None
53
+ default_site_language: str = "en"
54
+
55
+ def all_objects(self) -> Generator:
56
+ """Return all objects to be serialized."""
57
+ adapter = getAdapter(self.site, interfaces.IObjectsExporter)
58
+ yield from adapter.get_objects(self.query, self.errors)
59
+
50
60
  def serialize(self, obj: DexterityContent) -> dict:
51
61
  """Serialize object."""
52
62
  obj_uid = content_utils.get_uid(obj)
@@ -142,19 +152,48 @@ class ContentExporter(BaseExporter):
142
152
  paths.insert(0, self.dump_metadata())
143
153
  return paths
144
154
 
155
+ def _prepare_query(
156
+ self, query: dict | None = None, options: argparse.Namespace | None = None
157
+ ) -> dict:
158
+ """Return the catalog query used to select the content to export.
159
+
160
+ An explicit ``query`` takes precedence over ``options.query``. When
161
+ neither is set, all content in the site is selected.
162
+
163
+ :param query: Explicit catalog query.
164
+ :param options: Export options, checked for a ``query`` attribute.
165
+ :returns: The catalog query to use.
166
+ """
167
+ query = query or getattr(options, "query", None)
168
+ return query if query else {"path": content_utils.get_obj_path(self.site)}
169
+
145
170
  def export_data(
146
171
  self,
147
172
  base_path: Path,
148
- data_hooks: list[Callable] = None,
149
- obj_hooks: list[Callable] = None,
150
- query: dict | None = None,
173
+ data_hooks: list[Callable] | None = None,
174
+ obj_hooks: list[Callable] | None = None,
151
175
  options: argparse.Namespace | None = None,
176
+ query: dict | None = None,
177
+ **kwargs: Any,
152
178
  ) -> list[Path]:
179
+ """Export content to a ``content`` folder inside ``base_path``.
180
+
181
+ Arguments other than ``base_path`` should be passed by keyword, as
182
+ ``query`` comes after ``options`` in this signature.
183
+
184
+ :param base_path: Directory where the export is written.
185
+ :param data_hooks: Callables applied to the serialized data.
186
+ :param obj_hooks: Callables applied to each object before serialization.
187
+ :param options: Export options; ``options.query`` is used when no
188
+ ``query`` is passed.
189
+ :param query: Catalog query selecting the content to export. It takes
190
+ precedence over ``options.query``. Defaults to all content in the site.
191
+ :returns: Paths of the files written.
192
+ """
153
193
  # Content in a subpath of base_path
154
194
  base_path = base_path / self.name
155
- query = query if query else {}
156
195
  site = self.site
157
- self.query = query if query else {"path": content_utils.get_obj_path(site)}
196
+ self.query = self._prepare_query(query, options)
158
197
  metadata = types.ExportImportMetadata()
159
198
  self.metadata = metadata
160
199
  self.request[settings.EXPORT_CONTENT_METADATA_KEY] = metadata
@@ -24,7 +24,9 @@ class DiscussionsExporter(BaseExporter):
24
24
 
25
25
  from plone.exportimport.utils import discussions as utils
26
26
 
27
- discussions = utils.get_discussions()
27
+ content = self.exported_content()
28
+ uids = set(content) if content is not None else None
29
+ discussions = utils.get_discussions(uids=uids)
28
30
  filepath = self._dump(discussions, self.filepath)
29
31
  logger.debug(
30
32
  f"- Discussions: Wrote {len(discussions)} discussions to {filepath}"
@@ -12,7 +12,9 @@ class PortletsExporter(BaseExporter):
12
12
 
13
13
  def dump(self) -> list[Path]:
14
14
  """Serialize object and dump it to disk."""
15
- portlets = utils.get_portlets()
15
+ content = self.exported_content()
16
+ uids = set(content) if content is not None else None
17
+ portlets = utils.get_portlets(uids=uids)
16
18
  filepath = self._dump(portlets, self.filepath)
17
19
  logger.debug(f"- Portlets: Wrote {len(portlets)} portlets to {filepath}")
18
20
  return [filepath]
@@ -12,7 +12,9 @@ class RedirectsExporter(BaseExporter):
12
12
 
13
13
  def dump(self) -> list[Path]:
14
14
  """Serialize object and dump it to disk."""
15
- redirects = utils.get_redirects()
15
+ content = self.exported_content()
16
+ paths = set(content.values()) if content is not None else None
17
+ redirects = utils.get_redirects(paths=paths)
16
18
  filepath = self._dump(redirects, self.filepath)
17
19
  logger.debug(f"- Redirects: Wrote {len(redirects)} redirects to {filepath}")
18
20
  return [filepath]
@@ -12,7 +12,9 @@ class RelationsExporter(BaseExporter):
12
12
 
13
13
  def dump(self) -> list[Path]:
14
14
  """Serialize object and dump it to disk."""
15
- relations = utils.get_relations()
15
+ content = self.exported_content()
16
+ uids = set(content) if content is not None else None
17
+ relations = utils.get_relations(uids=uids)
16
18
  filepath = self._dump(relations, self.filepath)
17
19
  logger.debug(f"- Relations: Wrote {len(relations)} relations to {filepath}")
18
20
  return [filepath]
@@ -12,7 +12,9 @@ class TranslationsExporter(BaseExporter):
12
12
 
13
13
  def dump(self) -> list[Path]:
14
14
  """Serialize object and dump it to disk."""
15
- translations = utils.get_translations()
15
+ content = self.exported_content()
16
+ uids = set(content) if content is not None else None
17
+ translations = utils.get_translations(uids=uids)
16
18
  filepath = self._dump(translations, self.filepath)
17
19
  logger.debug(
18
20
  f"- Translations: Wrote {len(translations)} translations to {filepath}"
@@ -55,6 +55,14 @@ class Importer:
55
55
  report.append(importer.import_data(path))
56
56
  return report
57
57
 
58
+ @property
59
+ def incomplete(self) -> dict[str, list[str]]:
60
+ """Items imported with errors, mapping each path to its failed fields."""
61
+ result: dict[str, list[str]] = {}
62
+ for importer in self.importers.values():
63
+ result.update(getattr(importer, "incomplete", {}))
64
+ return result
65
+
58
66
 
59
67
  def get_importer(site: PloneSite | None = None) -> Importer:
60
68
  """Get the importer."""
@@ -13,11 +13,41 @@ from plone.exportimport.utils import request_provides
13
13
  import json
14
14
 
15
15
 
16
+ def _failed_fields(exc: Exception) -> list[str]:
17
+ """Return the names of the fields reported in a deserialization error.
18
+
19
+ plone.restapi collects field errors and raises a single
20
+ :class:`zExceptions.BadRequest` whose first argument is a list of dicts,
21
+ each with an optional ``field`` key.
22
+
23
+ :param exc: Exception raised by the deserializer.
24
+ :returns: Sorted field names, or an empty list if the error does not
25
+ name any field.
26
+ """
27
+ errors = exc.args[0] if exc.args else None
28
+ if not isinstance(errors, list):
29
+ return []
30
+ fields = {
31
+ error["field"]
32
+ for error in errors
33
+ if isinstance(error, dict) and error.get("field")
34
+ }
35
+ return sorted(fields)
36
+
37
+
16
38
  class ContentImporter(BaseImporter):
17
39
  name: str = "content"
18
40
  metadata: types.ExportImportMetadata | None = None
19
41
  languages: types.PortalLanguages | None = None
20
- dropped: set[str] = set()
42
+ dropped: set[str]
43
+ incomplete: dict[str, list[str]]
44
+
45
+ def __init__(self, site):
46
+ super().__init__(site)
47
+ # Items not created at all
48
+ self.dropped = set()
49
+ # Items created, but not fully deserialized: path -> failed fields
50
+ self.incomplete = {}
21
51
 
22
52
  def _cleanse_ordering(self, raw_data: dict[str, int]) -> dict[str, int]:
23
53
  """Prepare ordering data before deserialization.
@@ -60,6 +90,8 @@ class ContentImporter(BaseImporter):
60
90
  logger.error(
61
91
  f"{config.logger_prefix} Error deserializing {obj}", exc_info=exc
62
92
  )
93
+ # The object exists, but some of its data was not applied
94
+ self.incomplete[data["@id"]] = _failed_fields(exc)
63
95
  return obj
64
96
 
65
97
  def construct(self, item: dict) -> DexterityContent | None:
@@ -210,6 +242,12 @@ class ContentImporter(BaseImporter):
210
242
  logger.warning("List of items dropped during import")
211
243
  for item_path in dropped:
212
244
  logger.warning(f" - {item_path}")
245
+ # Report items that were created with missing data
246
+ if self.incomplete:
247
+ logger.error("List of items imported with errors")
248
+ for item_path, fields in sorted(self.incomplete.items()):
249
+ details = f" (fields: {', '.join(fields)})" if fields else ""
250
+ logger.error(f" - {item_path}{details}")
213
251
  return result
214
252
 
215
253
  def start(self):
@@ -21,3 +21,7 @@ class INamedExporter(Interface):
21
21
 
22
22
  class INamedImporter(Interface):
23
23
  """Component to import content from a Plone Site."""
24
+
25
+
26
+ class IObjectsExporter(Interface):
27
+ """Component to decide which objects are exported from a Plone Site."""
@@ -78,6 +78,11 @@ def export_blob(
78
78
 
79
79
  Root path: settings.EXPORT_PATH_KEY
80
80
  File path: settings.EXPORT_CONTENT_BLOB_FILEPATH
81
+
82
+ The file name is normalized with
83
+ :func:`plone.exportimport.utils.path.normalize_filename`, so the path
84
+ written to disk, the returned path and the ``_blob_files_`` entry are the
85
+ same on every filesystem.
81
86
  """
82
87
  request = getRequest()
83
88
  # Metadata
@@ -85,7 +90,7 @@ def export_blob(
85
90
  uid = settings.SITE_ROOT_UID if content_utils.is_site_root(obj) else obj.UID()
86
91
  content_export_path = Path(request[settings.EXPORT_PATH_KEY])
87
92
  blob_path = settings.EXPORT_CONTENT_BLOB_FILEPATH.format(
88
- UID=uid, fieldname=fieldname, filename=filename
93
+ UID=uid, fieldname=fieldname, filename=path_utils.normalize_filename(filename)
89
94
  )
90
95
  target_file = content_export_path / blob_path
91
96
  # Create target directory
@@ -10,14 +10,17 @@ class ExportImportLayer(PloneSandboxLayer):
10
10
 
11
11
  def setUpZope(self, app, configurationContext):
12
12
  import plone.exportimport
13
+ import plone.exportimport.testing
13
14
  import plone.volto
14
15
 
15
16
  self.loadZCML(package=plone.volto)
16
17
  self.loadZCML(package=plone.exportimport)
18
+ self.loadZCML(package=plone.exportimport.testing)
17
19
 
18
20
  def setUpPloneSite(self, portal):
19
21
  st = portal.portal_setup
20
22
  st.runAllImportStepsFromProfile("plone.volto:default")
23
+ st.runAllImportStepsFromProfile("plone.exportimport.testing:testing")
21
24
 
22
25
  # Enable plone.constraintypes behavior,
23
26
  # which is not enabled by default in plone.volto