Skip to content

Commit 1d3f5d3

Browse files
authored
Merge pull request #175 from collective/support_iterator
2 parents f37abfc + fe46627 commit 1d3f5d3

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ Changelog
1919
- Create documents as containers for items without parent when documents are folderish.
2020
[JeffersonBledsoe]
2121

22+
- Add support for passing any iterator as data-source to the import.
23+
[pbauer]
24+
25+
- Add example for importing collective.jsonify data to documentation.
26+
[pbauer]
27+
28+
2229
1.7 (2023-01-20)
2330
----------------
2431

README.rst

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,6 +1864,265 @@ See https://6.docs.plone.org/backend/upgrading/version-specific-migration/migrat
18641864
return soup.decode()
18651865
18661866
1867+
Migrate very old Plone Versions with data created by collective.jsonify
1868+
-----------------------------------------------------------------------
1869+
1870+
Versions older than Plone 4 do not support ``plone.restapi`` which is required to serialize the content used by ``collective.exportimport``.
1871+
1872+
To migrate Plone 1, 2 and 3 to Plone 6 you can use ``collective.jsonify`` for the export and ``collective.exportimport`` for the import.
1873+
1874+
Export
1875+
******
1876+
1877+
Use https://github.com/collective/collective.jsonify to export content.
1878+
1879+
You include the methods of ``collective.jsonify`` using `External Methods`.
1880+
See https://github.com/collective/collective.jsonify/blob/master/docs/install.rst for more info.
1881+
1882+
To work better with ``collective.exportimport`` you could extend the exported data using the feature ``additional_wrappers``.
1883+
Add info on the parent of an item to make it easier for ``collective.exportimport`` to import the data.
1884+
1885+
Here is a full example for `json_methods.py` which should be in `BUILDOUT_ROOT/parts/instance/Extensions/`
1886+
1887+
.. code-block:: python
1888+
1889+
def extend_item(obj, item):
1890+
"""Extend to work better well with collective.exportimport"""
1891+
from Acquisition import aq_parent
1892+
parent = aq_parent(obj)
1893+
item["parent"] = {
1894+
"@id": parent.absolute_url(),
1895+
"@type": getattr(parent, "portal_type", None),
1896+
}
1897+
if getattr(parent.aq_base, "UID", None) is not None:
1898+
item["parent"]["UID"] = parent.UID()
1899+
1900+
return item
1901+
1902+
1903+
Here is a full example for ``json_methods.py`` which should be in ``<BUILDOUT_ROOT>/parts/instance/Extensions/``
1904+
1905+
.. code-block:: python
1906+
1907+
from collective.jsonify.export import export_content as export_content_orig
1908+
from collective.jsonify.export import get_item
1909+
1910+
EXPORTED_TYPES = [
1911+
"Folder",
1912+
"Document",
1913+
"News Item",
1914+
"Event",
1915+
"Link",
1916+
"Topic",
1917+
"File",
1918+
"Image",
1919+
"RichTopic",
1920+
]
1921+
1922+
EXTRA_SKIP_PATHS = [
1923+
"/Plone/archiv/",
1924+
"/Plone/do-not-import/",
1925+
]
1926+
1927+
# Path from which to continue the export.
1928+
# The export walks the whole site respecting the order.
1929+
# It will ignore everything untill this path is reached.
1930+
PREVIOUS = ""
1931+
1932+
def export_content(self):
1933+
return export_content_orig(
1934+
self,
1935+
basedir="/var/lib/zope/json",
1936+
skip_callback=skip_item,
1937+
extra_skip_classname=[],
1938+
extra_skip_id=[],
1939+
extra_skip_paths=EXTRA_SKIP_PATHS,
1940+
batch_start=0,
1941+
batch_size=10000,
1942+
batch_previous_path=PREVIOUS or None,
1943+
)
1944+
1945+
def skip_item(item):
1946+
"""Return True if the item should be skipped"""
1947+
portal_type = getattr(item, "portal_type", None)
1948+
if portal_type not in EXPORTED_TYPES:
1949+
return True
1950+
1951+
def extend_item(obj, item):
1952+
"""Extend to work better well with collective.exportimport"""
1953+
from Acquisition import aq_parent
1954+
parent = aq_parent(obj)
1955+
item["parent"] = {
1956+
"@id": parent.absolute_url(),
1957+
"@type": getattr(parent, "portal_type", None),
1958+
}
1959+
if getattr(parent.aq_base, "UID", None) is not None:
1960+
item["parent"]["UID"] = parent.UID()
1961+
1962+
return item
1963+
1964+
To use these create three "External Method" in the ZMI root at the Zope root to use that:
1965+
1966+
* id: "export_content", module name: "json_methods", function name: "export_content"
1967+
* id: "get_item", module name: "json_methods", function name: "get_item"
1968+
* id: "extend_item", module name: "json_methods", function name: "extend_item"
1969+
1970+
Then you can pass the extender to the export using a query-string: http://localhost:8080/Plone/export_content?additional_wrappers=extend_item
1971+
1972+
1973+
Import
1974+
******
1975+
1976+
Two issues need to be dealt with to allow ``collective.exportimport`` to import the data generated by ``collective.jsonify``.
1977+
1978+
#. The data is in directories instead of in one large json-file.
1979+
#. The json is not in the expected format.
1980+
1981+
Starting with version 1.8 you can pass an iterator to the import.
1982+
1983+
You need to create a directory-walker that sorts the json-files the right way.
1984+
By default it would import them in the order `1.json`, `10.json`, `100.json`, `101.json` and so on.
1985+
1986+
.. code-block:: python
1987+
1988+
from pathlib import Path
1989+
1990+
def filesystem_walker(path=None):
1991+
root = Path(path)
1992+
assert(root.is_dir())
1993+
folders = sorted([i for i in root.iterdir() if i.is_dir() and i.name.isdecimal()], key=lambda i: int(i.name))
1994+
for folder in folders:
1995+
json_files = sorted([i for i in folder.glob("*.json") if i.stem.isdecimal()], key=lambda i: int(i.stem))
1996+
for json_file in json_files:
1997+
logger.debug("Importing %s", json_file)
1998+
item = json.loads(json_file.read_text())
1999+
item["json_file"] = str(json_file)
2000+
item = prepare_data(item)
2001+
if item:
2002+
yield item
2003+
2004+
The walker takes the path to be the root with one or more directories holding the json-files.
2005+
The sorting of the files is done using the number in the filename.
2006+
2007+
The method ``prepare_data`` modifies the data before passing it to the import.
2008+
A very similar task is done by ``collective.exportimport`` during export.
2009+
2010+
.. code-block:: python
2011+
2012+
def prepare_data(item):
2013+
"""modify jsonify data to work with c.exportimport"""
2014+
2015+
# Drop relationfields or defer the import
2016+
item.pop("relatedItems", None)
2017+
2018+
mapping = {
2019+
# jsonify => exportimport
2020+
"_uid": "UID",
2021+
"_type": "@type",
2022+
"_path": "@id",
2023+
"_layout": "layout",
2024+
# AT fieldnames => DX fieldnames
2025+
"excludeFromNav": "exclude_from_nav",
2026+
"allowDiscussion": "allow_discussion",
2027+
"subject": "subjects",
2028+
"expirationDate": "expires",
2029+
"effectiveDate": "effective",
2030+
"creation_date": "created",
2031+
"modification_date": "modified",
2032+
"startDate": "start",
2033+
"endDate": "end",
2034+
"openEnd": "open_end",
2035+
"eventUrl": "event_url",
2036+
"wholeDay": "whole_day",
2037+
"contactEmail": "contact_email",
2038+
"contactName": "contact_name",
2039+
"contactPhone": "contact_phone",
2040+
"imageCaption": "image_caption",
2041+
}
2042+
for old, new in mapping.items():
2043+
item = migrate_field(item, old, new)
2044+
2045+
if item.get("constrainTypesMode", None) == 1:
2046+
item = migrate_field(item, "constrainTypesMode", "constrain_types_mode")
2047+
else:
2048+
item.pop("locallyAllowedTypes", None)
2049+
item.pop("immediatelyAddableTypes", None)
2050+
item.pop("constrainTypesMode", None)
2051+
2052+
if "id" not in item:
2053+
item["id"] = item["_id"]
2054+
return item
2055+
2056+
2057+
def migrate_field(item, old, new):
2058+
if item.get(old, _marker) is not _marker:
2059+
item[new] = item.pop(old)
2060+
return item
2061+
2062+
You can pass the generator ``filesystem_walker`` to the import:
2063+
2064+
.. code-block:: python
2065+
2066+
class ImportAll(BrowserView):
2067+
2068+
def __call__(self):
2069+
# ...
2070+
cfg = getConfiguration()
2071+
directory = Path(cfg.clienthome) / "import"
2072+
2073+
# import content
2074+
view = api.content.get_view("import_content", portal, request)
2075+
request.form["form.submitted"] = True
2076+
request.form["commit"] = 1000
2077+
view(iterator=filesystem_walker(directory / "mydata"))
2078+
2079+
# import default-pages
2080+
import_deferred = api.content.get_view("import_deferred", portal, request)
2081+
import_deferred()
2082+
2083+
2084+
class ImportDeferred(BrowserView):
2085+
2086+
def __call__(self):
2087+
self.title = "Import Deferred Settings (default pages)"
2088+
if not self.request.form.get("form.submitted", False):
2089+
return self.index()
2090+
2091+
for brain in api.content.find(portal_type="Folder"):
2092+
obj = brain.getObject()
2093+
annotations = IAnnotations(obj)
2094+
if DEFERRED_KEY not in annotations:
2095+
continue
2096+
2097+
default = annotations[DEFERRED_KEY].pop("_defaultpage", None)
2098+
if default and default in obj:
2099+
logger.info("Setting %s as default page for %s", default, obj.absolute_url())
2100+
obj.setDefaultPage(default)
2101+
if not annotations[DEFERRED_KEY]:
2102+
annotations.pop(DEFERRED_KEY)
2103+
api.portal.show_message("Done", self.request)
2104+
return self.index()
2105+
2106+
``collective.jsonify`` puts the info on relations, translations and default-pages in the export-file.
2107+
You can use the approach to defer imports to deal with that data after all items were imported.
2108+
The example ``ImportDeferred`` above uses that approach to set the default pages.
2109+
2110+
This ``global_obj_hook`` below stores that data in a annotation:
2111+
2112+
.. code-block:: python
2113+
2114+
def global_obj_hook(self, obj, item):
2115+
# Store deferred data in an annotation.
2116+
keys = ["_defaultpage"]
2117+
data = {}
2118+
for key in keys:
2119+
if value := item.get(key, None):
2120+
data[key] = value
2121+
if data:
2122+
annotations = IAnnotations(obj)
2123+
annotations[DEFERRED_KEY] = data
2124+
2125+
18672126
Written by
18682127
==========
18692128

src/collective/exportimport/import_content.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,14 @@ class ImportContent(BrowserView):
105105
# Example: {'which_price': 'normal'}
106106
DEFAULTS = {}
107107

108-
def __call__(self, jsonfile=None, return_json=False, limit=None, server_file=None):
108+
def __call__(
109+
self,
110+
jsonfile=None,
111+
return_json=False,
112+
limit=None,
113+
server_file=None,
114+
iterator=None
115+
):
109116
request = self.request
110117
self.limit = limit
111118
self.commit = int(request["commit"]) if request.get("commit") else None
@@ -179,6 +186,11 @@ def __call__(self, jsonfile=None, return_json=False, limit=None, server_file=Non
179186
if close_file:
180187
jsonfile.close()
181188

189+
if not jsonfile and iterator:
190+
self.start()
191+
msg = self.do_import(iterator)
192+
api.portal.show_message(msg, self.request)
193+
182194
self.finish()
183195

184196
if return_json:

0 commit comments

Comments
 (0)