Skip to content

Commit

Permalink
Merge branch '1.1' of https://github.com/conestack/cone.app into 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rnixx committed May 22, 2024
2 parents 9b943cc + 0b5a5c1 commit 0834483
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 20 deletions.
11 changes: 10 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ Changes
1.1a3 (unreleased)
------------------

- Nothing changed yet.
- ``node_info`` decorator stores name on node info instance.
[rnix]

- ``node_info`` decorator accepts additional keyword arguments to add custom
properties.
[rnix]

- Add ``node_available`` callback to check whether node is allowed to be
used in application.
[rnix]


1.1a2 (2024-02-12)
Expand Down
13 changes: 13 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,16 @@ This factory gets passed the settings dict as argument and returns an
root node properties above on the custom root object, call
``cone.app.configure_root`` with the root node and the settings dict as
arguments.


Application node availability
-----------------------------

In some applications it might be desired to have a dedicated mechanism to
decide whether specific nodes are allowed to be used, e.g. if a specific
feature set is available, etc. You can hook up such a custom logic via the
application config file:

- **cone.root.node_available**: Callable returning whether the node is allowed
to be used in this application. Gets passed the application model and a
node info instane as arguments.
2 changes: 1 addition & 1 deletion docs/source/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ and implements the following contracts:

- **nodeinfo**: Property containing ``cone.app.INodeInfo`` implementing object.
NodeInfo provides cardinality information and general node information which
is primary needed for authoring operations.
is primarily needed for authoring operations.


BaseNode
Expand Down
4 changes: 2 additions & 2 deletions docs/source/widgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1541,7 +1541,7 @@ ActionEdit

Renders ``edit`` tile to main content area.

Considered ``nodeinfo``:
Considered ``properties``:

- **action_edit**: Flag whether to render edit action.

Expand All @@ -1551,7 +1551,7 @@ ActionDelete

Invokes ``delete`` tile on node after confirming action.

Considered ``nodeinfo``:
Considered ``properties``:

- **action_delete**: Flag whether to render delete action.

Expand Down
5 changes: 5 additions & 0 deletions src/cone/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ def main(global_config, **settings):
security.ADMIN_PASSWORD = settings.get('cone.admin_password')
security.AUTHENTICATOR = settings.get('cone.authenticator')

# set node availability callback
node_available_callback = settings.pop('cone.root.node_available', None)
if node_available_callback:
security.node_available = import_from_string(node_available_callback)

auth_secret = settings.pop('cone.auth_secret', 'secret')
auth_cookie_name = settings.pop('cone.auth_cookie_name', 'auth_tkt')
auth_secure = settings.pop('cone.auth_secure', False)
Expand Down
19 changes: 10 additions & 9 deletions src/cone/app/browser/authoring.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
from cone.app import compat
from cone.app import security
from cone.app.browser import render_main_template
from cone.app.browser.actions import ActionContext
from cone.app.browser.ajax import AjaxEvent
from cone.app.browser.ajax import AjaxOverlay
from cone.app.browser.ajax import AjaxPath
from cone.app.browser.ajax import ajax_continue
from cone.app.browser.ajax import ajax_form_fiddle
from cone.app.browser.ajax import ajax_message
from cone.app.browser.ajax import AjaxEvent
from cone.app.browser.ajax import AjaxOverlay
from cone.app.browser.ajax import AjaxPath
from cone.app.browser.ajax import render_ajax_form
from cone.app.browser.form import FormTarget
from cone.app.browser.utils import make_query
from cone.app.browser.utils import make_url
from cone.app.model import AdapterNode
from cone.app.model import BaseNode
from cone.app.model import Properties
from cone.app.model import get_node_info
from cone.app.model import Properties
from cone.app.utils import app_config
from cone.app.utils import node_path
from cone.tile import Tile
from cone.tile import render_template
from cone.tile import render_tile
from cone.tile import Tile
from cone.tile import tile
from plumber import Behavior
from plumber import default
from plumber import override
from plumber import plumb
from pyramid.i18n import TranslationStringFactory
from pyramid.i18n import get_localizer
from pyramid.i18n import TranslationStringFactory
from pyramid.view import view_config
from webob.exc import HTTPFound
from yafowil.base import factory
Expand Down Expand Up @@ -334,7 +335,7 @@ def items(self):
return ret
for addable in addables:
info = get_node_info(addable)
if not info:
if not info or not security.node_available(self.model, addable):
continue
ret.append(self.make_item(addable, info))
return ret
Expand Down Expand Up @@ -369,8 +370,8 @@ def render(self):
@property
def info(self):
factory = self.request.params.get('factory')
allowed = self.model.nodeinfo.addables
if not factory or not allowed or factory not in allowed:
addables = self.model.nodeinfo.addables
if not factory or not addables or factory not in addables:
return None
return get_node_info(factory)

Expand Down
15 changes: 10 additions & 5 deletions src/cone/app/model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from cone.app import security
from cone.app.compat import configparser
from cone.app.compat import IS_PY2
from cone.app.compat import ITER_TYPES
Expand All @@ -14,7 +15,6 @@
from cone.app.interfaces import ISettingsNode
from cone.app.interfaces import ITranslation
from cone.app.interfaces import IUUIDAsName
from cone.app.security import acl_registry
from cone.app.utils import app_config
from cone.app.utils import DatetimeHelper
from node import schema
Expand Down Expand Up @@ -93,27 +93,30 @@ def get_node_info(name):


class node_info(object):
"""Node info decorator.
"""
"""Node info decorator."""

def __init__(self, name, title=None, description=None,
factory=None, icon=None, addables=[]):
factory=None, icon=None, addables=[], **kw):
self.name = name
self.title = title
self.description = description
self.factory = factory
self.icon = icon
self.addables = addables
self.kw = kw

def __call__(self, cls):
cls.node_info_name = self.name
info = NodeInfo()
info.name = self.name
info.node = cls
info.title = self.title
info.description = self.description
info.factory = self.factory
info.addables = self.addables
info.icon = self.icon
for name, value in self.kw.items():
setattr(info, name, value)
register_node_info(cls.node_info_name, info)
return cls

Expand All @@ -139,7 +142,9 @@ class AppNode(Behavior):
@default
@property
def __acl__(self):
return acl_registry.lookup(self.__class__, self.node_info_name)
if not security.node_available(self, self.node_info_name):
return [(Deny, Everyone, ALL_PERMISSIONS)]
return security.acl_registry.lookup(self.__class__, self.node_info_name)

@default
@instance_property
Expand Down
15 changes: 15 additions & 0 deletions src/cone/app/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ def lookup(self, obj=None, node_info_name='', default=DEFAULT_ACL):
acl_registry = ACLRegistry()


def default_node_available(model, node_info_name):
"""Default callback for checking node availability in UI.
:param model: application node to gain access to the application model.
:param node_info_name: Node info name the node to check availability.
"""
return True


# callbak for application node availability
node_available = default_node_available


@implementer(IOwnerSupport)
class OwnerSupport(Behavior):
"""Plumbing behavior providing ownership information.
Expand Down Expand Up @@ -296,6 +309,8 @@ class AdapterACL(Behavior):
@override
@property
def __acl__(self):
if not node_available(self, self.node_info_name):
return [(Deny, Everyone, ALL_PERMISSIONS)]
request = get_current_request()
acl_adapter = request.registry.queryAdapter(
self,
Expand Down
12 changes: 12 additions & 0 deletions src/cone/app/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ def wrapper(*a, **kw):
return wrapper


def reset_node_available(fn):
"""Decorator for tests modifying node_available callback
"""
def wrapper(*a, **kw):
node_available_orgin = security_module.node_available
try:
fn(*a, **kw)
finally:
security_module.node_available = node_available_orgin
return wrapper


class DummyRequest(BaseDummyRequest, AuthenticationAPIMixin):
_accept = None

Expand Down
5 changes: 4 additions & 1 deletion src/cone/app/testing/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
from pyramid.security import Allow
from pyramid.security import Deny
from pyramid.security import Everyone
from pyramid.static import static_view
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Interface


def testing_node_available(model, node_info_name):
return True


@plumbing(WorkflowState, WorkflowACL)
class WorkflowNode(BaseNode):
workflow_name = u'dummy'
Expand Down
9 changes: 9 additions & 0 deletions src/cone/app/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cone.app import layout_config
from cone.app import main_hook
from cone.app import make_remote_addr_middleware
from cone.app import security
from cone.app import testing
from cone.app.interfaces import ILayoutConfig
from cone.app.model import BaseNode
Expand Down Expand Up @@ -77,6 +78,7 @@ def test_register_config(self):
"Config with name 'dummy' already registered."
)

@testing.reset_node_available
def test_main(self):
# main hook
hooks = dict(called=0)
Expand Down Expand Up @@ -111,6 +113,8 @@ def decorated_main_hook(configurator, global_config, settings):
'cone.main_template': 'package.browser:templates/main.pt',
# ensure custom root node factory gets invoked
'cone.root.node_factory': 'cone.app.default_root_node_factory',
# ensure custom node_available factory gets invoked
'cone.root.node_available': 'cone.app.testing.mock.testing_node_available',
# ensure dummy main hooks called
'cone.plugins': 'cone.app.tests'
}
Expand All @@ -120,6 +124,11 @@ def decorated_main_hook(configurator, global_config, settings):
self.assertTrue(isinstance(router, Router))
self.assertEqual(hooks['called'], 2)

# Check custom node_available
self.assertTrue(
security.node_available is testing.mock.testing_node_available
)

# Remove custom main hook after testing
cone.app.main_hooks.remove(custom_main_hook)
cone.app.main_hooks.remove(decorated_main_hook)
Expand Down
19 changes: 19 additions & 0 deletions src/cone/app/tests/test_browser_authoring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from cone.app import compat
from cone.app import security
from cone.app import testing
from cone.app.browser.ajax import AjaxEvent
from cone.app.browser.ajax import AjaxMessage
Expand Down Expand Up @@ -845,6 +846,7 @@ def __call__(self):
""", node.treerepr())

@testing.reset_node_info_registry
@testing.reset_node_available
def test_add_items_dropdown(self):
@node_info(
name='mynode',
Expand Down Expand Up @@ -897,6 +899,23 @@ class MyNode(BaseNode):
expected = 'ajax:target="http://example.com/somechild?factory=anothernode"'
self.assertTrue(rendered.find(expected) != -1)

# now disable this other node type globally
def node_available(model, node_info_name):
if node_info_name == 'anothernode':
return False
return True
security.node_available = node_available

with self.layer.authenticated('manager'):
request = self.layer.new_request()
rendered = render_tile(root['somechild'], request, 'add_dropdown')

expected = 'ajax:target="http://example.com/somechild?factory=mynode"'
self.assertTrue(rendered.find(expected) != -1)

expected = 'ajax:target="http://example.com/somechild?factory=anothernode"'
self.assertFalse(rendered.find(expected) != -1)

# Test node without addables, results in empty listing.
# XXX: hide entire widget if no items
@node_info(name='nochildaddingnode')
Expand Down
5 changes: 4 additions & 1 deletion src/cone/app/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,17 +355,20 @@ def test_node_info(self):
description='My Node Descriptrion',
factory=None,
icon='icon',
addables=['othernode'])
addables=['othernode'],
custom_prop='custom_value')
class MyNode(BaseNode):
pass

info = get_node_info('mynode')
self.assertEqual(info.name, 'mynode')
self.assertTrue(info.node is MyNode)
self.assertEqual(info.title, 'My Node')
self.assertEqual(info.description, 'My Node Descriptrion')
self.assertEqual(info.factory, None)
self.assertEqual(info.addables, ['othernode'])
self.assertEqual(info.icon, 'icon')
self.assertEqual(info.custom_prop, 'custom_value')
self.assertEqual(MyNode.node_info_name, 'mynode')

def test_AppEnvironment(self):
Expand Down
Loading

0 comments on commit 0834483

Please sign in to comment.