From 974c8ee726444af93666ebbcb8fc8855f2a891a9 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 10 Jun 2026 12:39:09 +0200 Subject: [PATCH] default language controlpanel sync --- .../default-language-controlpanel-sync.bugfix | 1 + .../deserializer/controlpanels/configure.zcml | 1 + .../controlpanels/deserializers.py | 45 ++++++ .../test_services_controlpanel_language.py | 151 ++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 news/default-language-controlpanel-sync.bugfix create mode 100644 src/plone/restapi/deserializer/controlpanels/deserializers.py create mode 100644 src/plone/restapi/tests/test_services_controlpanel_language.py diff --git a/news/default-language-controlpanel-sync.bugfix b/news/default-language-controlpanel-sync.bugfix new file mode 100644 index 0000000000..c0e3255a9f --- /dev/null +++ b/news/default-language-controlpanel-sync.bugfix @@ -0,0 +1 @@ +Patching `@controlpanels/language` now syncs the site language on non-multilingual sites when `default_language` changes, while multilingual sites keep their existing language state. @sneridagh diff --git a/src/plone/restapi/deserializer/controlpanels/configure.zcml b/src/plone/restapi/deserializer/controlpanels/configure.zcml index e6202ceb3d..8fb2b3fe4f 100644 --- a/src/plone/restapi/deserializer/controlpanels/configure.zcml +++ b/src/plone/restapi/deserializer/controlpanels/configure.zcml @@ -5,5 +5,6 @@ + diff --git a/src/plone/restapi/deserializer/controlpanels/deserializers.py b/src/plone/restapi/deserializer/controlpanels/deserializers.py new file mode 100644 index 0000000000..79d5da7460 --- /dev/null +++ b/src/plone/restapi/deserializer/controlpanels/deserializers.py @@ -0,0 +1,45 @@ +from plone.dexterity.interfaces import IDexterityContent +from plone.i18n.interfaces import ILanguageSchema +from plone.restapi import HAS_MULTILINGUAL +from plone.restapi.deserializer.dxfields import ChoiceFieldDeserializer +from plone.restapi.interfaces import IControlpanelLayer +from plone.restapi.interfaces import IFieldDeserializer +from zope.component import adapter +from zope.component.hooks import getSite +from zope.interface import implementer +from zope.schema.interfaces import IChoice + +if HAS_MULTILINGUAL: + from plone.app.multilingual.interfaces import IPloneAppMultilingualInstalled + + +@implementer(IFieldDeserializer) +@adapter(IChoice, IDexterityContent, IControlpanelLayer) +class ControlpanelLanguageFieldDeserializer(ChoiceFieldDeserializer): + def __call__(self, value): + value = super().__call__(value) + + if ( + self.field.interface is ILanguageSchema + and self.field.getName() == "default_language" + ): + self._sync_site_language(value) + + return value + + def _sync_site_language(self, language): + if not IControlpanelLayer.providedBy(self.request): + return + + if HAS_MULTILINGUAL and IPloneAppMultilingualInstalled.providedBy(self.request): + return + + portal = getSite() + if portal is None: + return + + if portal.Language() == language: + return + + portal.setLanguage(language) + self.request["HTTP_ACCEPT_LANGUAGE"] = language diff --git a/src/plone/restapi/tests/test_services_controlpanel_language.py b/src/plone/restapi/tests/test_services_controlpanel_language.py new file mode 100644 index 0000000000..7b49a29f12 --- /dev/null +++ b/src/plone/restapi/tests/test_services_controlpanel_language.py @@ -0,0 +1,151 @@ +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.i18n.interfaces import ILanguageSchema +from plone.registry.interfaces import IRegistry +from plone.restapi import HAS_MULTILINGUAL +from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING +from plone.restapi.testing import PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING +from plone.restapi.testing import RelativeSession +from zope.component import getUtility + +import transaction +import unittest + +if HAS_MULTILINGUAL: + from plone.app.multilingual.interfaces import IPloneAppMultilingualInstalled + + +class TestLanguageControlpanel(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + self.api_session = RelativeSession(self.portal_url, test=self) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + def tearDown(self): + self.api_session.close() + + def test_update_language_syncs_site_language(self): + registry = getUtility(IRegistry) + language_settings = registry.forInterface( + ILanguageSchema, prefix="plone", check=False + ) + old_default_language = language_settings.default_language + old_site_language = self.portal.Language() + new_language = "de" + + try: + self.portal.setLanguage("en") + language_settings.default_language = "en" + transaction.commit() + + response = self.api_session.patch( + "/@controlpanels/language", + json={"default_language": new_language}, + ) + transaction.begin() + + self.assertEqual(204, response.status_code) + self.assertEqual(new_language, language_settings.default_language) + self.assertEqual(new_language, self.portal.Language()) + finally: + language_settings.default_language = old_default_language + self.portal.setLanguage(old_site_language) + transaction.commit() + + def test_update_non_language_choice_field_does_not_sync_site_language(self): + old_site_language = self.portal.Language() + + try: + self.portal.setLanguage("en") + transaction.commit() + response = self.api_session.patch( + "/@controlpanels/editing", + json={"default_editor": "TinyMCE"}, + ) + transaction.begin() + + self.assertEqual(204, response.status_code) + self.assertEqual("en", self.portal.Language()) + finally: + self.portal.setLanguage(old_site_language) + transaction.commit() + + def test_update_invalid_language_does_not_sync_site_language(self): + old_site_language = self.portal.Language() + + try: + self.portal.setLanguage("en") + transaction.commit() + response = self.api_session.patch( + "/@controlpanels/language", + json={"default_language": "not-a-language"}, + ) + transaction.begin() + + self.assertEqual(400, response.status_code) + self.assertEqual("en", self.portal.Language()) + finally: + self.portal.setLanguage(old_site_language) + transaction.commit() + + +@unittest.skipUnless(HAS_MULTILINGUAL, "plone.app.multilingual is not installed") +class TestMultilingualLanguageControlpanel(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + self.api_session = RelativeSession(self.portal_url, test=self) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + def tearDown(self): + self.api_session.close() + + def test_update_language_does_not_sync_site_language_on_multilingual_site(self): + if not IPloneAppMultilingualInstalled.providedBy(self.request): + self.skipTest("plone.app.multilingual is not enabled") + + registry = getUtility(IRegistry) + language_settings = registry.forInterface( + ILanguageSchema, prefix="plone", check=False + ) + old_default_language = language_settings.default_language + old_site_language = self.portal.Language() + new_language = "de" + + try: + self.portal.setLanguage("en") + language_settings.default_language = "en" + transaction.commit() + + response = self.api_session.patch( + "/@controlpanels/language", + json={"default_language": new_language}, + ) + transaction.begin() + + self.assertEqual(204, response.status_code) + self.assertEqual(new_language, language_settings.default_language) + self.assertEqual("en", self.portal.Language()) + finally: + language_settings.default_language = old_default_language + self.portal.setLanguage(old_site_language) + transaction.commit()