diff --git a/docs/source/ajax.rst b/docs/source/ajax.rst index 24e07ec0..013f1d42 100644 --- a/docs/source/ajax.rst +++ b/docs/source/ajax.rst @@ -198,6 +198,7 @@ a single or a list of continuation operation instances. from cone.app.browser.ajax import ajax_continue from cone.tile import Tile from cone.tile import tile + from cone.app.browser.utils import make_url @tile(name='exampleaction', permission='view') class ExampleAction(Tile): @@ -215,7 +216,7 @@ a single or a list of continuation operation instances. ) # queue continuation operations - ajax_continue(request, [overlay, event]) + ajax_continue(self.request, [overlay, event]) return u'' A shortcut for continuation message operations is located at diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index c1d38eba..d9703897 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -33,7 +33,7 @@ deployments. - **cone.admin_password**: Password of superuser. - **cone.authenticator**: Utility registration name of a - ``cone.app.interfaces.IAuthenticator`` impementation. + ``cone.app.interfaces.IAuthenticator`` implementation. Authentication Policy Configuration @@ -89,7 +89,7 @@ If desired, the concrete UGM implementation is created on application startup. - **ugm.backend**: Registration name of UGM implementation. A default file based UGM factory is registered under name ``file``, which -creates a ``cone.ugm.file.Ugm`` instance. +creates a ``node.ext.ugm.file.Ugm`` instance. Configuration is done through the following parameters. @@ -180,4 +180,4 @@ 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. + node info instance as arguments. diff --git a/docs/source/development.rst b/docs/source/development.rst index acfa288c..3e482b2e 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -39,6 +39,7 @@ Browser Resources Included resources: -- https://github.com/twbs/bootstrap/releases/tag/v5.0.2 -- https://github.com/twbs/icons/archive/v1.5.0.zip +- https://github.com/jquery/jquery/releases/tag/4.0.0-beta +- https://github.com/twbs/bootstrap/releases/tag/v5.3.3 +- https://github.com/twbs/icons/releases/tag/v1.11.3 - https://github.com/corejavascript/typeahead.js/releases/tag/v1.3.1 diff --git a/docs/source/forms.rst b/docs/source/forms.rst index 89a11131..0e0a3466 100644 --- a/docs/source/forms.rst +++ b/docs/source/forms.rst @@ -709,6 +709,7 @@ form's ``save`` function. from plumber import Behavior from plumber import plumb + from yafowil.base import factory class FormExtension(Behavior): """Plumbing behavior used as form extension. @@ -730,12 +731,12 @@ form's ``save`` function. }) # add new widget before save widget save_widget = self.form['save'] - self.form.insertbefore(roles_widget, save_widget) + self.form.insertbefore(widget, save_widget) @plumb def save(_next, self, widget, data): # fetch extension field value from form data - value = data.fetch('%s.generic' % self.form_name).extracted + value = data.fetch('%s.generic' % self.form.name).extracted # set extracted value to model attributes self.model.attrs['generic'] = value # call downstream ``save`` function diff --git a/docs/source/layout.rst b/docs/source/layout.rst index 072004ed..f60816ae 100644 --- a/docs/source/layout.rst +++ b/docs/source/layout.rst @@ -33,6 +33,30 @@ file:: cone.main_template = cone.example.browser:templates/main.pt +Rendering the Main Template +--------------------------- + +To render a view using the main template, use ``render_main_template`` from +``cone.app.browser``. + +.. code-block:: python + + from cone.app.browser import render_main_template + from pyramid.view import view_config + + @view_config(name='myview', permission='view') + def myview(model, request): + # Renders main template with 'mycontent' tile in content area + return render_main_template(model, request, 'mycontent') + +Parameters: + +- **model**: The application model node. +- **request**: The current request object. +- **contenttile**: Name of the tile to render in the content area. Defaults + to ``'content'``. + + Application Layout ------------------ @@ -67,10 +91,10 @@ one or more model classes with ``cone.app.layout_config`` decorator. pass @layout_config(CustomNodeOne, CustomNodeTwo) - class CustomLayoutConfig(LayoutConfig) + class CustomLayoutConfig(LayoutConfig): def __init__(self, model, request): - super(ExampleNodeLayoutConfig, self).__init__(model, request) + super(CustomLayoutConfig, self).__init__(model, request) self.mainmenu = True self.livesearch = True self.personaltools = True @@ -131,14 +155,14 @@ one or more model classes with ``cone.app.layout_config`` decorator. added to specify sidebar modes (either ``'stacked'`` or ``'toggle'``). - The ``limit_content_width`` setting has been added to replace the former ``columns_fluid`` setting. - - As of version 2.0, ``limit_content_width`` defaults to ``False``. + - As of version 2.0, ``limit_content_width`` defaults to ``True``. .. version-removed:: 2.0 ``mainmenu_fluid``, ``columns_fluid``, ``sidebar_left_grid_width`` and ``content_grid_width`` have been removed in ``cone.app 2.0`` in favor of a more flexible layout. - Use the ``columns_fluid`` setting instead to limit content width on + Use the ``limit_content_width`` setting instead to limit content width on large screens. .. deprecated:: 1.1 diff --git a/docs/source/model.rst b/docs/source/model.rst index 7efb638f..3e870165 100644 --- a/docs/source/model.rst +++ b/docs/source/model.rst @@ -105,6 +105,24 @@ used to serve the entry nodes of the application. return BaseNode() +LeafNode +-------- + +The ``cone.app.model.LeafNode`` behavior is used for application model nodes +that cannot have children. It disables child-related operations. + +.. code-block:: python + + from cone.app.model import BaseNode + from cone.app.model import LeafNode + from plumber import plumbing + + @plumbing(LeafNode) + class DocumentNode(BaseNode): + """A node that cannot contain children.""" + pass + + AdapterNode ----------- @@ -296,7 +314,7 @@ Available properties are provided by ``keys`` function. >>> from cone.app.model import Properties - >>> props = Properties + >>> props = Properties() >>> props.a = '1' >>> props.b = '2' >>> props.keys() @@ -362,7 +380,7 @@ property, ``ProtectedProperties`` behaves as if this property is inexistent. Metadata -------- -``cone.app.model.Metadada`` class inherits from ``cone.app.model.Properties`` +``cone.app.model.Metadata`` class inherits from ``cone.app.model.Properties`` and adds the marker interface ``cone.app.interfaces.IMetadata``. This object is for ``cone.app.interfaces.IApplicationNode.metadata``. @@ -510,18 +528,23 @@ Node Availability ~~~~~~~~~~~~~~~~~ The ``node_available`` callback can be used to control whether a registered -node type is available in the application context: +node type is available in the application context. -.. code-block:: python +The callback is configured via the application ini file: - from cone.app.model import node_available +.. code-block:: ini + + cone.root.node_available = my.package.check_node_available + +The callback function must have the following signature: + +.. code-block:: python - @node_available - def check_node_available(node_info, container): - """Return True if node type should be available for the container. + def check_node_available(model, node_info_name): + """Return True if node type should be available. - :param node_info: The NodeInfo instance - :param container: The parent container where node would be added + :param model: The application node to gain access to the application model. + :param node_info_name: The node info name of the node to check availability. :return: Boolean indicating availability """ # Custom logic to determine availability diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index b894f67b..51d4a94d 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -150,7 +150,7 @@ Plugin root node factories are registered to the application via from cone.app import main_hook from cone.app import register_entry - import cone.example.model import ExamplePlugin + from cone.example.model import ExamplePlugin @main_hook def example_main_hook(config, global_config, settings): @@ -180,7 +180,7 @@ and registered via ``cone.app.register_config``. name='example_settings', title='Example Settings', description='Settings for the example plugin', - icon='glyphicon glyphicon-cog') + icon='bi bi-gear') class ExampleSettings(SettingsNode): """Plugin settings node.""" # Category for grouping in settings UI @@ -223,7 +223,7 @@ settings should be editable by users without ``manage`` permission. .. note:: - As of version 1.1, settings are accessible to authenticated users, not just + Since version 1.1, settings are accessible to authenticated users, not just managers. The ``display`` property on ``SettingsNode`` controls visibility per settings node. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 85215620..b4fb312e 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -213,7 +213,7 @@ Register the resources in ``src/cone/example/browser/__init__.py``: ) cone_example_resources.add(wr.ScriptResource( name='cone-example-js', - depends='cone-app-protected-js', + depends='cone-app-js', resource='example.js' )) cone_example_resources.add(wr.StyleResource( @@ -348,22 +348,40 @@ user interface. The documentation how to properly integrate custom JavaScript into Ajax SSR can be found :ref:`here `. -9. Installation ---------------- +9. Run Application +------------------ -To install the application, create and activate the virtual environment: +After creating the virtual environment as described in section 2, run the +application: .. code-block:: sh - python3 -m venv venv - ./venv/bin/pip install -e . + ./venv/bin/pserve example.ini +The application is now available at ``localhost:8081``. -10. Run Application -------------------- + +Advanced: Using mxmake +---------------------- + +For larger projects, consider using `mxmake `_ +to manage your build process. mxmake generates a Makefile with common +development tasks. + +With mxmake, you get convenient make targets: + +- ``make install`` - Create virtual environment and install dependencies +- ``make test`` - Run tests +- ``make coverage`` - Run tests with coverage +- ``make docs`` - Build Sphinx documentation +- ``make lingua`` - Extract and compile translations +- ``make clean`` - Clean build artifacts + +To set up mxmake for your plugin: .. code-block:: sh - ./venv/bin/pserve example.ini + pip install mxmake + mxmake init -The application is now available at ``localhost:8081``. +See the ``cone.app`` repository for an example mxmake configuration. diff --git a/docs/source/security.rst b/docs/source/security.rst index 3897f410..d892ca41 100644 --- a/docs/source/security.rst +++ b/docs/source/security.rst @@ -54,6 +54,9 @@ The permissions used by default in ``cone.app`` are: - **change_state**: Grants access to change workflow state of an application model node. +- **change_order**: Grants access to change order of an application + model node. + - **manage**: Grants access to manage application settings. - **login**: Grants access to login to the application. @@ -76,13 +79,14 @@ The roles which come out of the box with ``cone.app`` are: - **editor**: This role is supposed to grant users permissions needed to add and edit application model nodes. By default, permissions assigned to - this role are ``viewer`` role permissions and ``add`` and ``edit``. + this role are ``viewer`` role permissions and ``add``, ``edit`` and + ``change_order``. - **admin**: This role is supposed to grant users permissions to duplicate model nodes, change the workflow state or grant access to parts of the - application model to other uses. By default, permissions assigned to + application model to other users. By default, permissions assigned to this role are ``editor`` role permissions and ``delete``, ``cut``, ``copy``, - ``paste``, ``manage_permissions`` and ``change_state``. + ``paste``, ``manage_permissions``, ``change_state`` and ``change_order``. - **manager**: This role is supposed to grant users permissions to access and modify the application settings. By default, permissions assigned to this @@ -135,12 +139,12 @@ application node. .. _security_acl_registry: -ALC Registry +ACL Registry ------------ A less immersive way for providing ACLs for model nodes is to use the ACL registry. The plumbing behavior ``cone.app.model.AppNode`` only returns -the ``cone.app.security.DEFAULT_ACL`` if no dedicated ALC for this node has +the ``cone.app.security.DEFAULT_ACL`` if no dedicated ACL for this node has been registered in the registry. Registering a custom ACL for application root which grants view access to the @@ -158,9 +162,9 @@ application root model node for unauthenticated uses looks like so: # permission sets authenticated_permissions = ['view'] viewer_permissions = authenticated_permissions + ['list'] - editor_permissions = viewer_permissions + ['add', 'edit'] + editor_permissions = viewer_permissions + ['add', 'edit', 'change_order'] admin_permissions = editor_permissions + [ - 'delete', 'cut', 'copy', 'paste', 'change_state', + 'delete', 'cut', 'copy', 'paste', 'manage_permissions', 'change_state', ] manager_permissions = admin_permissions + ['manage'] everyone_permissions = ['login', 'view'] @@ -178,7 +182,7 @@ application root model node for unauthenticated uses looks like so: acl_registry.register(custom_acl, AppRoot) -``cone.app.model.AppNode.__acl__`` tries to find a registered ALC by +``cone.app.model.AppNode.__acl__`` tries to find a registered ACL by ``self.__class__`` and ``self.node_info_name``, thus application nodes must be registered by both. @@ -276,7 +280,7 @@ Adapter ACL The ``cone.app.security.AdapterACL`` looks up the ACL via ``cone.app.interfaces.IACLAdapter`` interface. This can be useful to support -ALC customization on generic application model nodes. +ACL customization on generic application model nodes. Therefor the model node needs to plumb ``AdapterACL`` behavior. @@ -447,3 +451,72 @@ The utility name must be defined in application ini file. If a UGM implementation is configured, it gets used as fallback for authentication. + + +Security Utility Functions +-------------------------- + +``cone.app.security`` provides several utility functions for working with +authentication and authorization programmatically. + + +authenticate +~~~~~~~~~~~~ + +Authenticates a user with login and password. Tries authentication in order: +admin user credentials, custom authenticator utility, UGM backend. + +.. code-block:: python + + from cone.app.security import authenticate + + # Returns user ID if authentication successful, None otherwise + user_id = authenticate(request, login='username', password='secret') + if user_id: + # Authentication successful + pass + + +authenticated_user +~~~~~~~~~~~~~~~~~~ + +Returns the user principal object for the currently authenticated request. + +.. code-block:: python + + from cone.app.security import authenticated_user + + user = authenticated_user(request) + if user: + # user is a node.ext.ugm User object + print(user.attrs.get('fullname')) + + +principal_by_id +~~~~~~~~~~~~~~~ + +Looks up a user or group by principal ID. + +.. code-block:: python + + from cone.app.security import principal_by_id + + # Returns user or group object, or None if not found + principal = principal_by_id('user123') + if principal: + print(principal.attrs) + + +search_for_principals +~~~~~~~~~~~~~~~~~~~~~ + +Searches for users and groups matching a search term. + +.. code-block:: python + + from cone.app.security import search_for_principals + + # Returns list of matching principal objects + results = search_for_principals('john') + for principal in results: + print(principal.name) diff --git a/docs/source/widgets.rst b/docs/source/widgets.rst index 75885c17..39a2321e 100644 --- a/docs/source/widgets.rst +++ b/docs/source/widgets.rst @@ -90,7 +90,7 @@ node in order to get a reasonable result. return [{ 'value': 'Example', 'target': 'https://example.com/example', - 'icon': 'ion-ios7-gear' + 'icon': 'bi bi-gear' }] Another option to implement the serverside search logic is to overwrite the @@ -110,7 +110,7 @@ Another option to implement the serverside search logic is to overwrite the return [{ 'value': 'Example', 'target': 'https://example.com/example', - 'icon': 'ion-ios7-gear' + 'icon': 'bi bi-gear' }] ``cone.app`` uses `typeahead.js `_ @@ -173,7 +173,7 @@ To add more items to the dropdown, register an action with the @personal_tools_action(name='example') class ExampleAction(LinkAction): text = 'Example' - icon = 'ion-ios7-gear' + icon = 'bi bi-gear' event = 'contextchanged:#layout' @property @@ -230,7 +230,7 @@ Considered ``properties``: props.mainmenu_empty_title = False props.mainmenu_display_children = False props.default_content_tile = 'examplecontent' - props.icon = 'ion-ios7-gear' + props.icon = 'bi bi-gear' return props @instance_property @@ -325,7 +325,7 @@ Considered ``properties``: props.default_child = 'child' props.hide_if_default = False props.default_content_tile = 'examplecontent' - props.icon = 'ion-ios7-gear' + props.icon = 'bi bi-gear' return props @instance_property @@ -563,7 +563,7 @@ Navigation related actions are registered in the ``navigation`` group: @context_menu_item(group='navigation', name='link_to_somewhere') class LinkToSomewhereAction(LinkAction): id = 'toolbaraction-link-to-somewhere' - icon = 'glyphicon glyphicon-arrow-down' + icon = 'bi bi-arrow-down' event = 'contextchanged:#layout' text = 'Link to somewhere' @@ -936,19 +936,20 @@ for all children of model node. @property def vocab(self): count = len(self.model) - pages = count / self.slicesize + pages = count // self.slicesize if count % self.slicesize != 0: pages += 1 current = self.request.params.get('b_page', '0') + ret = [] for i in range(pages): query = make_query(b_page=str(i)) href = make_url( self.request, - path=path, + node=self.model, resource='viewname', query=query ) - target = make_url(self.request, path=path, query=query) + target = make_url(self.request, node=self.model, query=query) ret.append({ 'page': '{}'.format(i + 1), 'current': current == str(i), @@ -1090,7 +1091,7 @@ More customization options on ``BatchedItems`` class: to ``True``. - **title_css**: CSS classes to set on title container DOM element. -Can be used to change the size of the title area. + Can be used to change the size of the title area. - **default_slice_size**: Default number of items displayed in slice. Defaults to ``15``. @@ -1184,7 +1185,7 @@ Futher the implementation must provide ``col_defs``, ``item_count`` and # ``sort`` and ``order`` must be considered when creating the # sorted results. rows = list() - for child in self.model.values()[start:end]: + for child in list(self.model.values())[start:end]: row_data = RowData() row_data['column_a'] = child.attrs['attr_a'] row_data['column_b'] = child.attrs['attr_b'] @@ -1396,7 +1397,7 @@ are used as dropdown menu items. @property def items(self): item = model.Properties() - item.icon = 'ion-ios7-gear' + item.icon = 'bi bi-gear' item.url = item.target = make_url(self.request, node=self.model) item.action = 'example_action:NONE:NONE' item.title = 'Example Action' @@ -1630,3 +1631,125 @@ ActionState Renders workflow state dropdown menu. Action related node must implement ``cone.app.interfaces.IWorkflowState``. + + +Browser Utilities +================= + +``cone.app.browser.utils`` provides several utility functions used throughout +the application. + + +make_url +-------- + +Builds URLs for application nodes. + +.. code-block:: python + + from cone.app.browser.utils import make_url + + # URL to a node + url = make_url(request, node=model) + + # URL to a node with a specific resource/view + url = make_url(request, node=model, resource='edit') + + # URL with query parameters + url = make_url(request, node=model, query='param=value') + + # URL using a path list instead of node + url = make_url(request, path=['path', 'to', 'node']) + +Parameters: + +- **request**: The current request object. +- **path**: Optional path as list of path segments. +- **node**: Optional application node to build URL for. +- **resource**: Optional resource/view name to append. +- **query**: Optional query string to append. + + +make_query +---------- + +Builds query strings from keyword arguments. + +.. code-block:: python + + from cone.app.browser.utils import make_query + + # Build a simple query string + query = make_query(page='1', sort='name') + # Returns: 'page=1&sort=name' + + # Values are URL-encoded automatically + query = make_query(search='hello world') + # Returns: 'search=hello%20world' + +Parameters: + +- **quote_params**: Optional list of parameter names that should be URL-quoted. +- **\*\*kw**: Keyword arguments to include in query string. None values are skipped. + + +choose_name +----------- + +Generates a unique name for a node within a container. + +.. code-block:: python + + from cone.app.browser.utils import choose_name + + # Get unique name based on desired name + name = choose_name(container, 'document') + # Returns 'document' if not taken, or 'document-1', 'document-2', etc. + + +format_date +----------- + +Formats a datetime object for display. + +.. code-block:: python + + from cone.app.browser.utils import format_date + from datetime import datetime + + dt = datetime.now() + + # Long format (default) + formatted = format_date(dt, long=True) + # Returns: '22.01.2026 14:30' + + # Short format + formatted = format_date(dt, long=False) + # Returns: '22.01.2026' + + +node_icon +--------- + +Returns the icon CSS class for a node. + +.. code-block:: python + + from cone.app.browser.utils import node_icon + + icon_class = node_icon(model) + # Returns icon from node's properties or default icon + + +authenticated +------------- + +Checks if the current request is from an authenticated user. + +.. code-block:: python + + from cone.app.browser.utils import authenticated + + if authenticated(request): + # User is logged in + pass diff --git a/docs/source/workflows.rst b/docs/source/workflows.rst index fbef4333..456ca6e0 100644 --- a/docs/source/workflows.rst +++ b/docs/source/workflows.rst @@ -235,3 +235,73 @@ An implementation integrating the publication workflow as described in # Workflow state specific ACL's state_acls = publication_state_acls + + +Workflow Utility Functions +-------------------------- + +``cone.app.workflow`` provides several utility functions for working with +workflows programmatically. + + +lookup_workflow +~~~~~~~~~~~~~~~ + +Looks up the workflow for a given node. + +.. code-block:: python + + from cone.app.workflow import lookup_workflow + + workflow = lookup_workflow(node) + if workflow is not None: + # workflow is a repoze.workflow.Workflow instance + print(workflow.name) + + +lookup_state_data +~~~~~~~~~~~~~~~~~ + +Returns the state data dictionary for the current workflow state of a node. + +.. code-block:: python + + from cone.app.workflow import lookup_state_data + + state_data = lookup_state_data(node) + if state_data is not None: + # state_data contains keys like 'title', 'description' + print(state_data.get('title')) + print(state_data.get('description')) + + +initialize_workflow +~~~~~~~~~~~~~~~~~~~ + +Initializes the workflow state for a node. This is typically called +automatically when using the ``WorkflowState`` behavior, but can be +called manually if needed. + +.. code-block:: python + + from cone.app.workflow import initialize_workflow + + # Initialize workflow, only sets state if not already set + initialize_workflow(node) + + # Force re-initialization even if state already exists + initialize_workflow(node, force=True) + + +WfDropdown Tile +~~~~~~~~~~~~~~~ + +The ``wf_dropdown`` tile renders a dropdown menu for changing workflow states. +It displays available transitions for the current user based on permissions. + +.. code-block:: python + + from cone.tile import render_tile + + # Render the workflow dropdown + html = render_tile(model, request, 'wf_dropdown')