Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

control panel throws exception if accessed before addon upgrade step that adds field (controlpanel serializer so it handles schemas with new fields by returning field default) #1736

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/plone/restapi/serializer/controlpanels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from plone.dexterity.interfaces import IDexterityContent
from plone.registry.interfaces import IRegistry
from plone.registry.interfaces import IRecordsProxy
from plone.registry.recordsproxy import RecordsProxy
from plone.restapi.controlpanels import IControlpanel
from plone.restapi.interfaces import IFieldSerializer
from plone.restapi.interfaces import ISerializeToJson
Expand All @@ -12,12 +14,15 @@
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.interface import noLongerProvides
from zope.schema.interfaces import IField

import zope.schema


SERVICE_ID = "@controlpanels"

_marker = object()


@implementer(ISerializeToJsonSummary)
@adapter(IControlpanel)
Expand Down Expand Up @@ -82,7 +87,14 @@ def __call__(self):
self.controlpanel, self.controlpanel.context, self.controlpanel.request
)

proxy = self.registry.forInterface(self.schema, prefix=self.schema_prefix)
# We use a special proxy and check=False just in case the schema has a new field before an upgrade happens
# Note this doesn't yet handle if a schema field changes type, or the options change
proxy = self.registry.forInterface(
self.schema,
prefix=self.schema_prefix,
check=False,
factory=DefaultRecordsProxy,
)

# Temporarily provide IDexterityContent, so we can use DX field
# serializers
Expand Down Expand Up @@ -113,3 +125,34 @@ def __call__(self):
"schema": json_schema,
"data": json_data,
}


@implementer(IRecordsProxy)
class DefaultRecordsProxy(RecordsProxy):
"""Modified RecordsProxy which returns defaults if values missing"""

def __getattr__(self, name):
if not self.__dict__ or name in self.__dict__.keys():
return super(RecordsProxy, self).__getattr__(name)
if name not in self.__schema__:
raise AttributeError(name)
value = self.__registry__.get(self.__prefix__ + name, _marker)
if value is _marker:
# Instead of returning missing_value we return the default
# so if this is an upgrade the registry will eventually be
# set with the default on next save.
field = self.__schema__[name]
if IField.providedBy(field):
field = field.bind(self)
value = field.default
else:
value = self.__schema__[name].default

return value

def __setattr__(self, name, value):
if name in self.__schema__:
full_name = self.__prefix__ + name
self.__registry__[full_name] = value
else:
self.__dict__[name] = value
39 changes: 38 additions & 1 deletion src/plone/restapi/tests/test_services_controlpanels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from plone.app.testing import TEST_USER_ID
from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
from plone.restapi.testing import RelativeSession
from zope.component import getUtility
from plone.registry.interfaces import IRegistry
import transaction


import unittest


class TestControlpanelsEndpoint(unittest.TestCase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
Expand Down Expand Up @@ -119,3 +122,37 @@ def test_get_usergroup_control_panel(self):
# This control panel does not exist in Plone 5
response = self.api_session.get("/@controlpanels/usergroup")
self.assertEqual(200, response.status_code)

def test_get_schema_with_new_field(self):
# simulate startup with a change registry schema to ensure it doesn't break

registry = getUtility(IRegistry)
registry.records._values[
"plone.ext_editor"
] = True # ensure it's not the default
transaction.commit()

response = self.api_session.get("/@controlpanels/editing")
old_data = response.json()["data"]
self.assertEqual(old_data["ext_editor"], True)

# It's too hard to add another field so lets delete the registry data to
# simulate what it's like starting when the schema has a field and no
# data in the registry for it
# del registry.records['plone.available_editors']
del registry.records["plone.ext_editor"]
transaction.commit()

response = self.api_session.get("/@controlpanels/editing")
old_data = response.json()["data"]
self.assertEqual(old_data["ext_editor"], False)

# ensure there is no problem trying to set missing registry entries
new_values = {
"ext_editor": not old_data["ext_editor"],
}
response = self.api_session.patch("/@controlpanels/editing", json=new_values)

# check if the values changed
response = self.api_session.get("/@controlpanels/editing")
self.assertNotEqual(response.json(), old_data)