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. +

+ +
+
+

by Interface

+ +
+
+

by Prefix

+ +
+
+
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 @@ + + + + + + + + + +
+

Configuration Registry

+ +

+ The table below shows record currently managed by the configuration + registry. Click on a record to edit it. +

+
+
+
+
+

Records

+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+
+
+ + +
+
+
+ + + +
+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTitleDescriptionTypeValue
+ + + + +
+ ( + Delete record + ) +
+
+
+
+
+
+
+
+

Export

+

Will export the entire registry into a single XML file.

+
+ +
+ +
+
+
+
+
+
+

Import

+
+
+ + +
+ +
+ +
+
+
+
+

Add new record

+

+ + +
+
+
+
+ + 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])