@@ -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
0 commit comments