diff --git a/news/437.breaking.rst b/news/437.breaking.rst
new file mode 100644
index 00000000..e5808c51
--- /dev/null
+++ b/news/437.breaking.rst
@@ -0,0 +1,2 @@
+Move ``plone.app.registry`` Classic-UI browser templates and control panel
+views to ``plone.app.layout.controlpanels.registry``.
diff --git a/news/437.internal.rst b/news/437.internal.rst
new file mode 100644
index 00000000..a0a3da07
--- /dev/null
+++ b/news/437.internal.rst
@@ -0,0 +1,3 @@
+Added Classic-UI control panel views and templates for ``plone.app.registry``,
+including record listing, edit, delete, and XML export views, along with
+their full test suite.
diff --git a/setup.py b/setup.py
index 4b82ef69..21ced550 100644
--- a/setup.py
+++ b/setup.py
@@ -40,13 +40,16 @@
zip_safe=False,
python_requires=">=3.10",
install_requires=[
+ "lxml",
"plone.app.content",
"plone.app.dexterity",
"plone.app.linkintegrity",
+ "plone.app.registry",
"plone.app.relationfield",
"plone.app.workflow",
"plone.app.uuid",
"plone.app.viewletmanager >=1.2",
+ "plone.autoform",
"plone.base >=4.0.0a1",
"plone.dexterity",
"plone.formwidget.namedfile",
@@ -58,6 +61,7 @@
"Products.GenericSetup",
"Products.statusmessages",
"Products.ZCatalog",
+ "z3c.form",
"Zope",
],
extras_require=dict(
diff --git a/src/plone/app/layout/controlpanels/configure.zcml b/src/plone/app/layout/controlpanels/configure.zcml
index 00d1d512..2403feb8 100644
--- a/src/plone/app/layout/controlpanels/configure.zcml
+++ b/src/plone/app/layout/controlpanels/configure.zcml
@@ -33,4 +33,46 @@
layer="plone.app.layout.interfaces.IPloneAppLayoutLayer"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/controlpanels/registry.py b/src/plone/app/layout/controlpanels/registry.py
new file mode 100644
index 00000000..2771bb5b
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/registry.py
@@ -0,0 +1,516 @@
+from Acquisition import ImplicitAcquisitionWrapper
+from lxml import etree
+from lxml.etree import XMLSyntaxError
+from plone.app.registry.exportimport.handler import RegistryExporter
+from plone.app.registry.exportimport.handler import RegistryImporter
+from plone.autoform.form import AutoExtensibleForm
+from plone.base import PloneMessageFactory as _
+from plone.base.batch import Batch
+from plone.registry import field as registry_field
+from plone.registry import Record
+from plone.registry.interfaces import IRegistry
+from plone.z3cform import layout
+from Products.Five import BrowserView
+from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
+from Products.statusmessages.interfaces import IStatusMessage
+from z3c.form import button
+from z3c.form import field
+from z3c.form import form
+from z3c.form.action import ActionErrorOccurred
+from z3c.form.interfaces import WidgetActionExecutionError
+from zope import schema
+from zope.component import getUtility
+from zope.component.hooks import getSite
+from zope.event import notify
+from zope.interface import implementer
+from zope.interface import Interface
+from zope.interface import Invalid
+from zope.publisher.interfaces import IPublishTraverse
+from zope.schema.vocabulary import SimpleVocabulary
+
+import logging
+import os
+import re
+import string
+
+logger = logging.getLogger("plone.app.registry")
+
+
+class RegistryEditForm(AutoExtensibleForm, form.EditForm):
+ """Edit a records proxy based on an interface.
+
+ To use this, you should use the element in a registry.xml
+ GS import step to register records for a particular interface. Then
+ subclass this form and set the 'schema' class variable to point to
+ the same interface. You can use plone.autoform form hints to affect the
+ rendering of the form, or override the update() method as appropriate.
+
+ To get the standard control panel layout, use ControlPanelFormWrapper as
+ the form wrapper, e.g.
+
+ from plone.app.registry.browser.form import RegistryEditForm
+ from plone.app.registry.browser.form import ControlPanelFormWrapper
+ from my.package.interfaces import IMySettings
+ form plone.z3cform import layout
+
+ class MyForm(RegistryEditForm):
+ schema = IMySettings
+
+ MyFormView = layout.wrap_form(MyForm, ControlPanelFormWrapper)
+
+ Then register MyFormView as a browser view.
+ """
+
+ control_panel_view = "@@overview-controlpanel"
+ schema_prefix = None
+
+ def getContent(self):
+ return getUtility(IRegistry).forInterface(
+ self.schema, prefix=self.schema_prefix
+ )
+
+ def updateActions(self):
+ super().updateActions()
+ self.actions["save"].addClass("btn btn-primary")
+ self.actions["cancel"].addClass("btn btn-secondary")
+
+ @button.buttonAndHandler(_("Save"), name="save")
+ def handleSave(self, action):
+ data, errors = self.extractData()
+ if errors:
+ self.status = self.formErrorsMessage
+ return
+ self.applyChanges(data)
+ IStatusMessage(self.request).addStatusMessage(_("Changes saved."), "info")
+ self.request.response.redirect(self.request.getURL())
+
+ @button.buttonAndHandler(_("Cancel"), name="cancel")
+ def handleCancel(self, action):
+ IStatusMessage(self.request).addStatusMessage(_("Changes canceled."), "info")
+ self.request.response.redirect(
+ f"{getSite().absolute_url()}/{self.control_panel_view}"
+ )
+
+
+class ControlPanelFormWrapper(layout.FormWrapper):
+ """Use this form as the plone.z3cform layout wrapper to get the control
+ panel layout.
+ """
+
+ index = ViewPageTemplateFile("templates/registry_controlpanel_layout.pt")
+
+ @property
+ def control_panel_url(self):
+ return f"{getSite().absolute_url()}/@@overview-controlpanel"
+
+
+class RecordDeleteView(BrowserView):
+ def __call__(self):
+ if self.request.REQUEST_METHOD == "POST":
+ name = self.request.form.get("name")
+ if isinstance(name, list) and len(name) > 0:
+ name = name[0]
+ if self.request.form.get("form.buttons.delete"):
+ if name in self.context:
+ del self.context.records[name]
+ messages = IStatusMessage(self.request)
+ messages.add("Successfully deleted field %s" % name, type="info")
+ elif self.request.form.get("form.buttons.cancel") and name:
+ messages = IStatusMessage(self.request)
+ messages.add("Field %s was not deleted" % name, type="info")
+ return self.request.response.redirect(self.context.absolute_url())
+ return super().__call__()
+
+
+class RecordEditForm(form.EditForm):
+ """Edit a single record"""
+
+ record = None
+
+ @property
+ def action(self):
+ return f"{self.context.absolute_url()}/edit/{self.record.__name__}"
+
+ def getContent(self):
+ return ImplicitAcquisitionWrapper({"value": self.record.value}, self.context)
+
+ def update(self):
+ self.fields = field.Fields(
+ self.record.field,
+ )
+ super().update()
+
+ def updateActions(self):
+ super().updateActions()
+ self.actions["save"].addClass("btn btn-primary")
+ self.actions["cancel"].addClass("btn btn-secondary")
+
+ @property
+ def label(self):
+ return _("Edit record: ${name}", mapping={"name": self.record.__name__})
+
+ @button.buttonAndHandler(_("Save"), name="save")
+ def handleSave(self, action):
+ data, errors = self.extractData()
+ if errors:
+ self.status = self.formErrorsMessage
+ return
+ self.record.value = data["value"]
+ IStatusMessage(self.request).addStatusMessage(_("Changes saved."), "info")
+ self.request.response.redirect(self.context.absolute_url())
+
+ @button.buttonAndHandler(_("Cancel"), name="cancel")
+ def handleCancel(self, action):
+ IStatusMessage(self.request).addStatusMessage(_("Edit cancelled."), "info")
+ self.request.response.redirect(self.context.absolute_url())
+
+
+@implementer(IPublishTraverse)
+class RecordEditView(layout.FormWrapper):
+ form = RecordEditForm
+
+ def __init__(self, context, request):
+ super().__init__(context, request)
+ self.request["disable_border"] = True
+
+ def publishTraverse(self, request, name):
+ path = self.request["TraversalRequestNameStack"] + [name]
+ path.reverse()
+ key = "/".join(path)
+ del self.request["TraversalRequestNameStack"][:]
+ record = self.context.records[key]
+ self.record = record
+ self.form_instance.record = record
+ return self
+
+
+_current_dir = os.path.dirname(__file__)
+
+
+def _sort_first_lower(key):
+ return key[0].lower()
+
+
+class RegistryExporterView(BrowserView):
+ """this view make sane exports of the registry.
+
+ Main goal is to export in a way, that the output can be reused as
+ best practice settings
+ """
+
+ template = ViewPageTemplateFile("templates/registry_exportxml.pt")
+
+ def __call__(self):
+ interface = self.request.form.get("interface", None)
+ name = self.request.form.get("name", None)
+ if not interface and not name:
+ return self.template()
+ return self.export(sinterface=interface, sname=name)
+
+ def interfaces(self):
+ prefixes = []
+ registry = getUtility(IRegistry)
+ baseurl = "{}/@@configuration_registry_export_xml?interface=".format(
+ self.context.absolute_url()
+ )
+ for record in registry.records.values():
+ if record.interfaceName is None:
+ continue
+ name = record.interfaceName
+ url = f"{baseurl}{record.interfaceName}"
+ pair = (name, url)
+ if pair not in prefixes:
+ prefixes.append(pair)
+
+ return sorted(prefixes, key=_sort_first_lower)
+
+ def prefixes(self):
+ prefixes = []
+ registry = getUtility(IRegistry)
+ baseurl = "{}/@@configuration_registry_export_xml?".format(
+ self.context.absolute_url()
+ )
+ for record in registry.records.values():
+ if record.interfaceName == record.__name__:
+ continue
+
+ def add_split(part):
+ url = f"{baseurl}name={part}"
+ pair = (part, url)
+ if pair not in prefixes:
+ prefixes.append(pair)
+ if part.rfind("/") > part.rfind("."):
+ new_parts = part.rsplit("/", 1)
+ else:
+ new_parts = part.rsplit(".", 1)
+ if len(new_parts) > 1:
+ add_split(new_parts[0])
+
+ add_split(record.__name__)
+ return sorted(prefixes, key=_sort_first_lower)
+
+ def export(self, sinterface=None, sname=None):
+ registry = getUtility(IRegistry)
+ root = etree.Element("registry")
+ values = {} # full prefix to valuerecord
+ interface2values = {}
+ interface2prefix = {}
+ for record in registry.records.values():
+ if sinterface and sinterface != record.interfaceName:
+ continue
+ if sname and not record.__name__.startswith(sname):
+ continue
+ prefix, value_key = record.__name__.rsplit(".", 1)
+ xmlvalue = etree.Element("value")
+ if record.value is None:
+ continue
+ if isinstance(record.value, (list, tuple)):
+ for element in record.value:
+ xmlel = etree.SubElement(xmlvalue, "element")
+ xmlel.text = element
+ elif isinstance(record.value, bool):
+ xmlvalue.text = "True" if record.value else "False"
+ elif isinstance(record.value, str):
+ xmlvalue.text = record.value
+ else:
+ xmlvalue.text = str(record.value)
+
+ if record.interfaceName:
+ xmlvalue.attrib["key"] = value_key
+ if record.interfaceName not in interface2values:
+ interface2values[record.interfaceName] = []
+ interface2values[record.interfaceName].append(record.__name__)
+ interface2prefix[record.interfaceName] = prefix
+ values[record.__name__] = xmlvalue
+
+ for ifname in sorted(interface2values):
+ xmlrecord = etree.SubElement(root, "records")
+ xmlrecord.attrib["interface"] = ifname
+ xmlrecord.attrib["prefix"] = interface2prefix[ifname]
+ for value in sorted(interface2values[ifname]):
+ xmlrecord.append(values.pop(value))
+ for name, xmlvalue in values.items():
+ xmlrecord = etree.SubElement(root, "records")
+ xmlrecord.attrib["prefix"] = name
+ xmlrecord.append(xmlvalue)
+
+ self.request.response.setHeader("Content-Type", "text/xml")
+ filename = ""
+ if sinterface:
+ filename += sinterface
+ if sinterface and sname:
+ filename += "_-_"
+ if sname:
+ filename += sname
+ self.request.response.setHeader(
+ "Content-Disposition", f"attachment; filename={filename}.xml"
+ )
+ return etree.tostring(
+ root, pretty_print=True, xml_declaration=True, encoding="UTF-8"
+ )
+
+
+def _true(s, v):
+ return True
+
+
+def _is_in(s, v):
+ return s in v
+
+
+def _starts_with(s, v):
+ return v.startswith(s)
+
+
+_okay_prefixes = ["Products", "plone.app", "plone"]
+
+
+class FakeEnv:
+ def getLogger(self, name):
+ return logger
+
+ def shouldPurge(self):
+ return False
+
+
+_valid_field_name_chars = string.ascii_letters + "._"
+
+
+def checkFieldName(val):
+ # Reuse same regex as in plone.registry.registry._Records to allow dottedname with one '/'
+ validkey = re.compile(
+ r"([a-zA-Z][a-zA-Z0-9_-]*)((?:\.[a-zA-Z0-9][a-zA-Z0-9_-]*)*)"
+ r"([/][a-zA-Z0-9][a-zA-Z0-9_-]*)?((?:\.[a-zA-Z0-9][a-zA-Z0-9_-]*)*)$"
+ ).match
+ if not validkey(val):
+ raise Invalid("Not a valid field name")
+ return True
+
+
+class IAddFieldForm(Interface):
+ name = schema.TextLine(
+ title=_("label_field_name", default="Field Name"),
+ description='Must be in a format like "plone.my_name". Only letters, periods, underscores and up to one /.',
+ required=True,
+ constraint=checkFieldName,
+ )
+
+ title = schema.TextLine(
+ title=_("label_field_title", default="Field Title"), required=True
+ )
+
+ field_type = schema.Choice(
+ title="Field Type",
+ vocabulary=SimpleVocabulary.fromValues(
+ [
+ "Bytes",
+ "BytesLine",
+ "ASCII",
+ "ASCIILine",
+ "Text",
+ "TextLine",
+ "Bool",
+ "Int",
+ "Float",
+ "Decimal",
+ "Password",
+ "Datetime",
+ "Date",
+ "Timedelta",
+ "SourceText",
+ "URI",
+ "Id",
+ "DottedName",
+ # XXX not supporting these types yet as it requires additional config
+ # 'Tuple', 'List', 'Set', 'FrozenSet', 'Dict',
+ ]
+ ),
+ )
+
+ required = schema.Bool(title="Required", default=False)
+
+
+class RecordsControlPanel(AutoExtensibleForm, form.Form):
+ schema = IAddFieldForm
+ ignoreContext = True
+ submitted = False
+
+ template = ViewPageTemplateFile("templates/registry_records.pt")
+
+ @property
+ def action(self):
+ return f"{self.context.absolute_url()}#autotoc-item-autotoc-3"
+
+ def updateActions(self):
+ super().updateActions()
+ self.actions["addfield"].addClass("btn-primary")
+
+ @button.buttonAndHandler("Add field", name="addfield")
+ def action_addfield(self, action):
+ data, errors = self.extractData()
+ self.submitted = True
+ if not errors:
+ field_class = getattr(registry_field, data["field_type"], None)
+ if field_class is None:
+ notify(
+ ActionErrorOccurred(
+ action,
+ WidgetActionExecutionError(
+ "field_type", Invalid("Invalid Field")
+ ),
+ )
+ )
+ return
+ if data["name"] in self.context:
+ notify(
+ ActionErrorOccurred(
+ action,
+ WidgetActionExecutionError(
+ "name", Invalid("Field name already in use")
+ ),
+ )
+ )
+ return
+
+ new_field = field_class(title=data["title"], required=data["required"])
+ new_record = Record(new_field)
+ self.context.records[data["name"]] = new_record
+ messages = IStatusMessage(self.request)
+ messages.add("Successfully added field %s" % data["name"], type="info")
+ return self.request.response.redirect(
+ "{url}/edit/{field}".format(
+ url=self.context.absolute_url(), field=data["name"]
+ )
+ )
+
+ def import_registry(self):
+ try:
+ fi = self.request.form["file"]
+ body = fi.read()
+ except (AttributeError, KeyError):
+ messages = IStatusMessage(self.request)
+ messages.add("Must provide XML file", type="error")
+ body = None
+ if body is not None:
+ importer = RegistryImporter(self.context, FakeEnv())
+ try:
+ importer.importDocument(body)
+ except XMLSyntaxError:
+ messages = IStatusMessage(self.request)
+ messages.add("Must provide valid XML file", type="error")
+ return self.request.response.redirect(self.context.absolute_url())
+
+ def export_registry(self):
+ exporter = RegistryExporter(self.context, FakeEnv())
+ body = exporter.exportDocument()
+ resp = self.request.response
+ resp.setHeader("Content-type", "text/xml")
+ resp.setHeader("Content-Disposition", "attachment; filename=registry.xml")
+ resp.setHeader("Content-Length", len(body))
+ return body
+
+ @property
+ def control_panel_url(self):
+ return f"{getSite().absolute_url()}/@@overview-controlpanel"
+
+ def __call__(self):
+ form = self.request.form
+ if self.request.REQUEST_METHOD == "POST":
+ if form.get("button.exportregistry"):
+ return self.export_registry()
+ if form.get("button.importregistry"):
+ return self.import_registry()
+ search = form.get("q")
+ searchp = form.get("qp")
+ compare = _is_in
+ if searchp not in (None, ""):
+ search = searchp
+ if search is not None and search.startswith("prefix:"):
+ search = search[len("prefix:") :]
+ compare = _starts_with
+ if not search:
+ compare = _true
+
+ self.prefixes = {}
+ self.records = []
+ for record in self.context.records.values():
+ ifaceName = record.interfaceName
+ if ifaceName is not None:
+ recordPrefix = ifaceName.split(".")[-1]
+ prefixValue = record.interfaceName
+ else:
+ prefixValue = record.__name__
+ for prefix in _okay_prefixes:
+ name = record.__name__
+ if name.startswith(prefix):
+ recordPrefix = ".".join(
+ name.split(".")[: len(prefix.split(".")) + 1]
+ )
+ prefixValue = recordPrefix
+ break
+ if recordPrefix not in self.prefixes:
+ self.prefixes[recordPrefix] = prefixValue
+ if compare(search, prefixValue) or compare(search, record.__name__):
+ self.records.append(record)
+ self.records = Batch(self.records, 15, int(form.get("b_start", "0")), orphan=1)
+ return super().__call__()
diff --git a/src/plone/app/layout/controlpanels/templates/registry_controlpanel_layout.pt b/src/plone/app/layout/controlpanels/templates/registry_controlpanel_layout.pt
new file mode 100644
index 00000000..8793140d
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/templates/registry_controlpanel_layout.pt
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ View Title
+ View Description
+
+
+
+
+ Portal status message
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/controlpanels/templates/registry_delete_layout.pt b/src/plone/app/layout/controlpanels/templates/registry_delete_layout.pt
new file mode 100644
index 00000000..5a9ed412
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/templates/registry_delete_layout.pt
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
Delete Record
+
+
+ Are you certain you want to delete this record? This can not be undone
+ and can be potentially harmful if you don't know what you are doing.
+
+
+
Record
+
: ${request/form/name}
+
+
+
+
+
+
diff --git a/src/plone/app/layout/controlpanels/templates/registry_edit_layout.pt b/src/plone/app/layout/controlpanels/templates/registry_edit_layout.pt
new file mode 100644
index 00000000..1b78991f
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/templates/registry_edit_layout.pt
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
View Title
+
+
+ Use the form below to edit the value of this particular record.
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/controlpanels/templates/registry_exportxml.pt b/src/plone/app/layout/controlpanels/templates/registry_exportxml.pt
new file mode 100644
index 00000000..b5ca15e5
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/templates/registry_exportxml.pt
@@ -0,0 +1,36 @@
+
+ Export parts
+
+ Download of a XML-file optimized to be used in a GenericSetup profile of an add-on or policy profile.
+ It contains only the selected parts.
+
+
+
+
diff --git a/src/plone/app/layout/controlpanels/templates/registry_records.pt b/src/plone/app/layout/controlpanels/templates/registry_records.pt
new file mode 100644
index 00000000..f5c486cc
--- /dev/null
+++ b/src/plone/app/layout/controlpanels/templates/registry_records.pt
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Records
+
+
+
+
+
+
+
+
+ | Name |
+ Title |
+ Description |
+ Type |
+ Value |
+
+
+
+
+
+ |
+
+ |
+ |
+ |
+ |
+
+
+
+
+ (
+ Delete record
+ )
+
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+
+
Export
+
Will export the entire registry into a single XML file.
+
+
+
+
+
+
Import
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/layout/tests/test_registry.py b/src/plone/app/layout/tests/test_registry.py
new file mode 100644
index 00000000..cf8db4bd
--- /dev/null
+++ b/src/plone/app/layout/tests/test_registry.py
@@ -0,0 +1,103 @@
+from plone.app.layout.controlpanels.registry import ControlPanelFormWrapper
+from plone.app.layout.controlpanels.registry import RecordDeleteView
+from plone.app.layout.controlpanels.registry import RecordsControlPanel
+from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING
+from plone.registry import Record
+from plone.registry.field import TextLine
+from zope.component import getMultiAdapter
+
+import unittest
+
+
+class TestRegistryBaseControlpanel(unittest.TestCase):
+ layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING
+
+ def test_registry_base_controlpanel__control_panel_url(self):
+ """Test, if control_panel_url property of the base controlpanel returns
+ the correct url.
+ """
+ view = ControlPanelFormWrapper(None, None)
+ self.assertEqual(
+ view.control_panel_url, "http://nohost/plone/@@overview-controlpanel"
+ )
+
+
+class TestRecordsControlPanel(unittest.TestCase):
+ layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING
+
+ def setUp(self):
+ self.request = self.layer["request"]
+ self.portal = self.layer["portal"]
+
+ def test_records_control_panel__control_panel_url(self):
+ """Test, if control_panel_url property of the registry controlpanel
+ returns the correct url.
+ """
+ view = RecordsControlPanel(None, None)
+ self.assertEqual(
+ view.control_panel_url, "http://nohost/plone/@@overview-controlpanel"
+ )
+
+ def test_add_new_record(self):
+ self.request.form.update(
+ {
+ "form.widgets.name": "foobar",
+ "form.widgets.title": "Foobar",
+ "form.widgets.field_type": "TextLine",
+ "form.widgets.required-empty-marker": "1",
+ "form.buttons.addfield": "Add field",
+ }
+ )
+
+ registry = self.portal.portal_registry
+
+ view = RecordsControlPanel(registry, self.request)
+ view.update()
+
+ data, errors = view.extractData()
+ self.assertTrue(len(errors) == 0)
+
+ view.action_addfield(view, None)
+ self.assertTrue("foobar" in registry.records)
+
+ def test_delete(self):
+ registry = self.portal.portal_registry
+ new_field = TextLine()
+ new_record = Record(new_field)
+ registry.records["foobar"] = new_record
+ self.assertTrue("foobar" in registry.records)
+
+ self.request.form.update({"form.buttons.delete": "Yes", "name": "foobar"})
+ self.request.REQUEST_METHOD = "POST"
+ view = RecordDeleteView(registry, self.request)
+ view()
+ self.assertTrue("foobar" not in registry.records)
+
+ def test_edit_record_with_slash(self):
+ # Prepare a new record "foo/bar"
+ registry = self.portal.portal_registry
+ new_field = TextLine()
+ new_record = Record(new_field)
+ registry.records["foo/bar"] = new_record
+
+ # the record has no value set
+ self.assertIsNone(new_record.value)
+
+ # prepare a request
+ request = self.request.clone()
+ request.form = {
+ "form.widgets.value": "baz",
+ "form.buttons.save": "Save",
+ }
+ request.method = "POST"
+ request["TraversalRequestNameStack"] = []
+
+ # get the view and fake the traversal
+ edit_form = getMultiAdapter(
+ (registry, request),
+ name="edit",
+ ).publishTraverse(request, "foo/bar")
+
+ # call the view and verify the value has changed
+ edit_form()
+ self.assertEqual(new_record.value, "baz")
diff --git a/src/plone/app/layout/tests/test_registry_exportimport.py b/src/plone/app/layout/tests/test_registry_exportimport.py
new file mode 100644
index 00000000..6b6b7023
--- /dev/null
+++ b/src/plone/app/layout/tests/test_registry_exportimport.py
@@ -0,0 +1,1493 @@
+from lxml import etree
+from OFS.ObjectManager import ObjectManager
+from plone.app.registry import Registry
+from plone.app.registry.exportimport.handler import exportRegistry
+from plone.app.registry.exportimport.handler import importRegistry
+from plone.app.registry.tests import data
+from plone.registry import field
+from plone.registry import FieldRef
+from plone.registry import Record
+from plone.registry.interfaces import IFieldRef
+from plone.registry.interfaces import IInterfaceAwareRecord
+from plone.registry.interfaces import IRegistry
+from plone.testing import zca
+from Products.GenericSetup.tests.common import DummyExportContext
+from Products.GenericSetup.tests.common import (
+ DummyImportContext as BaseDummyImportContext,
+)
+from zope.component import provideUtility
+from zope.configuration import xmlconfig
+from zope.interface import alsoProvides
+
+import unittest
+
+configuration = """\
+
+
+
+
+
+
+"""
+
+
+class DummyImportContext(BaseDummyImportContext):
+ _directories = {}
+
+ def listDirectory(self, path):
+ return self._directories.get(path, [])
+
+ def isDirectory(self, path):
+ return path in self._directories
+
+
+class ExportImportTest(unittest.TestCase):
+ layer = zca.UNIT_TESTING
+
+ def setUp(self):
+ self.site = ObjectManager("plone")
+ self.registry = Registry("portal_registry")
+ provideUtility(provides=IRegistry, component=self.registry)
+ context = xmlconfig.string(configuration, execute=True)
+ try:
+ import Zope2.App.zcml
+
+ self._context = Zope2.App.zcml._context
+ Zope2.App.zcml._context = context
+ except ImportError:
+ pass
+
+ def tearDown(self):
+ try:
+ import Zope2.App.zcml
+
+ Zope2.App.zcml._context = self._context
+ except ImportError:
+ pass
+
+ def assertXmlEquals(self, expected, actual):
+ expected_tree = etree.XML(expected)
+ actual_tree = etree.XML(actual)
+
+ if etree.tostring(expected_tree) != etree.tostring(actual_tree):
+ print()
+ print("Expected:")
+ print(expected_tree)
+ print()
+
+ print()
+ print("Actual:")
+ print(actual_tree)
+ print()
+
+ raise AssertionError("XML mismatch")
+
+
+class TestImport(ExportImportTest):
+ def test_empty_import_no_purge(self):
+ xml = ""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+
+ def test_import_purge(self):
+ xml = ""
+ context = DummyImportContext(self.site, purge=True)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(0, len(self.registry.records))
+
+ def test_import_records(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(3, len(self.registry.records))
+
+ self.assertIn("plone.app.registry.tests.data.ITestSettings.name", self.registry)
+ self.assertIn("plone.app.registry.tests.data.ITestSettings.age", self.registry)
+
+ def test_import_records_disallowed(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ try:
+ importRegistry(context)
+ except TypeError:
+ pass
+ else:
+ self.fail()
+
+ def test_import_records_omit(self):
+ xml = """\
+
+
+ blob
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(3, len(self.registry.records))
+
+ self.assertIn(
+ "plone.app.registry.tests.data.ITestSettingsDisallowed.name", self.registry
+ )
+ self.assertIn(
+ "plone.app.registry.tests.data.ITestSettingsDisallowed.age", self.registry
+ )
+
+ def test_import_records_remove(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+ delete_xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": delete_xml}
+
+ importRegistry(context)
+
+ self.assertEqual(0, len(self.registry.records))
+
+ def test_import_records_delete_deprecated(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+ delete_xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": delete_xml}
+
+ importRegistry(context)
+
+ self.assertEqual(0, len(self.registry.records))
+
+ def test_import_records_remove_with_omit(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+ delete_xml = """\
+
+
+ name
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": delete_xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+
+ self.assertIn("plone.app.registry.tests.data.ITestSettings.name", self.registry)
+ self.assertNotIn(
+ "plone.app.registry.tests.data.ITestSettings.age", self.registry
+ )
+
+ def test_import_records_remove_with_value(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+ delete_xml = """\
+
+
+ Spam
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": delete_xml}
+
+ self.assertRaises(ValueError, importRegistry, context)
+
+ self.assertEqual(2, len(self.registry.records))
+
+ def test_import_records_with_prefix(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.name", self.registry)
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.age", self.registry)
+
+ def test_import_records_with_values(self):
+ xml = """\
+
+
+ Magic
+ 42
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.name", self.registry)
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.age", self.registry)
+
+ self.assertEqual(
+ self.registry["plone.app.registry.tests.data.SomethingElse.name"], "Magic"
+ )
+ self.assertEqual(
+ self.registry["plone.app.registry.tests.data.SomethingElse.age"], 42
+ )
+
+ def test_import_records_nonexistant_interface(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.assertRaises(ImportError, importRegistry, context)
+
+ def test_import_records_nonexistant_interface_condition_not_installed(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.assertRaises(ImportError, importRegistry, context)
+
+ def test_import_value_only(self):
+ xml = """\
+
+
+ Imported value
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Simple record", self.registry.records["test.export.simple"].field.title
+ )
+ self.assertEqual("Imported value", self.registry["test.export.simple"])
+
+ def test_import_value_only_condition_installed(self):
+ xml = """\
+
+
+ Imported value
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Simple record", self.registry.records["test.export.simple"].field.title
+ )
+ self.assertEqual("Sample value", self.registry["test.export.simple"])
+
+ def test_import_value_only_condition_have(self):
+ xml = """\
+
+
+ Imported value
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Simple record", self.registry.records["test.export.simple"].field.title
+ )
+ self.assertEqual("Imported value", self.registry["test.export.simple"])
+
+ def test_import_value_only_condition_not_have(self):
+ xml = """\
+
+
+ Imported value
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Simple record", self.registry.records["test.export.simple"].field.title
+ )
+ self.assertEqual("Sample value", self.registry["test.export.simple"])
+
+ def test_import_interface_and_value(self):
+ xml = """\
+
+
+ 2
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Age",
+ self.registry.records[
+ "plone.app.registry.tests.data.ITestSettingsDisallowed.age"
+ ].field.title,
+ )
+ self.assertEqual(
+ 2,
+ self.registry["plone.app.registry.tests.data.ITestSettingsDisallowed.age"],
+ )
+
+ def test_import_interface_with_differnet_name(self):
+ xml = """\
+
+
+ 2
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Age", self.registry.records["plone.registry.oops"].field.title
+ )
+ self.assertEqual(2, self.registry["plone.registry.oops"])
+
+ def test_import_interface_no_value(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Name",
+ self.registry.records[
+ "plone.app.registry.tests.data.ITestSettingsDisallowed.name"
+ ].field.title,
+ )
+ self.assertEqual(
+ "Mr. Registry",
+ self.registry["plone.app.registry.tests.data.ITestSettingsDisallowed.name"],
+ )
+
+ def test_import_field_only(self):
+ xml = """\
+
+
+
+ N/A
+ Simple record
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.TextLine
+ )
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(
+ "value", self.registry.records["test.registry.field"].field.__name__
+ )
+ self.assertEqual("N/A", self.registry["test.registry.field"])
+
+ def test_import_field_ref(self):
+ xml = """\
+
+
+
+ Another value
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ importRegistry(context)
+
+ self.assertEqual(2, len(self.registry.records))
+ self.assertTrue(
+ IFieldRef.providedBy(
+ self.registry.records["test.registry.field.override"].field
+ )
+ )
+ self.assertEqual(
+ "Simple record",
+ self.registry.records["test.registry.field.override"].field.title,
+ )
+ self.assertEqual(
+ "value",
+ self.registry.records["test.registry.field.override"].field.__name__,
+ )
+ self.assertEqual("Another value", self.registry["test.registry.field.override"])
+
+ def test_import_field_and_interface(self):
+ xml = """\
+
+
+
+ N/A
+ Simple record
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.ASCIILine
+ )
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual("N/A", self.registry["test.registry.field"])
+
+ def test_import_overwrite_field_with_field(self):
+ xml = """\
+
+
+
+ Nada
+ Simple record
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"), value="Old value"
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.ASCIILine
+ )
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual("Nada", self.registry["test.registry.field"])
+
+ def test_import_overwrite_field_with_interface(self):
+ xml = """\
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"), value="Old value"
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Int)
+ )
+ self.assertEqual(
+ "Age", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(None, self.registry["test.registry.field"])
+
+ def test_import_collection_field(self):
+ xml = """\
+
+
+
+ Simple record
+
+ 1
+ 3
+
+
+ Value
+
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"), value="Old value"
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.FrozenSet
+ )
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(frozenset([1, 3]), self.registry["test.registry.field"])
+
+ def test_import_collection_value(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Set(title="Simple record", value_type=field.Int(title="Val")),
+ value={1},
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Set)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(frozenset([4, 6]), self.registry["test.registry.field"])
+
+ def test_import_collection_nopurge(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Set(title="Simple record", value_type=field.Int(title="Val")),
+ value={1},
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Set)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(frozenset([1, 4, 6]), self.registry["test.registry.field"])
+
+ def test_import_collection_list_append(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.List(title="Simple record", value_type=field.Int(title="Val")),
+ value=[2, 4],
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual([2, 4, 6], self.registry["test.registry.field"])
+
+ def test_import_collection_tuple_append(self):
+ xml = """\
+
+
+
+ b
+ c
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Tuple(title="Simple record", value_type=field.TextLine(title="Val")),
+ value=(
+ "a",
+ "b",
+ ),
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ (
+ "a",
+ "b",
+ "c",
+ ),
+ self.registry["test.registry.field"],
+ )
+
+ def test_import_collection_set_append(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Set(title="Simple record", value_type=field.Int(title="Val")),
+ value={2, 4},
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual({2, 4, 6}, self.registry["test.registry.field"])
+
+ def test_import_collection_frozenset_append(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.FrozenSet(title="Simple record", value_type=field.Int(title="Val")),
+ value=frozenset([2, 4]),
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(frozenset([2, 4, 6]), self.registry["test.registry.field"])
+
+ def test_import_dict_field(self):
+ xml = """\
+
+
+
+ Simple record
+
+ 1
+ 3
+
+
+ Key
+
+
+ Value
+
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"), value="Old value"
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Dict)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual({"a": 1, "b": 3}, self.registry["test.registry.field"])
+
+ def test_import_dict_value(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Dict(
+ title="Simple record",
+ key_type=field.ASCIILine(title="Key"),
+ value_type=field.Int(title="Val"),
+ ),
+ value={"a": 1},
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Dict)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual({"x": 4, "y": 6}, self.registry["test.registry.field"])
+
+ def test_import_dict_nopurge(self):
+ xml = """\
+
+
+
+ 4
+ 6
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.Dict(
+ title="Simple record",
+ key_type=field.ASCIILine(title="Key"),
+ value_type=field.Int(title="Val"),
+ ),
+ value={"a": 1},
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Dict)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual({"a": 1, "x": 4, "y": 6}, self.registry["test.registry.field"])
+
+ def test_import_choice_field(self):
+ xml = """\
+
+
+
+ Simple record
+
+ One
+ Two
+
+
+
+
+"""
+
+ self.registry.records["test.registry.field"] = Record(
+ field.TextLine(title="Simple record", default="N/A"), value="Old value"
+ )
+
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(self.registry.records["test.registry.field"].field, field.Choice)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(
+ ["One", "Two"],
+ [
+ t.value
+ for t in self.registry.records["test.registry.field"].field.vocabulary
+ ],
+ )
+ self.assertEqual(None, self.registry["test.registry.field"])
+
+ def test_import_with_comments(self):
+ xml = """\
+
+
+
+ Magic
+ 42
+
+
+
+
+ N/A
+
+ Simple record
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(3, len(self.registry.records))
+
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.TextLine
+ )
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertEqual(
+ "value", self.registry.records["test.registry.field"].field.__name__
+ )
+ self.assertEqual("N/A", self.registry["test.registry.field"])
+
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.name", self.registry)
+ self.assertIn("plone.app.registry.tests.data.SomethingElse.age", self.registry)
+ self.assertEqual(
+ self.registry["plone.app.registry.tests.data.SomethingElse.name"], "Magic"
+ )
+ self.assertEqual(
+ self.registry["plone.app.registry.tests.data.SomethingElse.age"], 42
+ )
+
+ def test_remove(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(0, len(self.registry.records))
+
+ def test_delete_deprecated(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(0, len(self.registry.records))
+
+ def test_delete_not_found(self):
+ xml = """\
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertEqual(
+ "Simple record", self.registry.records["test.export.simple"].field.title
+ )
+ self.assertEqual("Sample value", self.registry["test.export.simple"])
+
+ def test_import_folder(self):
+ xml1 = """\
+
+
+
+ N/A
+ Simple record
+
+
+
+"""
+ xml2 = """\
+
+
+
+ N/A
+ Simple record
+
+
+
+"""
+ xml3 = """\
+
+
+
+ N/A
+ Simple record
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {
+ "registry.xml": xml1,
+ ".ignored_file": "",
+ "registry/foo2.xml": xml2,
+ "registry/foo3.xml": xml3,
+ }
+ context._directories = {
+ "registry": [
+ "foo2.xml",
+ "foo3.xml",
+ ".ignored_file",
+ ]
+ }
+ importRegistry(context)
+
+ self.assertEqual(3, len(self.registry.records))
+
+ for idx in range(1, 4):
+ fieldname = "test.registry.foobar%i" % idx
+ self.assertTrue(
+ isinstance(self.registry.records[fieldname].field, field.TextLine)
+ )
+ self.assertEqual(
+ "Simple record", self.registry.records[fieldname].field.title
+ )
+ self.assertEqual("value", self.registry.records[fieldname].field.__name__)
+ self.assertEqual("N/A", self.registry[fieldname])
+
+ def test_import_jsonfield_only(self):
+ xml = """\
+
+
+
+ {}
+ JSON record
+
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.JSONField
+ )
+ )
+ self.assertEqual(
+ "JSON record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertDictEqual({}, self.registry["test.registry.field"])
+
+ def test_import_jsonfield_with_value(self):
+ xml = """\
+
+
+
+ {}
+ JSON record
+
+ {"items":[{"color":"red","value":"one"},{"color":"röd","value":"two"}]}
+
+
+"""
+ context = DummyImportContext(self.site, purge=False)
+ context._files = {"registry.xml": xml}
+
+ importRegistry(context)
+
+ self.assertEqual(1, len(self.registry.records))
+ self.assertTrue(
+ isinstance(
+ self.registry.records["test.registry.field"].field, field.JSONField
+ )
+ )
+ self.assertEqual(
+ "JSON record", self.registry.records["test.registry.field"].field.title
+ )
+ self.assertDictEqual(
+ {
+ "items": [
+ {"color": "red", "value": "one"},
+ {"color": "röd", "value": "two"},
+ ]
+ },
+ self.registry["test.registry.field"],
+ )
+
+
+class TestExport(ExportImportTest):
+ def test_export_empty(self):
+ xml = """"""
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_simple(self):
+ xml = """\
+
+
+
+ N/A
+ Simple record
+
+ Sample value
+
+"""
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_interface(self):
+ xml = """\
+
+
+
+ 0
+ Age
+
+
+
+
+
+ Mr. Registry
+ Name
+
+ Mr. Registry
+
+
+
+ N/A
+ Simple record
+
+ Sample value
+
+"""
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ self.registry.registerInterface(data.ITestSettings)
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_field_ref(self):
+ xml = """\
+
+
+
+ N/A
+ Simple record
+
+ Sample value
+
+
+
+ Another value
+
+"""
+
+ self.registry.records["test.export.simple"] = refRecord = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ self.registry.records["test.export.simple.override"] = Record(
+ FieldRef(refRecord.__name__, refRecord.field), value="Another value"
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_collection(self):
+ xml = """\
+
+
+
+ Simple record
+
+ Val
+
+
+
+ 2
+
+
+"""
+ self.registry.records["test.export.simple"] = Record(
+ field.List(title="Simple record", value_type=field.Int(title="Val")),
+ value=[2],
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_dict(self):
+ xml = """\
+
+
+
+
+
+ Key
+
+ Dict
+
+ Value
+
+
+
+ 1
+
+
+"""
+
+ self.registry.records["test.export.dict"] = Record(
+ field.Dict(
+ title="Dict",
+ default={},
+ key_type=field.ASCIILine(title="Key"),
+ value_type=field.Int(title="Value"),
+ ),
+ value={"a": 1},
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_choice(self):
+ xml = """\
+
+
+
+ Simple record
+ dummy.vocab
+
+
+
+"""
+
+ self.registry.records["test.export.choice"] = Record(
+ field.Choice(title="Simple record", vocabulary="dummy.vocab")
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_missing_schema_does_not_error(self):
+ xml = """\
+
+
+
+ N/A
+ Simple record
+
+ Sample value
+
+"""
+
+ self.registry.records["test.export.simple"] = Record(
+ field.TextLine(title="Simple record", default="N/A"),
+ value="Sample value",
+ )
+
+ # Note: These are nominally read-only!
+ self.registry.records["test.export.simple"].field.interfaceName = (
+ "non.existent.ISchema"
+ )
+ self.registry.records["test.export.simple"].field.fieldName = "blah"
+
+ alsoProvides(self.registry.records["test.export.simple"], IInterfaceAwareRecord)
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])
+
+ def test_export_with_jsonfield(self):
+ xml = """\
+
+
+
+ {}
+ Dict
+
+ {'items': [{'color': 'red', 'value': 'one'}, {'color': 'röd', 'value': 'two'}]}
+
+"""
+
+ self.registry.records["test.export.field"] = Record(
+ field.JSONField(
+ title="Dict",
+ default={},
+ ),
+ value={
+ "items": [
+ {"color": "red", "value": "one"},
+ {"color": "röd", "value": "two"},
+ ]
+ },
+ )
+
+ context = DummyExportContext(self.site)
+ exportRegistry(context)
+
+ self.assertEqual("registry.xml", context._wrote[0][0])
+ self.assertXmlEquals(xml, context._wrote[0][1])