Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add export and import for non-default registry settings #130

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/collective/exportimport/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@
permission="cmf.ManagePortal"
/>

<browser:page
name="export_registry"
for="zope.interface.Interface"
class=".export_registry.ExportRegistry"
template="templates/export_registry.pt"
permission="cmf.ManagePortal"
/>

<!-- Serializers -->
<adapter zcml:condition="installed Products.Archetypes"
factory=".serializer.ATFileFieldSerializer" />
Expand Down Expand Up @@ -233,6 +241,14 @@
permission="cmf.ManagePortal"
/>

<browser:page
name="import_registry"
for="zope.interface.Interface"
class=".import_registry.ImportRegistry"
template="templates/import_registry.pt"
permission="cmf.ManagePortal"
/>

<browser:page
name="fix_html"
for="zope.interface.Interface"
Expand Down
100 changes: 100 additions & 0 deletions src/collective/exportimport/export_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from collective.exportimport.export_other import BaseExport
from logging import getLogger
from plone.dexterity.interfaces import IDexterityContent
from plone.registry.interfaces import IRegistry
from plone.restapi.interfaces import IFieldSerializer
from plone.restapi.serializer.converters import json_compatible
from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.dottedname.resolve import resolve
from zope.interface import alsoProvides
from zope.interface import noLongerProvides

logger = getLogger(__name__)


class ExportRegistry(BaseExport):

# This can hold interfaces, keys and prefixes
IGNORELIST = [
"plone.app.querystring.interfaces.IQueryField",
"plone.app.querystring.interfaces.IQueryOperation",
"Products.CMFPlone.interfaces.resources.IResourceRegistry",
]

def __call__(self, download_to_server=False):
self.title = "Export registry settings"
self.download_to_server = download_to_server
self.interfaces = self.request.form.get("interfaces", [])
if not isinstance(self.interfaces, list):
self.interfaces = [self.interfaces]
self.skip_defaults = self.request.form.get("skip_defaults", True)
self.all_interfacenames_with_prefix = self.get_all_interfacenames_with_prefix()
if not self.request.form.get("form.submitted", False):
return self.index()

data = self.registry_config(interfaces=self.interfaces, skip_defaults=self.skip_defaults)
self.download(data)

def registry_config(self, interfaces=[], skip_defaults=True):
results = {}
registry = getUtility(IRegistry)
for path in interfaces:
try:
iface = resolve(path)
except:
logger.debug("{} not used in this site".format(path))
continue
items = {}
prefix = self.all_interfacenames_with_prefix[path] or path
proxy = registry.forInterface(iface, check=False, prefix=prefix)
for key, field in proxy.__schema__.namesAndDescriptions():
default = field.default
name = prefix + '.' + key

value = registry.get(name, None)
if value is None:
# This means that the default value is used
continue

if skip_defaults and value == default:
continue

alsoProvides(proxy, IDexterityContent)
serializer = queryMultiAdapter((field, proxy, self.request), IFieldSerializer)
noLongerProvides(proxy, IDexterityContent)
if serializer:
value = serializer()
else:
value = json_compatible(value)
items[name] = value

if items:
results[path] = items
return results

def get_all_interfacenames_with_prefix(self):
results = []
res = {}
registry = getUtility(IRegistry)
for record in registry.records.values():
key = record.__name__
prefix = ".".join(key.split(".")[:-1])
interfacename = record.interfaceName

if record.interfaceName == record.__name__:
continue

if not interfacename or interfacename in self.IGNORELIST:
continue
if key in self.IGNORELIST or prefix in self.IGNORELIST:
continue

if interfacename == prefix:
prefix = None
results.append((interfacename, prefix))

for interfacename, prefix in sorted(results):
res[interfacename] = prefix
return res
99 changes: 99 additions & 0 deletions src/collective/exportimport/import_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
from collective.exportimport.interfaces import IMigrationMarker
from logging import getLogger
from plone import api
from plone.dexterity.interfaces import IDexterityContent
from plone.registry.interfaces import IRegistry
from plone.restapi.interfaces import IFieldDeserializer
from Products.Five import BrowserView
from Products.Five import BrowserView
from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.interface import alsoProvides
from zope.interface import noLongerProvides
from ZPublisher.HTTPRequest import FileUpload

import json

logger = getLogger(__name__)


class ImportRegistry(BrowserView):
"""Import registry settings"""

def __call__(self, jsonfile=None, return_json=False):
if jsonfile:
self.portal = api.portal.get()
status = "success"
try:
if isinstance(jsonfile, str):
return_json = True
data = json.loads(jsonfile)
elif isinstance(jsonfile, FileUpload):
data = json.loads(jsonfile.read())
else:
raise ("Data is neither text nor upload.")
except Exception as e:
status = "error"
logger.error(e)
api.portal.show_message(
u"Failure while uploading: {}".format(e),
request=self.request,
)
else:
results = self.import_registry(data)
msg = u"Imported {} registry records".format(len(results))
api.portal.show_message(msg, self.request)
for msg in results:
api.portal.show_message(msg, self.request)
if return_json:
msg = {"state": status, "msg": msg}
return json.dumps(msg)

return self.index()

def import_registry(self, data):
alsoProvides(self.request, IMigrationMarker)
registry = getUtility(IRegistry)
records = registry.records
results = []
for interface, item in data.items():
for key in item:
try:
proxy = records[key]
except KeyError:
logger.info(u"Registry has no record for %s", key)
continue
current_value = proxy.value
value = item[key]
if current_value == value:
logger.debug(u"No changes to %s as %r", key, value)
continue

try:
proxy.field.validate(value)
except:
alsoProvides(proxy, IDexterityContent)
deserializer = queryMultiAdapter((proxy.field, proxy, self.request), IFieldDeserializer)
if deserializer:
try:
value = deserializer(value)
except Exception as e:
logger.error(u"Could not import %s as %s", value, key, exc_info=True)
pass
noLongerProvides(proxy, IDexterityContent)

if current_value == value:
logger.debug(u"No changes to %s as %r", key, value)
continue

try:
registry[key] = value
except Exception:
logger.error(u"Could not import %s as %r", value, key, exc_info=True)
continue
else:
msg = u"Imported {} as {}".format(key, value)
logger.info(msg)
results.append(msg)
return results
115 changes: 115 additions & 0 deletions src/collective/exportimport/templates/export_registry.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone.z3cform"
metal:use-macro="context/main_template/macros/master">

<div metal:fill-slot="main">
<tal:main-macro metal:define-macro="main">

<h1 class="documentFirstHeading" tal:content="python: view.title" i18n:translate="">
Export registry records
</h1>

<form id="export_registry" action="@@export_other" tal:attributes="action request/URL" method="post" enctype="multipart/form-data">

<div class="field mb-3">
<label for="interfaces">
<span i18n:translate="">Settings to export</span>
</label>
<div class="widget" id="interfaces">
<input type="checkbox" class="checkboxType" name="checkall" id="checkall"
title="Toggle all" i18n:attributes="title label_toggle" />
<label for="checkall">Select all/none</label><br />
<script type="text/javascript">
// Check or Uncheck All checkboxes
$("#checkall").change(function(){
var checked = $(this).is(':checked');
if(checked){
$("form#export_registry #interfaces input[type='checkbox']").each(function(){
$(this).prop("checked",true);
});
}else{
$("form#export_registry #interfaces input[type='checkbox']").each(function(){
$(this).prop("checked",false);
});
}
});

// Changing state of CheckAll checkbox
$("form#export_registry").on("click", "form#export_registry #interfaces input[name='interfaces']", function(){
if($("form#export_registry #interfaces input[type='checkbox']").length == $("form#export_registry #ifacename input[type='checkbox']:checked").length) {
$("#checkall").prop("checked", true);
} else {
$("#checkall").prop("checked", false);
}

});
</script>
<tal:types tal:repeat="ifacename python: view.all_interfacenames_with_prefix">
<input type="checkbox"
name="interfaces"
class="checkboxType"
tal:attributes="value python:ifacename; id python:ifacename;">
<label tal:attributes="for python:ifacename">
<span tal:replace="python:ifacename" />
<span tal:condition="python:view.all_interfacenames_with_prefix[ifacename]" tal:replace="python: ' (prefix: ' + view.all_interfacenames_with_prefix[ifacename] + ')'" />
</label>
<br />
</tal:types>
</div>
</div>

<div class="field mb-3">
<label>
<input
type="checkbox"
class="form-check-input"
name="skip_defaults:boolean"
id="skip_defaults"
tal:attributes="checked python:'checked' if view.skip_defaults else None"
/>
Skip default values
<span class="formHelp">
Ignore settings that were not changed
</span>
</label>
</div>

<div class="field mb-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="download_to_server:int" value="0" id="download_local" checked="checked">
<label for="download_local" class="form-check-label" i18n:translate="">
Download to local machine
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="download_to_server:int" value="1" id="download_server">
<label for="download_server" class="form-check-label" i18n:translate="">
Save to file on server
</label>
</div>
</div>

<div class="formControls" class="form-group">
<button class="btn btn-primary submit-widget button-field context"
type="submit" name="form.submitted" value="Export" tal:content="python: view.title" i18n:translate="">Export
</button>
</div>
</form>

<div metal:use-macro="context/@@exportimport_links/links">
Links to all exports and imports
</div>

<div tal:define="help_text python: getattr(view, 'help_text', None)"
tal:condition="python: help_text">
<h3>Help</h3>
<div tal:replace="structure python: help_text"></div>
</div>

</tal:main-macro>
</div>

</html>
48 changes: 48 additions & 0 deletions src/collective/exportimport/templates/import_registry.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone.z3cform"
metal:use-macro="context/main_template/macros/master">

<div metal:fill-slot="main">
<tal:main-macro metal:define-macro="main">

<h1 class="documentFirstHeading">Import Registry Records</h1>

<p class="documentDescription">Here you can upload a json-file.</p>

<form action="@@import_registry" tal:attributes="action request/URL" method="post" enctype="multipart/form-data">
<div class="form-group">
<input type="file" name="jsonfile"/><br/>
</div>
<div class="formControls" class="form-group">
<button class="btn btn-primary submit-widget button-field context"
type="submit" name="form.submitted" value="Import">Import
</button>
</div>
</form>

<div metal:use-macro="context/@@exportimport_links/links">
Links to all exports and imports
</div>

<div>
<h3>Help</h3>
<p>Here is a example for the expected format. This is the format created by collective.exportimport when used for export.</p>
<pre>
{
"plone.app.discussion.interfaces.IDiscussionSettings": {
"globally_enabled": true
},
"Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings": {
"allowed": false
}
}
</pre>
</div>

</tal:main-macro>
</div>

</html>
Loading