Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/3288.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use extended expression context for Control Panel actions @erral
6 changes: 4 additions & 2 deletions src/Products/CMFPlone/PloneControlPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from plone.base.interfaces import IControlPanel
from Products.CMFCore.ActionInformation import ActionInformation
from Products.CMFCore.ActionProviderBase import ActionProviderBase
from Products.CMFCore.Expression import createExprContext
from Products.CMFCore.Expression import Expression
from Products.CMFCore.permissions import ManagePortal
from Products.CMFCore.permissions import View
Expand All @@ -16,6 +15,7 @@
from Products.CMFCore.utils import registerToolInterface
from Products.CMFCore.utils import UniqueObject
from Products.CMFPlone.PloneBaseTool import PloneBaseTool
from Products.CMFPlone.utils import get_current_context
from zope.component.hooks import getSite
from zope.i18n import translate
from zope.i18nmessageid import Message
Expand Down Expand Up @@ -121,8 +121,10 @@ def maySeeSomeConfiglets(self):
security.declarePublic("enumConfiglets")

def enumConfiglets(self, group=None):
context = get_current_context()
portal = getToolByName(self, "portal_url").getPortalObject()
context = createExprContext(self, portal, self)

context = self._getExprContext(context or portal)
res = []
for a in self.listActions():
verified = 0
Expand Down
78 changes: 78 additions & 0 deletions src/Products/CMFPlone/tests/test_controlpanel_expr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.testing import PRODUCTS_CMFPLONE_INTEGRATION_TESTING
from zope.globalrequest import setRequest

import unittest


class TestControlPanelExpressions(unittest.TestCase):
layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.cp = getToolByName(self.portal, "portal_controlpanel")
self.folder = self.portal["test-folder"]

def test_enum_configlets_uses_plone_context_state_on_portal(self):
# This condition uses plone_context_state which is provided by the new _getExprContext
# is_portal_root() will be true on portal root.
self.cp.addAction(
id="test-configlet",
name="Test Configlet",
action="string:test",
condition="python:plone_context_state.is_portal_root()",
permission="cmf.ManagePortal",
category="Plone",
visible=True,
)

self.request.set("PARENTS", [self.portal])
setRequest(self.request)

configlets = [c["id"] for c in self.cp.enumConfiglets(group="Plone")]
self.assertIn(
"test-configlet", configlets, "Configlet should be visible on portal root"
)

def test_enum_configlets_uses_plone_context_state_on_folder(self):
# This condition uses plone_context_state which is provided by the new _getExprContext
# is_portal_root() will be false on the folder.
self.cp.addAction(
id="test-configlet",
name="Test Configlet",
action="string:test",
condition="python:plone_context_state.is_portal_root()",
permission="cmf.ManagePortal",
category="Plone",
visible=True,
)

self.request.set("PARENTS", [self.folder, self.portal])
setRequest(self.request)

configlets = [c["id"] for c in self.cp.enumConfiglets(group="Plone")]
self.assertNotIn(
"test-configlet", configlets, "Configlet should NOT be visible on folder"
)

def test_enum_configlets_uses_plone_portal_state(self):
# This condition uses plone_portal_state
self.cp.addAction(
id="test-configlet-2",
name="Test Configlet 2",
action="string:test",
condition="python:plone_portal_state.portal_url() is not None",
permission="cmf.ManagePortal",
category="Plone",
visible=True,
)

self.request.set("PARENTS", [self.portal])
setRequest(self.request)
configlets = [c["id"] for c in self.cp.enumConfiglets(group="Plone")]
self.assertIn(
"test-configlet-2",
configlets,
"Configlet should be visible as plone_portal_state is available",
)
63 changes: 63 additions & 0 deletions src/Products/CMFPlone/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,66 @@ def test_getSiteLogo_with_no_setting(self):
logo_url, logo_type = getSiteLogo(include_type=True)
self.assertTrue("http://nohost/plone/++resource++plone-logo.svg" in logo_url)
self.assertEqual("image/svg+xml", logo_type)


class GetCurrentContextTests(unittest.TestCase):
layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.layer["request"]

def test_get_current_context_no_request(self):
from Products.CMFPlone.utils import get_current_context
from zope.globalrequest import clearRequest

clearRequest()
self.assertIsNone(get_current_context())

def test_get_current_context_with_published_view(self):
from Products.CMFPlone.utils import get_current_context
from zope.globalrequest import setRequest

class FakeView:
def __init__(self, context):
self.context = context

view = FakeView(self.portal)
self.request.set("PUBLISHED", view)
setRequest(self.request)

self.assertEqual(get_current_context(), self.portal)

def test_get_current_context_with_published_view_parent(self):
from Products.CMFPlone.utils import get_current_context
from zope.globalrequest import setRequest

class FakeView:
def __init__(self, context):
self.__parent__ = context

view = FakeView(self.portal)
self.request.set("PUBLISHED", view)
setRequest(self.request)

self.assertEqual(get_current_context(), self.portal)

def test_get_current_context_from_parents(self):
from Products.CMFPlone.utils import get_current_context
from zope.globalrequest import setRequest

self.request.set("PARENTS", [self.portal])
setRequest(self.request)

self.assertEqual(get_current_context(), self.portal)

def test_get_current_context_non_contentish_ignored(self):
from Products.CMFPlone.utils import get_current_context
from zope.globalrequest import setRequest

# Something that is not IContentish
non_content = object()
self.request.set("PARENTS", [non_content])
setRequest(self.request)

self.assertIsNone(get_current_context())
32 changes: 32 additions & 0 deletions src/Products/CMFPlone/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from plone.base.interfaces.siteroot import IPloneSiteRoot
from plone.i18n.normalizer.interfaces import IIDNormalizer
from plone.registry.interfaces import IRegistry
from Products.CMFCore.interfaces import IContentish
from Products.CMFCore.permissions import ManageUsers
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.utils import ToolInit as CMFCoreToolInit
Expand All @@ -35,6 +36,7 @@
from zope.deferredimport import deprecated as deprecated_import
from zope.deprecation import deprecate
from zope.deprecation import deprecated # noqa: F401
from zope.globalrequest import getRequest
from zope.interface import implementedBy

import OFS
Expand Down Expand Up @@ -514,3 +516,33 @@ def _safe_format(inst, method):
as we do in CMFPlone/__init__.py.
"""
return SafeFormatter(inst).safe_format


def get_current_context():
# 1. Fetch the global request
request = getRequest()
if not request:
return None

# 2. Inspect what Zope is currently publishing
published = request.get("PUBLISHED", None)

# If a BrowserView is being published, look for __parent__ or context
context = getattr(published, "__parent__", None)
if context is None:
context = getattr(published, "context", None)

# 3. Fall back to the Zope traversal lineage (PARENTS)
if context is None:
parents = request.get("PARENTS", [])
if parents:
# The immediate parent of the published view/method is the context object
context = parents[0]

# 4. Content verification (Optional but Recommended)
# This ensures you got actual Plone content,
# rather than a skin template or system resource
if context and IContentish.providedBy(context):
return context

return None