From 60c8df8f6d6d6d0ce717e108c1194a3098dcfae1 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Mon, 26 Jan 2026 12:32:03 +0100
Subject: [PATCH 01/25] basic setup
---
docs/TODO.md | 6 +
docs/source/quickstart.rst | 72 ++++-
examples/cone.example/example.ini | 23 +-
examples/cone.example/pyproject.toml | 15 +
examples/cone.example/setup.py | 22 --
examples/cone.example/src/cone/__init__.py | 1 -
.../cone.example/src/cone/example/__init__.py | 49 +++-
.../src/cone/example/ajax/__init__.py | 0
.../src/cone/example/ajax/browser.py | 159 +++++++++++
.../example/ajax/templates/ajax_playground.pt | 137 +++++++++
.../src/cone/example/browser/__init__.py | 269 ++++++++++--------
.../example/browser/static/cone.example.css | 38 +++
.../src/cone/example/configure.zcml | 3 +-
.../src/cone/example/document/__init__.py | 0
.../src/cone/example/document/browser.py | 243 ++++++++++++++++
.../src/cone/example/document/model.py | 93 ++++++
.../document/templates/document_source.pt | 55 ++++
.../document/templates/document_view.pt | 36 +++
.../src/cone/example/document_workflow.zcml | 98 +++++++
.../cone.example/src/cone/example/model.py | 204 ++++---------
.../src/cone/example/project/__init__.py | 0
.../src/cone/example/project/browser.py | 268 +++++++++++++++++
.../src/cone/example/project/model.py | 183 ++++++++++++
.../project/templates/project_items.pt | 29 ++
.../example/project/templates/task_view.pt | 36 +++
.../src/cone/example/publication.zcml | 56 ----
.../src/cone/example/settings/__init__.py | 0
.../src/cone/example/settings/browser.py | 83 ++++++
.../src/cone/example/settings/model.py | 44 +++
.../src/cone/example/task_workflow.zcml | 98 +++++++
.../cone.example/src/cone/example/testing.py | 24 ++
.../src/cone/example/tests/__init__.py | 0
.../src/cone/example/wiki/__init__.py | 0
.../src/cone/example/wiki/browser.py | 248 ++++++++++++++++
.../src/cone/example/wiki/model.py | 95 +++++++
.../cone/example/wiki/templates/wiki_view.pt | 55 ++++
36 files changed, 2375 insertions(+), 367 deletions(-)
create mode 100644 docs/TODO.md
create mode 100644 examples/cone.example/pyproject.toml
delete mode 100644 examples/cone.example/setup.py
delete mode 100644 examples/cone.example/src/cone/__init__.py
create mode 100644 examples/cone.example/src/cone/example/ajax/__init__.py
create mode 100644 examples/cone.example/src/cone/example/ajax/browser.py
create mode 100644 examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
create mode 100644 examples/cone.example/src/cone/example/document/__init__.py
create mode 100644 examples/cone.example/src/cone/example/document/browser.py
create mode 100644 examples/cone.example/src/cone/example/document/model.py
create mode 100644 examples/cone.example/src/cone/example/document/templates/document_source.pt
create mode 100644 examples/cone.example/src/cone/example/document/templates/document_view.pt
create mode 100644 examples/cone.example/src/cone/example/document_workflow.zcml
create mode 100644 examples/cone.example/src/cone/example/project/__init__.py
create mode 100644 examples/cone.example/src/cone/example/project/browser.py
create mode 100644 examples/cone.example/src/cone/example/project/model.py
create mode 100644 examples/cone.example/src/cone/example/project/templates/project_items.pt
create mode 100644 examples/cone.example/src/cone/example/project/templates/task_view.pt
delete mode 100644 examples/cone.example/src/cone/example/publication.zcml
create mode 100644 examples/cone.example/src/cone/example/settings/__init__.py
create mode 100644 examples/cone.example/src/cone/example/settings/browser.py
create mode 100644 examples/cone.example/src/cone/example/settings/model.py
create mode 100644 examples/cone.example/src/cone/example/task_workflow.zcml
create mode 100644 examples/cone.example/src/cone/example/testing.py
create mode 100644 examples/cone.example/src/cone/example/tests/__init__.py
create mode 100644 examples/cone.example/src/cone/example/wiki/__init__.py
create mode 100644 examples/cone.example/src/cone/example/wiki/browser.py
create mode 100644 examples/cone.example/src/cone/example/wiki/model.py
create mode 100644 examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
diff --git a/docs/TODO.md b/docs/TODO.md
new file mode 100644
index 00000000..1a8410ac
--- /dev/null
+++ b/docs/TODO.md
@@ -0,0 +1,6 @@
+Docs for 2.0 TODO
+=================
+
+[ ] fix quickstart documentation - unreleased packages, test all approaches
+[ ] seperate docs for integrated and seperate plugin
+[ ] clear up confusion around quickstart (maybe rename "creating a plugin", "create an app")
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
index b4fb312e..288972e8 100644
--- a/docs/source/quickstart.rst
+++ b/docs/source/quickstart.rst
@@ -56,7 +56,7 @@ dependency. Add the following to ``pyproject.toml``.
.. code-block:: toml
[build-system]
- requires = ["setuptools>=61.0"]
+ requires = ["setuptools>=61.0,<81"]
build-backend = "setuptools.build_meta"
[project]
@@ -75,7 +75,8 @@ dependency. Add the following to ``pyproject.toml``.
2. Virtual Environment
----------------------
-Create a virtual environment and install the package:
+Create a virtual environment and install the package.
+From the cone.app root directory, run:
.. code-block:: sh
@@ -99,6 +100,13 @@ Alternatively, using ``uv`` (faster):
uv pip install -e .
+.. Error::
+
+ If your package depends on unreleased versions of other ``cone.*`` packages,
+ ensure to install those packages first in editable mode before installing
+ your package. See :ref:`Developing on unreleased versions `.
+
+
3. Application Configuration
----------------------------
@@ -352,11 +360,16 @@ into Ajax SSR can be found :ref:`here `.
------------------
After creating the virtual environment as described in section 2, run the
-application:
+application from the ``cone.app`` root directory:
.. code-block:: sh
- ./venv/bin/pserve example.ini
+ ./venv/bin/pserve examples/cone.example/example.ini
+
+.. .. XXX: document running from cone.example directory
+.. .. code-block:: sh
+
+.. ./venv/bin/pserve example.ini
The application is now available at ``localhost:8081``.
@@ -385,3 +398,54 @@ To set up mxmake for your plugin:
mxmake init
See the ``cone.app`` repository for an example mxmake configuration.
+
+
+.. _developing_unreleased:
+
+Advanced: Developing with unreleased package versions
+-----------------------------------------------------
+
+When developing your plugin with unreleased versions of ``cone.*`` packages,
+you need to ensure that those packages are installed in your virtual environment.
+
+To do so, first checkout and install the unreleased packages in sources/:
+
+.. code-block:: sh
+
+ # cone.app root directory
+ make install
+
+Follow up by creating your virtual environment as described in section 2:
+
+.. code-block:: sh
+
+ # cone.app root directory
+ set -e
+ rm -rf ./venv/
+ python3 -m venv venv
+ source ./venv/bin/activate
+
+Then, install the unreleased application packages in editable mode:
+
+.. code-block:: sh
+
+ # cone.app root directory
+ # venv
+ pip install -e sources/cone.tile
+ # add other unreleased cone.* packages as needed
+
+ # Then install cone.app
+ pip install -e .
+
+Finally, install your application package in editable mode:
+
+.. code-block:: sh
+
+ # cone.app root directory
+ # venv
+ pip install -e examples/cone.example
+
+ # exit the venv
+ deactivate
+
+You are now ready to run your application as described in section 9.
\ No newline at end of file
diff --git a/examples/cone.example/example.ini b/examples/cone.example/example.ini
index f92ef530..c5bd6930 100644
--- a/examples/cone.example/example.ini
+++ b/examples/cone.example/example.ini
@@ -21,11 +21,12 @@ pyramid.debug_templates = true
pyramid.default_locale_name = en
# available languages
-cone.available_languages = de, en
+cone.available_languages = en, de
# cone.app admin user and password
-#cone.admin_user =
-#cone.admin_password =
+cone.admin_user = admin
+cone.admin_password = admin
+#cone.authenticator =
# cone.app auth tkt settings
cone.auth_secret = 12345
@@ -39,21 +40,19 @@ cone.auth_secret = 12345
#cone.auth_path =
#cone.auth_wild_domain =
+# application main template
+#cone.main_template = package.browser:templates/main.pt
+
# plugins to be loaded
cone.plugins = cone.example
# application root node settings
+#cone.root.node_factory = package.root_node_factory
cone.root.title = cone.example
-#cone.root.default_child = example
-#cone.root.default_content_tile =
+#cone.root.default_child = example # XXX throws error
+#cone.root.default_content_tile =
#cone.root.mainmenu_empty_title = false
-ugm.backend = file
-ugm.users_file = var/ugm/users
-ugm.groups_file = var/ugm/groups
-ugm.roles_file = var/ugm/roles
-ugm.datadir = var/ugm/data
-
[pipeline:main]
pipeline =
- example
+ example
\ No newline at end of file
diff --git a/examples/cone.example/pyproject.toml b/examples/cone.example/pyproject.toml
new file mode 100644
index 00000000..d377926f
--- /dev/null
+++ b/examples/cone.example/pyproject.toml
@@ -0,0 +1,15 @@
+[build-system]
+requires = ["setuptools>=61.0,<81"] # XXX: depends on PIL
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "cone.example"
+version = "0.1"
+description = "Example cone plugin"
+dependencies = [
+ "waitress",
+ "cone.app"
+]
+
+[tool.setuptools.packages.find]
+where = ["src"]
\ No newline at end of file
diff --git a/examples/cone.example/setup.py b/examples/cone.example/setup.py
deleted file mode 100644
index 4f76c5a2..00000000
--- a/examples/cone.example/setup.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from setuptools import find_packages
-from setuptools import setup
-
-
-version = '0.1'
-shortdesc = 'Example cone plugin'
-
-
-setup(
- name='cone.example',
- version=version,
- description=shortdesc,
- packages=find_packages('src'),
- package_dir={'': 'src'},
- namespace_packages=['cone'],
- include_package_data=True,
- zip_safe=False,
- install_requires=[
- 'cone.app',
- 'waitress'
- ]
-)
diff --git a/examples/cone.example/src/cone/__init__.py b/examples/cone.example/src/cone/__init__.py
deleted file mode 100644
index de40ea7c..00000000
--- a/examples/cone.example/src/cone/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
diff --git a/examples/cone.example/src/cone/example/__init__.py b/examples/cone.example/src/cone/example/__init__.py
index f0764a72..8a81281a 100644
--- a/examples/cone.example/src/cone/example/__init__.py
+++ b/examples/cone.example/src/cone/example/__init__.py
@@ -1,27 +1,56 @@
from cone.app import main_hook
+from cone.app import register_config
from cone.app import register_entry
+from cone.example.browser import _configure_layout_configs
from cone.example.browser import configure_resources
-from cone.example.model import EntryFolder
from cone.example.model import LiveSearch
@main_hook
def example_main_hook(config, global_config, settings):
- """Function which gets called at application startup to initialize
- this plugin.
+ """Application startup hook.
+
+ Registers all entry nodes, settings, search adapters, static resources,
+ and scans browser packages for tiles and views.
"""
- # register live search adapter
+ # Register live search adapter
config.registry.registerAdapter(LiveSearch)
- # add translation
+ # Add translation directories
config.add_translation_dirs('cone.example:locale/')
- # register plugin entry nodes
- for i in range(1, 6):
- register_entry(f'folder_{i}', EntryFolder)
+ # Register entry nodes in the main menu
+ from cone.example.document.model import DocumentLibrary
+ from cone.example.project.model import ProjectBoard
+ from cone.example.wiki.model import Wiki
+ from cone.example.ajax.browser import AjaxPlayground
+
+ register_entry('documents', DocumentLibrary)
+ register_entry('projects', ProjectBoard)
+ register_entry('wiki', Wiki)
+ register_entry('ajax_playground', AjaxPlayground)
+
+ # Register settings node
+ from cone.example.settings.model import ExampleSettings
+ register_config('example_settings', ExampleSettings)
- # static resources
+ # Register custom add model factory for Task
+ from cone.app.model import get_node_info
+ from cone.example.project.browser import task_addmodel_factory
+ task_info = get_node_info('task')
+ if task_info:
+ task_info.factory = task_addmodel_factory
+
+ # Static resources
configure_resources(config, settings)
- # scan browser package
+ # Register layout configs (deferred to avoid circular imports)
+ _configure_layout_configs()
+
+ # Scan browser packages for tile and view registrations
config.scan('cone.example.browser')
+ config.scan('cone.example.document.browser')
+ config.scan('cone.example.project.browser')
+ config.scan('cone.example.wiki.browser')
+ config.scan('cone.example.settings.browser')
+ config.scan('cone.example.ajax.browser')
diff --git a/examples/cone.example/src/cone/example/ajax/__init__.py b/examples/cone.example/src/cone/example/ajax/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/ajax/browser.py b/examples/cone.example/src/cone/example/ajax/browser.py
new file mode 100644
index 00000000..9092cd2d
--- /dev/null
+++ b/examples/cone.example/src/cone/example/ajax/browser.py
@@ -0,0 +1,159 @@
+from cone.app.browser.ajax import ajax_continue
+from cone.app.browser.ajax import ajax_message
+from cone.app.browser.ajax import ajax_status_message
+from cone.app.browser.ajax import AjaxAction
+from cone.app.browser.ajax import AjaxEvent
+from cone.app.browser.ajax import AjaxMessage
+from cone.app.browser.ajax import AjaxOverlay
+from cone.app.browser.ajax import AjaxPath
+from cone.app.browser.layout import ProtectedContentTile
+from cone.app.browser.utils import make_url
+from cone.app.model import AppNode
+from cone.app.model import BaseNode
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import node_info
+from cone.example.model import _
+from cone.tile import tile
+from node.utils import instance_property
+
+
+@node_info(
+ name='ajax_playground',
+ title=_('ajax_playground', default='AJAX Playground'),
+ icon='bi-lightning')
+class AjaxPlayground(BaseNode):
+ """Entry node for demonstrating all AJAX operation types.
+
+ This node provides tiles that trigger each of the six AJAX
+ continuation operations available in cone.app:
+ - AjaxPath: Set browser URL path
+ - AjaxAction: Render tile into DOM element
+ - AjaxEvent: Trigger JS event
+ - AjaxMessage: Display a message
+ - AjaxOverlay: Show/close overlay
+ - ajax_continue: Send multiple operations
+ """
+
+ @instance_property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.action_up = True
+ props.action_view = True
+ return props
+
+ @instance_property
+ def metadata(self):
+ md = Metadata()
+ md.title = _('ajax_playground', default='AJAX Playground')
+ md.description = _(
+ 'ajax_playground_desc',
+ default='Demonstrates all AJAX operation types')
+ md.icon = 'bi-lightning'
+ return md
+
+
+@tile(name='content',
+ path='cone.example.ajax:templates/ajax_playground.pt',
+ interface=AjaxPlayground,
+ permission='login')
+class AjaxPlaygroundView(ProtectedContentTile):
+
+ @property
+ def ajax_path_url(self):
+ return make_url(self.request, node=self.model)
+
+ @property
+ def ajax_action_url(self):
+ return make_url(self.request, node=self.model)
+
+
+# Tile triggered by AjaxAction demo
+@tile(name='ajax_demo_content',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxDemoContent(ProtectedContentTile):
+
+ def render(self):
+ return '' \
+ 'AjaxAction worked! ' \
+ 'This content was loaded via AjaxAction and inserted ' \
+ 'into the DOM.
'
+
+
+# Tile for AjaxPath demo
+@tile(name='ajax_path_demo',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxPathDemo(ProtectedContentTile):
+
+ def render(self):
+ url = make_url(self.request, node=self.model)
+ ajax_continue(self.request, [
+ AjaxPath(
+ path='/'.join(self.model.path),
+ target=url,
+ event='contextchanged:#layout'
+ )
+ ])
+ return ''
+
+
+# Tile for AjaxEvent demo
+@tile(name='ajax_event_demo',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxEventDemo(ProtectedContentTile):
+
+ def render(self):
+ url = make_url(self.request, node=self.model)
+ ajax_continue(self.request, [
+ AjaxEvent(url, 'contextchanged', '#layout')
+ ])
+ return ''
+
+
+# Tile for AjaxMessage demo
+@tile(name='ajax_message_demo',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxMessageDemo(ProtectedContentTile):
+
+ def render(self):
+ ajax_message(
+ self.request,
+ 'This is a demo message from AjaxMessage!',
+ 'info'
+ )
+ return ''
+
+
+# Tile for AjaxAction demo
+@tile(name='ajax_action_demo',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxActionDemo(ProtectedContentTile):
+
+ def render(self):
+ url = make_url(self.request, node=self.model)
+ ajax_continue(self.request, [
+ AjaxAction(url, 'ajax_demo_content', 'inner', '#ajax-demo-target')
+ ])
+ return ''
+
+
+# Tile for combined operations demo
+@tile(name='ajax_combined_demo',
+ interface=AjaxPlayground,
+ permission='view')
+class AjaxCombinedDemo(ProtectedContentTile):
+
+ def render(self):
+ url = make_url(self.request, node=self.model)
+ ajax_continue(self.request, [
+ AjaxAction(url, 'ajax_demo_content', 'inner', '#ajax-demo-target'),
+ AjaxMessage('Combined: action + message + event!', 'success'),
+ AjaxEvent(url, 'contextchanged', '#layout'),
+ ])
+ return ''
diff --git a/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt b/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
new file mode 100644
index 00000000..59436ac2
--- /dev/null
+++ b/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+ This page demonstrates all six AJAX continuation operation types
+ available in cone.app. Click each button to see the operation in action.
+
+
+
+
+
+
+
+
+
+ AjaxPath
+
+
+ Sets the browser URL path without page reload.
+ Updates browser history.
+
+
+ Trigger AjaxPath
+
+
+
+
+
+
+
+
+
+
+ AjaxAction
+
+
+ Renders a tile and inserts the result into a DOM element.
+
+
+ Trigger AjaxAction
+
+
+
+
+
+
+
+
+
+
+ AjaxEvent
+
+
+ Triggers a JavaScript event on a DOM element.
+ Used for inter-component communication.
+
+
+ Trigger AjaxEvent
+
+
+
+
+
+
+
+
+
+
+ AjaxMessage
+
+
+ Displays a notification message to the user.
+ Supports info, warning, and error flavors.
+
+
+ Trigger AjaxMessage
+
+
+
+
+
+
+
+
+
+
+ Combined Operations
+
+
+ Demonstrates ajax_continue with multiple operations
+ chained together (AjaxAction + AjaxMessage + AjaxEvent).
+
+
+ Trigger Combined
+
+
+
+
+
+
+
+
+
+
+ AjaxAction target area - content will be inserted here.
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/browser/__init__.py b/examples/cone.example/src/cone/example/browser/__init__.py
index db562d34..d3c82998 100644
--- a/examples/cone.example/src/cone/example/browser/__init__.py
+++ b/examples/cone.example/src/cone/example/browser/__init__.py
@@ -1,28 +1,24 @@
-from cone.app.browser.authoring import ContentAddForm
-from cone.app.browser.authoring import ContentEditForm
-from cone.app.browser.form import AddFormTarget
-from cone.app.browser.form import EditFormTarget
-from cone.app.browser.form import Form
+from cone.app import DefaultLayoutConfig
+from cone.app import layout_config
+from cone.app.browser.actions import LinkAction
+from cone.app.browser.actions import TemplateAction
+from cone.app.browser.contextmenu import context_menu_group
+from cone.app.browser.contextmenu import context_menu_item
+from cone.app.browser.contextmenu import ContextMenuToolbar
+from cone.app.browser.layout import personal_tools_action
from cone.app.browser.layout import ProtectedContentTile
-from cone.app.browser.utils import choose_name
-from cone.app.utils import add_creation_metadata
-from cone.app.utils import update_creation_metadata
-from cone.example.model import EntryFolder
-from cone.example.model import Folder
-from cone.example.model import Item
-from cone.example.model import Translation
+from cone.app.browser.utils import make_url
+from cone.app.browser.utils import request_property
+from cone.app.model import Properties
+from cone.example.model import _
from cone.tile import tile
-from node.utils import UNSET
-from plumber import plumbing
-from pyramid.i18n import TranslationStringFactory
-from yafowil.base import factory
-from yafowil.persistence import write_mapping_writer
import os
import webresource as wr
-_ = TranslationStringFactory('cone.example')
-
+###############################################################################
+# Static Resources
+###############################################################################
resources_dir = os.path.join(os.path.dirname(__file__), 'static')
cone_example_resources = wr.ResourceGroup(
@@ -41,104 +37,137 @@ def configure_resources(config, settings):
config.set_resource_include('cone-example-css', 'authenticated')
-@tile(name='view',
- path='templates/view.pt',
- interface=EntryFolder,
- permission='login')
-@tile(name='view',
- path='templates/view.pt',
- interface=Folder,
- permission='login')
-@tile(name='content',
- path='templates/view.pt',
- interface=Item,
- permission='login')
-class ViewContent(ProtectedContentTile):
- pass
-
-
-class ExampleForm(Form):
-
- def prepare(self):
- self.form = form = factory(
- 'form',
- name='contentform',
- props={
- 'action': self.form_action,
- 'persist_writer': write_mapping_writer
- })
- form['title'] = factory(
- 'field:label:help:error:translation:text',
- value=self.model.attrs.get('title', UNSET),
- props={
- 'factory': Translation,
- 'label': _('title', default='Title'),
- 'help': _('title_description', default='Enter a title'),
- 'required': _('title_required', default='Title is mandatory')
- })
- form['description'] = factory(
- 'field:label:help:error:translation:textarea',
- value=self.model.attrs.get('description', UNSET),
- props={
- 'factory': Translation,
- 'label': _('description', default='Description'),
- 'help': _(
- 'description_description',
- default='Enter a description'
- ),
- 'rows': 4
- })
- form['save'] = factory(
- 'submit',
- props={
- 'action': 'save',
- 'expression': True,
- 'handler': self.save,
- 'next': self.next,
- 'label': _('save', default='Save')
- })
- form['cancel'] = factory(
- 'submit',
- props={
- 'action': 'cancel',
- 'expression': True,
- 'skip': True,
- 'next': self.next,
- 'label': _('cancel', default='Cancel')
- })
-
- def save(self, widget, data):
- data.write(self.model.attrs)
-
-
-@plumbing(AddFormTarget)
-class ExampleAddForm(ExampleForm):
-
- def save(self, widget, data):
- add_creation_metadata(self.request, self.model.attrs)
- super(ExampleAddForm, self).save(widget, data)
- parent = self.model.parent
- parent[choose_name(parent, self.model.metadata.title)] = self.model
-
-
-@plumbing(EditFormTarget)
-class ExampleEditForm(ExampleForm):
-
- def save(self, widget, data):
- update_creation_metadata(self.request, self.model.attrs)
- super(ExampleEditForm, self).save(widget, data)
-
-
-@tile(name='addform', interface=Folder, permission='add')
-@tile(name='addform', interface=Item, permission='add')
-@plumbing(ContentAddForm)
-class ExampleContentAddForm(ExampleAddForm):
- ...
-
-
-@tile(name='editform', interface=EntryFolder, permission='edit')
-@tile(name='editform', interface=Folder, permission='edit')
-@tile(name='editform', interface=Item, permission='edit')
-@plumbing(ContentEditForm)
-class ExampleContentEditForm(ExampleEditForm):
- ...
+###############################################################################
+# Layout Configs
+#
+# Layout configs control the appearance of the page for different node types.
+# They determine which sidebar tiles are rendered, whether the main menu,
+# search bar, and path bar are shown, etc.
+###############################################################################
+
+# Import model classes here to avoid circular imports at module level.
+# layout_config decorators register factories looked up by model class.
+
+def _configure_layout_configs():
+ """Register layout configs after model classes are available."""
+ from cone.example.document.model import Document
+ from cone.example.document.model import DocumentFolder
+ from cone.example.document.model import DocumentLibrary
+ from cone.example.project.model import ProjectBoard
+ from cone.example.project.model import Task
+ from cone.example.wiki.model import Wiki
+ from cone.example.wiki.model import WikiPage
+ from cone.example.ajax.browser import AjaxPlayground
+
+ @layout_config(DocumentLibrary, DocumentFolder)
+ class DocumentContainerLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = ['navtree']
+
+ @layout_config(Document)
+ class DocumentLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = ['navtree']
+
+ @layout_config(ProjectBoard)
+ class ProjectBoardLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = ['navtree']
+
+ @layout_config(Task)
+ class TaskLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = ['navtree']
+
+ @layout_config(Wiki, WikiPage)
+ class WikiLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = ['navtree']
+
+ @layout_config(AjaxPlayground)
+ class AjaxPlaygroundLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super().__init__(model=model, request=request)
+ self.sidebar_left = []
+
+
+###############################################################################
+# Custom Personal Tools Action
+#
+# Adds a custom item to the personal tools dropdown menu (top right).
+###############################################################################
+
+@personal_tools_action(name='example_info')
+class ExampleInfoAction(LinkAction):
+ """Custom personal tools action - shows a link in the user dropdown."""
+ text = _('example_info', default='Example Info')
+ icon = 'bi-info-circle'
+ event = 'contextchanged:#layout'
+ path = 'href'
+
+ @property
+ def target(self):
+ return make_url(self.request, node=self.model.root)
+
+ href = target
+
+ @property
+ def display(self):
+ return bool(self.request.authenticated_userid)
+
+
+###############################################################################
+# Custom Context Menu Group
+#
+# Adds a custom group to the context menu with custom actions.
+###############################################################################
+
+@context_menu_group(name='example_tools')
+class ExampleToolsToolbar(ContextMenuToolbar):
+ """Custom context menu toolbar group for example-specific actions."""
+
+
+@context_menu_item(group='example_tools', name='example_action')
+class ExampleContextAction(LinkAction):
+ """Custom context menu action demonstrating LinkAction in context menu."""
+ css = 'nav-link'
+ text = _('refresh', default='Refresh')
+ icon = 'bi-arrow-clockwise'
+ event = 'contextchanged:#layout'
+
+ @property
+ def target(self):
+ return make_url(self.request, node=self.model)
+
+ @property
+ def href(self):
+ return make_url(self.request, node=self.model)
+
+ @property
+ def display(self):
+ return self.permitted('view')
+
+
+###############################################################################
+# request_property Demo
+#
+# request_property caches a computed value for the duration of a single
+# request, avoiding redundant computation.
+###############################################################################
+
+class RequestPropertyDemo:
+ """Demonstrates request_property decorator.
+
+ The decorated method is called once per request and the result is cached.
+ Subsequent access returns the cached value.
+ """
+
+ @request_property
+ def expensive_computation(self):
+ """This would only be computed once per request."""
+ return {'computed': True, 'data': [1, 2, 3]}
diff --git a/examples/cone.example/src/cone/example/browser/static/cone.example.css b/examples/cone.example/src/cone/example/browser/static/cone.example.css
index a53f4db8..aa82d393 100644
--- a/examples/cone.example/src/cone/example/browser/static/cone.example.css
+++ b/examples/cone.example/src/cone/example/browser/static/cone.example.css
@@ -1,3 +1,41 @@
+/* Document workflow states */
+#contextmenu a.state-draft,
+tr.state-draft td.title a {
+ color: #6c757d;
+}
+#contextmenu a.state-review,
+tr.state-review td.title a {
+ color: #fd7e14;
+}
+#contextmenu a.state-published,
+tr.state-published td.title a {
+ color: #198754;
+}
+#contextmenu a.state-archived,
+tr.state-archived td.title a {
+ color: #6c757d;
+ text-decoration: line-through;
+}
+
+/* Task workflow states */
+#contextmenu a.state-todo,
+tr.state-todo td.title a {
+ color: #0d6efd;
+}
+#contextmenu a.state-in_progress,
+tr.state-in_progress td.title a {
+ color: #fd7e14;
+}
+#contextmenu a.state-done,
+tr.state-done td.title a {
+ color: #198754;
+}
+#contextmenu a.state-closed,
+tr.state-closed td.title a {
+ color: #6c757d;
+}
+
+/* Publication workflow states (backward compat) */
#contextmenu a.state-private,
tr.state-private td.title a {
color: red;
diff --git a/examples/cone.example/src/cone/example/configure.zcml b/examples/cone.example/src/cone/example/configure.zcml
index bad07151..1416468b 100644
--- a/examples/cone.example/src/cone/example/configure.zcml
+++ b/examples/cone.example/src/cone/example/configure.zcml
@@ -1,6 +1,7 @@
-
+
+
diff --git a/examples/cone.example/src/cone/example/document/__init__.py b/examples/cone.example/src/cone/example/document/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/document/browser.py b/examples/cone.example/src/cone/example/document/browser.py
new file mode 100644
index 00000000..5922a3af
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document/browser.py
@@ -0,0 +1,243 @@
+from cone.app.browser.authoring import ContentAddForm
+from cone.app.browser.authoring import ContentEditForm
+from cone.app.browser.content import content_view_action
+from cone.app.browser.content import content_view_tile
+from cone.app.browser.form import AddFormTarget
+from cone.app.browser.form import EditFormTarget
+from cone.app.browser.form import Form
+from cone.app.browser.layout import ProtectedContentTile
+from cone.app.browser.utils import choose_name
+from cone.app.utils import add_creation_metadata
+from cone.app.utils import update_creation_metadata
+from cone.example.document.model import Document
+from cone.example.document.model import DocumentFolder
+from cone.example.document.model import DocumentLibrary
+from cone.example.model import Translation
+from cone.example.model import _
+from cone.tile import tile
+from node.utils import UNSET
+from plumber import plumbing
+from yafowil.base import factory
+from yafowil.persistence import write_mapping_writer
+
+
+# View tiles for DocumentLibrary and DocumentFolder (container view)
+@tile(name='view',
+ path='cone.example.browser:templates/view.pt',
+ interface=DocumentLibrary,
+ permission='login')
+@tile(name='view',
+ path='cone.example.browser:templates/view.pt',
+ interface=DocumentFolder,
+ permission='login')
+class DocumentContainerView(ProtectedContentTile):
+ pass
+
+
+# Content view tile for Document - main view
+@content_view_tile(
+ name='content',
+ path='cone.example.document:templates/document_view.pt',
+ interface=Document,
+ permission='login')
+@content_view_action(
+ name='view',
+ tilename='content',
+ interface=Document,
+ permission='view',
+ text=_('view', default='View'),
+ icon='bi-eye')
+class DocumentView(ProtectedContentTile):
+ pass
+
+
+# Content view tile for Document - source/raw view
+@content_view_tile(
+ name='source',
+ path='cone.example.document:templates/document_source.pt',
+ interface=Document,
+ permission='login')
+@content_view_action(
+ name='source',
+ tilename='source',
+ interface=Document,
+ permission='edit',
+ text=_('source', default='Source'),
+ icon='bi-code')
+class DocumentSourceView(ProtectedContentTile):
+ pass
+
+
+# Base form for Document types
+class DocumentForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='documentform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'help': _('title_help', default='Enter a title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'help': _('description_help', default='Enter a description'),
+ 'rows': 3
+ })
+ form['body'] = factory(
+ 'field:label:help:error:textarea',
+ value=self.model.attrs.get('body', UNSET),
+ props={
+ 'label': _('body', default='Body'),
+ 'help': _('body_help', default='Enter the document body'),
+ 'rows': 10
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ def save(self, widget, data):
+ data.write(self.model.attrs)
+
+
+# Folder form (title + description only)
+class FolderForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='folderform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'help': _('title_help', default='Enter a title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'help': _('description_help', default='Enter a description'),
+ 'rows': 3
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ def save(self, widget, data):
+ data.write(self.model.attrs)
+
+
+@plumbing(AddFormTarget)
+class DocumentAddForm(DocumentForm):
+
+ def save(self, widget, data):
+ add_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+ parent = self.model.parent
+ parent[choose_name(parent, self.model.metadata.title)] = self.model
+
+
+@plumbing(EditFormTarget)
+class DocumentEditForm(DocumentForm):
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+
+
+@plumbing(AddFormTarget)
+class FolderAddForm(FolderForm):
+
+ def save(self, widget, data):
+ add_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+ parent = self.model.parent
+ parent[choose_name(parent, self.model.metadata.title)] = self.model
+
+
+@plumbing(EditFormTarget)
+class FolderEditForm(FolderForm):
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+
+
+# Add forms
+@tile(name='addform', interface=Document, permission='add')
+@plumbing(ContentAddForm)
+class DocumentContentAddForm(DocumentAddForm):
+ ...
+
+
+@tile(name='addform', interface=DocumentFolder, permission='add')
+@plumbing(ContentAddForm)
+class FolderContentAddForm(FolderAddForm):
+ ...
+
+
+# Edit forms
+@tile(name='editform', interface=Document, permission='edit')
+@plumbing(ContentEditForm)
+class DocumentContentEditForm(DocumentEditForm):
+ ...
+
+
+@tile(name='editform', interface=DocumentLibrary, permission='edit')
+@tile(name='editform', interface=DocumentFolder, permission='edit')
+@plumbing(ContentEditForm)
+class FolderContentEditForm(FolderEditForm):
+ ...
diff --git a/examples/cone.example/src/cone/example/document/model.py b/examples/cone.example/src/cone/example/document/model.py
new file mode 100644
index 00000000..19269131
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document/model.py
@@ -0,0 +1,93 @@
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import Metadata
+from cone.app.model import node_info
+from cone.app.model import Properties
+from cone.app.model import ProtectedProperties
+from cone.app.model import UUIDAttributeAware
+from cone.app.security import OwnerSupport
+from cone.app.security import PrincipalACL
+from cone.example.model import _
+from cone.example.model import BaseContainer
+from cone.example.model import Translation
+from cone.example.model import WorkflowNode
+from node.utils import instance_property
+from plumber import plumbing
+from zope.interface import implementer
+
+
+@node_info(
+ name='document_library',
+ title=_('document_library', default='Document Library'),
+ icon='bi-collection',
+ addables=['document_folder', 'document'])
+class DocumentLibrary(BaseContainer):
+
+ @property
+ def properties(self):
+ props = super().properties
+ props.mainmenu_display_children = False
+ return props
+
+
+@node_info(
+ name='document_folder',
+ title=_('document_folder', default='Document Folder'),
+ icon='bi-folder',
+ addables=['document_folder', 'document'])
+class DocumentFolder(BaseContainer):
+
+ @property
+ def properties(self):
+ props = super().properties
+ props.action_delete = True
+ return props
+
+
+@node_info(
+ name='document',
+ title=_('document', default='Document'),
+ icon='bi-file-earmark-text')
+@plumbing(OwnerSupport, PrincipalACL, UUIDAttributeAware)
+@implementer(INavigationLeaf)
+class Document(WorkflowNode):
+ workflow_name = 'document_workflow'
+ role_inheritance = True
+
+ @instance_property
+ def principal_roles(self):
+ return {}
+
+ @property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.action_up = True
+ props.action_view = True
+ props.action_edit = True
+ props.action_delete = True
+ props.action_sharing = True
+ return props
+
+ @property
+ def metadata(self):
+ md = Metadata()
+ md.icon = self.nodeinfo.icon
+ title = self.attrs.get('title')
+ md.title = title.value if title else self.name
+ description = self.attrs.get('description')
+ md.description = description.value if description else ''
+ md.creator = self.attrs.get('creator', '')
+ md.created = self.attrs.get('created')
+ md.modified = self.attrs.get('modified')
+ return md
+
+ @property
+ def protected_properties(self):
+ """Demonstrates ProtectedProperties - body requires edit permission."""
+ props = ProtectedProperties(
+ self,
+ permissions={
+ 'body': ['edit'],
+ })
+ props.body = self.attrs.get('body', '')
+ return props
diff --git a/examples/cone.example/src/cone/example/document/templates/document_source.pt b/examples/cone.example/src/cone/example/document/templates/document_source.pt
new file mode 100644
index 00000000..ab0e25e6
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document/templates/document_source.pt
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
Attributes
+
+
+
+ Key
+ Value
+
+
+
+
+ key
+ value
+
+
+
+
+
Properties
+
+
+
+ UUID
+ uuid
+
+
+ Owner
+ owner
+
+
+ Workflow State
+ state
+
+
+ Path
+ path
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/document/templates/document_view.pt b/examples/cone.example/src/cone/example/document/templates/document_view.pt
new file mode 100644
index 00000000..56f6f6a5
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document/templates/document_view.pt
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ No content yet.
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/document_workflow.zcml b/examples/cone.example/src/cone/example/document_workflow.zcml
new file mode 100644
index 00000000..322103e8
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document_workflow.zcml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/model.py b/examples/cone.example/src/cone/example/model.py
index 2d989507..237efa5b 100644
--- a/examples/cone.example/src/cone/example/model.py
+++ b/examples/cone.example/src/cone/example/model.py
@@ -1,15 +1,12 @@
from cone.app.browser.utils import make_url
from cone.app.interfaces import IApplicationNode
from cone.app.interfaces import ILiveSearch
-from cone.app.interfaces import INavigationLeaf
from cone.app.model import AppNode
from cone.app.model import CopySupport
from cone.app.model import Metadata
-from cone.app.model import node_info
from cone.app.model import Properties
from cone.app.model import Translation as TranslationBehavior
from cone.app.security import PrincipalACL
-from cone.app.utils import add_creation_metadata
from cone.app.workflow import WorkflowACL
from cone.app.workflow import WorkflowState
from node.behaviors import Attributes
@@ -26,7 +23,6 @@
from pyramid.security import Allow
from pyramid.security import Deny
from pyramid.security import Everyone
-from pyramid.threadlocal import get_current_request
from zope.component import adapter
from zope.interface import implementer
@@ -34,6 +30,9 @@
_ = TranslationStringFactory('cone.example')
+# Translation-aware string storage.
+# Stores values per language key (e.g., 'en', 'de'). The ``value`` property
+# returns the translation for the current request locale.
@plumbing(
NodeInit,
MappingNode,
@@ -43,6 +42,28 @@ class Translation:
...
+# Default ACL shared across example node types.
+DEFAULT_EXAMPLE_ACL = [
+ (Allow, 'system.Authenticated', ['view']),
+ (Allow, 'role:viewer', ['view', 'list']),
+ (Allow, 'role:editor', [
+ 'view', 'list', 'add', 'edit', 'cut', 'copy', 'paste',
+ 'change_order'
+ ]),
+ (Allow, 'role:admin', [
+ 'view', 'list', 'add', 'edit', 'delete', 'cut', 'copy', 'paste',
+ 'change_order', 'change_state', 'manage_permissions'
+ ]),
+ (Allow, 'role:manager', [
+ 'view', 'list', 'add', 'edit', 'delete', 'cut', 'copy', 'paste',
+ 'change_order', 'change_state', 'manage_permissions', 'manage'
+ ]),
+ (Allow, Everyone, ['login']),
+ (Deny, Everyone, ALL_PERMISSIONS),
+]
+
+
+# Base node with workflow support, ordered storage, and attributes.
@plumbing(
AppNode,
WorkflowState,
@@ -53,34 +74,34 @@ class Translation:
MappingNode,
MappingOrder,
OdictStorage)
-class PublicationWorkflowNode:
- workflow_name = 'publication'
+class WorkflowNode:
+ workflow_name = None
workflow_tsf = staticmethod(_)
- default_acl = [
- (Allow, 'system.Authenticated', ['view']),
- (Allow, 'role:viewer', ['view', 'list']),
- (Allow, 'role:editor', [
- 'view', 'list', 'add', 'edit', 'cut', 'copy', 'paste',
- 'change_order'
- ]),
- (Allow, 'role:admin', [
- 'view', 'list', 'add', 'edit', 'delete', 'cut', 'copy', 'paste',
- 'change_order', 'change_state', 'manage_permissions'
- ]),
- (Allow, 'role:manager', [
- 'view', 'list', 'add', 'edit', 'delete', 'cut', 'copy', 'paste',
- 'change_order', 'change_state', 'manage_permissions', 'manage'
- ]),
- (Allow, Everyone, ['login']),
- (Deny, Everyone, ALL_PERMISSIONS),
- ]
+ default_acl = DEFAULT_EXAMPLE_ACL
def __call__(self):
...
+# Base node without workflow, ordered storage, and attributes.
+@plumbing(
+ AppNode,
+ MappingAdopt,
+ Attributes,
+ NodeInit,
+ MappingNode,
+ MappingOrder,
+ OdictStorage)
+class ContainerNode:
+ default_acl = DEFAULT_EXAMPLE_ACL
+
+ def __call__(self):
+ ...
+
+
+# Container base with PrincipalACL and CopySupport.
@plumbing(PrincipalACL, CopySupport)
-class BaseContainer(PublicationWorkflowNode):
+class BaseContainer(ContainerNode):
role_inheritance = True
@instance_property
@@ -105,124 +126,23 @@ def properties(self):
def metadata(self):
md = Metadata()
md.icon = self.nodeinfo.icon
- md.title = self.attrs['title'].value
- md.description = self.attrs['description'].value
- md.creator = self.attrs['creator']
- md.created = self.attrs['created']
- md.modified = self.attrs['modified']
+ title = self.attrs.get('title')
+ md.title = title.value if title else self.name
+ description = self.attrs.get('description')
+ md.description = description.value if description else ''
+ md.creator = self.attrs.get('creator', '')
+ md.created = self.attrs.get('created')
+ md.modified = self.attrs.get('modified')
return md
-@node_info(
- name='entry_folder',
- title=_('folder', default='Folder'),
- icon='bi-folder',
- addables=['folder', 'item'])
-class EntryFolder(BaseContainer):
-
- def __init__(self, name=None, parent=None):
- super().__init__(name=name, parent=parent)
- create_content(self)
-
- @property
- def properties(self):
- props = super().properties
- props.mainmenu_display_children = True
- return props
-
-
-@node_info(
- name='folder',
- title=_('folder', default='Folder'),
- icon='bi-folder',
- addables=['folder', 'item'])
-class Folder(BaseContainer):
-
- @property
- def properties(self):
- props = super().properties
- props.action_delete = True
- return props
-
-
-@node_info(
- name='item',
- title=_('item', default='Item'),
- icon='bi-file')
-@plumbing(PrincipalACL)
-@implementer(INavigationLeaf)
-class Item(PublicationWorkflowNode):
- role_inheritance = True
-
- @instance_property
- def principal_roles(self):
- return {}
-
- @property
- def properties(self):
- props = Properties()
- props.in_navtree = True
- props.action_up = True
- props.action_view = True
- props.action_edit = True
- props.action_delete = True
- props.action_sharing = True
- return props
-
- @property
- def metadata(self):
- md = Metadata()
- md.icon = self.nodeinfo.icon
- md.title = self.attrs['title'].value
- md.description = self.attrs['description'].value
- md.creator = self.attrs['creator']
- md.created = self.attrs['created']
- md.modified = self.attrs['modified']
- return md
-
-
-def create_content(node):
- request = get_current_request()
- name = node.name
-
- add_creation_metadata(request, node.attrs)
-
- title = node.attrs['title'] = Translation()
- title['en'] = f'Folder {name[name.rfind("_") + 1:]}'
- title['de'] = f'Ordner {name[name.rfind("_") + 1:]}'
-
- description = node.attrs['description'] = Translation()
- description['en'] = f'Folder Description'
- description['de'] = f'Ordner Beschreibung'
-
- for i in range(1, 21):
- folder = node[f'folder_{i}'] = Folder()
- add_creation_metadata(request, folder.attrs)
-
- title = folder.attrs['title'] = Translation()
- title['en'] = f'Folder {i}'
- title['de'] = f'Ordner {i}'
-
- description = folder.attrs['description'] = Translation()
- description['en'] = f'Folder Description'
- description['de'] = f'Ordner Beschreibung'
-
- for j in range(1, 21):
- item = folder[f'item_{j}'] = Item()
- add_creation_metadata(request, item.attrs)
-
- title = item.attrs['title'] = Translation()
- title['en'] = f'Item {j}'
- title['de'] = f'Object {j}'
-
- description = item.attrs['description'] = Translation()
- description['en'] = f'Item Description'
- description['de'] = f'Object Beschreibung'
-
-
@implementer(ILiveSearch)
@adapter(IApplicationNode)
class LiveSearch(object):
+ """Live search adapter.
+
+ Searches child node metadata for title and description matches.
+ """
def __init__(self, model):
self.model = model
@@ -231,14 +151,16 @@ def search(self, request, query):
result = []
for child in self.model.values():
md = child.metadata
+ title = md.title or ''
+ description = md.description or ''
if (
- md.title.lower().find(query.lower()) > -1 or
- md.description.lower().find(query.lower()) > -1
+ title.lower().find(query.lower()) > -1
+ or description.lower().find(query.lower()) > -1
):
result.append({
- 'value': md.title,
+ 'value': title,
'target': make_url(request, node=child),
'icon': md.icon,
- 'description': md.description,
+ 'description': description,
})
return result
diff --git a/examples/cone.example/src/cone/example/project/__init__.py b/examples/cone.example/src/cone/example/project/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/project/browser.py b/examples/cone.example/src/cone/example/project/browser.py
new file mode 100644
index 00000000..eb0329f3
--- /dev/null
+++ b/examples/cone.example/src/cone/example/project/browser.py
@@ -0,0 +1,268 @@
+from cone.app.browser.authoring import ContentAddForm
+from cone.app.browser.authoring import ContentEditForm
+from cone.app.browser.batch import BatchedItems
+from cone.app.browser.form import AddFormTarget
+from cone.app.browser.form import EditFormTarget
+from cone.app.browser.form import Form
+from cone.app.browser.layout import ProtectedContentTile
+from cone.app.browser.utils import make_url
+from cone.app.utils import add_creation_metadata
+from cone.app.utils import update_creation_metadata
+from cone.example.model import Translation
+from cone.example.model import _
+from cone.example.project.model import Project
+from cone.example.project.model import ProjectBoard
+from cone.example.project.model import Task
+from cone.example.project.model import TaskData
+from cone.tile import tile
+from node.utils import UNSET
+from plumber import plumbing
+from yafowil.base import factory
+from yafowil.persistence import write_mapping_writer
+
+
+# View tiles
+@tile(name='view',
+ path='cone.example.browser:templates/view.pt',
+ interface=ProjectBoard,
+ permission='login')
+class ProjectBoardView(ProtectedContentTile):
+ pass
+
+
+@tile(name='view',
+ path='cone.example.browser:templates/view.pt',
+ interface=Project,
+ permission='login')
+class ProjectView(ProtectedContentTile):
+ pass
+
+
+@tile(name='content',
+ path='cone.example.project:templates/task_view.pt',
+ interface=Task,
+ permission='login')
+class TaskView(ProtectedContentTile):
+ pass
+
+
+# BatchedItems tile for ProjectBoard - demonstrates batched/searchable items
+@tile(name='batched_projects',
+ path='cone.app.browser:templates/batched_items.pt',
+ interface=ProjectBoard,
+ permission='view')
+class ProjectBatchedItems(BatchedItems):
+ items_id = 'projects'
+ slice_template = 'cone.example.project:templates/project_items.pt'
+ default_slice_size = 10
+ show_title = True
+ show_filter = True
+
+ @property
+ def title(self):
+ return self.model.metadata.title
+
+ @property
+ def item_count(self):
+ term = self.filter_term
+ if term:
+ return len([
+ c for c in self.model.values()
+ if term.lower() in (c.metadata.title or '').lower()
+ ])
+ return len(self.model)
+
+ @property
+ def slice_items(self):
+ start, end = self.current_slice
+ children = list(self.model.values())
+ term = self.filter_term
+ if term:
+ children = [
+ c for c in children
+ if term.lower() in (c.metadata.title or '').lower()
+ ]
+ items = []
+ for child in children[start:end]:
+ items.append({
+ 'title': child.metadata.title,
+ 'description': child.metadata.description or '',
+ 'icon': child.metadata.icon or 'bi-clipboard',
+ 'target': make_url(self.request, node=child),
+ })
+ return items
+
+
+# Project form
+class ProjectForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='projectform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'help': _('project_title_help', default='Enter a project title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'help': _('project_desc_help', default='Enter a project description'),
+ 'rows': 4
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ def save(self, widget, data):
+ data.write(self.model.attrs)
+
+
+# Task form
+class TaskForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='taskform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'help': _('task_title_help', default='Enter a task title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'help': _('task_desc_help', default='Enter a task description'),
+ 'rows': 4
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ def save(self, widget, data):
+ data.write(self.model.attrs)
+
+
+@plumbing(AddFormTarget)
+class ProjectAddForm(ProjectForm):
+
+ def save(self, widget, data):
+ from cone.app.browser.utils import choose_name
+ add_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+ parent = self.model.parent
+ parent[choose_name(parent, self.model.metadata.title)] = self.model
+
+
+@plumbing(EditFormTarget)
+class ProjectEditForm(ProjectForm):
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+
+
+# For Task add form, we need to create a TaskData and wrap in Task
+def task_addmodel_factory(parent, nodeinfo):
+ """Custom add model factory for Task.
+ Creates a TaskData wrapped by Task (AdapterNode)."""
+ task = Task(TaskData(), None, None)
+ task.__parent__ = parent
+ return task
+
+
+@plumbing(AddFormTarget)
+class TaskAddForm(TaskForm):
+
+ def save(self, widget, data):
+ add_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+ parent = self.model.parent
+ # UUIDAsName: name is determined by UUID
+ parent[self.model.__name__] = self.model
+
+
+@plumbing(EditFormTarget)
+class TaskEditForm(TaskForm):
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+
+
+@tile(name='addform', interface=Project, permission='add')
+@plumbing(ContentAddForm)
+class ProjectContentAddForm(ProjectAddForm):
+ ...
+
+
+@tile(name='editform', interface=Project, permission='edit')
+@plumbing(ContentEditForm)
+class ProjectContentEditForm(ProjectEditForm):
+ ...
+
+
+@tile(name='addform', interface=Task, permission='add')
+@plumbing(ContentAddForm)
+class TaskContentAddForm(TaskAddForm):
+ ...
+
+
+@tile(name='editform', interface=Task, permission='edit')
+@plumbing(ContentEditForm)
+class TaskContentEditForm(TaskEditForm):
+ ...
diff --git a/examples/cone.example/src/cone/example/project/model.py b/examples/cone.example/src/cone/example/project/model.py
new file mode 100644
index 00000000..8f7c9d9a
--- /dev/null
+++ b/examples/cone.example/src/cone/example/project/model.py
@@ -0,0 +1,183 @@
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import AdapterNode
+from cone.app.model import AppNode
+from cone.app.model import BaseNode
+from cone.app.model import Categories
+from cone.app.model import FactoryNode
+from cone.app.model import Metadata
+from cone.app.model import NamespaceUUID
+from cone.app.model import Properties
+from cone.app.model import UUIDAsName
+from cone.app.model import node_info
+from cone.app.security import PrincipalACL
+from cone.app.workflow import WorkflowACL
+from cone.app.workflow import WorkflowState
+from cone.example.model import _
+from cone.example.model import DEFAULT_EXAMPLE_ACL
+from cone.example.model import Translation
+from node.behaviors import Attributes
+from node.behaviors import MappingAdopt
+from node.behaviors import MappingNode
+from node.behaviors import MappingOrder
+from node.behaviors import NodeInit
+from node.behaviors import OdictStorage
+from node.utils import instance_property
+from odict import odict
+from plumber import plumbing
+from pyramid.security import ALL_PERMISSIONS
+from pyramid.security import Allow
+from pyramid.security import Deny
+from pyramid.security import Everyone
+from zope.interface import implementer
+
+
+@node_info(
+ name='project_board',
+ title=_('project_board', default='Project Board'),
+ icon='bi-kanban',
+ addables=['project'])
+class ProjectBoard(FactoryNode):
+ """Entry container using FactoryNode pattern.
+
+ FactoryNode lazily instantiates children from the ``factories`` dict.
+ Children are volatile and recreated on invalidation.
+ """
+ factories = odict()
+
+ @property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.default_content_tile = 'listing'
+ props.action_up = True
+ props.action_view = True
+ props.action_edit = False
+ props.action_list = True
+ props.action_add = True
+ return props
+
+ @property
+ def metadata(self):
+ md = Metadata()
+ md.title = _('project_board', default='Project Board')
+ md.description = _('project_board_desc',
+ default='Manage projects and tasks')
+ md.icon = 'bi-kanban'
+ return md
+
+
+@node_info(
+ name='project',
+ title=_('project', default='Project'),
+ icon='bi-clipboard',
+ addables=['task'])
+@plumbing(NamespaceUUID, PrincipalACL, Categories, AppNode,
+ MappingAdopt, Attributes, NodeInit, MappingNode,
+ MappingOrder, OdictStorage)
+class Project:
+ """Container demonstrating NamespaceUUID and Categories.
+
+ NamespaceUUID: UUID is calculated from node path + namespace.
+ Categories: provides a list of categorization translation strings.
+ """
+ categories = [
+ _('cat_development', default='Development'),
+ _('cat_design', default='Design')
+ ]
+ role_inheritance = True
+ default_acl = DEFAULT_EXAMPLE_ACL
+
+ @instance_property
+ def principal_roles(self):
+ return {}
+
+ @property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.default_content_tile = 'listing'
+ props.action_up = True
+ props.action_view = True
+ props.action_edit = True
+ props.action_delete = True
+ props.action_list = True
+ props.action_sharing = True
+ props.action_add = True
+ return props
+
+ @property
+ def metadata(self):
+ md = Metadata()
+ md.icon = 'bi-clipboard'
+ title = self.attrs.get('title')
+ md.title = title.value if title else self.name
+ description = self.attrs.get('description')
+ md.description = description.value if description else ''
+ md.creator = self.attrs.get('creator', '')
+ md.created = self.attrs.get('created')
+ md.modified = self.attrs.get('modified')
+ return md
+
+ def __call__(self):
+ ...
+
+
+# Internal data model for Task - demonstrates that AdapterNode wraps
+# a simpler data model.
+@plumbing(NodeInit, MappingNode, Attributes, OdictStorage)
+class TaskData:
+ """Simple data node wrapped by TaskAdapter."""
+ ...
+
+
+@node_info(
+ name='task',
+ title=_('task', default='Task'),
+ icon='bi-check2-square')
+@plumbing(UUIDAsName, WorkflowState, WorkflowACL, PrincipalACL, Categories)
+@implementer(INavigationLeaf)
+class Task(AdapterNode):
+ """Leaf node demonstrating UUIDAsName, AdapterNode wrapping,
+ and a custom task workflow.
+
+ UUIDAsName: The node's __name__ is its UUID string.
+ AdapterNode: Wraps a TaskData instance, proxying attrs.
+ WorkflowState: Workflow state stored in attrs['state'].
+ Categories: Categorization support.
+ """
+ workflow_name = 'task_workflow'
+ workflow_tsf = staticmethod(_)
+ role_inheritance = True
+ categories = [
+ _('cat_development', default='Development'),
+ _('cat_design', default='Design')
+ ]
+ default_acl = DEFAULT_EXAMPLE_ACL
+
+ @instance_property
+ def principal_roles(self):
+ return {}
+
+ @property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.action_up = True
+ props.action_view = True
+ props.action_edit = True
+ props.action_delete = True
+ props.action_sharing = True
+ return props
+
+ @property
+ def metadata(self):
+ md = Metadata()
+ md.icon = 'bi-check2-square'
+ title = self.attrs.get('title')
+ md.title = title.value if title else str(self.uuid) if self.uuid else self.name
+ description = self.attrs.get('description')
+ md.description = description.value if description else ''
+ md.creator = self.attrs.get('creator', '')
+ md.created = self.attrs.get('created')
+ md.modified = self.attrs.get('modified')
+ return md
diff --git a/examples/cone.example/src/cone/example/project/templates/project_items.pt b/examples/cone.example/src/cone/example/project/templates/project_items.pt
new file mode 100644
index 00000000..bb21855f
--- /dev/null
+++ b/examples/cone.example/src/cone/example/project/templates/project_items.pt
@@ -0,0 +1,29 @@
+
+
+
+
+
+ No projects found.
+
+
+
diff --git a/examples/cone.example/src/cone/example/project/templates/task_view.pt b/examples/cone.example/src/cone/example/project/templates/task_view.pt
new file mode 100644
index 00000000..7482c5f4
--- /dev/null
+++ b/examples/cone.example/src/cone/example/project/templates/task_view.pt
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+ Description
+
+
+
+ UUID
+ uuid
+ Path
+ path
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/publication.zcml b/examples/cone.example/src/cone/example/publication.zcml
deleted file mode 100644
index 788eda8c..00000000
--- a/examples/cone.example/src/cone/example/publication.zcml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/cone.example/src/cone/example/settings/__init__.py b/examples/cone.example/src/cone/example/settings/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/settings/browser.py b/examples/cone.example/src/cone/example/settings/browser.py
new file mode 100644
index 00000000..54ab85e8
--- /dev/null
+++ b/examples/cone.example/src/cone/example/settings/browser.py
@@ -0,0 +1,83 @@
+from cone.app.browser.form import Form
+from cone.app.browser.settings import SettingsForm
+from cone.app.browser.settings import settings_form
+from cone.example.model import _
+from cone.example.settings.model import ExampleSettings
+from plumber import plumbing
+from yafowil.base import factory
+
+
+@settings_form(interface=ExampleSettings)
+@plumbing(SettingsForm)
+class ExampleSettingsForm(Form):
+ """Settings form demonstrating the settings_form decorator.
+
+ settings_form registers the form tile as 'editform' for the
+ given interface. SettingsForm behavior provides heading and
+ contextmenu handling.
+ """
+
+ def prepare(self):
+ model = self.model
+ props = model.config_properties
+ self.form = form = factory(
+ 'form',
+ name='example_settings_form',
+ props={
+ 'action': self.form_action,
+ })
+ form['items_per_page'] = factory(
+ 'field:label:help:error:number',
+ value=props.get('items_per_page', '15'),
+ props={
+ 'label': _('items_per_page', default='Items per Page'),
+ 'help': _('items_per_page_help',
+ default='Number of items shown per page'),
+ 'datatype': int,
+ 'min': 5,
+ 'max': 100,
+ })
+ form['enable_notifications'] = factory(
+ 'field:label:help:error:select',
+ value=props.get('enable_notifications', 'true'),
+ props={
+ 'label': _('enable_notifications',
+ default='Enable Notifications'),
+ 'help': _('notifications_help',
+ default='Toggle notification emails'),
+ 'vocabulary': [
+ ('true', _('yes', default='Yes')),
+ ('false', _('no', default='No')),
+ ],
+ })
+ form['default_language'] = factory(
+ 'field:label:help:error:select',
+ value=props.get('default_language', 'en'),
+ props={
+ 'label': _('default_language', default='Default Language'),
+ 'help': _('language_help',
+ default='Default language for new content'),
+ 'vocabulary': [
+ ('en', _('english', default='English')),
+ ('de', _('german', default='German')),
+ ],
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+
+ def save(self, widget, data):
+ props = self.model.config_properties
+ props.items_per_page = data.fetch(
+ 'example_settings_form.items_per_page').extracted
+ props.enable_notifications = data.fetch(
+ 'example_settings_form.enable_notifications').extracted
+ props.default_language = data.fetch(
+ 'example_settings_form.default_language').extracted
+ props()
diff --git a/examples/cone.example/src/cone/example/settings/model.py b/examples/cone.example/src/cone/example/settings/model.py
new file mode 100644
index 00000000..a76d7a5e
--- /dev/null
+++ b/examples/cone.example/src/cone/example/settings/model.py
@@ -0,0 +1,44 @@
+from cone.app.model import ConfigProperties
+from cone.app.model import Metadata
+from cone.app.model import SettingsNode
+from cone.app.model import node_info
+from cone.example.model import _
+from node.utils import instance_property
+import os
+import tempfile
+
+
+@node_info(
+ name='example_settings',
+ title=_('example_settings', default='Example Settings'),
+ description=_('example_settings_desc',
+ default='Configure example application settings'),
+ icon='bi-sliders')
+class ExampleSettings(SettingsNode):
+ """Settings node demonstrating ConfigProperties.
+
+ ConfigProperties persists settings to a config file using ConfigParser.
+ The settings_form decorator (in browser.py) creates the edit form tile.
+ """
+ category = _('example_category', default='Example')
+
+ @instance_property
+ def config_properties(self):
+ config_dir = os.environ.get(
+ 'CONE_EXAMPLE_CONFIG_DIR',
+ os.path.join(tempfile.gettempdir(), 'cone_example')
+ )
+ os.makedirs(config_dir, exist_ok=True)
+ config_path = os.path.join(config_dir, 'example_settings.cfg')
+ return ConfigProperties(config_path, data={
+ 'items_per_page': '15',
+ 'enable_notifications': 'true',
+ 'default_language': 'en',
+ })
+
+ @instance_property
+ def metadata(self):
+ md = Metadata()
+ md.title = self.nodeinfo.title
+ md.description = self.nodeinfo.description
+ return md
diff --git a/examples/cone.example/src/cone/example/task_workflow.zcml b/examples/cone.example/src/cone/example/task_workflow.zcml
new file mode 100644
index 00000000..b3dd17c7
--- /dev/null
+++ b/examples/cone.example/src/cone/example/task_workflow.zcml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/testing.py b/examples/cone.example/src/cone/example/testing.py
new file mode 100644
index 00000000..d3e75516
--- /dev/null
+++ b/examples/cone.example/src/cone/example/testing.py
@@ -0,0 +1,24 @@
+from cone.app.testing import Security as BaseSecurityLayer
+from yafowil.base import factory
+from yafowil.bootstrap import configure_factory
+
+
+class ExampleSecurity(BaseSecurityLayer):
+ """Test layer for cone.example.
+
+ Extends the base cone.app security layer to include cone.example
+ as a plugin. This ensures all entry nodes, settings, browser
+ registrations, and ZCML workflows are loaded.
+ """
+
+ def make_app(self, **kw):
+ kw.setdefault('cone.plugins', 'node.ext.ugm\ncone.example')
+ super().make_app(**kw)
+
+ def setUp(self, args=None):
+ self.make_app()
+ factory.push_state()
+ configure_factory('bootstrap5')
+
+
+security = ExampleSecurity()
diff --git a/examples/cone.example/src/cone/example/tests/__init__.py b/examples/cone.example/src/cone/example/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/wiki/__init__.py b/examples/cone.example/src/cone/example/wiki/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/cone.example/src/cone/example/wiki/browser.py b/examples/cone.example/src/cone/example/wiki/browser.py
new file mode 100644
index 00000000..84a8fde4
--- /dev/null
+++ b/examples/cone.example/src/cone/example/wiki/browser.py
@@ -0,0 +1,248 @@
+from cone.app.browser import RelatedViewProvider
+from cone.app.browser.authoring import ContentAddForm
+from cone.app.browser.authoring import ContentEditForm
+from cone.app.browser.form import AddFormTarget
+from cone.app.browser.form import EditFormTarget
+from cone.app.browser.form import Form
+from cone.app.browser.form import YAMLAddFormTarget
+from cone.app.browser.form import YAMLEditFormTarget
+from cone.app.browser.form import YAMLForm
+from cone.app.browser.layout import ProtectedContentTile
+from cone.app.browser.utils import choose_name
+from cone.app.browser.utils import make_url
+from cone.app.utils import add_creation_metadata
+from cone.app.utils import update_creation_metadata
+from cone.example.model import Translation
+from cone.example.model import _
+from cone.example.wiki.model import Wiki
+from cone.example.wiki.model import WikiPage
+from cone.tile import tile
+from node.utils import UNSET
+from plumber import plumbing
+from pyramid.i18n import TranslationStringFactory
+from yafowil.base import factory
+from yafowil.persistence import write_mapping_writer
+import os
+
+
+# View tile for Wiki container
+@tile(name='view',
+ path='cone.example.browser:templates/view.pt',
+ interface=Wiki,
+ permission='login')
+class WikiContainerView(ProtectedContentTile):
+ pass
+
+
+# View tile for WikiPage
+@tile(name='content',
+ path='cone.example.wiki:templates/wiki_view.pt',
+ interface=WikiPage,
+ permission='login')
+class WikiPageView(ProtectedContentTile):
+
+ @property
+ def references(self):
+ """Resolve reference UUIDs to actual nodes for display."""
+ refs = self.model.attrs.get('references', [])
+ if not refs:
+ return []
+ result = []
+ # Walk up to find wiki container to search for referenced pages
+ wiki = self.model.parent
+ for page in wiki.values():
+ if hasattr(page, 'uuid') and str(page.uuid) in refs:
+ result.append({
+ 'title': page.metadata.title,
+ 'target': make_url(self.request, node=page),
+ 'icon': page.metadata.icon or 'bi-journal-text',
+ })
+ return result
+
+
+# WikiPage form using standard yafowil factory
+class WikiPageForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='wikipageform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'help': _('wiki_title_help', default='Enter a page title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'help': _('wiki_desc_help', default='Short description'),
+ 'rows': 2
+ })
+ form['body'] = factory(
+ 'field:label:help:error:textarea',
+ value=self.model.attrs.get('body', UNSET),
+ props={
+ 'label': _('body', default='Content'),
+ 'help': _('wiki_body_help', default='Enter wiki page content'),
+ 'rows': 10
+ })
+ # Reference browser widget for linking to other wiki pages
+ form['references'] = factory(
+ 'field:label:help:error:reference',
+ value=self.reference_value,
+ props={
+ 'label': _('references', default='Related Pages'),
+ 'help': _('references_help',
+ default='Select related wiki pages'),
+ 'multivalued': True,
+ 'target': self.reference_target,
+ 'root': self.reference_root,
+ 'referencable': 'wiki_page',
+ 'lookup': self.reference_lookup,
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ @property
+ def reference_value(self):
+ refs = self.model.attrs.get('references')
+ if refs:
+ return refs
+ return []
+
+ @property
+ def reference_target(self):
+ return make_url(self.request, node=self.model.root)
+
+ @property
+ def reference_root(self):
+ # Find the wiki container
+ wiki = self.model.parent
+ return '/' + '/'.join(wiki.path[1:]) if wiki else '/'
+
+ def reference_lookup(self, uuid):
+ """Lookup label for a reference UUID."""
+ wiki = self.model.parent
+ for page in wiki.values():
+ if hasattr(page, 'uuid') and str(page.uuid) == uuid:
+ return page.metadata.title
+ return uuid
+
+ def save(self, widget, data):
+ # Extract references separately since reference widget has special
+ # extraction
+ refs = data.fetch('wikipageform.references')
+ if refs.extracted:
+ self.model.attrs['references'] = refs.extracted
+ # Write other fields
+ data.write(self.model.attrs)
+
+
+@plumbing(AddFormTarget)
+class WikiPageAddForm(WikiPageForm):
+
+ def save(self, widget, data):
+ add_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+ parent = self.model.parent
+ parent[choose_name(parent, self.model.metadata.title)] = self.model
+
+
+@plumbing(EditFormTarget)
+class WikiPageEditForm(WikiPageForm):
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ super().save(widget, data)
+
+
+@tile(name='addform', interface=WikiPage, permission='add')
+@plumbing(ContentAddForm)
+class WikiPageContentAddForm(WikiPageAddForm):
+ ...
+
+
+@tile(name='editform', interface=WikiPage, permission='edit')
+@plumbing(ContentEditForm)
+class WikiPageContentEditForm(WikiPageEditForm):
+ ...
+
+
+# Edit form for Wiki container (just title/description)
+@tile(name='editform', interface=Wiki, permission='edit')
+@plumbing(ContentEditForm, EditFormTarget)
+class WikiEditForm(Form):
+
+ def prepare(self):
+ self.form = form = factory(
+ 'form',
+ name='wikiform',
+ props={
+ 'action': self.form_action,
+ 'persist_writer': write_mapping_writer
+ })
+ form['title'] = factory(
+ 'field:label:help:error:translation:text',
+ value=self.model.attrs.get('title', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('title', default='Title'),
+ 'required': _('title_required', default='Title is required')
+ })
+ form['description'] = factory(
+ 'field:label:help:error:translation:textarea',
+ value=self.model.attrs.get('description', UNSET),
+ props={
+ 'factory': Translation,
+ 'label': _('description', default='Description'),
+ 'rows': 3
+ })
+ form['save'] = factory(
+ 'submit',
+ props={
+ 'action': 'save',
+ 'expression': True,
+ 'handler': self.save,
+ 'next': self.next,
+ 'label': _('save', default='Save')
+ })
+ form['cancel'] = factory(
+ 'submit',
+ props={
+ 'action': 'cancel',
+ 'expression': True,
+ 'skip': True,
+ 'next': self.next,
+ 'label': _('cancel', default='Cancel')
+ })
+
+ def save(self, widget, data):
+ update_creation_metadata(self.request, self.model.attrs)
+ data.write(self.model.attrs)
diff --git a/examples/cone.example/src/cone/example/wiki/model.py b/examples/cone.example/src/cone/example/wiki/model.py
new file mode 100644
index 00000000..9a623787
--- /dev/null
+++ b/examples/cone.example/src/cone/example/wiki/model.py
@@ -0,0 +1,95 @@
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import AppNode
+from cone.app.model import Categories
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import UUIDAttributeAware
+from cone.app.model import node_info
+from cone.app.security import PrincipalACL
+from cone.example.model import _
+from cone.example.model import BaseContainer
+from cone.example.model import DEFAULT_EXAMPLE_ACL
+from cone.example.model import Translation
+from node.behaviors import Attributes
+from node.behaviors import MappingAdopt
+from node.behaviors import MappingNode
+from node.behaviors import MappingOrder
+from node.behaviors import NodeInit
+from node.behaviors import OdictStorage
+from node.utils import instance_property
+from plumber import plumbing
+from zope.interface import implementer
+
+
+@node_info(
+ name='wiki',
+ title=_('wiki', default='Wiki'),
+ icon='bi-book',
+ addables=['wiki_page'])
+class Wiki(BaseContainer):
+ """Entry container for wiki pages.
+
+ Acts as the reference browser root for WikiPage references.
+ """
+
+ @property
+ def properties(self):
+ props = super().properties
+ props.mainmenu_display_children = False
+ return props
+
+
+@node_info(
+ name='wiki_page',
+ title=_('wiki_page', default='Wiki Page'),
+ icon='bi-journal-text')
+@plumbing(UUIDAttributeAware, PrincipalACL, Categories, AppNode,
+ MappingAdopt, Attributes, NodeInit, MappingNode,
+ MappingOrder, OdictStorage)
+@implementer(INavigationLeaf)
+class WikiPage:
+ """Leaf node demonstrating Categories, UUIDAttributeAware,
+ and reference browser widget for linking to other pages.
+
+ Categories: Categorization support with translation strings.
+ UUIDAttributeAware: UUID stored in node attributes.
+ Reference browser: Used in the form to select related pages.
+ """
+ categories = [
+ _('cat_general', default='General'),
+ _('cat_technical', default='Technical'),
+ _('cat_howto', default='How-To')
+ ]
+ role_inheritance = True
+ default_acl = DEFAULT_EXAMPLE_ACL
+
+ @instance_property
+ def principal_roles(self):
+ return {}
+
+ @property
+ def properties(self):
+ props = Properties()
+ props.in_navtree = True
+ props.action_up = True
+ props.action_view = True
+ props.action_edit = True
+ props.action_delete = True
+ props.action_sharing = True
+ return props
+
+ @property
+ def metadata(self):
+ md = Metadata()
+ md.icon = 'bi-journal-text'
+ title = self.attrs.get('title')
+ md.title = title.value if title else self.name
+ description = self.attrs.get('description')
+ md.description = description.value if description else ''
+ md.creator = self.attrs.get('creator', '')
+ md.created = self.attrs.get('created')
+ md.modified = self.attrs.get('modified')
+ return md
+
+ def __call__(self):
+ ...
diff --git a/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt b/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
new file mode 100644
index 00000000..32665d36
--- /dev/null
+++ b/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ No content yet.
+
+
+
+
+
+
+
From 54cbaa8075576bf0c80cab737db7f60c9178a6d4 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Mon, 26 Jan 2026 12:57:18 +0100
Subject: [PATCH 02/25] add modal-xl class to rendered Ajax Tile Errors
---
src/cone/app/browser/ajax.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/cone/app/browser/ajax.py b/src/cone/app/browser/ajax.py
index 81c274bc..35d2babc 100644
--- a/src/cone/app/browser/ajax.py
+++ b/src/cone/app/browser/ajax.py
@@ -51,7 +51,7 @@ def ajax_tile(model, request):
except Exception:
logging.exception('Error within ajax tile')
tb = format_traceback()
- continuation = AjaxContinue([AjaxMessage(tb, 'error', None)])
+ continuation = AjaxContinue([AjaxMessage(tb, 'error', None, 'modal-xl')])
return dict(
mode='NONE',
selector='NONE',
From b3cf76ecf568eadcded8413cc38f2e4991e4c3b7 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Mon, 26 Jan 2026 13:37:59 +0100
Subject: [PATCH 03/25] fix ajax_playground
---
.../src/cone/example/ajax/browser.py | 14 ++++++----
.../example/ajax/templates/ajax_playground.pt | 28 +++++++++++--------
.../src/cone/example/project/browser.py | 4 +--
3 files changed, 27 insertions(+), 19 deletions(-)
diff --git a/examples/cone.example/src/cone/example/ajax/browser.py b/examples/cone.example/src/cone/example/ajax/browser.py
index 9092cd2d..7534333d 100644
--- a/examples/cone.example/src/cone/example/ajax/browser.py
+++ b/examples/cone.example/src/cone/example/ajax/browser.py
@@ -1,18 +1,16 @@
from cone.app.browser.ajax import ajax_continue
from cone.app.browser.ajax import ajax_message
-from cone.app.browser.ajax import ajax_status_message
from cone.app.browser.ajax import AjaxAction
from cone.app.browser.ajax import AjaxEvent
from cone.app.browser.ajax import AjaxMessage
-from cone.app.browser.ajax import AjaxOverlay
from cone.app.browser.ajax import AjaxPath
from cone.app.browser.layout import ProtectedContentTile
from cone.app.browser.utils import make_url
-from cone.app.model import AppNode
from cone.app.model import BaseNode
from cone.app.model import Metadata
-from cone.app.model import Properties
from cone.app.model import node_info
+from cone.app.model import Properties
+from cone.app.utils import node_path
from cone.example.model import _
from cone.tile import tile
from node.utils import instance_property
@@ -87,12 +85,13 @@ def render(self):
interface=AjaxPlayground,
permission='view')
class AjaxPathDemo(ProtectedContentTile):
+ # XXX: make this demo more obvious, e.g. by changing some content
def render(self):
url = make_url(self.request, node=self.model)
ajax_continue(self.request, [
AjaxPath(
- path='/'.join(self.model.path),
+ path='/'.join(node_path(self.model)),
target=url,
event='contextchanged:#layout'
)
@@ -105,6 +104,7 @@ def render(self):
interface=AjaxPlayground,
permission='view')
class AjaxEventDemo(ProtectedContentTile):
+ # XXX: make this demo more obvious, e.g. by changing some content
def render(self):
url = make_url(self.request, node=self.model)
@@ -148,12 +148,14 @@ def render(self):
interface=AjaxPlayground,
permission='view')
class AjaxCombinedDemo(ProtectedContentTile):
+ # XXX: this makes little sense, as the AjaxAction will be overridden
+ # by the AjaxEvent. Find a better combination demo.
def render(self):
url = make_url(self.request, node=self.model)
ajax_continue(self.request, [
AjaxAction(url, 'ajax_demo_content', 'inner', '#ajax-demo-target'),
- AjaxMessage('Combined: action + message + event!', 'success'),
+ AjaxMessage('Combined: action + message + event!', 'success', None),
AjaxEvent(url, 'contextchanged', '#layout'),
])
return ''
diff --git a/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt b/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
index 59436ac2..380175a6 100644
--- a/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
+++ b/examples/cone.example/src/cone/example/ajax/templates/ajax_playground.pt
@@ -1,5 +1,6 @@
+ xmlns:ajax="http://namespaces.conestack.org/ajax"
+ omit-tag="True">
@@ -31,8 +32,9 @@
+ ajax:bind="click"
+ ajax:target="${context.ajax_path_url}"
+ ajax:action="ajax_path_demo:NONE:NONE">
Trigger AjaxPath
@@ -51,8 +53,9 @@
+ ajax:bind="click"
+ ajax:target="${context.ajax_action_url}"
+ ajax:action="ajax_action_demo:NONE:NONE">
Trigger AjaxAction
@@ -72,8 +75,9 @@
+ ajax:bind="click"
+ ajax:target="${context.ajax_action_url}"
+ ajax:action="ajax_event_demo:NONE:NONE">
Trigger AjaxEvent
@@ -93,8 +97,9 @@
+ ajax:bind="click"
+ ajax:target="${context.ajax_action_url}"
+ ajax:action="ajax_message_demo:NONE:NONE">
Trigger AjaxMessage
@@ -114,8 +119,9 @@
+ ajax:bind="click"
+ ajax:target="${context.ajax_action_url}"
+ ajax:action="ajax_combined_demo:NONE:NONE">
Trigger Combined
diff --git a/examples/cone.example/src/cone/example/project/browser.py b/examples/cone.example/src/cone/example/project/browser.py
index eb0329f3..bde0f199 100644
--- a/examples/cone.example/src/cone/example/project/browser.py
+++ b/examples/cone.example/src/cone/example/project/browser.py
@@ -203,7 +203,7 @@ class ProjectAddForm(ProjectForm):
def save(self, widget, data):
from cone.app.browser.utils import choose_name
add_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(ProjectAddForm, self).save(widget, data)
parent = self.model.parent
parent[choose_name(parent, self.model.metadata.title)] = self.model
@@ -213,7 +213,7 @@ class ProjectEditForm(ProjectForm):
def save(self, widget, data):
update_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(ProjectEditForm, self).save(widget, data)
# For Task add form, we need to create a TaskData and wrap in Task
From d781fd3aa7e4104d0a38faedcb9b4e78e9165d75 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Mon, 26 Jan 2026 13:49:35 +0100
Subject: [PATCH 04/25] add example to change navbar height variable via css
---
.../src/cone/example/browser/static/cone.example.css | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/examples/cone.example/src/cone/example/browser/static/cone.example.css b/examples/cone.example/src/cone/example/browser/static/cone.example.css
index aa82d393..e9f06360 100644
--- a/examples/cone.example/src/cone/example/browser/static/cone.example.css
+++ b/examples/cone.example/src/cone/example/browser/static/cone.example.css
@@ -44,3 +44,8 @@ tr.state-private td.title a {
tr.state-public td.title a {
color: green;
}
+
+
+:root {
+ /* --navbar-height: 100px; */ /* Example of custom navbar height */
+}
\ No newline at end of file
From 3be549ad9fe6af4bd96699da8d766e1f849fe3a3 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Tue, 27 Jan 2026 09:24:57 +0100
Subject: [PATCH 05/25] extend ajax message example
---
examples/cone.example/src/cone/example/ajax/browser.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/examples/cone.example/src/cone/example/ajax/browser.py b/examples/cone.example/src/cone/example/ajax/browser.py
index 7534333d..04fe56b7 100644
--- a/examples/cone.example/src/cone/example/ajax/browser.py
+++ b/examples/cone.example/src/cone/example/ajax/browser.py
@@ -124,7 +124,9 @@ def render(self):
ajax_message(
self.request,
'This is a demo message from AjaxMessage!',
- 'info'
+ 'info',
+ 'modal-lg',
+ 'Demo Message'
)
return ''
From 76d8427c2874bbd71632b08fad574231387ab81a Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Tue, 27 Jan 2026 09:25:28 +0100
Subject: [PATCH 06/25] allow title for ajax_message and ajax_status_message
---
src/cone/app/browser/ajax.py | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/cone/app/browser/ajax.py b/src/cone/app/browser/ajax.py
index 35d2babc..96bf81e5 100644
--- a/src/cone/app/browser/ajax.py
+++ b/src/cone/app/browser/ajax.py
@@ -76,18 +76,18 @@ def ajax_continue(request, operations):
request.environ['cone.app.continuation'].append(operations)
-def ajax_message(request, payload, flavor='message', css=None):
+def ajax_message(request, payload, flavor='message', css=None, title=None):
"""Convenience to add ajax message operation to ajax continuation
operations.
"""
- ajax_continue(request, AjaxMessage(payload, flavor, None, css))
+ ajax_continue(request, AjaxMessage(payload, flavor, None, css, title))
-def ajax_status_message(request, payload, css=None):
+def ajax_status_message(request, payload, css=None, title=None):
"""Convenience to add ajax status message operation to ajax continuation
operations.
"""
- ajax_continue(request, AjaxMessage(payload, None, '#status_message', css))
+ ajax_continue(request, AjaxMessage(payload, None, '#status_message', css, title))
class AjaxPath(object):
@@ -189,7 +189,7 @@ class AjaxMessage(object):
"""Ajax message continuation operation.
"""
- def __init__(self, payload, flavor, selector, css=''):
+ def __init__(self, payload, flavor, selector, css='', title=None):
"""Create ajax message continuation operation.
:param payload: Message payload as text or markup.
@@ -202,6 +202,7 @@ def __init__(self, payload, flavor, selector, css=''):
self.flavor = flavor
self.selector = selector
self.css = css
+ self.title = title
def as_json(self):
return {
@@ -209,7 +210,8 @@ def as_json(self):
'payload': self.payload,
'flavor': self.flavor,
'selector': self.selector,
- 'css': self.css
+ 'css': self.css,
+ 'title': self.title
}
From 7e50d7be72732760e2ce1353e64e3c1317176e7a Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Tue, 27 Jan 2026 12:21:21 +0100
Subject: [PATCH 07/25] populate with examples. create landing page. improve
views and fix issues
---
.../cone.example/src/cone/example/__init__.py | 26 ++-
.../src/cone/example/browser/__init__.py | 49 +++-
.../example/browser/static/cone.example.css | 66 ++++++
.../example/browser/static/images/icon.svg | 98 ++++++++
.../cone/example/browser/templates/landing.pt | 133 +++++++++++
.../src/cone/example/document/browser.py | 25 +-
.../src/cone/example/document/model.py | 4 +-
.../document/templates/document_source.pt | 2 +-
.../document/templates/library_view.pt | 36 +++
.../cone.example/src/cone/example/populate.py | 218 ++++++++++++++++++
.../src/cone/example/project/browser.py | 6 +-
.../src/cone/example/project/model.py | 3 +
.../example/project/templates/board_view.pt | 37 +++
.../example/project/templates/task_view.pt | 2 +-
.../src/cone/example/wiki/browser.py | 6 +-
.../src/cone/example/wiki/model.py | 2 +-
.../wiki/templates/wiki_container_view.pt | 35 +++
.../cone/example/wiki/templates/wiki_view.pt | 2 +-
18 files changed, 719 insertions(+), 31 deletions(-)
create mode 100644 examples/cone.example/src/cone/example/browser/static/images/icon.svg
create mode 100644 examples/cone.example/src/cone/example/browser/templates/landing.pt
create mode 100644 examples/cone.example/src/cone/example/document/templates/library_view.pt
create mode 100644 examples/cone.example/src/cone/example/populate.py
create mode 100644 examples/cone.example/src/cone/example/project/templates/board_view.pt
create mode 100644 examples/cone.example/src/cone/example/wiki/templates/wiki_container_view.pt
diff --git a/examples/cone.example/src/cone/example/__init__.py b/examples/cone.example/src/cone/example/__init__.py
index 8a81281a..0b26e379 100644
--- a/examples/cone.example/src/cone/example/__init__.py
+++ b/examples/cone.example/src/cone/example/__init__.py
@@ -24,10 +24,26 @@ def example_main_hook(config, global_config, settings):
from cone.example.project.model import ProjectBoard
from cone.example.wiki.model import Wiki
from cone.example.ajax.browser import AjaxPlayground
+ from cone.example.populate import populate_documents
+ from cone.example.populate import populate_projects
+ from cone.example.populate import populate_wiki
- register_entry('documents', DocumentLibrary)
+ def make_document_library():
+ lib = DocumentLibrary()
+ populate_documents(lib)
+ return lib
+
+ def make_wiki():
+ wiki = Wiki()
+ populate_wiki(wiki)
+ return wiki
+
+ # ProjectBoard uses FactoryNode — populate registers class-level factories
+ populate_projects()
+
+ register_entry('documents', make_document_library)
register_entry('projects', ProjectBoard)
- register_entry('wiki', Wiki)
+ register_entry('wiki', make_wiki)
register_entry('ajax_playground', AjaxPlayground)
# Register settings node
@@ -47,6 +63,12 @@ def example_main_hook(config, global_config, settings):
# Register layout configs (deferred to avoid circular imports)
_configure_layout_configs()
+ # add static view for example images
+ config.add_static_view(
+ name='example-images',
+ path='cone.example.browser:static/images'
+ )
+
# Scan browser packages for tile and view registrations
config.scan('cone.example.browser')
config.scan('cone.example.document.browser')
diff --git a/examples/cone.example/src/cone/example/browser/__init__.py b/examples/cone.example/src/cone/example/browser/__init__.py
index d3c82998..e7b3b0b7 100644
--- a/examples/cone.example/src/cone/example/browser/__init__.py
+++ b/examples/cone.example/src/cone/example/browser/__init__.py
@@ -1,7 +1,6 @@
from cone.app import DefaultLayoutConfig
from cone.app import layout_config
from cone.app.browser.actions import LinkAction
-from cone.app.browser.actions import TemplateAction
from cone.app.browser.contextmenu import context_menu_group
from cone.app.browser.contextmenu import context_menu_item
from cone.app.browser.contextmenu import ContextMenuToolbar
@@ -9,7 +8,7 @@
from cone.app.browser.layout import ProtectedContentTile
from cone.app.browser.utils import make_url
from cone.app.browser.utils import request_property
-from cone.app.model import Properties
+from cone.app.model import AppRoot
from cone.example.model import _
from cone.tile import tile
import os
@@ -62,40 +61,74 @@ def _configure_layout_configs():
@layout_config(DocumentLibrary, DocumentFolder)
class DocumentContainerLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(DocumentContainerLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = ['navtree']
@layout_config(Document)
class DocumentLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(DocumentLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = ['navtree']
@layout_config(ProjectBoard)
class ProjectBoardLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(ProjectBoardLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = ['navtree']
@layout_config(Task)
class TaskLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(TaskLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = ['navtree']
@layout_config(Wiki, WikiPage)
class WikiLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(WikiLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = ['navtree']
+ @layout_config(AppRoot)
+ class RootLayoutConfig(DefaultLayoutConfig):
+ def __init__(self, model=None, request=None):
+ super(RootLayoutConfig, self).__init__(model=model, request=request)
+ self.sidebar_left = []
+ self.limit_content_width = False
+
@layout_config(AjaxPlayground)
class AjaxPlaygroundLayoutConfig(DefaultLayoutConfig):
def __init__(self, model=None, request=None):
- super().__init__(model=model, request=request)
+ super(AjaxPlaygroundLayoutConfig, self).__init__(model=model, request=request)
self.sidebar_left = []
+###############################################################################
+# Landing Page (Root Content Tile)
+###############################################################################
+
+@tile(name='content',
+ path='cone.example.browser:templates/landing.pt',
+ interface=AppRoot,
+ permission='login')
+class LandingPage(ProtectedContentTile):
+
+ @property
+ def documents_url(self):
+ return make_url(self.request, node=self.model['documents'])
+
+ @property
+ def projects_url(self):
+ return make_url(self.request, node=self.model['projects'])
+
+ @property
+ def wiki_url(self):
+ return make_url(self.request, node=self.model['wiki'])
+
+ @property
+ def ajax_url(self):
+ return make_url(self.request, node=self.model['ajax_playground'])
+
+
###############################################################################
# Custom Personal Tools Action
#
diff --git a/examples/cone.example/src/cone/example/browser/static/cone.example.css b/examples/cone.example/src/cone/example/browser/static/cone.example.css
index e9f06360..1c855a18 100644
--- a/examples/cone.example/src/cone/example/browser/static/cone.example.css
+++ b/examples/cone.example/src/cone/example/browser/static/cone.example.css
@@ -46,6 +46,72 @@ tr.state-public td.title a {
}
+/* Example module header accents */
+.example-header {
+ border-left: 4px solid #dee2e6;
+}
+.example-header-documents {
+ border-left-color: #0d6efd;
+}
+.example-header-projects {
+ border-left-color: #fd7e14;
+}
+.example-header-wiki {
+ border-left-color: #198754;
+}
+
+/* Feature badges per module */
+.badge.example-badge-documents {
+ background-color: #cfe2ff;
+ color: #084298;
+}
+.badge.example-badge-projects {
+ background-color: #fff3cd;
+ color: #664d03;
+}
+.badge.example-badge-wiki {
+ background-color: #d1e7dd;
+ color: #0f5132;
+}
+
+/* Node type icons in listings */
+tr.node-type-document_folder td.title a::before {
+ content: "\f3d7";
+ font-family: "bootstrap-icons";
+ margin-right: 0.4em;
+ color: #fd7e14;
+}
+tr.node-type-document td.title a::before {
+ content: "\f38b";
+ font-family: "bootstrap-icons";
+ margin-right: 0.4em;
+ color: #0d6efd;
+}
+tr.node-type-project td.title a::before {
+ content: "\f290";
+ font-family: "bootstrap-icons";
+ margin-right: 0.4em;
+ color: #fd7e14;
+}
+tr.node-type-task td.title a::before {
+ content: "\f271";
+ font-family: "bootstrap-icons";
+ margin-right: 0.4em;
+ color: #6f42c1;
+}
+tr.node-type-wiki_page td.title a::before {
+ content: "\f444";
+ font-family: "bootstrap-icons";
+ margin-right: 0.4em;
+ color: #198754;
+}
+
+/* Landing page */
+.landing-hero {
+ border-bottom: 1px solid #dee2e6;
+ margin-bottom: 1rem;
+}
+
:root {
/* --navbar-height: 100px; */ /* Example of custom navbar height */
}
\ No newline at end of file
diff --git a/examples/cone.example/src/cone/example/browser/static/images/icon.svg b/examples/cone.example/src/cone/example/browser/static/images/icon.svg
new file mode 100644
index 00000000..dbbb6e0a
--- /dev/null
+++ b/examples/cone.example/src/cone/example/browser/static/images/icon.svg
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/browser/templates/landing.pt b/examples/cone.example/src/cone/example/browser/templates/landing.pt
new file mode 100644
index 00000000..ff679e36
--- /dev/null
+++ b/examples/cone.example/src/cone/example/browser/templates/landing.pt
@@ -0,0 +1,133 @@
+
+
+
+
+
+ cone.example
+
+
+ A collection of example modules demonstrating cone.app features
+ and patterns.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/document/browser.py b/examples/cone.example/src/cone/example/document/browser.py
index 5922a3af..35af80fd 100644
--- a/examples/cone.example/src/cone/example/document/browser.py
+++ b/examples/cone.example/src/cone/example/document/browser.py
@@ -21,16 +21,21 @@
from yafowil.persistence import write_mapping_writer
-# View tiles for DocumentLibrary and DocumentFolder (container view)
+# View tile for DocumentLibrary
@tile(name='view',
- path='cone.example.browser:templates/view.pt',
+ path='cone.example.document:templates/library_view.pt',
interface=DocumentLibrary,
permission='login')
+class DocumentLibraryView(ProtectedContentTile):
+ pass
+
+
+# View tile for DocumentFolder
@tile(name='view',
path='cone.example.browser:templates/view.pt',
interface=DocumentFolder,
permission='login')
-class DocumentContainerView(ProtectedContentTile):
+class DocumentFolderView(ProtectedContentTile):
pass
@@ -46,7 +51,8 @@ class DocumentContainerView(ProtectedContentTile):
interface=Document,
permission='view',
text=_('view', default='View'),
- icon='bi-eye')
+ icon='bi-eye',
+ css='dropdown-item')
class DocumentView(ProtectedContentTile):
pass
@@ -63,7 +69,8 @@ class DocumentView(ProtectedContentTile):
interface=Document,
permission='edit',
text=_('source', default='Source'),
- icon='bi-code')
+ icon='bi-code',
+ css='dropdown-item')
class DocumentSourceView(ProtectedContentTile):
pass
@@ -185,7 +192,7 @@ class DocumentAddForm(DocumentForm):
def save(self, widget, data):
add_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(DocumentAddForm, self).save(widget, data)
parent = self.model.parent
parent[choose_name(parent, self.model.metadata.title)] = self.model
@@ -195,7 +202,7 @@ class DocumentEditForm(DocumentForm):
def save(self, widget, data):
update_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(DocumentEditForm, self).save(widget, data)
@plumbing(AddFormTarget)
@@ -203,7 +210,7 @@ class FolderAddForm(FolderForm):
def save(self, widget, data):
add_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(FolderAddForm, self).save(widget, data)
parent = self.model.parent
parent[choose_name(parent, self.model.metadata.title)] = self.model
@@ -213,7 +220,7 @@ class FolderEditForm(FolderForm):
def save(self, widget, data):
update_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(FolderEditForm, self).save(widget, data)
# Add forms
diff --git a/examples/cone.example/src/cone/example/document/model.py b/examples/cone.example/src/cone/example/document/model.py
index 19269131..9fc1cab6 100644
--- a/examples/cone.example/src/cone/example/document/model.py
+++ b/examples/cone.example/src/cone/example/document/model.py
@@ -24,7 +24,7 @@ class DocumentLibrary(BaseContainer):
@property
def properties(self):
- props = super().properties
+ props = super(DocumentLibrary, self).properties
props.mainmenu_display_children = False
return props
@@ -38,7 +38,7 @@ class DocumentFolder(BaseContainer):
@property
def properties(self):
- props = super().properties
+ props = super(DocumentFolder, self).properties
props.action_delete = True
return props
diff --git a/examples/cone.example/src/cone/example/document/templates/document_source.pt b/examples/cone.example/src/cone/example/document/templates/document_source.pt
index ab0e25e6..a1edfca6 100644
--- a/examples/cone.example/src/cone/example/document/templates/document_source.pt
+++ b/examples/cone.example/src/cone/example/document/templates/document_source.pt
@@ -45,7 +45,7 @@
Path
- path
+ path
diff --git a/examples/cone.example/src/cone/example/document/templates/library_view.pt b/examples/cone.example/src/cone/example/document/templates/library_view.pt
new file mode 100644
index 00000000..0733744e
--- /dev/null
+++ b/examples/cone.example/src/cone/example/document/templates/library_view.pt
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/populate.py b/examples/cone.example/src/cone/example/populate.py
new file mode 100644
index 00000000..7ec07933
--- /dev/null
+++ b/examples/cone.example/src/cone/example/populate.py
@@ -0,0 +1,218 @@
+from datetime import datetime
+
+from cone.example.model import Translation
+
+
+def make_translation(en_value, de_value):
+ t = Translation()
+ t['en'] = en_value
+ t['de'] = de_value
+ return t
+
+
+def populate_documents(library):
+ """Populate DocumentLibrary with example nodes."""
+ if len(library) > 0:
+ return
+
+ from cone.example.document.model import Document
+ from cone.example.document.model import DocumentFolder
+
+ # Folder with a published document inside
+ folder = DocumentFolder()
+ folder.attrs['title'] = make_translation('Reports', 'Berichte')
+ folder.attrs['description'] = make_translation(
+ 'Quarterly and annual reports', 'Vierteljährliche und jährliche Berichte')
+ folder.attrs['creator'] = 'admin'
+ folder.attrs['created'] = datetime(2025, 1, 15)
+ folder.attrs['modified'] = datetime(2025, 1, 15)
+ library['reports'] = folder
+
+ report = Document()
+ report.attrs['title'] = make_translation('Q4 Sales Report', 'Q4 Verkaufsbericht')
+ report.attrs['description'] = make_translation(
+ 'Fourth quarter sales summary', 'Zusammenfassung der Verkäufe im vierten Quartal')
+ report.attrs['body'] = (
+ 'Total revenue: $1.2M\n'
+ 'Growth: 15% YoY\n'
+ 'Top region: EMEA'
+ )
+ report.attrs['creator'] = 'admin'
+ report.attrs['created'] = datetime(2025, 1, 10)
+ report.attrs['modified'] = datetime(2025, 1, 12)
+ report.attrs['state'] = 'published'
+ folder['q4-sales-report'] = report
+
+ # Draft document at root
+ guide = Document()
+ guide.attrs['title'] = make_translation('Getting Started Guide', 'Einsteigeranleitung')
+ guide.attrs['description'] = make_translation(
+ 'How to use the document library', 'So verwenden Sie die Dokumentebibliothek')
+ guide.attrs['body'] = (
+ 'Welcome to the Document Library.\n\n'
+ 'This example demonstrates:\n'
+ '- Hierarchical folders and documents\n'
+ '- Workflow states (draft, review, published, archived)\n'
+ '- Protected properties (body requires edit permission)\n'
+ '- Translation-aware title and description fields'
+ )
+ guide.attrs['creator'] = 'admin'
+ guide.attrs['created'] = datetime(2025, 2, 1)
+ guide.attrs['modified'] = datetime(2025, 2, 1)
+ # state stays 'draft' (initial)
+ library['getting-started'] = guide
+
+ # Document in review
+ notes = Document()
+ notes.attrs['title'] = make_translation('Release Notes v2.0', 'Versionshinweise v2.0')
+ notes.attrs['description'] = make_translation(
+ 'Upcoming release notes pending review',
+ 'Ausstehende Versionshinweise zur Überprüfung'
+ )
+ notes.attrs['body'] = (
+ 'New features:\n'
+ '- Improved search\n'
+ '- Batch operations\n'
+ '- Reference browser widget'
+ )
+ notes.attrs['creator'] = 'editor'
+ notes.attrs['created'] = datetime(2025, 3, 5)
+ notes.attrs['modified'] = datetime(2025, 3, 8)
+ notes.attrs['state'] = 'review'
+ library['release-notes-v2'] = notes
+
+
+def populate_wiki(wiki):
+ """Populate Wiki with example pages."""
+ if len(wiki) > 0:
+ return
+
+ from cone.example.wiki.model import WikiPage
+
+ getting_started = WikiPage()
+ getting_started.attrs['title'] = make_translation('Getting Started', 'Erste Schritte')
+ getting_started.attrs['description'] = make_translation(
+ 'Introduction to the wiki system', 'Einführung in das Wiki System')
+ getting_started.attrs['body'] = (
+ 'Welcome to the Wiki.\n\n'
+ 'This example demonstrates:\n'
+ '- Reference browser widget for linking pages\n'
+ '- Categories (General, Technical, How-To)\n'
+ '- UUID-aware nodes'
+ )
+ getting_started.attrs['creator'] = 'admin'
+ getting_started.attrs['created'] = datetime(2025, 1, 1)
+ getting_started.attrs['modified'] = datetime(2025, 1, 1)
+ wiki['getting-started'] = getting_started
+
+ api_ref = WikiPage()
+ api_ref.attrs['title'] = make_translation('API Reference', 'API Referenz')
+ api_ref.attrs['description'] = make_translation(
+ 'Technical API documentation', 'Technische API Dokumentation')
+ api_ref.attrs['body'] = (
+ 'cone.app API\n\n'
+ 'Model:\n'
+ '- node_info: Declare node type metadata\n'
+ '- BaseNode / FactoryNode: Node base classes\n'
+ '- AdapterNode: Wrap external data models\n\n'
+ 'Browser:\n'
+ '- tile: Register view tiles\n'
+ '- layout_config: Configure page layout'
+ )
+ api_ref.attrs['creator'] = 'admin'
+ api_ref.attrs['created'] = datetime(2025, 1, 5)
+ api_ref.attrs['modified'] = datetime(2025, 1, 10)
+ wiki['api-reference'] = api_ref
+
+ deploy = WikiPage()
+ deploy.attrs['title'] = make_translation('How to Deploy', 'So wird deployed')
+ deploy.attrs['description'] = make_translation(
+ 'Step-by-step deployment guide', 'Schritt-für-Schritt Anleitung zum Deployment')
+ deploy.attrs['body'] = (
+ 'Deployment Steps:\n\n'
+ '1. Install dependencies: pip install -e .\n'
+ '2. Configure mx.ini\n'
+ '3. Run: pserve mx.ini'
+ )
+ deploy.attrs['creator'] = 'admin'
+ deploy.attrs['created'] = datetime(2025, 2, 1)
+ deploy.attrs['modified'] = datetime(2025, 2, 1)
+ # Set references to other pages
+ refs = []
+ if hasattr(getting_started, 'uuid') and getting_started.uuid:
+ refs.append(str(getting_started.uuid))
+ if hasattr(api_ref, 'uuid') and api_ref.uuid:
+ refs.append(str(api_ref.uuid))
+ if refs:
+ deploy.attrs['references'] = refs
+ wiki['how-to-deploy'] = deploy
+
+
+def populate_projects():
+ """Register example project factories on ProjectBoard.
+
+ FactoryNode children are defined via class-level ``factories`` dict.
+ Each factory creates a pre-configured Project with Tasks.
+ """
+ from cone.example.project.model import ProjectBoard
+
+ if ProjectBoard.factories:
+ return
+
+ def make_website_redesign(name=None, parent=None):
+ from cone.example.project.model import Project, Task, TaskData
+ project = Project()
+ project.attrs['title'] = make_translation('Website Redesign', 'Website Neugestaltung')
+ project.attrs['description'] = make_translation(
+ 'Redesign the company website with modern UI', 'Neugestaltung der Firmenwebsite mit modernem UI')
+ project.attrs['creator'] = 'admin'
+ project.attrs['created'] = datetime(2025, 1, 1)
+ project.attrs['modified'] = datetime(2025, 3, 1)
+
+ task1 = Task(TaskData(), None, None)
+ task1.attrs['title'] = make_translation('Design mockups', 'Gestaltung von Mockups')
+ task1.attrs['description'] = make_translation(
+ 'Create wireframes and visual mockups', 'Erstellung von Wireframes und Visual Mockups')
+ task1.attrs['creator'] = 'designer'
+ task1.attrs['created'] = datetime(2025, 1, 5)
+ task1.attrs['modified'] = datetime(2025, 2, 1)
+ task1.attrs['state'] = 'in_progress'
+ project[str(task1.uuid)] = task1
+
+ task2 = Task(TaskData(), None, None)
+ task2.attrs['title'] = make_translation('Implement frontend', 'Frontend implementieren')
+ task2.attrs['description'] = make_translation(
+ 'Build responsive frontend components', 'Bauen von responsive Frontend-Komponenten')
+ task2.attrs['creator'] = 'developer'
+ task2.attrs['created'] = datetime(2025, 2, 1)
+ task2.attrs['modified'] = datetime(2025, 2, 1)
+ project[str(task2.uuid)] = task2
+
+ return project
+
+ def make_mobile_app(name=None, parent=None):
+ from cone.example.project.model import Project, Task, TaskData
+ project = Project()
+ project.attrs['title'] = make_translation('Mobile App', 'Mobile App')
+ project.attrs['description'] = make_translation(
+ 'Native mobile application for iOS and Android', 'Native Mobile Applikation für iOs und Android')
+ project.attrs['creator'] = 'admin'
+ project.attrs['created'] = datetime(2025, 2, 15)
+ project.attrs['modified'] = datetime(2025, 3, 10)
+
+ task = Task(TaskData(), None, None)
+ task.attrs['title'] = make_translation('Setup CI/CD pipeline', 'CI/CD-Pipeline einrichten')
+ task.attrs['description'] = make_translation(
+ 'Configure automated builds and deployment',
+ 'Automatisierte Builds und Deployment konfigurieren'
+ )
+ task.attrs['creator'] = 'devops'
+ task.attrs['created'] = datetime(2025, 2, 20)
+ task.attrs['modified'] = datetime(2025, 3, 5)
+ task.attrs['state'] = 'done'
+ project[str(task.uuid)] = task
+
+ return project
+
+ ProjectBoard.factories['website-redesign'] = make_website_redesign
+ ProjectBoard.factories['mobile-app'] = make_mobile_app
diff --git a/examples/cone.example/src/cone/example/project/browser.py b/examples/cone.example/src/cone/example/project/browser.py
index bde0f199..7e8e610d 100644
--- a/examples/cone.example/src/cone/example/project/browser.py
+++ b/examples/cone.example/src/cone/example/project/browser.py
@@ -23,7 +23,7 @@
# View tiles
@tile(name='view',
- path='cone.example.browser:templates/view.pt',
+ path='cone.example.project:templates/board_view.pt',
interface=ProjectBoard,
permission='login')
class ProjectBoardView(ProtectedContentTile):
@@ -230,7 +230,7 @@ class TaskAddForm(TaskForm):
def save(self, widget, data):
add_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(TaskAddForm, self).save(widget, data)
parent = self.model.parent
# UUIDAsName: name is determined by UUID
parent[self.model.__name__] = self.model
@@ -241,7 +241,7 @@ class TaskEditForm(TaskForm):
def save(self, widget, data):
update_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(TaskEditForm, self).save(widget, data)
@tile(name='addform', interface=Project, permission='add')
diff --git a/examples/cone.example/src/cone/example/project/model.py b/examples/cone.example/src/cone/example/project/model.py
index 8f7c9d9a..4305cebe 100644
--- a/examples/cone.example/src/cone/example/project/model.py
+++ b/examples/cone.example/src/cone/example/project/model.py
@@ -169,6 +169,9 @@ def properties(self):
props.action_sharing = True
return props
+ def __call__(self):
+ ...
+
@property
def metadata(self):
md = Metadata()
diff --git a/examples/cone.example/src/cone/example/project/templates/board_view.pt b/examples/cone.example/src/cone/example/project/templates/board_view.pt
new file mode 100644
index 00000000..3f44a4dd
--- /dev/null
+++ b/examples/cone.example/src/cone/example/project/templates/board_view.pt
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/project/templates/task_view.pt b/examples/cone.example/src/cone/example/project/templates/task_view.pt
index 7482c5f4..9a6fb8a1 100644
--- a/examples/cone.example/src/cone/example/project/templates/task_view.pt
+++ b/examples/cone.example/src/cone/example/project/templates/task_view.pt
@@ -28,7 +28,7 @@
tal:content="str(getattr(model, 'uuid', 'N/A'))">uuid
Path
path
+ tal:content="'/'.join([_ for _ in model.path if _ is not None])">path
diff --git a/examples/cone.example/src/cone/example/wiki/browser.py b/examples/cone.example/src/cone/example/wiki/browser.py
index 84a8fde4..2a1c9122 100644
--- a/examples/cone.example/src/cone/example/wiki/browser.py
+++ b/examples/cone.example/src/cone/example/wiki/browser.py
@@ -27,7 +27,7 @@
# View tile for Wiki container
@tile(name='view',
- path='cone.example.browser:templates/view.pt',
+ path='cone.example.wiki:templates/wiki_container_view.pt',
interface=Wiki,
permission='login')
class WikiContainerView(ProtectedContentTile):
@@ -170,7 +170,7 @@ class WikiPageAddForm(WikiPageForm):
def save(self, widget, data):
add_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(WikiPageAddForm, self).save(widget, data)
parent = self.model.parent
parent[choose_name(parent, self.model.metadata.title)] = self.model
@@ -180,7 +180,7 @@ class WikiPageEditForm(WikiPageForm):
def save(self, widget, data):
update_creation_metadata(self.request, self.model.attrs)
- super().save(widget, data)
+ super(WikiPageEditForm, self).save(widget, data)
@tile(name='addform', interface=WikiPage, permission='add')
diff --git a/examples/cone.example/src/cone/example/wiki/model.py b/examples/cone.example/src/cone/example/wiki/model.py
index 9a623787..d6182d4a 100644
--- a/examples/cone.example/src/cone/example/wiki/model.py
+++ b/examples/cone.example/src/cone/example/wiki/model.py
@@ -34,7 +34,7 @@ class Wiki(BaseContainer):
@property
def properties(self):
- props = super().properties
+ props = super(Wiki, self).properties
props.mainmenu_display_children = False
return props
diff --git a/examples/cone.example/src/cone/example/wiki/templates/wiki_container_view.pt b/examples/cone.example/src/cone/example/wiki/templates/wiki_container_view.pt
new file mode 100644
index 00000000..55438ba3
--- /dev/null
+++ b/examples/cone.example/src/cone/example/wiki/templates/wiki_container_view.pt
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
diff --git a/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt b/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
index 32665d36..99c4383f 100644
--- a/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
+++ b/examples/cone.example/src/cone/example/wiki/templates/wiki_view.pt
@@ -34,7 +34,7 @@
@@ -17,10 +13,7 @@ class MyLayoutConfig(DefaultLayoutConfig):
Store user preferences in session for dynamic layout.
-
if request.session.get('show_tutorial', True):
- self.sidebar_right = ['tutorial']
-else:
- self.sidebar_right = []
+
@@ -28,9 +21,7 @@ else:
Trigger layout refresh after changing session state.
-
ajax_continue(request, [
- AjaxEvent(url, 'contextchanged', '#layout')
-])
+
diff --git a/examples/cone.example/src/cone/example/project/browser.py b/examples/cone.example/src/cone/example/project/browser.py
index e063b1d3..f3c70a89 100644
--- a/examples/cone.example/src/cone/example/project/browser.py
+++ b/examples/cone.example/src/cone/example/project/browser.py
@@ -8,6 +8,7 @@
from cone.app.browser.utils import make_url
from cone.app.utils import add_creation_metadata
from cone.app.utils import update_creation_metadata
+from cone.example.browser.utils import code_block
from cone.example.model import Translation
from cone.example.model import _
from cone.example.project.model import Project
@@ -279,7 +280,26 @@ class TaskContentEditForm(TaskEditForm):
strict=False,
)
class ProjectBoardTutorial(Tile):
- ...
+
+ code_factory_node = """\
+class ProjectBoard(FactoryNode):
+ factories = odict()"""
+
+ code_batched_items = """\
+class ProjectBatchedItems(BatchedItems):
+ slice_template = '...items.pt'
+ show_filter = True
+
+ @property
+ def slice_items(self):
+ start, end = self.current_slice
+ return items[start:end]"""
+
+ def example_factory_node(self):
+ return code_block(self.code_factory_node, 'python')
+
+ def example_batched_items(self):
+ return code_block(self.code_batched_items, 'python')
@tile(
@@ -290,7 +310,32 @@ class ProjectBoardTutorial(Tile):
strict=False,
)
class ProjectTutorial(Tile):
- ...
+
+ code_namespace_uuid = """\
+@plumbing(NamespaceUUID, ...)
+class Project:
+ # uuid = uuid5(namespace, path)"""
+
+ code_categories = """\
+categories = [
+ _('cat_development', default='Development'),
+ _('cat_design', default='Design')
+]"""
+
+ code_principal_acl = """\
+@plumbing(PrincipalACL, ...)
+class Project:
+ role_inheritance = True
+ default_acl = [...]"""
+
+ def example_namespace_uuid(self):
+ return code_block(self.code_namespace_uuid, 'python')
+
+ def example_categories(self):
+ return code_block(self.code_categories, 'python')
+
+ def example_principal_acl(self):
+ return code_block(self.code_principal_acl, 'python')
@tile(
@@ -301,4 +346,27 @@ class ProjectTutorial(Tile):
strict=False,
)
class TaskTutorial(Tile):
- ...
+
+ code_adapter_node = """\
+class Task(AdapterNode):
+ # wraps TaskData instance
+ # self.attrs proxies to model.attrs"""
+
+ code_uuid_as_name = """\
+@plumbing(UUIDAsName, ...)
+class Task(AdapterNode):
+ # self.__name__ == str(self.uuid)"""
+
+ code_workflow_state = """\
+@plumbing(WorkflowState, WorkflowACL)
+class Task(AdapterNode):
+ workflow_name = 'task_workflow'"""
+
+ def example_adapter_node(self):
+ return code_block(self.code_adapter_node, 'python')
+
+ def example_uuid_as_name(self):
+ return code_block(self.code_uuid_as_name, 'python')
+
+ def example_workflow_state(self):
+ return code_block(self.code_workflow_state, 'python')
diff --git a/examples/cone.example/src/cone/example/project/templates/tutorial_board.pt b/examples/cone.example/src/cone/example/project/templates/tutorial_board.pt
index 7e9d6341..180e1524 100644
--- a/examples/cone.example/src/cone/example/project/templates/tutorial_board.pt
+++ b/examples/cone.example/src/cone/example/project/templates/tutorial_board.pt
@@ -5,23 +5,15 @@
Lazily creates children from a factories dict. Children are volatile.
-
class ProjectBoard(FactoryNode):
- factories = odict()
+
-
Paginated listings with search. Override slice_items and item_count.
-
class ProjectBatchedItems(BatchedItems):
- slice_template = '...items.pt'
- show_filter = True
-
- @property
- def slice_items(self):
- start, end = self.current_slice
- return items[start:end]
+
Paginated listings with search. Override slice_items and item_count.
+
diff --git a/examples/cone.example/src/cone/example/project/templates/tutorial_project.pt b/examples/cone.example/src/cone/example/project/templates/tutorial_project.pt
index 3588c7c1..1e1844c2 100644
--- a/examples/cone.example/src/cone/example/project/templates/tutorial_project.pt
+++ b/examples/cone.example/src/cone/example/project/templates/tutorial_project.pt
@@ -5,9 +5,7 @@
UUID calculated from node path + namespace. Deterministic.
-
@plumbing(NamespaceUUID, ...)
-class Project:
- # uuid = uuid5(namespace, path)
+
@@ -15,10 +13,7 @@ class Project:
Categorization with i18n translation strings.
-
categories = [
- _('cat_development', default='Development'),
- _('cat_design', default='Design')
-]
+
@@ -26,10 +21,7 @@ class Project:
Per-node role assignments. Enables sharing UI.
-
@plumbing(PrincipalACL, ...)
-class Project:
- role_inheritance = True
- default_acl = [...]
+
diff --git a/examples/cone.example/src/cone/example/project/templates/tutorial_task.pt b/examples/cone.example/src/cone/example/project/templates/tutorial_task.pt
index c656171b..8b34072b 100644
--- a/examples/cone.example/src/cone/example/project/templates/tutorial_task.pt
+++ b/examples/cone.example/src/cone/example/project/templates/tutorial_task.pt
@@ -5,29 +5,23 @@
Wraps a data node, proxying attributes. Separates storage from UI.
-
class Task(AdapterNode):
- # wraps TaskData instance
- # self.attrs proxies to model.attrs
+
-
Node's __name__ is its UUID string.
-
@plumbing(UUIDAsName, ...)
-class Task(AdapterNode):
- # self.__name__ == str(self.uuid)
+
Node's __name__ is its UUID string.
+
-
State machine. State stored in attrs['state'].
-
@plumbing(WorkflowState, WorkflowACL)
-class Task(AdapterNode):
- workflow_name = 'task_workflow'
+
State machine. State stored in attrs['state'].
+
diff --git a/examples/cone.example/src/cone/example/wiki/browser.py b/examples/cone.example/src/cone/example/wiki/browser.py
index 0656fe20..c3b66ce7 100644
--- a/examples/cone.example/src/cone/example/wiki/browser.py
+++ b/examples/cone.example/src/cone/example/wiki/browser.py
@@ -12,6 +12,7 @@
from cone.app.browser.utils import make_url
from cone.app.utils import add_creation_metadata
from cone.app.utils import update_creation_metadata
+from cone.example.browser.utils import code_block
from cone.example.model import Translation
from cone.example.model import _
from cone.example.wiki.model import Wiki
@@ -259,7 +260,26 @@ def save(self, widget, data):
strict=False,
)
class WikiContainerTutorial(Tile):
- ...
+
+ code_wiki_container = """\
+@node_info(
+ name='wiki',
+ addables=['wiki_page'])
+class Wiki(BaseContainer):
+ ..."""
+
+ code_node_info = """\
+@node_info(
+ name='wiki',
+ title=_('wiki', default='Wiki'),
+ icon='bi-book',
+ addables=['wiki_page'])"""
+
+ def example_wiki_container(self):
+ return code_block(self.code_wiki_container, 'python')
+
+ def example_node_info(self):
+ return code_block(self.code_node_info, 'python')
@tile(
@@ -270,4 +290,31 @@ class WikiContainerTutorial(Tile):
strict=False,
)
class WikiPageTutorial(Tile):
- ...
+
+ code_reference_browser = """\
+form['references'] = factory(
+ 'field:...:reference',
+ props={
+ 'multivalued': True,
+ 'referencable': 'wiki_page',
+ 'root': '/wiki',
+ })"""
+
+ code_uuid_attribute_aware = """\
+@plumbing(UUIDAttributeAware, ...)
+class WikiPage:
+ # self.uuid reads/writes attrs"""
+
+ code_navigation_leaf = """\
+@implementer(INavigationLeaf)
+class WikiPage:
+ ..."""
+
+ def example_reference_browser(self):
+ return code_block(self.code_reference_browser, 'python')
+
+ def example_uuid_attribute_aware(self):
+ return code_block(self.code_uuid_attribute_aware, 'python')
+
+ def example_navigation_leaf(self):
+ return code_block(self.code_navigation_leaf, 'python')
diff --git a/examples/cone.example/src/cone/example/wiki/templates/tutorial_page.pt b/examples/cone.example/src/cone/example/wiki/templates/tutorial_page.pt
index 089e16ff..29035c23 100644
--- a/examples/cone.example/src/cone/example/wiki/templates/tutorial_page.pt
+++ b/examples/cone.example/src/cone/example/wiki/templates/tutorial_page.pt
@@ -5,13 +5,7 @@
Widget for selecting related nodes by UUID.
-
form['references'] = factory(
- 'field:...:reference',
- props={
- 'multivalued': True,
- 'referencable': 'wiki_page',
- 'root': '/wiki',
- })
+
@@ -19,9 +13,7 @@
UUID stored in node attrs. Auto-generated on first access.
-
@plumbing(UUIDAttributeAware, ...)
-class WikiPage:
- # self.uuid reads/writes attrs
+
@@ -29,9 +21,7 @@ class WikiPage:
Marks node as leaf - no children shown in navtree.
-
@implementer(INavigationLeaf)
-class WikiPage:
- ...
+
diff --git a/examples/cone.example/src/cone/example/wiki/templates/tutorial_wiki.pt b/examples/cone.example/src/cone/example/wiki/templates/tutorial_wiki.pt
index 73e05950..ada2b3c5 100644
--- a/examples/cone.example/src/cone/example/wiki/templates/tutorial_wiki.pt
+++ b/examples/cone.example/src/cone/example/wiki/templates/tutorial_wiki.pt
@@ -5,11 +5,7 @@
Entry container for wiki pages. Acts as reference browser root.
-
@node_info(
- name='wiki',
- addables=['wiki_page'])
-class Wiki(BaseContainer):
- ...
+
@@ -17,11 +13,7 @@ class Wiki(BaseContainer):
Registers node type with metadata and allowed children.
-
@node_info(
- name='wiki',
- title=_('wiki', default='Wiki'),
- icon='bi-book',
- addables=['wiki_page'])
+
From e0ceee0aba557ea7e6683e24c99e84c2ccd92d42 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Fri, 30 Jan 2026 12:58:26 +0100
Subject: [PATCH 18/25] update mainmenu active item color for dark mode
---
scss/colors.scss | 8 ++++++++
scss/header.scss | 3 ---
src/cone/app/browser/static/cone/cone.app.css | 15 ++++++++++++---
src/cone/app/browser/static/cone/cone.app.min.css | 2 +-
4 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/scss/colors.scss b/scss/colors.scss
index ef0c3d70..4a75947c 100644
--- a/scss/colors.scss
+++ b/scss/colors.scss
@@ -54,6 +54,10 @@ $color-mode-type: data;
.bg-primary-100 {
background-color: $primary-100-dark;
}
+
+ #mainmenu li.nav-item.active .nav-link {
+ color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
+ }
}
/* light mode */
@@ -92,6 +96,10 @@ $color-mode-type: data;
.bg-primary-100 {
background-color: $primary-100;
}
+
+ #mainmenu li.nav-item.active .nav-link {
+ color: $primary;
+ }
}
/* import everything where we need changed bootstrap variables for here */
diff --git a/scss/header.scss b/scss/header.scss
index 88de477c..54d092db 100644
--- a/scss/header.scss
+++ b/scss/header.scss
@@ -146,9 +146,6 @@
padding-left: 8px;
white-space: nowrap;
}
- .nav-item.active .nav-link {
- color: $primary;
- }
}
#personaltools {
diff --git a/src/cone/app/browser/static/cone/cone.app.css b/src/cone/app/browser/static/cone/cone.app.css
index 1f665e1b..3d1c726b 100644
--- a/src/cone/app/browser/static/cone/cone.app.css
+++ b/src/cone/app/browser/static/cone/cone.app.css
@@ -33,6 +33,9 @@
[data-bs-theme=dark] .bg-primary-100 {
background-color: #2c4265;
}
+[data-bs-theme=dark] #mainmenu li.nav-item.active .nav-link {
+ color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
+}
/* light mode */
[data-bs-theme=light] #content_area {
@@ -62,6 +65,9 @@
[data-bs-theme=light] .bg-primary-100 {
background-color: #dde9fb;
}
+[data-bs-theme=light] #mainmenu li.nav-item.active .nav-link {
+ color: #4171b6;
+}
/* import everything where we need changed bootstrap variables for here */
:root {
@@ -283,9 +289,6 @@ tr.selectable td {
padding-left: 8px;
white-space: nowrap;
}
-#main-area #header-main #mainmenu .nav-item.active .nav-link {
- color: #4171b6;
-}
#main-area #header-main #personaltools #language-dropdown > div.dropdown-toggle > a {
height: 20px;
}
@@ -413,6 +416,9 @@ tr.selectable td {
[data-bs-theme=dark] .bg-primary-100 {
background-color: #2c4265;
}
+[data-bs-theme=dark] #mainmenu li.nav-item.active .nav-link {
+ color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
+}
/* light mode */
[data-bs-theme=light] #content_area {
@@ -442,6 +448,9 @@ tr.selectable td {
[data-bs-theme=light] .bg-primary-100 {
background-color: #dde9fb;
}
+[data-bs-theme=light] #mainmenu li.nav-item.active .nav-link {
+ color: #4171b6;
+}
/* import everything where we need changed bootstrap variables for here */
/* sidebar */
diff --git a/src/cone/app/browser/static/cone/cone.app.min.css b/src/cone/app/browser/static/cone/cone.app.min.css
index fb44996e..16f56a51 100644
--- a/src/cone/app/browser/static/cone/cone.app.min.css
+++ b/src/cone/app/browser/static/cone/cone.app.min.css
@@ -1 +1 @@
-:root{--primary-100: #dde9fb;--primary-100-dark: #2c4265}[data-bs-theme=dark] #content_area{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #navbar-content-wrapper{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #sidebar_left{background-color:#343b45}[data-bs-theme=dark] #footer{color:#f8f9fa}[data-bs-theme=dark] #contextmenu{color:#f8f9fa;background-color:#343a40}[data-bs-theme=dark] tr.selectable.selected td{background-color:#344555}[data-bs-theme=dark] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#485f76}[data-bs-theme=dark] .bg-primary-100{background-color:#2c4265}[data-bs-theme=light] #content_area{color:#212529;background-color:#f8f9fa}[data-bs-theme=light] #navbar-content-wrapper{color:#212529;background-color:#fff}[data-bs-theme=light] #sidebar_left{background-color:#414c5b}[data-bs-theme=light] #footer{color:#212529}[data-bs-theme=light] #contextmenu{color:#212529;background-color:#fff}[data-bs-theme=light] tr.selectable.selected td{background-color:#dfeefb}[data-bs-theme=light] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#cfdce7}[data-bs-theme=light] .bg-primary-100{background-color:#dde9fb}:root{--sidebar-width: 300px;--navbar-height: 50px}html{height:100%;width:100%}body{height:100%;overflow-y:hidden !important}#footer{z-index:1}.referencebrowser_trigger{cursor:pointer}select.form-control.referencebrowser[multiple=multiple]+.referencebrowser_trigger{max-width:max-content;margin-top:.5rem;margin-bottom:1rem}#referencebrowser{border-bottom:0;border-left:0;border-right:0;border-radius:0;left:calc(-1*var(--bs-modal-padding));width:calc(100% + var(--bs-modal-padding)*2)}#referencebrowser td.title{width:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(0,0,0,0)}::-webkit-scrollbar-thumb{background:var(--bs-primary);border-radius:3px}@supports(-moz-appearance: none){*{scrollbar-width:auto;scrollbar-color:var(--bs-primary) rgba(0,0,0,0)}}#footer-spacer{flex:1}#content{container:main-content/inline-size;height:100%;width:100%;position:relative}#content_area{height:100%}#content_area.has-pathbar{height:calc(100% - 40px)}#main-area{width:0 !important}tr.selectable td{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}#main-area.full .batched_items_slice_size{max-width:max-content}#main-area.full .batched_items_filter{max-width:300px}#input-contentform-save{margin-right:1rem}#header-logo{z-index:4}#header-logo>span,#header-logo-placeholder>span{font-size:1.5rem !important}#main-area.compact:not(.super-compact) #header-logo{order:0}#main-area.compact:not(.super-compact) #personaltools{order:1;margin-left:auto}#main-area.compact:not(.super-compact) .navbar-toggler{order:2}#main-area.compact:not(.super-compact) #navbar-content-wrapper{order:3}#main-area.compact #header-main.mobile-menu-open #navbar-content-wrapper{max-height:calc(100vh - 50px);position:relative;overflow:hidden;padding:0 !important;z-index:3}@media screen and (max-width: 768px){#main-area.compact #header-main.mobile-menu-open #navbar-content-wrapper{z-index:1200}}#main-area.compact #header-main.mobile-menu-open #mainmenu{align-items:baseline}#main-area.compact #header-main.mobile-menu-open #mainmenu .scrollbar{display:none !important}#main-area.compact #header-main.mobile-menu-open #mainmenu .scrollable-content{right:0 !important}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu{border:none}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu>li .dropdown-item{padding:8px 20px}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu>li:first-child .dropdown-item{padding-top:0}#main-area.super-compact #personaltools{height:50px;justify-content:space-between;margin:auto 20px}#main-area.super-compact #personaltools #colortoggler{padding-left:0}#main-area.full #header-logo{order:1}#main-area.full #mainmenu{height:var(--navbar-height)}#main-area.full #mainmenu .dropdown-menu{position:fixed}#main-area.full #personaltools{height:100%}#main-area #header-main{height:var(--navbar-height)}#main-area #header-main.navbar-expand #header-content{flex-direction:row-reverse}#main-area #header-main.navbar-expand #navbar-content-wrapper{box-shadow:none !important;background:none}#main-area #header-main:not(.navbar-expand) #header-content{flex-direction:row}#main-area #header-main:not(.navbar-expand) #navbar-content{flex-direction:column}#main-area #header-main:not(.navbar-expand) #livesearch{order:0;width:100%;padding-left:55px !important;padding-right:15px !important}#main-area #header-main:not(.navbar-expand) #mainmenu{order:1;margin-left:10px !important;margin-top:20px;margin-bottom:20px}#main-area #header-main:not(.navbar-expand) #mainmenu ul>li{display:block !important}#main-area #header-main #navbar-content-wrapper{min-width:0;z-index:1}#main-area #header-main #navbar-content-wrapper #navbar-content{min-width:0}#main-area #header-main #mainmenu>ul{padding-left:8px;white-space:nowrap}#main-area #header-main #mainmenu .nav-item.active .nav-link{color:#4171b6}#main-area #header-main #personaltools #language-dropdown>div.dropdown-toggle>a{height:20px}#main-area #header-main #personaltools #language-dropdown>div.dropdown-toggle>a img{vertical-align:baseline}#main-area #header-main #personaltools #colortoggler #colortoggle-switch{width:2rem;height:1rem}#personaltools-dropdown .dropdown-item a{text-decoration:none;color:var(--bs-secondary-color)}#pathbar{z-index:5}#form-loginform{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;height:100%}#form-loginform #field-loginform-user,#form-loginform #field-loginform-password{display:grid;grid-template-columns:1fr 2fr;margin-bottom:.5rem;gap:1rem}@media(max-width: 400px){#form-loginform #field-loginform-user,#form-loginform #field-loginform-password{grid-template-columns:auto;gap:0}}#form-loginform #input-loginform-login{margin-top:1.5rem;width:200px}[data-bs-theme=light] #header-logo #header-logo-light{display:none}[data-bs-theme=dark] #header-logo #header-logo-dark{display:none}.scrollable-x,.scrollable-y{position:relative;overflow:hidden}.scrollable-content{position:relative}.scrollbar{display:none;position:absolute;background:#dee2e6;border-radius:3px;z-index:1000}.scrollbar .scroll-handle{position:relative;border-radius:3px;background:#4171b6}.scrollbar-top .scrollbar{top:0}.scrollbar-bottom .scrollbar{bottom:0}.scrollbar-left .scrollbar{left:0}.scrollbar-right .scrollbar{right:0}:root{--primary-100: #dde9fb;--primary-100-dark: #2c4265}[data-bs-theme=dark] #content_area{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #navbar-content-wrapper{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #sidebar_left{background-color:#343b45}[data-bs-theme=dark] #footer{color:#f8f9fa}[data-bs-theme=dark] #contextmenu{color:#f8f9fa;background-color:#343a40}[data-bs-theme=dark] tr.selectable.selected td{background-color:#344555}[data-bs-theme=dark] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#485f76}[data-bs-theme=dark] .bg-primary-100{background-color:#2c4265}[data-bs-theme=light] #content_area{color:#212529;background-color:#f8f9fa}[data-bs-theme=light] #navbar-content-wrapper{color:#212529;background-color:#fff}[data-bs-theme=light] #sidebar_left{background-color:#414c5b}[data-bs-theme=light] #footer{color:#212529}[data-bs-theme=light] #contextmenu{color:#212529;background-color:#fff}[data-bs-theme=light] tr.selectable.selected td{background-color:#dfeefb}[data-bs-theme=light] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#cfdce7}[data-bs-theme=light] .bg-primary-100{background-color:#dde9fb}#sidebar_left,#sidebar_right{height:100%;max-height:100%;user-select:none}#sidebar_left ul.list-group,#sidebar_left ul.list-group li,#sidebar_right ul.list-group,#sidebar_right ul.list-group li{background-color:inherit}#sidebar_left .sidebar-controls,#sidebar_right .sidebar-controls{position:absolute;bottom:0}#sidebar_left .sidebar-controls .sidebar-control,#sidebar_right .sidebar-controls .sidebar-control{position:relative;z-index:10;border-bottom-left-radius:0;border-bottom-right-radius:0}#sidebar_left .lock-state,#sidebar_right .lock-state{position:relative;top:calc(100% - 2rem);width:min-content;height:0;z-index:100}@media(max-width: 767.98px){#sidebar_left .lock-state,#sidebar_right .lock-state{display:none}}#sidebar_left .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn,#sidebar_right .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn{background-color:var(--bs-border-color);border-color:rgba(0,0,0,0);color:var(--bs-body-color)}#sidebar_left .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn i.bi-lock,#sidebar_right .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn i.bi-lock{display:none}#sidebar_left .lock-state input[type=checkbox]:checked+.lock-state-btn,#sidebar_right .lock-state input[type=checkbox]:checked+.lock-state-btn{background-color:var(--bs-dark-border-subtle);border-color:rgba(0,0,0,0);color:var(--bs-body-color)}#sidebar_left .lock-state input[type=checkbox]:checked+.lock-state-btn i.bi-unlock,#sidebar_right .lock-state input[type=checkbox]:checked+.lock-state-btn i.bi-unlock{display:none}#sidebar_left #navtree,#sidebar_right #navtree{max-width:100%}#sidebar_left #navtree .nav-link,#sidebar_right #navtree .nav-link{z-index:1000;white-space:nowrap}#sidebar_left #navtree .nav-link.active,#sidebar_right #navtree .nav-link.active{background-color:rgba(0,0,0,0);font-weight:bold}#sidebar_left #navtree .nav-link i.btn-closed,#sidebar_right #navtree .nav-link i.btn-closed{vertical-align:text-bottom}#sidebar_left #navtree .nav-link span,#sidebar_right #navtree .nav-link span{display:inline-block;width:calc(100% - 40px);text-overflow:ellipsis;overflow-x:hidden;vertical-align:text-bottom}#sidebar_left #navtree #navigation-collapse.no-collapse>ul>li,#sidebar_right #navtree #navigation-collapse.no-collapse>ul>li{padding-left:0 !important}#sidebar_left #sidebar_resize,#sidebar_right #sidebar_resize{position:relative;width:0;height:0}#sidebar_left #sidebar_resize #sidebar_resizer,#sidebar_right #sidebar_resize #sidebar_resizer{position:absolute;z-index:3;height:100vh;width:20px;transform:translateX(-50%);cursor:col-resize}#sidebar_left #sidebar_collapse,#sidebar_right #sidebar_collapse{position:relative;top:100%;width:0;height:0}#sidebar_left #sidebar_collapse .collapse_btn,#sidebar_right #sidebar_collapse .collapse_btn{position:absolute;top:100%;border-radius:100%;z-index:1000}#sidebar_left.collapsed,#sidebar_right.collapsed{width:0px !important;min-width:0 !important}#sidebar_left.collapsed #sidebar_resize,#sidebar_right.collapsed #sidebar_resize{display:none}#sidebar_left.collapsed #sidebar_collapse .collapse_btn .btn-open,#sidebar_right.collapsed #sidebar_collapse .collapse_btn .btn-open{display:none}#sidebar_left.collapsed .sidebar-controls,#sidebar_right.collapsed .sidebar-controls{display:none}#sidebar_left.responsive-collapsed:not(.expanded) .sidebar-controls,#sidebar_right.responsive-collapsed:not(.expanded) .sidebar-controls{display:none}#sidebar_left.expanded #sidebar_collapse .collapse_btn .btn-closed,#sidebar_right.expanded #sidebar_collapse .collapse_btn .btn-closed{display:none}@media(min-width: 1400px){#sidebar_left:not(.collapsed) #sidebar_collapse .collapse_btn,#sidebar_right:not(.collapsed) #sidebar_collapse .collapse_btn{transform:translateX(-20px)}#sidebar_left:not(.collapsed) #sidebar_collapse .collapse_btn .btn-closed,#sidebar_right:not(.collapsed) #sidebar_collapse .collapse_btn .btn-closed{display:none}}@media(max-width: 1399.98px){#sidebar_left:not(.expanded),#sidebar_right:not(.expanded){width:0 !important;min-width:0 !important;border:none !important}#sidebar_left:not(.expanded) #sidebar_resize,#sidebar_right:not(.expanded) #sidebar_resize{display:none}#sidebar_left:not(.expanded) #sidebar_collapse .collapse_btn .btn-open,#sidebar_right:not(.expanded) #sidebar_collapse .collapse_btn .btn-open{display:none}}@media(min-width: 768px)and (max-width: 1399.98px){#sidebar_left.expanded #sidebar_collapse .collapse_btn,#sidebar_right.expanded #sidebar_collapse .collapse_btn{transform:translateX(-20px)}}@media(max-width: 767.98px){#sidebar_left #sidebar_resize,#sidebar_right #sidebar_resize{display:none}#sidebar_left.expanded,#sidebar_right.expanded{width:100% !important;z-index:3}}#sidebar_left #sidebar_resize{left:100%}#sidebar_left #sidebar_collapse{left:100%}@media(max-width: 767.98px){#sidebar_left.expanded #sidebar_collapse{left:calc(100% - 50px)}}#sidebar_left.responsive-expanded .lock-state{left:100%;transform:translateX(-10px)}#sidebar_left.expanded .lock-state{left:100% !important;transform:translateX(-10px) !important}#sidebar_left.responsive-collapsed .lock-state{left:calc(100% + 10px)}#sidebar_left.collapsed .lock-state{left:calc(100% + 10px) !important;transform:none !important}#sidebar_right{background-color:var(--bs-body-bg);border-left:1px solid var(--bs-border-color)}#sidebar_right .sidebar-controls{right:0}#sidebar_right #sidebar_collapse{left:0}#sidebar_right.collapsed{border:0}#sidebar_right.collapsed .collapse_btn{right:0}#sidebar_right.responsive-expanded .lock-state{transform:translateX(-10px)}#sidebar_right.expanded .lock-state{transform:translateX(-10px) !important}#sidebar_right.responsive-collapsed .lock-state{transform:translateX(calc(-100% - 10px))}#sidebar_right.collapsed .lock-state{transform:translateX(calc(-100% - 10px)) !important}@media(max-width: 1399.98px){#sidebar_right:not(.expanded) #sidebar_collapse .collapse_btn{right:0}}@media(max-width: 767.98px){#sidebar_right.expanded{transform:translateX(-2rem)}}#sidebar_left.static,#sidebar_right.static{position:absolute;z-index:10;top:0;bottom:0}#sidebar_left.static{left:0}#sidebar_right.static{right:0}.table_length,.table_filter{width:unset}#contents>div.card-body div.row>div{flex:1 0 auto;flex-wrap:nowrap}.table-footer{gap:1rem 0}.table-footer .table_info{white-space:nowrap}#main-area.full .table_length{max-width:max-content}#main-area.full .table_filter{max-width:300px}#settings-grid-container .settings-grid a i{font-size:44px}@media(max-width: 450px){#settings-grid-container .settings-grid a i{font-size:30px}}
+:root{--primary-100: #dde9fb;--primary-100-dark: #2c4265}[data-bs-theme=dark] #content_area{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #navbar-content-wrapper{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #sidebar_left{background-color:#343b45}[data-bs-theme=dark] #footer{color:#f8f9fa}[data-bs-theme=dark] #contextmenu{color:#f8f9fa;background-color:#343a40}[data-bs-theme=dark] tr.selectable.selected td{background-color:#344555}[data-bs-theme=dark] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#485f76}[data-bs-theme=dark] .bg-primary-100{background-color:#2c4265}[data-bs-theme=dark] #mainmenu li.nav-item.active .nav-link{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1))}[data-bs-theme=light] #content_area{color:#212529;background-color:#f8f9fa}[data-bs-theme=light] #navbar-content-wrapper{color:#212529;background-color:#fff}[data-bs-theme=light] #sidebar_left{background-color:#414c5b}[data-bs-theme=light] #footer{color:#212529}[data-bs-theme=light] #contextmenu{color:#212529;background-color:#fff}[data-bs-theme=light] tr.selectable.selected td{background-color:#dfeefb}[data-bs-theme=light] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#cfdce7}[data-bs-theme=light] .bg-primary-100{background-color:#dde9fb}[data-bs-theme=light] #mainmenu li.nav-item.active .nav-link{color:#4171b6}:root{--sidebar-width: 300px;--navbar-height: 50px}html{height:100%;width:100%}body{height:100%;overflow-y:hidden !important}#footer{z-index:1}.referencebrowser_trigger{cursor:pointer}select.form-control.referencebrowser[multiple=multiple]+.referencebrowser_trigger{max-width:max-content;margin-top:.5rem;margin-bottom:1rem}#referencebrowser{border-bottom:0;border-left:0;border-right:0;border-radius:0;left:calc(-1*var(--bs-modal-padding));width:calc(100% + var(--bs-modal-padding)*2)}#referencebrowser td.title{width:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(0,0,0,0)}::-webkit-scrollbar-thumb{background:var(--bs-primary);border-radius:3px}@supports(-moz-appearance: none){*{scrollbar-width:auto;scrollbar-color:var(--bs-primary) rgba(0,0,0,0)}}#footer-spacer{flex:1}#content{container:main-content/inline-size;height:100%;width:100%;position:relative}#content_area{height:100%}#content_area.has-pathbar{height:calc(100% - 40px)}#main-area{width:0 !important}tr.selectable td{user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}#main-area.full .batched_items_slice_size{max-width:max-content}#main-area.full .batched_items_filter{max-width:300px}#input-contentform-save{margin-right:1rem}#header-logo{z-index:4}#header-logo>span,#header-logo-placeholder>span{font-size:1.5rem !important}#main-area.compact:not(.super-compact) #header-logo{order:0}#main-area.compact:not(.super-compact) #personaltools{order:1;margin-left:auto}#main-area.compact:not(.super-compact) .navbar-toggler{order:2}#main-area.compact:not(.super-compact) #navbar-content-wrapper{order:3}#main-area.compact #header-main.mobile-menu-open #navbar-content-wrapper{max-height:calc(100vh - 50px);position:relative;overflow:hidden;padding:0 !important;z-index:3}@media screen and (max-width: 768px){#main-area.compact #header-main.mobile-menu-open #navbar-content-wrapper{z-index:1200}}#main-area.compact #header-main.mobile-menu-open #mainmenu{align-items:baseline}#main-area.compact #header-main.mobile-menu-open #mainmenu .scrollbar{display:none !important}#main-area.compact #header-main.mobile-menu-open #mainmenu .scrollable-content{right:0 !important}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu{border:none}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu>li .dropdown-item{padding:8px 20px}#main-area.compact #header-main.mobile-menu-open #mainmenu .dropdown-menu>li:first-child .dropdown-item{padding-top:0}#main-area.super-compact #personaltools{height:50px;justify-content:space-between;margin:auto 20px}#main-area.super-compact #personaltools #colortoggler{padding-left:0}#main-area.full #header-logo{order:1}#main-area.full #mainmenu{height:var(--navbar-height)}#main-area.full #mainmenu .dropdown-menu{position:fixed}#main-area.full #personaltools{height:100%}#main-area #header-main{height:var(--navbar-height)}#main-area #header-main.navbar-expand #header-content{flex-direction:row-reverse}#main-area #header-main.navbar-expand #navbar-content-wrapper{box-shadow:none !important;background:none}#main-area #header-main:not(.navbar-expand) #header-content{flex-direction:row}#main-area #header-main:not(.navbar-expand) #navbar-content{flex-direction:column}#main-area #header-main:not(.navbar-expand) #livesearch{order:0;width:100%;padding-left:55px !important;padding-right:15px !important}#main-area #header-main:not(.navbar-expand) #mainmenu{order:1;margin-left:10px !important;margin-top:20px;margin-bottom:20px}#main-area #header-main:not(.navbar-expand) #mainmenu ul>li{display:block !important}#main-area #header-main #navbar-content-wrapper{min-width:0;z-index:1}#main-area #header-main #navbar-content-wrapper #navbar-content{min-width:0}#main-area #header-main #mainmenu>ul{padding-left:8px;white-space:nowrap}#main-area #header-main #personaltools #language-dropdown>div.dropdown-toggle>a{height:20px}#main-area #header-main #personaltools #language-dropdown>div.dropdown-toggle>a img{vertical-align:baseline}#main-area #header-main #personaltools #colortoggler #colortoggle-switch{width:2rem;height:1rem}#personaltools-dropdown .dropdown-item a{text-decoration:none;color:var(--bs-secondary-color)}#pathbar{z-index:5}#form-loginform{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;height:100%}#form-loginform #field-loginform-user,#form-loginform #field-loginform-password{display:grid;grid-template-columns:1fr 2fr;margin-bottom:.5rem;gap:1rem}@media(max-width: 400px){#form-loginform #field-loginform-user,#form-loginform #field-loginform-password{grid-template-columns:auto;gap:0}}#form-loginform #input-loginform-login{margin-top:1.5rem;width:200px}[data-bs-theme=light] #header-logo #header-logo-light{display:none}[data-bs-theme=dark] #header-logo #header-logo-dark{display:none}.scrollable-x,.scrollable-y{position:relative;overflow:hidden}.scrollable-content{position:relative}.scrollbar{display:none;position:absolute;background:#dee2e6;border-radius:3px;z-index:1000}.scrollbar .scroll-handle{position:relative;border-radius:3px;background:#4171b6}.scrollbar-top .scrollbar{top:0}.scrollbar-bottom .scrollbar{bottom:0}.scrollbar-left .scrollbar{left:0}.scrollbar-right .scrollbar{right:0}:root{--primary-100: #dde9fb;--primary-100-dark: #2c4265}[data-bs-theme=dark] #content_area{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #navbar-content-wrapper{color:#f8f9fa;background-color:#212529}[data-bs-theme=dark] #sidebar_left{background-color:#343b45}[data-bs-theme=dark] #footer{color:#f8f9fa}[data-bs-theme=dark] #contextmenu{color:#f8f9fa;background-color:#343a40}[data-bs-theme=dark] tr.selectable.selected td{background-color:#344555}[data-bs-theme=dark] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#485f76}[data-bs-theme=dark] .bg-primary-100{background-color:#2c4265}[data-bs-theme=dark] #mainmenu li.nav-item.active .nav-link{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1))}[data-bs-theme=light] #content_area{color:#212529;background-color:#f8f9fa}[data-bs-theme=light] #navbar-content-wrapper{color:#212529;background-color:#fff}[data-bs-theme=light] #sidebar_left{background-color:#414c5b}[data-bs-theme=light] #footer{color:#212529}[data-bs-theme=light] #contextmenu{color:#212529;background-color:#fff}[data-bs-theme=light] tr.selectable.selected td{background-color:#dfeefb}[data-bs-theme=light] .table-striped>tbody>tr.selectable.selected:nth-child(2n+1)>td{background-color:#cfdce7}[data-bs-theme=light] .bg-primary-100{background-color:#dde9fb}[data-bs-theme=light] #mainmenu li.nav-item.active .nav-link{color:#4171b6}#sidebar_left,#sidebar_right{height:100%;max-height:100%;user-select:none}#sidebar_left ul.list-group,#sidebar_left ul.list-group li,#sidebar_right ul.list-group,#sidebar_right ul.list-group li{background-color:inherit}#sidebar_left .sidebar-controls,#sidebar_right .sidebar-controls{position:absolute;bottom:0}#sidebar_left .sidebar-controls .sidebar-control,#sidebar_right .sidebar-controls .sidebar-control{position:relative;z-index:10;border-bottom-left-radius:0;border-bottom-right-radius:0}#sidebar_left .lock-state,#sidebar_right .lock-state{position:relative;top:calc(100% - 2rem);width:min-content;height:0;z-index:100}@media(max-width: 767.98px){#sidebar_left .lock-state,#sidebar_right .lock-state{display:none}}#sidebar_left .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn,#sidebar_right .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn{background-color:var(--bs-border-color);border-color:rgba(0,0,0,0);color:var(--bs-body-color)}#sidebar_left .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn i.bi-lock,#sidebar_right .lock-state input[type=checkbox]:not(:checked)+.lock-state-btn i.bi-lock{display:none}#sidebar_left .lock-state input[type=checkbox]:checked+.lock-state-btn,#sidebar_right .lock-state input[type=checkbox]:checked+.lock-state-btn{background-color:var(--bs-dark-border-subtle);border-color:rgba(0,0,0,0);color:var(--bs-body-color)}#sidebar_left .lock-state input[type=checkbox]:checked+.lock-state-btn i.bi-unlock,#sidebar_right .lock-state input[type=checkbox]:checked+.lock-state-btn i.bi-unlock{display:none}#sidebar_left #navtree,#sidebar_right #navtree{max-width:100%}#sidebar_left #navtree .nav-link,#sidebar_right #navtree .nav-link{z-index:1000;white-space:nowrap}#sidebar_left #navtree .nav-link.active,#sidebar_right #navtree .nav-link.active{background-color:rgba(0,0,0,0);font-weight:bold}#sidebar_left #navtree .nav-link i.btn-closed,#sidebar_right #navtree .nav-link i.btn-closed{vertical-align:text-bottom}#sidebar_left #navtree .nav-link span,#sidebar_right #navtree .nav-link span{display:inline-block;width:calc(100% - 40px);text-overflow:ellipsis;overflow-x:hidden;vertical-align:text-bottom}#sidebar_left #navtree #navigation-collapse.no-collapse>ul>li,#sidebar_right #navtree #navigation-collapse.no-collapse>ul>li{padding-left:0 !important}#sidebar_left #sidebar_resize,#sidebar_right #sidebar_resize{position:relative;width:0;height:0}#sidebar_left #sidebar_resize #sidebar_resizer,#sidebar_right #sidebar_resize #sidebar_resizer{position:absolute;z-index:3;height:100vh;width:20px;transform:translateX(-50%);cursor:col-resize}#sidebar_left #sidebar_collapse,#sidebar_right #sidebar_collapse{position:relative;top:100%;width:0;height:0}#sidebar_left #sidebar_collapse .collapse_btn,#sidebar_right #sidebar_collapse .collapse_btn{position:absolute;top:100%;border-radius:100%;z-index:1000}#sidebar_left.collapsed,#sidebar_right.collapsed{width:0px !important;min-width:0 !important}#sidebar_left.collapsed #sidebar_resize,#sidebar_right.collapsed #sidebar_resize{display:none}#sidebar_left.collapsed #sidebar_collapse .collapse_btn .btn-open,#sidebar_right.collapsed #sidebar_collapse .collapse_btn .btn-open{display:none}#sidebar_left.collapsed .sidebar-controls,#sidebar_right.collapsed .sidebar-controls{display:none}#sidebar_left.responsive-collapsed:not(.expanded) .sidebar-controls,#sidebar_right.responsive-collapsed:not(.expanded) .sidebar-controls{display:none}#sidebar_left.expanded #sidebar_collapse .collapse_btn .btn-closed,#sidebar_right.expanded #sidebar_collapse .collapse_btn .btn-closed{display:none}@media(min-width: 1400px){#sidebar_left:not(.collapsed) #sidebar_collapse .collapse_btn,#sidebar_right:not(.collapsed) #sidebar_collapse .collapse_btn{transform:translateX(-20px)}#sidebar_left:not(.collapsed) #sidebar_collapse .collapse_btn .btn-closed,#sidebar_right:not(.collapsed) #sidebar_collapse .collapse_btn .btn-closed{display:none}}@media(max-width: 1399.98px){#sidebar_left:not(.expanded),#sidebar_right:not(.expanded){width:0 !important;min-width:0 !important;border:none !important}#sidebar_left:not(.expanded) #sidebar_resize,#sidebar_right:not(.expanded) #sidebar_resize{display:none}#sidebar_left:not(.expanded) #sidebar_collapse .collapse_btn .btn-open,#sidebar_right:not(.expanded) #sidebar_collapse .collapse_btn .btn-open{display:none}}@media(min-width: 768px)and (max-width: 1399.98px){#sidebar_left.expanded #sidebar_collapse .collapse_btn,#sidebar_right.expanded #sidebar_collapse .collapse_btn{transform:translateX(-20px)}}@media(max-width: 767.98px){#sidebar_left #sidebar_resize,#sidebar_right #sidebar_resize{display:none}#sidebar_left.expanded,#sidebar_right.expanded{width:100% !important;z-index:3}}#sidebar_left #sidebar_resize{left:100%}#sidebar_left #sidebar_collapse{left:100%}@media(max-width: 767.98px){#sidebar_left.expanded #sidebar_collapse{left:calc(100% - 50px)}}#sidebar_left.responsive-expanded .lock-state{left:100%;transform:translateX(-10px)}#sidebar_left.expanded .lock-state{left:100% !important;transform:translateX(-10px) !important}#sidebar_left.responsive-collapsed .lock-state{left:calc(100% + 10px)}#sidebar_left.collapsed .lock-state{left:calc(100% + 10px) !important;transform:none !important}#sidebar_right{background-color:var(--bs-body-bg);border-left:1px solid var(--bs-border-color)}#sidebar_right .sidebar-controls{right:0}#sidebar_right #sidebar_collapse{left:0}#sidebar_right.collapsed{border:0}#sidebar_right.collapsed .collapse_btn{right:0}#sidebar_right.responsive-expanded .lock-state{transform:translateX(-10px)}#sidebar_right.expanded .lock-state{transform:translateX(-10px) !important}#sidebar_right.responsive-collapsed .lock-state{transform:translateX(calc(-100% - 10px))}#sidebar_right.collapsed .lock-state{transform:translateX(calc(-100% - 10px)) !important}@media(max-width: 1399.98px){#sidebar_right:not(.expanded) #sidebar_collapse .collapse_btn{right:0}}@media(max-width: 767.98px){#sidebar_right.expanded{transform:translateX(-2rem)}}#sidebar_left.static,#sidebar_right.static{position:absolute;z-index:10;top:0;bottom:0}#sidebar_left.static{left:0}#sidebar_right.static{right:0}.table_length,.table_filter{width:unset}#contents>div.card-body div.row>div{flex:1 0 auto;flex-wrap:nowrap}.table-footer{gap:1rem 0}.table-footer .table_info{white-space:nowrap}#main-area.full .table_length{max-width:max-content}#main-area.full .table_filter{max-width:300px}#settings-grid-container .settings-grid a i{font-size:44px}@media(max-width: 450px){#settings-grid-container .settings-grid a i{font-size:30px}}
From 4ce226ae102fb118d3253a088403936f6206dbce Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Fri, 30 Jan 2026 13:25:02 +0100
Subject: [PATCH 19/25] example test suite WIP
---
examples/cone.example/pyproject.toml | 13 +-
.../src/cone/example/browser/__init__.py | 4 +-
.../example/browser/static/cone.example.css | 4 +-
.../src/cone/example/tests/__init__.py | 17 ++
.../src/cone/example/tests/test_ajax.py | 118 +++++++++++
.../src/cone/example/tests/test_browser.py | 145 ++++++++++++++
.../src/cone/example/tests/test_document.py | 175 ++++++++++++++++
.../src/cone/example/tests/test_layout.py | 168 ++++++++++++++++
.../src/cone/example/tests/test_model.py | 168 ++++++++++++++++
.../src/cone/example/tests/test_project.py | 187 ++++++++++++++++++
.../src/cone/example/tests/test_settings.py | 74 +++++++
.../src/cone/example/tests/test_wiki.py | 144 ++++++++++++++
12 files changed, 1212 insertions(+), 5 deletions(-)
create mode 100644 examples/cone.example/src/cone/example/tests/test_ajax.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_browser.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_document.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_layout.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_model.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_project.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_settings.py
create mode 100644 examples/cone.example/src/cone/example/tests/test_wiki.py
diff --git a/examples/cone.example/pyproject.toml b/examples/cone.example/pyproject.toml
index 70075843..e561dc11 100644
--- a/examples/cone.example/pyproject.toml
+++ b/examples/cone.example/pyproject.toml
@@ -12,5 +12,16 @@ dependencies = [
"pygments"
]
+[project.optional-dependencies]
+test = [
+ "pytest",
+ "zope.pytestlayer",
+]
+
[tool.setuptools.packages.find]
-where = ["src"]
\ No newline at end of file
+where = ["src"]
+
+[tool.pytest.ini_options]
+consider_namespace_packages = true
+addopts = ["--import-mode=importlib"]
+pythonpath = "src"
diff --git a/examples/cone.example/src/cone/example/browser/__init__.py b/examples/cone.example/src/cone/example/browser/__init__.py
index 5eaed573..ee84926b 100644
--- a/examples/cone.example/src/cone/example/browser/__init__.py
+++ b/examples/cone.example/src/cone/example/browser/__init__.py
@@ -234,9 +234,7 @@ def __init__(self, model=None, request=None):
@layout_config(AjaxPlayground)
class AjaxPlaygroundLayoutConfig(ExampleLayoutConfig):
- def __init__(self, model=None, request=None):
- super(AjaxPlaygroundLayoutConfig, self).__init__(model=model, request=request)
- self.sidebar_left = []
+ pass
###############################################################################
diff --git a/examples/cone.example/src/cone/example/browser/static/cone.example.css b/examples/cone.example/src/cone/example/browser/static/cone.example.css
index 114e50cf..a66bbabf 100644
--- a/examples/cone.example/src/cone/example/browser/static/cone.example.css
+++ b/examples/cone.example/src/cone/example/browser/static/cone.example.css
@@ -208,10 +208,12 @@ tr.state-public td.title a {
/* Pygments code highlighting */
.highlight {
padding: 0.75rem 1rem;
+ border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
+ box-shadow: inset 1px 1px 0px 0px #00000012;
font-size: 0.85rem;
overflow-x: auto;
}
.highlight pre {
margin: 0;
-}
+}
\ No newline at end of file
diff --git a/examples/cone.example/src/cone/example/tests/__init__.py b/examples/cone.example/src/cone/example/tests/__init__.py
index e69de29b..3d062221 100644
--- a/examples/cone.example/src/cone/example/tests/__init__.py
+++ b/examples/cone.example/src/cone/example/tests/__init__.py
@@ -0,0 +1,17 @@
+"""cone.example test suite.
+
+This package contains tests for the cone.example demonstration application.
+Tests are organized by module:
+
+- test_model.py: Base model classes (Translation, WorkflowNode, ContainerNode, etc.)
+- test_document.py: Document module (DocumentLibrary, DocumentFolder, Document)
+- test_project.py: Project module (ProjectBoard, Project, Task)
+- test_wiki.py: Wiki module (Wiki, WikiPage)
+- test_layout.py: Layout configuration and demo
+- test_settings.py: Settings module
+- test_browser.py: Main browser components (landing page, context menu)
+- test_ajax.py: AJAX playground demonstrations
+
+Run tests with pytest:
+ pytest examples/cone.example/src/cone/example/tests/
+"""
diff --git a/examples/cone.example/src/cone/example/tests/test_ajax.py b/examples/cone.example/src/cone/example/tests/test_ajax.py
new file mode 100644
index 00000000..0283017a
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_ajax.py
@@ -0,0 +1,118 @@
+from cone.app import get_root
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.ajax.browser import AjaxPlayground
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+
+
+class TestAjaxModel(TileTestCase):
+ layer = testing.security
+
+ def test_AjaxPlayground_node_info(self):
+ info = get_node_info('ajax_playground')
+ self.assertEqual(info.name, 'ajax_playground')
+ self.assertEqual(info.icon, 'bi-lightning')
+
+ def test_AjaxPlayground(self):
+ playground = AjaxPlayground()
+ playground.__name__ = 'ajax_playground'
+ # Properties
+ props = playground.properties
+ self.assertIsInstance(props, Properties)
+ self.assertTrue(props.in_navtree)
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ # Metadata
+ md = playground.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-lightning')
+
+
+class TestAjaxBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_ajax_playground_content_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'content')
+ self.assertIsNotNone(result)
+ # Content should contain AJAX demo elements
+ self.assertIn('ajax', result.lower())
+
+ def test_ajax_demo_content_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_demo_content')
+ self.assertIsNotNone(result)
+
+ def test_ajax_path_demo_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_path_demo')
+ # Result is empty string (tile triggers ajax_continue)
+ self.assertEqual(result, '')
+
+ def test_ajax_path_result_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_path_result')
+ self.assertIsNotNone(result)
+
+ def test_ajax_event_demo_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_event_demo')
+ self.assertEqual(result, '')
+
+ def test_ajax_message_demo_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_message_demo')
+ self.assertEqual(result, '')
+
+ def test_ajax_action_demo_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_action_demo')
+ self.assertEqual(result, '')
+
+ def test_ajax_combined_demo_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_combined_demo')
+ self.assertEqual(result, '')
+
+ def test_ajax_combined_result_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'ajax_combined_result')
+ self.assertIsNotNone(result)
+
+ def test_tutorial_content_tile(self):
+ root = get_root()
+ playground = root['ajax_playground']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(playground, request, 'tutorial_content')
+ self.assertIsNotNone(result)
diff --git a/examples/cone.example/src/cone/example/tests/test_browser.py b/examples/cone.example/src/cone/example/tests/test_browser.py
new file mode 100644
index 00000000..25546882
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_browser.py
@@ -0,0 +1,145 @@
+from cone.app import get_root
+from cone.app.browser import render_main_template
+from cone.example import testing
+from cone.example.browser import ExampleContextAction
+from cone.example.browser import ExampleToolsToolbar
+from cone.example.browser import LandingPage
+from cone.example.browser.utils import code_block
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+
+
+class TestBrowserResources(TileTestCase):
+ layer = testing.security
+
+ def test_code_block_python(self):
+ # code_block generates syntax-highlighted HTML
+ code = 'def hello():\n return "world"'
+ result = code_block(code, 'python')
+ # Result should contain highlight class
+ self.assertIn('highlight', result)
+ # Result should contain span elements for syntax
+ self.assertIn(' -1)
+
+ def test_render_main_template_unauthenticated(self):
+ root = get_root()
+ request = self.layer.new_request()
+ res = render_main_template(root, request)
+ # Unauthenticated users should not see mainmenu
+ self.assertFalse(res.text.find('id="mainmenu"') > -1)
+
+
+class TestEntryNodes(TileTestCase):
+ layer = testing.security
+
+ def test_entry_nodes_registered(self):
+ # Verify all entry nodes are registered in root
+ root = get_root()
+ # Layout demo
+ self.assertIn('layout', root)
+ # Documents
+ self.assertIn('documents', root)
+ # Projects
+ self.assertIn('projects', root)
+ # Wiki
+ self.assertIn('wiki', root)
+ # AJAX playground
+ self.assertIn('ajax_playground', root)
+ # Settings
+ self.assertIn('settings', root)
+
+ def test_entry_nodes_types(self):
+ from cone.example.document.model import DocumentLibrary
+ from cone.example.layout.model import LayoutDemo
+ from cone.example.project.model import ProjectBoard
+ from cone.example.wiki.model import Wiki
+ from cone.example.ajax.browser import AjaxPlayground
+ root = get_root()
+ self.assertIsInstance(root['layout'], LayoutDemo)
+ self.assertIsInstance(root['documents'], DocumentLibrary)
+ self.assertIsInstance(root['projects'], ProjectBoard)
+ self.assertIsInstance(root['wiki'], Wiki)
+ self.assertIsInstance(root['ajax_playground'], AjaxPlayground)
diff --git a/examples/cone.example/src/cone/example/tests/test_document.py b/examples/cone.example/src/cone/example/tests/test_document.py
new file mode 100644
index 00000000..42425794
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_document.py
@@ -0,0 +1,175 @@
+from cone.app import get_root
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.document.model import Document
+from cone.example.document.model import DocumentFolder
+from cone.example.document.model import DocumentLibrary
+from cone.example.model import Translation
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+from datetime import datetime
+
+
+class TestDocumentModel(TileTestCase):
+ layer = testing.security
+
+ def test_DocumentLibrary_node_info(self):
+ info = get_node_info('document_library')
+ self.assertEqual(info.name, 'document_library')
+ self.assertEqual(info.icon, 'bi-collection')
+ self.assertEqual(info.addables, ['document_folder', 'document'])
+
+ def test_DocumentLibrary(self):
+ library = DocumentLibrary()
+ library.__name__ = 'documents'
+ # Properties
+ props = library.properties
+ self.assertIsInstance(props, Properties)
+ self.assertFalse(props.mainmenu_display_children)
+ self.assertTrue(props.in_navtree)
+ self.assertEqual(props.default_content_tile, 'listing')
+ # Can add folders and documents
+ folder = DocumentFolder()
+ folder.__name__ = 'folder1'
+ library['folder1'] = folder
+ self.assertIn('folder1', library)
+
+ def test_DocumentFolder_node_info(self):
+ info = get_node_info('document_folder')
+ self.assertEqual(info.name, 'document_folder')
+ self.assertEqual(info.icon, 'bi-folder')
+ self.assertEqual(info.addables, ['document_folder', 'document'])
+
+ def test_DocumentFolder(self):
+ folder = DocumentFolder()
+ folder.__name__ = 'folder'
+ # Properties
+ props = folder.properties
+ self.assertTrue(props.action_delete)
+ self.assertTrue(props.in_navtree)
+ # Can nest folders
+ subfolder = DocumentFolder()
+ subfolder.__name__ = 'subfolder'
+ folder['subfolder'] = subfolder
+ self.assertIn('subfolder', folder)
+ # Can add documents
+ doc = Document()
+ doc.__name__ = 'doc'
+ folder['doc'] = doc
+ self.assertIn('doc', folder)
+
+ def test_Document_node_info(self):
+ info = get_node_info('document')
+ self.assertEqual(info.name, 'document')
+ self.assertEqual(info.icon, 'bi-file-earmark-text')
+ # Documents are leaf nodes - empty addables
+ self.assertEqual(info.addables, [])
+
+ def test_Document(self):
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ # Document implements INavigationLeaf
+ self.assertTrue(INavigationLeaf.providedBy(doc))
+ # Has workflow
+ self.assertEqual(doc.workflow_name, 'document_workflow')
+ # Properties
+ props = doc.properties
+ self.assertTrue(props.in_navtree)
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ self.assertTrue(props.action_edit)
+ self.assertTrue(props.action_delete)
+ self.assertTrue(props.action_sharing)
+ # role_inheritance enabled
+ self.assertTrue(doc.role_inheritance)
+ # principal_roles is empty dict
+ self.assertEqual(doc.principal_roles, {})
+
+ def test_Document_metadata(self):
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ # Set attributes
+ title = Translation()
+ title['en'] = 'Test Document'
+ doc.attrs['title'] = title
+ description = Translation()
+ description['en'] = 'A test document'
+ doc.attrs['description'] = description
+ doc.attrs['creator'] = 'testuser'
+ doc.attrs['created'] = datetime(2024, 1, 1, 12, 0, 0)
+ doc.attrs['modified'] = datetime(2024, 1, 2, 12, 0, 0)
+ # Get metadata
+ md = doc.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-file-earmark-text')
+ self.assertEqual(md.creator, 'testuser')
+ self.assertEqual(md.created, datetime(2024, 1, 1, 12, 0, 0))
+ self.assertEqual(md.modified, datetime(2024, 1, 2, 12, 0, 0))
+
+ def test_Document_protected_properties(self):
+ from cone.app.model import ProtectedProperties
+ root = get_root()
+ request = self.layer.new_request()
+ # Document has OwnerSupport which requires request context
+ with self.layer.authenticated('max'):
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ doc.__parent__ = root['documents']
+ doc.attrs['body'] = 'Document body content'
+ # protected_properties returns a ProtectedProperties instance
+ pp = doc.protected_properties
+ self.assertIsInstance(pp, ProtectedProperties)
+ # Body attribute exists on protected_properties
+ self.assertTrue(hasattr(pp, 'body'))
+
+ def test_Document_uuid(self):
+ # Document uses UUIDAttributeAware - UUID stored in attrs
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ # Access uuid triggers creation
+ uuid = doc.uuid
+ self.assertIsNotNone(uuid)
+ # UUID is stored in attrs
+ self.assertIn('uuid', doc.attrs)
+
+
+class TestDocumentBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_library_view_tile(self):
+ root = get_root()
+ library = root['documents']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(library, request, 'view')
+ # View tile should render without error
+ self.assertIsNotNone(result)
+
+ def test_document_content_tile(self):
+ root = get_root()
+ library = root['documents']
+ # Create a document
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ title = Translation()
+ title['en'] = 'Test Doc'
+ doc.attrs['title'] = title
+ library['testdoc'] = doc
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(doc, request, 'content')
+ self.assertIsNotNone(result)
+ # Clean up
+ del library['testdoc']
+
+ def test_tutorial_content_tile(self):
+ root = get_root()
+ library = root['documents']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(library, request, 'tutorial_content')
+ # Tutorial content should render code examples
+ self.assertIsNotNone(result)
diff --git a/examples/cone.example/src/cone/example/tests/test_layout.py b/examples/cone.example/src/cone/example/tests/test_layout.py
new file mode 100644
index 00000000..80216f8a
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_layout.py
@@ -0,0 +1,168 @@
+from cone.app import get_root
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.browser import DynamicLayoutConfig
+from cone.example.browser import ExampleLayoutConfig
+from cone.example.browser import LAYOUT_DEMO_DEFAULTS
+from cone.example.layout.model import LayoutDemo
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+
+
+class TestLayoutModel(TileTestCase):
+ layer = testing.security
+
+ def test_LayoutDemo_node_info(self):
+ info = get_node_info('layout_demo')
+ self.assertEqual(info.name, 'layout_demo')
+ self.assertEqual(info.icon, 'bi-layout-sidebar-inset-reverse')
+ # LayoutDemo is a leaf node - empty addables
+ self.assertEqual(info.addables, [])
+
+ def test_LayoutDemo(self):
+ demo = LayoutDemo()
+ demo.__name__ = 'layout'
+ # Properties
+ props = demo.properties
+ self.assertIsInstance(props, Properties)
+ self.assertTrue(props.in_navtree)
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ # Metadata
+ md = demo.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-layout-sidebar-inset-reverse')
+
+
+class TestLayoutConfig(TileTestCase):
+ layer = testing.security
+
+ def test_ExampleLayoutConfig(self):
+ config = ExampleLayoutConfig()
+ # Default sidebar configuration
+ self.assertEqual(config.sidebar_left, ['navtree'])
+ self.assertEqual(config.sidebar_right, ['tutorial'])
+ self.assertEqual(config.sidebar_right_min_width, 400)
+
+ def test_DynamicLayoutConfig_defaults(self):
+ # Without request, DynamicLayoutConfig uses defaults
+ config = DynamicLayoutConfig()
+ # These are set in __init__ only if request is provided
+ # Without request, values should be from parent class defaults
+ self.assertTrue(hasattr(config, 'mainmenu'))
+
+ def test_DynamicLayoutConfig_with_session(self):
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ # Set session values
+ request.session['layout.mainmenu'] = False
+ request.session['layout.livesearch'] = False
+ request.session['layout.pathbar'] = False
+ request.session['layout.sidebar_left'] = []
+ request.session['layout.sidebar_right'] = []
+ # Create config with request
+ config = DynamicLayoutConfig(request=request)
+ # Config should read from session
+ self.assertFalse(config.mainmenu)
+ self.assertFalse(config.livesearch)
+ self.assertFalse(config.pathbar)
+ self.assertEqual(config.sidebar_left, [])
+ self.assertEqual(config.sidebar_right, [])
+
+ def test_LAYOUT_DEMO_DEFAULTS(self):
+ # Verify default values
+ self.assertTrue(LAYOUT_DEMO_DEFAULTS['mainmenu'])
+ self.assertTrue(LAYOUT_DEMO_DEFAULTS['livesearch'])
+ self.assertTrue(LAYOUT_DEMO_DEFAULTS['personaltools'])
+ self.assertTrue(LAYOUT_DEMO_DEFAULTS['pathbar'])
+ self.assertEqual(LAYOUT_DEMO_DEFAULTS['sidebar_left'], ['navtree'])
+ self.assertFalse(LAYOUT_DEMO_DEFAULTS['sidebar_left_static'])
+ self.assertEqual(LAYOUT_DEMO_DEFAULTS['sidebar_left_min_width'], 150)
+ self.assertEqual(LAYOUT_DEMO_DEFAULTS['sidebar_right'], ['tutorial'])
+ self.assertFalse(LAYOUT_DEMO_DEFAULTS['sidebar_right_static'])
+ self.assertEqual(LAYOUT_DEMO_DEFAULTS['sidebar_right_min_width'], 150)
+ self.assertTrue(LAYOUT_DEMO_DEFAULTS['limit_content_width'])
+ self.assertFalse(LAYOUT_DEMO_DEFAULTS['center_content'])
+
+
+class TestLayoutBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_layout_demo_content_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(layout, request, 'content')
+ self.assertIsNotNone(result)
+
+ def test_toggle_layout_bool_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ request.params['setting'] = 'mainmenu'
+ with self.layer.authenticated('max'):
+ # Initial state from defaults
+ initial = request.session.get(
+ 'layout.mainmenu',
+ LAYOUT_DEMO_DEFAULTS['mainmenu']
+ )
+ # Toggle
+ result = render_tile(layout, request, 'toggle_layout_bool')
+ # State should be toggled
+ new_state = request.session.get('layout.mainmenu')
+ self.assertEqual(new_state, not initial)
+
+ def test_toggle_sidebar_tile_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ request.params['sidebar'] = 'left'
+ request.params['tile'] = 'navtree'
+ with self.layer.authenticated('max'):
+ # Initial state
+ initial = request.session.get(
+ 'layout.sidebar_left',
+ LAYOUT_DEMO_DEFAULTS['sidebar_left']
+ )
+ # Toggle
+ result = render_tile(layout, request, 'toggle_sidebar_tile')
+ # navtree should be removed
+ new_state = request.session.get('layout.sidebar_left', [])
+ self.assertNotIn('navtree', new_state)
+
+ def test_set_layout_number_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ request.params['setting'] = 'sidebar_left_min_width'
+ request.params['value'] = '200'
+ with self.layer.authenticated('max'):
+ result = render_tile(layout, request, 'set_layout_number')
+ # Value should be set
+ new_value = request.session.get('layout.sidebar_left_min_width')
+ self.assertEqual(new_value, 200)
+
+ def test_reset_layout_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ # Set some session values
+ request.session['layout.mainmenu'] = False
+ request.session['layout.pathbar'] = False
+ # Reset
+ result = render_tile(layout, request, 'reset_layout')
+ # Session should no longer have layout keys
+ self.assertNotIn('layout.mainmenu', request.session)
+ self.assertNotIn('layout.pathbar', request.session)
+
+ def test_tutorial_content_tile(self):
+ root = get_root()
+ layout = root['layout']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(layout, request, 'tutorial_content')
+ self.assertIsNotNone(result)
diff --git a/examples/cone.example/src/cone/example/tests/test_model.py b/examples/cone.example/src/cone/example/tests/test_model.py
new file mode 100644
index 00000000..6b085c10
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_model.py
@@ -0,0 +1,168 @@
+from cone.app import get_root
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.example import testing
+from cone.example.model import BaseContainer
+from cone.example.model import ContainerNode
+from cone.example.model import DEFAULT_EXAMPLE_ACL
+from cone.example.model import LiveSearch
+from cone.example.model import Translation
+from cone.example.model import WorkflowNode
+from node.tests import NodeTestCase
+from pyramid.security import ALL_PERMISSIONS
+
+
+class TestModel(NodeTestCase):
+ layer = testing.security
+
+ def test_DEFAULT_EXAMPLE_ACL(self):
+ # Check that ACL has expected structure
+ self.assertEqual(len(DEFAULT_EXAMPLE_ACL), 7)
+ # system.Authenticated gets view
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[0], (
+ 'Allow', 'system.Authenticated', ['view']
+ ))
+ # role:viewer gets view, list
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[1], (
+ 'Allow', 'role:viewer', ['view', 'list']
+ ))
+ # role:editor gets expanded permissions
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[2][0], 'Allow')
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[2][1], 'role:editor')
+ self.assertIn('add', DEFAULT_EXAMPLE_ACL[2][2])
+ self.assertIn('edit', DEFAULT_EXAMPLE_ACL[2][2])
+ # role:admin gets delete and manage_permissions
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[3][1], 'role:admin')
+ self.assertIn('delete', DEFAULT_EXAMPLE_ACL[3][2])
+ self.assertIn('manage_permissions', DEFAULT_EXAMPLE_ACL[3][2])
+ # role:manager gets manage
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[4][1], 'role:manager')
+ self.assertIn('manage', DEFAULT_EXAMPLE_ACL[4][2])
+ # Everyone gets login
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[5], (
+ 'Allow', 'system.Everyone', ['login']
+ ))
+ # Deny all other permissions
+ self.assertEqual(DEFAULT_EXAMPLE_ACL[6], (
+ 'Deny', 'system.Everyone', ALL_PERMISSIONS
+ ))
+
+ def test_Translation(self):
+ # Translation is a mapping-like object that stores values per language
+ trans = Translation()
+ # Set translations
+ trans['en'] = 'Hello'
+ trans['de'] = 'Hallo'
+ # Verify storage
+ self.assertEqual(trans['en'], 'Hello')
+ self.assertEqual(trans['de'], 'Hallo')
+ # Translation behaves like a mapping
+ self.assertIn('en', trans)
+ self.assertIn('de', trans)
+ self.assertEqual(len(trans), 2)
+
+ def test_WorkflowNode(self):
+ # WorkflowNode is base class for nodes with workflow support
+ # Note: WorkflowNode has workflow_name=None, which causes initialization
+ # to fail when trying to lookup workflow. In practice, subclasses
+ # always set workflow_name to a valid workflow name.
+ # Test the class attributes instead
+ self.assertIsNone(WorkflowNode.workflow_name)
+ self.assertEqual(WorkflowNode.default_acl, DEFAULT_EXAMPLE_ACL)
+ # Test with a concrete subclass that has a workflow (Document)
+ from cone.example.document.model import Document
+ doc = Document()
+ doc.__name__ = 'testdoc'
+ self.assertEqual(doc.workflow_name, 'document_workflow')
+ self.assertEqual(doc.default_acl, DEFAULT_EXAMPLE_ACL)
+ # Has attributes storage
+ doc.attrs['test'] = 'value'
+ self.assertEqual(doc.attrs['test'], 'value')
+
+ def test_ContainerNode(self):
+ # ContainerNode is base class without workflow
+ node = ContainerNode()
+ # Has default_acl
+ self.assertEqual(node.default_acl, DEFAULT_EXAMPLE_ACL)
+ # Can store children
+ child = ContainerNode()
+ node['child'] = child
+ self.assertIn('child', node)
+ # Has attributes
+ node.attrs['key'] = 'value'
+ self.assertEqual(node.attrs['key'], 'value')
+ # Supports ordering
+ node['second'] = ContainerNode()
+ self.assertEqual(list(node.keys()), ['child', 'second'])
+
+ def test_BaseContainer(self):
+ # BaseContainer adds PrincipalACL and CopySupport
+ node = BaseContainer()
+ # Check role_inheritance
+ self.assertTrue(node.role_inheritance)
+ # principal_roles is empty dict by default
+ self.assertEqual(node.principal_roles, {})
+ # Has properties
+ props = node.properties
+ self.assertIsInstance(props, Properties)
+ self.assertTrue(props.in_navtree)
+ self.assertEqual(props.default_content_tile, 'listing')
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ self.assertTrue(props.action_edit)
+ self.assertTrue(props.action_list)
+ self.assertTrue(props.action_sharing)
+ self.assertTrue(props.action_move)
+ self.assertTrue(props.action_add)
+
+ def test_BaseContainer_metadata(self):
+ # Test metadata property of BaseContainer
+ node = BaseContainer()
+ node.__name__ = 'testnode'
+ # Set title and description translations
+ title = Translation()
+ title['en'] = 'Test Title'
+ node.attrs['title'] = title
+ description = Translation()
+ description['en'] = 'Test Description'
+ node.attrs['description'] = description
+ node.attrs['creator'] = 'testuser'
+ # Get metadata
+ md = node.metadata
+ self.assertIsInstance(md, Metadata)
+ # Without request, title.value may not resolve, but structure exists
+ self.assertIsNotNone(md.creator)
+ self.assertEqual(md.creator, 'testuser')
+
+ def test_LiveSearch(self):
+ # LiveSearch adapter searches child nodes
+ root = get_root()
+ # Create a container with children
+ container = BaseContainer()
+ container.__name__ = 'container'
+ container.__parent__ = root
+ # Add child with title
+ child = BaseContainer()
+ child.__name__ = 'child'
+ child.__parent__ = container
+ title = Translation()
+ title['en'] = 'Searchable Title'
+ child.attrs['title'] = title
+ container['child'] = child
+ # Create search adapter
+ adapter = LiveSearch(container)
+ self.assertIs(adapter.model, container)
+ # Search requires request
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ results = adapter.search(request, 'Searchable')
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['value'], 'Searchable Title')
+ # Search is case-insensitive
+ with self.layer.authenticated('max'):
+ results = adapter.search(request, 'searchable')
+ self.assertEqual(len(results), 1)
+ # No match returns empty
+ with self.layer.authenticated('max'):
+ results = adapter.search(request, 'nonexistent')
+ self.assertEqual(len(results), 0)
diff --git a/examples/cone.example/src/cone/example/tests/test_project.py b/examples/cone.example/src/cone/example/tests/test_project.py
new file mode 100644
index 00000000..5e0c4c87
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_project.py
@@ -0,0 +1,187 @@
+from cone.app import get_root
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.model import Translation
+from cone.example.project.model import Project
+from cone.example.project.model import ProjectBoard
+from cone.example.project.model import Task
+from cone.example.project.model import TaskData
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+from datetime import datetime
+
+
+class TestProjectModel(TileTestCase):
+ layer = testing.security
+
+ def test_ProjectBoard_node_info(self):
+ info = get_node_info('project_board')
+ self.assertEqual(info.name, 'project_board')
+ self.assertEqual(info.icon, 'bi-kanban')
+ self.assertEqual(info.addables, ['project'])
+
+ def test_ProjectBoard(self):
+ board = ProjectBoard()
+ board.__name__ = 'projects'
+ # ProjectBoard is a FactoryNode
+ # Properties
+ props = board.properties
+ self.assertIsInstance(props, Properties)
+ self.assertTrue(props.in_navtree)
+ self.assertEqual(props.default_content_tile, 'listing')
+ self.assertFalse(props.action_edit)
+ self.assertTrue(props.action_add)
+ # Metadata
+ md = board.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-kanban')
+
+ def test_ProjectBoard_factories(self):
+ # ProjectBoard.factories contains factory callables
+ # In the example, these are populated by populate_projects()
+ root = get_root()
+ board = root['projects']
+ # Board should exist and be a ProjectBoard
+ self.assertIsInstance(board, ProjectBoard)
+ # After population, factories may contain entries
+ # (depends on whether populate was called)
+ self.assertIsInstance(board.factories, dict)
+
+ def test_Project_node_info(self):
+ info = get_node_info('project')
+ self.assertEqual(info.name, 'project')
+ self.assertEqual(info.icon, 'bi-clipboard')
+ self.assertEqual(info.addables, ['task'])
+
+ def test_Project(self):
+ project = Project()
+ project.__name__ = 'testproject'
+ # Has categories
+ self.assertEqual(len(project.categories), 2)
+ # role_inheritance enabled
+ self.assertTrue(project.role_inheritance)
+ # principal_roles is empty dict
+ self.assertEqual(project.principal_roles, {})
+ # Properties
+ props = project.properties
+ self.assertTrue(props.in_navtree)
+ self.assertEqual(props.default_content_tile, 'listing')
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ self.assertTrue(props.action_edit)
+ self.assertTrue(props.action_delete)
+ self.assertTrue(props.action_list)
+ self.assertTrue(props.action_sharing)
+ self.assertTrue(props.action_add)
+
+ def test_Project_metadata(self):
+ project = Project()
+ project.__name__ = 'testproject'
+ # Set attributes
+ title = Translation()
+ title['en'] = 'Test Project'
+ project.attrs['title'] = title
+ description = Translation()
+ description['en'] = 'A test project'
+ project.attrs['description'] = description
+ project.attrs['creator'] = 'testuser'
+ project.attrs['created'] = datetime(2024, 1, 1, 12, 0, 0)
+ # Get metadata
+ md = project.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-clipboard')
+ self.assertEqual(md.creator, 'testuser')
+
+ def test_Project_uuid(self):
+ # Project uses NamespaceUUID - UUID calculated from path + namespace
+ project = Project()
+ project.__name__ = 'testproject'
+ uuid = project.uuid
+ self.assertIsNotNone(uuid)
+
+ def test_TaskData(self):
+ # TaskData is the internal data model wrapped by Task
+ data = TaskData()
+ # Has attributes storage
+ data.attrs['test'] = 'value'
+ self.assertEqual(data.attrs['test'], 'value')
+
+ def test_Task_node_info(self):
+ info = get_node_info('task')
+ self.assertEqual(info.name, 'task')
+ self.assertEqual(info.icon, 'bi-check2-square')
+ # Tasks are leaf nodes - empty addables
+ self.assertEqual(info.addables, [])
+
+ def test_Task(self):
+ # Task wraps TaskData via AdapterNode
+ data = TaskData()
+ task = Task(model=data)
+ task.__name__ = 'testtask'
+ # Task implements INavigationLeaf
+ self.assertTrue(INavigationLeaf.providedBy(task))
+ # Has workflow
+ self.assertEqual(task.workflow_name, 'task_workflow')
+ # Has categories
+ self.assertEqual(len(task.categories), 2)
+ # role_inheritance enabled
+ self.assertTrue(task.role_inheritance)
+ # Properties
+ props = task.properties
+ self.assertTrue(props.in_navtree)
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ self.assertTrue(props.action_edit)
+ self.assertTrue(props.action_delete)
+ self.assertTrue(props.action_sharing)
+
+ def test_Task_metadata(self):
+ data = TaskData()
+ task = Task(model=data)
+ task.__name__ = 'testtask'
+ # Set attributes via task (proxied to data)
+ title = Translation()
+ title['en'] = 'Test Task'
+ task.attrs['title'] = title
+ description = Translation()
+ description['en'] = 'A test task'
+ task.attrs['description'] = description
+ task.attrs['creator'] = 'testuser'
+ # Get metadata
+ md = task.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-check2-square')
+ self.assertEqual(md.creator, 'testuser')
+
+ def test_Task_uuid_as_name(self):
+ # Task uses UUIDAsName - __name__ is UUID string
+ data = TaskData()
+ task = Task(model=data)
+ # After UUID is set, name becomes UUID string
+ uuid = task.uuid
+ self.assertIsNotNone(uuid)
+ # The task's name should be the UUID string
+ self.assertEqual(task.__name__, str(uuid))
+
+
+class TestProjectBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_board_view_tile(self):
+ root = get_root()
+ board = root['projects']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(board, request, 'view')
+ self.assertIsNotNone(result)
+
+ def test_tutorial_content_tile(self):
+ root = get_root()
+ board = root['projects']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(board, request, 'tutorial_content')
+ self.assertIsNotNone(result)
diff --git a/examples/cone.example/src/cone/example/tests/test_settings.py b/examples/cone.example/src/cone/example/tests/test_settings.py
new file mode 100644
index 00000000..4299fc37
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_settings.py
@@ -0,0 +1,74 @@
+from cone.app import get_root
+from cone.app.model import Metadata
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.settings.model import ExampleSettings
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+import os
+import shutil
+import tempfile
+
+
+class TestSettingsModel(TileTestCase):
+ layer = testing.security
+
+ def test_ExampleSettings_node_info(self):
+ info = get_node_info('example_settings')
+ self.assertEqual(info.name, 'example_settings')
+ self.assertEqual(info.icon, 'bi-sliders')
+
+ def test_ExampleSettings(self):
+ settings = ExampleSettings()
+ settings.__name__ = 'example_settings'
+ # Has category
+ self.assertIsNotNone(settings.category)
+ # Metadata
+ md = settings.metadata
+ self.assertIsInstance(md, Metadata)
+
+ def test_ExampleSettings_config_properties(self):
+ # Set up temp directory for config
+ config_dir = tempfile.mkdtemp()
+ os.environ['CONE_EXAMPLE_CONFIG_DIR'] = config_dir
+ try:
+ settings = ExampleSettings()
+ settings.__name__ = 'example_settings'
+ # Get config properties
+ props = settings.config_properties
+ # Check default values
+ self.assertEqual(props.items_per_page, '15')
+ self.assertEqual(props.enable_notifications, 'true')
+ self.assertEqual(props.default_language, 'en')
+ # Modify a value and save
+ props.items_per_page = '20'
+ props() # Save to file
+ # Verify the config file was written
+ config_path = os.path.join(config_dir, 'example_settings.cfg')
+ self.assertTrue(os.path.exists(config_path))
+ finally:
+ # Clean up
+ del os.environ['CONE_EXAMPLE_CONFIG_DIR']
+ shutil.rmtree(config_dir)
+
+
+class TestSettingsBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_settings_registered(self):
+ root = get_root()
+ settings_root = root['settings']
+ # ExampleSettings should be registered
+ self.assertIn('example_settings', settings_root)
+ settings = settings_root['example_settings']
+ self.assertIsInstance(settings, ExampleSettings)
+
+ def test_settings_display(self):
+ root = get_root()
+ settings = root['settings']['example_settings']
+ # Settings display requires manager permission
+ request = self.layer.new_request()
+ self.assertFalse(settings.display)
+ with self.layer.authenticated('manager'):
+ # With manager auth, display should be True
+ self.assertTrue(settings.display)
diff --git a/examples/cone.example/src/cone/example/tests/test_wiki.py b/examples/cone.example/src/cone/example/tests/test_wiki.py
new file mode 100644
index 00000000..28620404
--- /dev/null
+++ b/examples/cone.example/src/cone/example/tests/test_wiki.py
@@ -0,0 +1,144 @@
+from cone.app import get_root
+from cone.app.interfaces import INavigationLeaf
+from cone.app.model import Metadata
+from cone.app.model import Properties
+from cone.app.model import get_node_info
+from cone.example import testing
+from cone.example.model import Translation
+from cone.example.wiki.model import Wiki
+from cone.example.wiki.model import WikiPage
+from cone.tile import render_tile
+from cone.tile.tests import TileTestCase
+from datetime import datetime
+
+
+class TestWikiModel(TileTestCase):
+ layer = testing.security
+
+ def test_Wiki_node_info(self):
+ info = get_node_info('wiki')
+ self.assertEqual(info.name, 'wiki')
+ self.assertEqual(info.icon, 'bi-book')
+ self.assertEqual(info.addables, ['wiki_page'])
+
+ def test_Wiki(self):
+ wiki = Wiki()
+ wiki.__name__ = 'wiki'
+ # Properties
+ props = wiki.properties
+ self.assertIsInstance(props, Properties)
+ self.assertFalse(props.mainmenu_display_children)
+ self.assertTrue(props.in_navtree)
+ self.assertEqual(props.default_content_tile, 'listing')
+ # Can add wiki pages
+ page = WikiPage()
+ page.__name__ = 'page1'
+ wiki['page1'] = page
+ self.assertIn('page1', wiki)
+
+ def test_WikiPage_node_info(self):
+ info = get_node_info('wiki_page')
+ self.assertEqual(info.name, 'wiki_page')
+ self.assertEqual(info.icon, 'bi-journal-text')
+ # Wiki pages are leaf nodes - empty addables
+ self.assertEqual(info.addables, [])
+
+ def test_WikiPage(self):
+ page = WikiPage()
+ page.__name__ = 'testpage'
+ # WikiPage implements INavigationLeaf
+ self.assertTrue(INavigationLeaf.providedBy(page))
+ # Has categories
+ self.assertEqual(len(page.categories), 3)
+ # role_inheritance enabled
+ self.assertTrue(page.role_inheritance)
+ # principal_roles is empty dict
+ self.assertEqual(page.principal_roles, {})
+ # Properties
+ props = page.properties
+ self.assertTrue(props.in_navtree)
+ self.assertTrue(props.action_up)
+ self.assertTrue(props.action_view)
+ self.assertTrue(props.action_edit)
+ self.assertTrue(props.action_delete)
+ self.assertTrue(props.action_sharing)
+
+ def test_WikiPage_metadata(self):
+ page = WikiPage()
+ page.__name__ = 'testpage'
+ # Set attributes
+ title = Translation()
+ title['en'] = 'Test Page'
+ page.attrs['title'] = title
+ description = Translation()
+ description['en'] = 'A test wiki page'
+ page.attrs['description'] = description
+ page.attrs['creator'] = 'testuser'
+ page.attrs['created'] = datetime(2024, 1, 1, 12, 0, 0)
+ page.attrs['modified'] = datetime(2024, 1, 2, 12, 0, 0)
+ # Get metadata
+ md = page.metadata
+ self.assertIsInstance(md, Metadata)
+ self.assertEqual(md.icon, 'bi-journal-text')
+ self.assertEqual(md.creator, 'testuser')
+ self.assertEqual(md.created, datetime(2024, 1, 1, 12, 0, 0))
+
+ def test_WikiPage_uuid(self):
+ # WikiPage uses UUIDAttributeAware - UUID stored in attrs
+ page = WikiPage()
+ page.__name__ = 'testpage'
+ uuid = page.uuid
+ self.assertIsNotNone(uuid)
+ # UUID is stored in attrs
+ self.assertIn('uuid', page.attrs)
+
+ def test_WikiPage_references(self):
+ # WikiPage can store references to other pages
+ page = WikiPage()
+ page.__name__ = 'testpage'
+ # References are stored as a list of UUIDs
+ page.attrs['references'] = []
+ self.assertEqual(page.attrs['references'], [])
+ # Add a reference UUID
+ import uuid
+ ref_uuid = str(uuid.uuid4())
+ page.attrs['references'].append(ref_uuid)
+ self.assertEqual(len(page.attrs['references']), 1)
+
+
+class TestWikiBrowser(TileTestCase):
+ layer = testing.security
+
+ def test_wiki_view_tile(self):
+ root = get_root()
+ wiki = root['wiki']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(wiki, request, 'view')
+ self.assertIsNotNone(result)
+
+ def test_wiki_page_content_tile(self):
+ root = get_root()
+ wiki = root['wiki']
+ # Create a wiki page
+ page = WikiPage()
+ page.__name__ = 'testpage'
+ title = Translation()
+ title['en'] = 'Test Page'
+ page.attrs['title'] = title
+ page.attrs['references'] = []
+ wiki['testpage'] = page
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(page, request, 'content')
+ self.assertIsNotNone(result)
+ # Clean up
+ del wiki['testpage']
+
+ def test_tutorial_content_tile(self):
+ root = get_root()
+ wiki = root['wiki']
+ request = self.layer.new_request()
+ with self.layer.authenticated('max'):
+ result = render_tile(wiki, request, 'tutorial_content')
+ self.assertIsNotNone(result)
From 60aee31bc31f2c252061bb3f58c65954abe73e20 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Fri, 30 Jan 2026 13:33:41 +0100
Subject: [PATCH 20/25] Fix ExampleSettingsForm: Add missing EditFormTarget
---
.../src/cone/example/settings/browser.py | 3 ++-
.../src/cone/example/tests/test_settings.py | 13 +++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/examples/cone.example/src/cone/example/settings/browser.py b/examples/cone.example/src/cone/example/settings/browser.py
index 54ab85e8..19deaf74 100644
--- a/examples/cone.example/src/cone/example/settings/browser.py
+++ b/examples/cone.example/src/cone/example/settings/browser.py
@@ -1,3 +1,4 @@
+from cone.app.browser.form import EditFormTarget
from cone.app.browser.form import Form
from cone.app.browser.settings import SettingsForm
from cone.app.browser.settings import settings_form
@@ -8,7 +9,7 @@
@settings_form(interface=ExampleSettings)
-@plumbing(SettingsForm)
+@plumbing(SettingsForm, EditFormTarget)
class ExampleSettingsForm(Form):
"""Settings form demonstrating the settings_form decorator.
diff --git a/examples/cone.example/src/cone/example/tests/test_settings.py b/examples/cone.example/src/cone/example/tests/test_settings.py
index 4299fc37..7d5f2947 100644
--- a/examples/cone.example/src/cone/example/tests/test_settings.py
+++ b/examples/cone.example/src/cone/example/tests/test_settings.py
@@ -72,3 +72,16 @@ def test_settings_display(self):
with self.layer.authenticated('manager'):
# With manager auth, display should be True
self.assertTrue(settings.display)
+
+ def test_settings_form_tile(self):
+ root = get_root()
+ settings = root['settings']['example_settings']
+ request = self.layer.new_request()
+ # Settings form requires manager permission
+ with self.layer.authenticated('manager'):
+ result = render_tile(settings, request, 'editform')
+ self.assertIsNotNone(result)
+ # Form should contain expected fields
+ self.assertIn('items_per_page', result)
+ self.assertIn('enable_notifications', result)
+ self.assertIn('default_language', result)
From cc68f880a5e4a9ee4c6f9f0cba90bfaabc21e03c Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Fri, 30 Jan 2026 13:57:37 +0100
Subject: [PATCH 21/25] fix python tests
---
src/cone/app/model.py | 6 +++---
src/cone/app/tests/test_browser_ajax.py | 6 ++++--
src/cone/app/tests/test_browser_sharing.py | 9 ++++++---
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/cone/app/model.py b/src/cone/app/model.py
index 6a41894c..f19fdf6f 100644
--- a/src/cone/app/model.py
+++ b/src/cone/app/model.py
@@ -158,9 +158,9 @@ def properties(self):
@instance_property
def metadata(self):
metadata = Metadata()
- nodeinfo = self.nodeinfo
- if nodeinfo.title:
- metadata.title = nodeinfo.title
+ info = get_node_info(self.node_info_name)
+ if info and info.title:
+ metadata.title = info.title
elif self.name:
metadata.title = self.name
else:
diff --git a/src/cone/app/tests/test_browser_ajax.py b/src/cone/app/tests/test_browser_ajax.py
index 8f948748..1919d3ae 100644
--- a/src/cone/app/tests/test_browser_ajax.py
+++ b/src/cone/app/tests/test_browser_ajax.py
@@ -163,7 +163,8 @@ def test_AjaxMessage(self):
'type': 'message',
'payload': payload,
'flavor': flavor,
- 'selector': selector
+ 'selector': selector,
+ 'title': None
})
def test_AjaxOverlay(self):
@@ -388,7 +389,8 @@ def test_AjaxFormContinue(self):
'payload': 'Some info message',
'flavor': 'info',
'selector': 'None',
- 'css': ''
+ 'css': '',
+ 'title': None
}, {
'type': 'overlay',
'action': 'someaction',
diff --git a/src/cone/app/tests/test_browser_sharing.py b/src/cone/app/tests/test_browser_sharing.py
index 606f0aa5..456b0854 100644
--- a/src/cone/app/tests/test_browser_sharing.py
+++ b/src/cone/app/tests/test_browser_sharing.py
@@ -212,7 +212,8 @@ def test_add_role(self):
'flavor': 'error',
'type': 'message',
'payload': u"Can not add role 'manager' for principal 'viewer'",
- 'selector': None
+ 'selector': None,
+ 'title': None
}],
'payload': u'',
'mode': 'NONE',
@@ -280,7 +281,8 @@ def test_remove_role(self):
'flavor': 'error',
'type': 'message',
'payload': u"Can not remove role 'inexistent' for principal 'viewer'",
- 'selector': None
+ 'selector': None,
+ 'title': None
}],
'payload': u'',
'mode': 'NONE',
@@ -302,7 +304,8 @@ def test_remove_role(self):
'flavor': 'error',
'type': 'message',
'payload': u"Can not remove role 'manager' for principal 'foo'",
- 'selector': None
+ 'selector': None,
+ 'title': None
}],
'payload': u'',
'mode': 'NONE',
From bc6ad8fc21029fef3c1cb07402d1a3000edebfc2 Mon Sep 17 00:00:00 2001
From: Lena Daxenbichler
Date: Mon, 2 Feb 2026 11:22:53 +0100
Subject: [PATCH 22/25] collapse static sidebars by default. add
limit_page_width flag.
---
js/src/sidebar.js | 18 ++++++--
src/cone/app/__init__.py | 1 +
src/cone/app/browser/layout.py | 6 +--
src/cone/app/browser/static/cone/cone.app.js | 18 ++++++--
.../app/browser/static/cone/cone.app.min.js | 2 +-
src/cone/app/browser/templates/layout.pt | 16 ++++---
src/cone/app/browser/templates/pathbar.pt | 42 +++++++++++--------
src/cone/app/interfaces.py | 1 +
src/cone/app/tests/test_app.py | 1 +
9 files changed, 70 insertions(+), 35 deletions(-)
diff --git a/js/src/sidebar.js b/js/src/sidebar.js
index bba71a00..0d98b68e 100644
--- a/js/src/sidebar.js
+++ b/js/src/sidebar.js
@@ -101,6 +101,8 @@ export class Sidebar extends ResizeAware(ts.Motion) {
const width = Math.max(this.min_width, this.sidebar_width);
elem.css('width', width + 'px');
+ const static_data = this.elem.data('static');
+ this.static = static_data === true || static_data === 'True';
this.moving = false;
this.trigger_event = this.trigger_event.bind(this);
this.scrollbar = ts.query_elem('.scrollable-y', elem).data('scrollbar');
@@ -199,12 +201,20 @@ export class Sidebar extends ResizeAware(ts.Motion) {
this.elem.removeClass('collapsed');
this.elem.removeClass('expanded');
}
- if (this.collapsed) {
+ if (this.static) {
+ if (!this.collapsed) {
+ this.collapse();
+ }
this.elem.removeClass('responsive-expanded');
- this.elem.addClass('responsive-collapsed');
- } else {
- this.elem.addClass('responsive-expanded');
this.elem.removeClass('responsive-collapsed');
+ } else {
+ if (this.collapsed) {
+ this.elem.removeClass('responsive-expanded');
+ this.elem.addClass('responsive-collapsed');
+ } else {
+ this.elem.addClass('responsive-expanded');
+ this.elem.removeClass('responsive-collapsed');
+ }
}
if (this.collapsed !== this.responsive_collapsed) {
diff --git a/src/cone/app/__init__.py b/src/cone/app/__init__.py
index 9f1e9c34..d1df0328 100644
--- a/src/cone/app/__init__.py
+++ b/src/cone/app/__init__.py
@@ -71,6 +71,7 @@ def __init__(self, model=None, request=None):
self.livesearch = True
self.personaltools = True
self.limit_content_width = True
+ self.limit_page_width = False
self.center_content = False
self.pathbar = True
self.sidebar_left_mode = 'stacked' # 'toggle' or 'stacked'
diff --git a/src/cone/app/browser/layout.py b/src/cone/app/browser/layout.py
index 278ce73d..7725f129 100644
--- a/src/cone/app/browser/layout.py
+++ b/src/cone/app/browser/layout.py
@@ -19,16 +19,16 @@
from cone.app.utils import node_path
from cone.tile import render_template
from cone.tile import render_tile
-from cone.tile import Tile
from cone.tile import tile
+from cone.tile import Tile
from node.utils import LocationIterator
from node.utils import safe_decode
from odict import odict
from pyramid.i18n import get_localizer
from pyramid.i18n import negotiate_locale_name
from pyramid.i18n import TranslationStringFactory
-import warnings
import json
+import warnings
_ = TranslationStringFactory('cone.app')
@@ -285,7 +285,7 @@ def create_item(self, node, empty_title, selected):
path='templates/pathbar.pt',
permission='view',
strict=False)
-class PathBar(Tile):
+class PathBar(LayoutConfigTile):
@property
def items(self):
diff --git a/src/cone/app/browser/static/cone/cone.app.js b/src/cone/app/browser/static/cone/cone.app.js
index 73ecee40..137cdfdd 100644
--- a/src/cone/app/browser/static/cone/cone.app.js
+++ b/src/cone/app/browser/static/cone/cone.app.js
@@ -1120,6 +1120,8 @@ var cone = (function (exports, $, ts) {
this.min_width = elem.data('min-width') || 115;
const width = Math.max(this.min_width, this.sidebar_width);
elem.css('width', width + 'px');
+ const static_data = this.elem.data('static');
+ this.static = static_data === true || static_data === 'True';
this.moving = false;
this.trigger_event = this.trigger_event.bind(this);
this.scrollbar = ts.query_elem('.scrollable-y', elem).data('scrollbar');
@@ -1187,12 +1189,20 @@ var cone = (function (exports, $, ts) {
this.elem.removeClass('collapsed');
this.elem.removeClass('expanded');
}
- if (this.collapsed) {
+ if (this.static) {
+ if (!this.collapsed) {
+ this.collapse();
+ }
this.elem.removeClass('responsive-expanded');
- this.elem.addClass('responsive-collapsed');
- } else {
- this.elem.addClass('responsive-expanded');
this.elem.removeClass('responsive-collapsed');
+ } else {
+ if (this.collapsed) {
+ this.elem.removeClass('responsive-expanded');
+ this.elem.addClass('responsive-collapsed');
+ } else {
+ this.elem.addClass('responsive-expanded');
+ this.elem.removeClass('responsive-collapsed');
+ }
}
if (this.collapsed !== this.responsive_collapsed) {
this.responsive_collapsed = this.collapsed;
diff --git a/src/cone/app/browser/static/cone/cone.app.min.js b/src/cone/app/browser/static/cone/cone.app.min.js
index 1fe835c5..1899509b 100644
--- a/src/cone/app/browser/static/cone/cone.app.min.js
+++ b/src/cone/app/browser/static/cone/cone.app.min.js
@@ -1 +1 @@
-var cone=function(e,t,s){"use strict";class i{constructor(e,t){this.elem=e,this.name=t}set_filter(e){let t=this.elem,i=s.ajax.parse_target(t.attr("ajax:target")),o=t.attr("ajax:event");if(i.params[this.name]=e,t.attr("ajax:path")){let l=t.attr("ajax:path-event");l||(l=o),s.ajax.path({path:i.path+i.query+"&"+this.name+"="+e,event:l,target:i})}let l=o.split(":");s.ajax.trigger({name:l[0],selector:l[1],target:i})}}class o extends i{static initialize(e,s=".batched_items_slice_size select"){t(s,e).each(function(){new o(t(this))})}constructor(e){super(e,"size"),e.off("change").on("change",this.change_handle.bind(this))}change_handle(e){let s=t("option:selected",this.elem).first();this.set_filter(s.val())}}class l extends i{static initialize(e,s=".batched_items_filter input",i="term"){t(s,e).each(function(){new l(t(this),i)})}constructor(e,t){super(e,t),e.off("focus").on("focus",this.focus_handle.bind(this)),e.off("keypress").on("keypress",this.keypress_handle.bind(this)),e.off("keyup").on("keyup",this.keyup_handle.bind(this)),e.off("change").on("change",this.change_handle.bind(this))}focus_handle(e){let t=this.elem;t.hasClass("empty_filter")&&(t.val(""),t.removeClass("empty_filter"))}keypress_handle(e){13==e.keyCode&&e.preventDefault()}keyup_handle(e){13==e.keyCode&&(e.preventDefault(),this.set_filter(this.elem.val()))}change_handle(e){e.preventDefault(),this.set_filter(this.elem.val())}}class r{static get media_query(){return window.matchMedia("(prefers-color-scheme: dark)")}static get stored_theme(){return localStorage.getItem("cone-app-color-theme")}static set stored_theme(e){localStorage.setItem("cone-app-color-theme",e)}static get preferred_theme(){return this.stored_theme?this.stored_theme:this.media_query.matches?"dark":"light"}static watch(e){this.media_query.addEventListener("change",e)}static set_theme(e){const t=document.documentElement;"auto"===e&&this.media_query.matches?t.setAttribute("data-bs-theme","dark"):t.setAttribute("data-bs-theme",e)}constructor(){r.bind(),r.set_theme(r.preferred_theme)}static bind(){this.boundCallback=this.callback.bind(this),this.watch(this.boundCallback)}static callback(){const e=this.stored_theme;"light"!==e&&"dark"!==e&&r.set_theme(r.preferred_theme)}static unbind(){this.boundCallback&&(this.media_query.removeEventListener("change",this.boundCallback),document.documentElement.removeAttribute("data-bs-theme"))}}class a extends s.ChangeListener{static initialize(e){const t=s.query_elem("#colortoggle-switch",e);t&&new a(t)}constructor(e){super({elem:e}),this.update=this.update.bind(this),this.update(),r.watch(this.update())}update(){const e=r.preferred_theme,t=this.elem,s=t.is(":checked");"dark"!==e||s?"light"===e&&s&&t.prop("checked",!1):t.prop("checked",!0)}on_change(){const e=this.elem.is(":checked")?"dark":"light";r.set_theme(e),r.stored_theme=e}}class n{static initialize(e){new n(e)}constructor(e){this.cut_cookie="cone.app.copysupport.cut",this.copy_cookie="cone.app.copysupport.copy",this.context=e,this.paste_action=t("a#toolbaraction-paste",e),this.paste_action.off("click").on("click",this.handle_paste.bind(this)),this.copyable=t("table tr.selectable.copysupportitem",e),this.copyable.length&&(this.cut_action=t("a#toolbaraction-cut",e),this.cut_action.off("click").on("click",this.handle_cut.bind(this)),this.copy_action=t("a#toolbaraction-copy",e),this.copy_action.off("click").on("click",this.handle_copy.bind(this)),this.selectable=this.copyable.selectable({on_firstclick:this.on_firstclick.bind(this),on_select:this.on_select.bind(this)}).data("selectable"),this.read_selected_from_cookie(this.cut_cookie,"copysupport_cut"),this.read_selected_from_cookie(this.copy_cookie,""))}on_firstclick(e,t){}on_select(e){}write_selected_to_cookie(e){let i=t(this.selectable.selected),o=new Array;i.each(function(){o.push(t(this).attr("ajax:target"))});let l=o.join("::");s.create_cookie(e,l),l.length?t(this.paste_action).removeClass("disabled"):t(this.paste_action).addClass("disabled")}read_selected_from_cookie(e,i){let o=s.read_cookie(e);if(!o)return;let l,r,a=o.split("::"),n=this;t("table tr.selectable",this.context).each(function(){l=t(this),r=l.attr("ajax:target");for(let e in a)if(a[e]==r){l.addClass("selected"),i&&l.addClass(i),n.selectable.add(l.get(0));break}})}handle_cut(e){e.preventDefault(),s.create_cookie(this.copy_cookie,"",0),this.write_selected_to_cookie(this.cut_cookie),this.copyable.removeClass("copysupport_cut"),t(this.selectable.selected).addClass("copysupport_cut")}handle_copy(e){e.preventDefault(),s.create_cookie(this.cut_cookie,"",0),this.write_selected_to_cookie(this.copy_cookie),this.copyable.removeClass("copysupport_cut")}handle_paste(e){e.preventDefault();let i=t(e.currentTarget);if(i.hasClass("disabled"))return;let o=s.ajax.parse_target(i.attr("ajax:target"));s.ajax.action({name:"paste",mode:"NONE",selector:"NONE",url:o.url,params:o.params})}}let h={shift_down:!1,ctrl_down:!1};class c{constructor(){t(window).on("keydown",this.key_down.bind(this)),t(window).on("keyup",this.key_up.bind(this))}key_down(e){switch(e.keyCode||e.which){case 16:h.shift_down=!0;break;case 17:h.ctrl_down=!0}}key_up(e){switch(e.keyCode||e.which){case 16:h.shift_down=!1;break;case 17:h.ctrl_down=!1}}}class d{static initialize(e,t=null){const i=s.query_elem("input#search-text",e);i&&(null===t&&(t=cone.LiveSearch),new t(i))}constructor(e){this.elem=e,this.target=`${e.data("search-target")}/livesearch`,this.content=t("#content"),this.result=null,this._term="",this._minlen=3,this._delay=250,this._timeout_event=null,this._in_progress=!1,this.on_keydown=this.on_keydown.bind(this),this.on_change=this.on_change.bind(this),this.on_result=this.on_result.bind(this),e.on("keydown",this.on_keydown),e.on("change",this.on_change)}search(){this._in_progress=!0,s.http_request({url:this.target,params:{term:this._term},type:"json",success:this.on_result}),this._in_progress=!1}render_no_results(){s.compile_template(this,'\n No search results \n ',this.result)}render_suggestion(e){s.compile_template(this,`\n \n
\n
\n ${void 0===e.description?"":e.description}\n
\n
\n `,this.result)}on_result(e,t,i){if(this.content.empty(),s.compile_template(this,`\n \n
\n
Search results for "${this._term}" \n \n
\n `,this.content),e.length){s.compile_template(this,`\n \n ${e.length} Results \n
\n `,this.result);for(const t of e)this.render_suggestion(t)}else this.render_no_results();this.result.tsajax()}on_keydown(e){13!==e.keyCode&&s.clock.schedule_frame(()=>{this._term!==this.elem.val()&&this.elem.trigger("change")})}on_change(e){if(this._in_progress)return;const t=this.elem.val();this._term!==t&&(this._term=t,this._term.length{this._timeout_event=null,this.search()},this._delay)))}}class _{static initialize(e){if(!e)return;let s=e.parents("div.modal");if(!s.length)return;let i=s.data("overlay"),o=i.ref_target;t("a.addreference",e).each(function(){new p(t(this),o,i)}),t("a.removereference",e).each(function(){new m(t(this),o,i)})}constructor(e,t,s){this.elem=e,this.target=t,this.target_tag=t.get(0).tagName,this.overlay=s}single_value(){return"INPUT"==this.target_tag}multi_value(){return"SELECT"==this.target_tag}toggle_enabled(e){t("a",e.parent()).toggleClass("disabled")}reset_selected(e){let s=new Array;this.single_value()&&s.push(e.attr("value")),this.multi_value()&&t("[selected=selected]",e).each(function(){s.push(t(this).attr("value"))}),this.set_selected_on_ajax_target(e.parent(),s);let i=this.overlay,o=this;t("div.referencebrowser a",i.elem).each(function(){let e=t(this);e.attr("ajax:target")&&o.set_selected_on_ajax_target(e,s)})}set_selected_on_ajax_target(e,t){let i=s.ajax.parse_target(e.attr("ajax:target"));i.params.selected=t.join(",");let o=new Array;for(let e in i.params)o.push(e+"="+i.params[e]);e.attr("ajax:target",i.url+"?"+o.join("&"))}}class p extends _{constructor(e,t,s){super(e,t,s),e.off("click").on("click",this.add_reference.bind(this))}add_reference(e){e.preventDefault();let s=this.elem,i=this.target,o=s.attr("id");o=o.substring(4,o.length);let l=t(".reftitle",s.parent()).html();if(this.single_value()){i.attr("value",l);let e='[name="'+i.attr("name")+'.uid"]';return t(e).attr("value",o),this.set_selected_on_ajax_target(i.parent(),[o]),void this.overlay.close()}if(this.multi_value()){if(t('[value="'+o+'"]',i.parent()).length)return;let e=t(" ");e.val(o).html(l).attr("selected","selected"),i.append(e),i.trigger("change")}this.reset_selected(i),this.toggle_enabled(s)}}class m extends _{constructor(e,t,s){super(e,t,s),e.off("click").on("click",this.remove_reference.bind(this))}remove_reference(e){e.preventDefault();let s=this.elem,i=this.target,o=s.attr("id");if(o=o.substring(4,o.length),this.single_value()){i.attr("value","");let e='[name="'+i.attr("name")+'.uid"]';t(e).attr("value","")}if(this.multi_value()){let e='[value="'+o+'"]';if(!t(e,i.parent()).length)return;t(e,i).remove(),i.trigger("change")}this.reset_selected(i),this.toggle_enabled(s)}}class u{static initialize(e){t(".referencebrowser_trigger",e).each(function(){new u(t(this))})}constructor(e){this.wrapper=e.parent();let s=`[name="${e.data("reference-name")}"]`;this.target=t(s,this.wrapper),e.off("click").on("click",this.load_ref_browser.bind(this))}load_ref_browser(e){e.preventDefault(),s.ajax.overlay({action:"referencebrowser",title:"Referencebrowser",css:"modal-lg",target:this.wrapper.attr("ajax:target"),on_complete:this.on_complete.bind(this)}).ref_target=this.target}on_complete(e){let s=this.target;t("a.addreference",e.elem).each(function(){new p(t(this),s,e)}),t("a.removereference",e.elem).each(function(){new m(t(this),s,e)})}}function b(e,t){u.initialize(t)}function g(e,s,i){t(".referencebrowser_trigger",s).each(function(){let s=t(this),o=s.data("reference-name"),l=e.base_id.replace(/\-/g,".");s.data("reference-name",e.set_value_index(o,l,i,"."))})}t(function(){void 0!==window.yafowil_array&&(yafowil_array.on_array_event("on_add",b),yafowil_array.on_array_event("on_index",g))});class f extends s.Events{on_sidebar_left_resize(e){}on_sidebar_right_resize(e){}on_main_area_mode(e){}}const w=new f;class v extends s.Events{static initialize(e){const t=s.query_elem("#main-area",e);t&&new v(t)}constructor(e){super(),this.elem=e,new s.Property(this,"is_compact",null),new s.Property(this,"is_super_compact",null),this.set_mode=this.set_mode.bind(this),w.on("on_sidebar_left_resize",this.set_mode),w.on("on_sidebar_right_resize",this.set_mode),t(window).on("resize",this.set_mode),this.set_mode(),s.ajax.attach(this,e)}destroy(){t(window).off("resize",this.set_mode),w.off("on_sidebar_left_resize",this.set_mode),w.off("on_sidebar_right_resize",this.set_mode)}set_mode(){this.is_compact=this.elem.outerWidth()<992,this.is_super_compact=this.elem.outerWidth()<576}on_is_compact(e){e?(this.elem.removeClass("full"),this.elem.addClass("compact")):(this.elem.removeClass("compact"),this.elem.addClass("full")),w.trigger("on_main_area_mode",this)}on_is_super_compact(e){e?this.elem.addClass("super-compact"):this.elem.removeClass("super-compact"),w.trigger("on_main_area_mode",this)}}class k extends s.Events{constructor(e){super(),this.elem=e,new s.Property(this,"is_compact",null),new s.Property(this,"is_super_compact",null),new s.Property(this,"is_sidebar_collapsed",null),this.set_mode=this.set_mode.bind(this),w.on("on_main_area_mode",this.set_mode),this.on_sidebar_left_resize=this.on_sidebar_left_resize.bind(this),w.on("on_sidebar_left_resize",this.on_sidebar_left_resize),w.on("on_sidebar_right_resize",this.on_sidebar_left_resize),s.ajax.attach(this,e)}destroy(){w.off("on_main_area_mode",this.set_mode),w.off("on_sidebar_left_resize",this.on_sidebar_left_resize),w.off("on_sidebar_right_resize",this.on_sidebar_left_resize)}set_mode(e,t){this.is_compact=t.is_compact,this.is_super_compact=t.is_super_compact}on_is_compact(e){e?(this.elem.removeClass("full"),this.elem.addClass("compact")):(this.elem.removeClass("compact"),this.elem.addClass("full"))}on_is_super_compact(e){e?this.elem.addClass("super-compact"):this.elem.removeClass("super-compact")}on_sidebar_left_resize(e,t){this.is_sidebar_left_collapsed=t.collapsed}on_sidebar_right_resize(e,t){this.is_sidebar_right_collapsed=t.collapsed}on_is_sidebar_left_collapsed(e){}on_is_sidebar_right_collapsed(e){}}const z=e=>class extends e{constructor(...e){super(...e),this.elem&&s.ajax.attach(this,this.elem),this.on_window_resize=this.on_window_resize.bind(this),t(window).on("resize",this.on_window_resize)}on_window_resize(e){super.on_window_resize&&super.on_window_resize(e)}destroy(){try{super.destroy()}catch(e){console.warn(e)}finally{t(window).off("resize",this.on_window_resize)}}};class y extends s.Motion{static initialize(e){t(".scrollable-x",e).each(function(){new x(t(this))}),t(".scrollable-y",e).each(function(){new C(t(this))})}constructor(e){if(super(e),this.elem=e,this.elem.data("scrollbar"))return void console.warn("cone.app: Only one Scrollbar can be bound to each element.");this.elem.data("scrollbar",this),this.content=s.query_elem("> .scrollable-content",e),this.on_scroll=this.on_scroll.bind(this),this.on_click=this.on_click.bind(this),this.on_hover=this.on_hover.bind(this),this.on_window_resize=this.on_window_resize.bind(this),this.compile(),this.position=0,this.scroll_step=50,new s.Property(this,"disabled",!1),s.clock.schedule_frame(()=>this.render());const i=t(window).width()<=768;new s.Property(this,"is_mobile",i)}on_window_resize(e){this.is_mobile=t(window).innerWidth()<=768,this.position=this.safe_position(this.position),this.render()}get position(){return this._position||0}set position(e){this._position=this.safe_position(e),this.update(),this.trigger("on_position",this._position)}get pointer_events(){return"all"===this.elem.css("pointer-events")}set pointer_events(e){this.elem.css("pointer-events",e?"all":"none")}fade_timer(){this.scrollbar.is(":visible")||this.scrollbar.fadeIn("fast"),this.fade_out_timeout&&clearTimeout(this.fade_out_timeout),this.fade_out_timeout=setTimeout(()=>{this.scrollbar.fadeOut("slow")},700)}on_is_mobile(e){e&&this.contentsize>this.scrollsize?(this.scrollbar.stop(!0,!0).show(),this.elem.off("mouseenter mouseleave",this.on_hover)):(this.scrollbar.stop(!0,!0).hide(),this.elem.on("mouseenter mouseleave",this.on_hover))}bind(){this.pointer_events=!0,this.elem.on("mousewheel wheel",this.on_scroll),this.scrollbar.on("click",this.on_click),this.set_scope(this.thumb,t(document),this.elem)}unbind(){this.elem.off("mousewheel wheel",this.on_scroll),this.elem.off("mouseenter mouseleave",this.on_hover),this.scrollbar.off("click",this.on_click),t(this.thumb).off("mousedown",this._down_handle)}destroy(){this.fade_out_timeout&&clearTimeout(this.fade_out_timeout),this.unbind(),this.elem.removeData("scrollbar")}compile(){s.compile_template(this,'\n \n ',this.elem)}render(e){this.scrollbar.css(e,this.scrollsize),this.contentsize<=this.scrollsize?this.thumbsize=this.scrollsize:this.thumbsize=Math.pow(this.scrollsize,2)/this.contentsize,this.thumb.css(e,this.thumbsize),this.update(),this.position=this.safe_position(this.position)}safe_position(e){if("number"!=typeof e)throw new Error(`Scrollbar position must be a Number, position is: "${e}".`);if(this.contentsize<=this.scrollsize)return 0;const t=this.contentsize-this.scrollsize;return e>=t?e=t:e<=0&&(e=0),e}on_disabled(e){e?this.unbind():this.bind()}on_hover(e){e.preventDefault(),e.stopPropagation();const t=this.elem;(t.has(e.target).length>0||t.is(e.target))&&this.contentsize>this.scrollsize&&("mouseenter"===e.type?this.scrollbar.stop(!0,!0).fadeIn():"mouseleave"===e.type&&e.relatedTarget!==t.get(0)&&this.scrollbar.stop(!0,!0).fadeOut())}on_scroll(e){if(this.contentsize<=this.scrollsize)return;let t=e.originalEvent;"number"==typeof t.deltaY&&(t.deltaY>0?this.position+=this.scroll_step:t.deltaY<0&&(this.position-=this.scroll_step))}on_click(e){e.preventDefault(),this.thumb.addClass("active");let t=this.pos_from_evt(e)-this.offset-this.thumbsize/2;this.position=this.contentsize*t/this.scrollsize,this.thumb.removeClass("active")}touchstart(e){const t=e.originalEvent.touches[0];this._touch_pos=this.pos_from_evt(t),this._start_position=this.position}touchmove(e){if(this.contentsize<=this.scrollsize)return;const t=e.originalEvent.touches[0],s=this.pos_from_evt(t)-this._touch_pos;this.position=this._start_position-s,this.fade_timer()}touchend(e){delete this._touch_pos,delete this._start_position}down(e){this._mouse_pos=this.pos_from_evt(e)-this.offset,this._thumb_pos=this.position/(this.contentsize/this.scrollsize),this.elem.off("mouseenter mouseleave",this.on_hover),this.thumb.addClass("active")}move(e){let t=this.pos_from_evt(e)-this.offset,s=this._thumb_pos+t-this._mouse_pos;this.position=this.contentsize*s/this.scrollsize}up(e){delete this._mouse_pos,delete this._thumb_pos,this.elem.on("mouseenter mouseleave",this.on_hover),this.thumb.removeClass("active")}}class x extends(z(y)){get offset(){return this.elem.offset().left}get contentsize(){return this.content.outerWidth()}get scrollsize(){const e=parseFloat(this.elem.css("padding-right")),t=parseFloat(this.elem.css("padding-left"));return this.elem.outerWidth()-t-e}compile(){super.compile(),this.thumb.css("height","6px"),this.scrollbar.css("height","6px").css("width",this.scrollsize),this.thumbsize=this.scrollsize/(this.contentsize/this.scrollsize),this.thumb.css("width",this.thumbsize)}render(){super.render("width")}update(){let e=this.position/(this.contentsize/this.scrollsize);this.content.css("right",this.position+"px"),this.thumb.css("left",e+"px")}pos_from_evt(e){return e.pageX}}class C extends(z(y)){get offset(){return this.elem.offset().top}get contentsize(){return this.content.outerHeight()}get scrollsize(){const e=parseFloat(this.elem.css("padding-top")),t=parseFloat(this.elem.css("padding-bottom"));return this.elem.outerHeight()-e-t}compile(){super.compile(),this.thumb.css("width","6px"),this.scrollbar.css("width","6px").css("top","0px").css("height",this.scrollsize),this.thumbsize=this.scrollsize/(this.contentsize/this.scrollsize),this.thumb.css("height",this.thumbsize)}render(){super.render("height")}update(){let e=this.position/(this.contentsize/this.scrollsize);this.content.css("bottom",this.position+"px"),this.thumb.css("top",e+"px")}pos_from_evt(e){return e.pageY}}class j{static initialize(e){new j(e)}constructor(e){t("input.add_remove_role_for_principal",e).off("change").on("change",this.set_principal_role)}set_principal_role(e){e.preventDefault();let i,o=t(this);i=this.checked?"add_principal_role":"remove_principal_role";let l=o.parent().attr("ajax:target"),r={id:o.attr("name"),role:o.attr("value")};s.ajax.action({name:i,mode:"NONE",selector:"NONE",url:l,params:r})}}class S extends s.Events{constructor(e,i){super(i),this.elem=i;const o=this.target=i.data("target");this.parent=e,this.related_tile=t(`[data-tile="${o}"]`,this.parent.tiles_container),this.on_click=this.on_click.bind(this),this.compile(),s.ajax.attach(this,i)}compile(){this.elem.on("click",this.on_click)}on_click(e){e.preventDefault(),this.parent.activate_tile(this)}activate_tile(){this.elem.addClass("active"),this.related_tile.removeClass("d-none"),this.parent.sidebar.elem.attr("tile",this.target)}deactivate_tile(){this.elem.removeClass("active"),this.related_tile.addClass("d-none")}destroy(){this.elem.off("click",this.on_click)}}class q extends s.Events{constructor(e,s){super(s),this.sidebar=e,this.elem=s,this.mode=e.elem.data("mode")||"stacked",this.tiles=e.elem.data("tiles")??[],this.navigation=t(".sidebar-controls",this.sidebar.elem),this.tiles_container=t(".sidebar-tiles",this.elem),this.controls=[],this.compile(),"toggle"===this.mode&&this.controls.length>0&&this.activate_tile(this.controls[0])}compile(){"toggle"===this.mode&&this.tiles.length>1?t(".sidebar-control",this.navigation).each((e,s)=>{this.controls.push(new S(this,t(s)))}):this.navigation.addClass("d-none")}activate_tile(e){"stacked"!==this.mode&&(this.deactivate_all(),e.activate_tile())}deactivate_all(){for(const e of this.controls){if("stacked"===this.mode)return;e.deactivate_tile()}}}class I extends(z(s.Motion)){constructor(e){super(e),this.elem=e,this.min_width=e.data("min-width")||115;const i=Math.max(this.min_width,this.sidebar_width);if(e.css("width",i+"px"),this.moving=!1,this.trigger_event=this.trigger_event.bind(this),this.scrollbar=s.query_elem(".scrollable-y",e).data("scrollbar"),this.on_click=this.on_click.bind(this),this.collapse_elem=s.query_elem("#sidebar_collapse",e),this.collapse_elem.on("click",this.on_click),this.on_lock=this.on_lock.bind(this),this.lock_input=s.query_elem(".lock-state-input",e),this.lock_elem=s.query_elem(".lock-state-btn",e),this.lock_elem.on("click",this.on_lock),this.resizer_elem=s.query_elem("#sidebar_resizer",e),this.set_scope(this.resizer_elem,t(document)),this.responsive_toggle=this.responsive_toggle.bind(this),this.responsive_toggle(),void 0!==this.locked&&null!==this.locked){if(this.lock_input.prop("checked",!0).trigger("change"),this.disable_lock)return;this.locked.collapsed?this.collapse():this.expand()}this.disable_or_enable_interaction=this.disable_or_enable_interaction.bind(this),this.disable_or_enable_interaction(),t("html, body").css("overscroll-behavior","auto");const o=t(".sidebar-content",e);this.sidebar_content=new q(this,o),s.ajax.attach(this,e)}get collapsed(){return this.elem.outerWidth()<=0}on_window_resize(e){this.responsive_toggle()}on_lock(e){!this.lock_input.get(0).checked?this.set_state():(this.unset_state(),this.elem.removeClass("collapsed"),this.elem.removeClass("expanded")),this.collapsed?this.elem.addClass("collapsed"):this.elem.addClass("expanded"),this.disable_or_enable_interaction()}disable_or_enable_interaction(){this.locked&&!this.disable_lock?(t(".collapse_btn",this.collapse_elem).addClass("disabled"),this.resizer_elem.addClass("d-none")):(t(".collapse_btn",this.collapse_elem).removeClass("disabled"),this.resizer_elem.removeClass("d-none"))}responsive_toggle(){this.locked||(this.elem.removeClass("collapsed"),this.elem.removeClass("expanded")),this.collapsed?(this.elem.removeClass("responsive-expanded"),this.elem.addClass("responsive-collapsed")):(this.elem.addClass("responsive-expanded"),this.elem.removeClass("responsive-collapsed")),this.collapsed!==this.responsive_collapsed&&(this.responsive_collapsed=this.collapsed,this.trigger_event()),t(window).width()<768?(this.disable_lock=!0,this.locked&&!this.locked.collapsed&&this.collapse()):(this.disable_lock=!1,this.locked&&!this.locked.collapsed&&this.collapsed?this.expand():this.locked&&this.locked.collapsed&&!this.collapsed&&this.collapse()),this.locked&&this.disable_or_enable_interaction()}collapse(){t("html, body").css("overscroll-behavior","auto"),this.elem.removeClass("expanded").addClass("collapsed"),this.trigger_event()}expand(){t("html, body").css("overscroll-behavior","none"),this.elem.removeClass("collapsed").addClass("expanded"),this.trigger_event()}on_click(e){this.collapsed?this.expand():this.collapse(),void 0===this.locked||null===this.locked||this.disable_lock||this.set_state()}move(e){this.locked||(this.moving=!0,this.scrollbar.pointer_events=!1,this.sidebar_width=this.get_width_from_event(e),this.elem.css("width",this.sidebar_width),this.trigger_event())}up(){this.scrollbar.pointer_events=!0,this.trigger_event(),this.moving=!1}on_sibling_sidebar_resize(e,s){const i=t(window).width()-this.elem.outerWidth()-300,o=t(window).width()<768,l=this.locked&&!this.locked.collapsed,r=s.elem.outerWidth()>=i;!s.collapsed&&o?(this.collapse(),this.elem.addClass("d-none")):s.collapsed||!r||s.moving&&this.locked?s.collapsed&&l&&this.collapsed&&!o?(this.expand(),this.elem.removeClass("d-none")):s.collapsed&&this.elem.removeClass("d-none"):this.collapse()}destroy(){this.reset_state(),t(window).off("resize",this.on_window_resize),this.collapse_elem.off(),this.scrollbar=null,this.elem.off(),this.lock_elem.off("click",this.on_lock)}}class E extends I{static initialize(e){const t=s.query_elem("#sidebar_left",e);t&&new E(t)}constructor(e){super(e),this.on_sidebar_right_resize=this.on_sidebar_right_resize.bind(this),w.on("on_sidebar_right_resize",this.on_sidebar_right_resize)}get locked(){return JSON.parse(localStorage.getItem("cone.app.sidebar_left.locked"))}get sidebar_width(){return localStorage.getItem("cone-app-sidebar-left-width")||300}set sidebar_width(e){localStorage.setItem("cone-app-sidebar-left-width",e)}trigger_event(){w.trigger("on_sidebar_left_resize",this)}get_width_from_event(e){let s=e.pageX,i=0;t("#sidebar_right").length>0&&(i=t("#sidebar_right").outerWidth());const o=t(window).width()-i-300;return s=Math.max(this.min_width,Math.min(s,o)),parseInt(s)}on_sidebar_right_resize(e,t){this.on_sibling_sidebar_resize(e,t)}set_state(){localStorage.setItem("cone.app.sidebar_left.locked",JSON.stringify({collapsed:this.collapsed}))}unset_state(){localStorage.removeItem("cone.app.sidebar_left.locked")}destroy(){super.destroy(),w.off("on_sidebar_right_resize",this.on_sidebar_right_resize)}}class N extends I{static initialize(e){const t=s.query_elem("#sidebar_right",e);t&&new N(t)}constructor(e){super(e),this.on_sidebar_left_resize=this.on_sidebar_left_resize.bind(this),w.on("on_sidebar_left_resize",this.on_sidebar_left_resize)}get locked(){return JSON.parse(localStorage.getItem("cone.app.sidebar_right.locked"))}get sidebar_width(){return localStorage.getItem("cone-app-sidebar-right-width")||300}set sidebar_width(e){localStorage.setItem("cone-app-sidebar-right-width",e)}trigger_event(){w.trigger("on_sidebar_right_resize",this)}get_width_from_event(e){let s=t(window).outerWidth()-e.pageX,i=0;t("#sidebar_left").length>0&&(i=t("#sidebar_left").outerWidth());const o=t(window).width()-i-300;return s=Math.max(this.min_width,Math.min(s,o)),parseInt(s)}on_sidebar_left_resize(e,t){this.on_sibling_sidebar_resize(e,t)}set_state(){localStorage.setItem("cone.app.sidebar_right.locked",JSON.stringify({collapsed:this.collapsed}))}unset_state(){localStorage.removeItem("cone.app.sidebar_right.locked")}destroy(){super.destroy(),w.off("on_sidebar_left_resize",this.on_sidebar_left_resize)}}class D{static initialize(e){o.initialize(e,".table_length select"),l.initialize(e,".table_filter input")}}class T{static initialize(e){t(".translation-nav",e).each(function(){new T(t(this))})}constructor(e){t("div.invalid-feedback",e.parent()).show(),this.nav_elem=e,this.fields_elem=e.next(),this.show_lang_handle=this.show_lang_handle.bind(this),t("li > a",e).on("click",this.show_lang_handle),t("li.error",e).length?t("li.error:first > a",e).trigger("click"):t("li > a.active",e).trigger("click"),this.fields_elem.show()}show_lang_handle(e){e.preventDefault(),t("li > a",this.nav_elem).removeClass("active"),this.fields_elem.children().hide();let s=t(e.currentTarget);s.addClass("active"),t(s.attr("href"),this.fields_elem).show()}}class M extends k{static initialize(e){const t=s.query_elem("#mainmenu",e);t&&new M(t)}constructor(e){super(e),this.elem=e,this.scrollbar=e.data("scrollbar"),this.elems=t(".nav-link.dropdown-toggle",e),this.open_dropdown=null,this.on_show_dropdown_desktop=this.on_show_dropdown_desktop.bind(this),this.on_hide_dropdown_desktop=this.on_hide_dropdown_desktop.bind(this),this.hide_dropdowns=this.hide_dropdowns.bind(this),this.scrollbar.on("on_position",this.hide_dropdowns)}get height(){return this.elem.outerHeight(!0)}on_sidebar_left_resize(e,t){super.on_sidebar_left_resize(e,t),requestAnimationFrame(()=>{this.scrollbar.render()})}on_is_compact(e){this.hide_dropdowns(),e?(this.scrollbar.off("on_position",this.hide_dropdowns),this.bind_dropdowns_mobile()):(this.bind_dropdowns_desktop(),this.scrollbar.on("on_position",this.hide_dropdowns))}on_show_dropdown_desktop(e){const s=e.target;this.open_dropdown=s;t(s).siblings("ul.dropdown-menu").css({top:this.height-1+"px",left:`${t(s).offset().left}px`})}on_hide_dropdown_desktop(e){const t=e.target;this.open_dropdown===t&&(this.open_dropdown=null)}bind_dropdowns_desktop(){this.elem.on("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.on("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop)}bind_dropdowns_mobile(){this.elem.off("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.off("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop)}hide_dropdowns(){this.elems.each((e,s)=>{t(s).dropdown("hide")})}destroy(){super.destroy(),this.elem.off("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.off("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop),this.scrollbar.off("on_position",this.hide_dropdowns),this.scrollbar.destroy()}}class O extends k{static initialize(e){const t=s.query_elem("#header-main",e);t&&new O(t)}constructor(e){super(e),this.elem=e,this.header_content=s.query_elem("#header-content",e),this.navbar_content_wrapper=s.query_elem("#navbar-content-wrapper",e),this.navbar_content=s.query_elem("#navbar-content",e),this.navbar_toggler=s.query_elem("#navbar-toggler",this.elem),this.personal_tools=s.query_elem("#personaltools",e),this.mainmenu=s.query_elem("#mainmenu",e),this.mainmenu_elems=t(".nav-link.dropdown-toggle",this.mainmenu),this.render_mobile_scrollbar=this.render_mobile_scrollbar.bind(this),this.mainmenu_elems.each((e,s)=>{t(s).on("shown.bs.dropdown",this.render_mobile_scrollbar),t(s).on("hidden.bs.dropdown",this.render_mobile_scrollbar)}),this.set_mobile_menu_open=this.set_mobile_menu_open.bind(this),this.set_mobile_menu_closed=this.set_mobile_menu_closed.bind(this),this.bind()}destroy(){super.destroy(),this.mobile_scrollbar&&(this.mobile_scrollbar.destroy(),this.mobile_scrollbar=null),this.mainmenu_elems.each((e,s)=>{t(s).off("shown.bs.dropdown",this.render_mobile_scrollbar),t(s).off("hidden.bs.dropdown",this.render_mobile_scrollbar)});const e=this.navbar_content_wrapper;e.off("show.bs.collapse shown.bs.collapse",this.set_mobile_menu_open),e.off("hide.bs.collapse hidden.bs.collapse",this.set_mobile_menu_closed)}render_mobile_scrollbar(){this.is_compact&&this.mobile_scrollbar&&this.mobile_scrollbar.render()}bind(){const e=this.navbar_content_wrapper;e.on("show.bs.collapse shown.bs.collapse",this.set_mobile_menu_open),e.on("hidden.bs.collapse",this.set_mobile_menu_closed)}set_mobile_menu_open(){this.elem.addClass("mobile-menu-open")}set_mobile_menu_closed(){this.elem.removeClass("mobile-menu-open")}on_is_compact(e){this.mobile_scrollbar&&(this.navbar_content.removeClass("scrollable-content"),this.mobile_scrollbar.destroy(),this.mobile_scrollbar=null),e?(this.elem.removeClass("full").removeClass("navbar-expand"),this.elem.addClass("compact"),this.navbar_content.addClass("scrollable-content"),this.mobile_scrollbar=new C(this.navbar_content_wrapper),this.navbar_content_wrapper.on("shown.bs.collapse",()=>{t("html, body").css("overscroll-behavior","none"),this.mobile_scrollbar.render()}),this.navbar_content_wrapper.on("hide.bs.collapse",()=>{t("html, body").css("overscroll-behavior","auto"),this.mobile_scrollbar.scrollbar.hide()})):(this.elem.removeClass("compact"),this.elem.addClass("full").addClass("navbar-expand"))}on_is_super_compact(e){const i=null!==s.query_elem("#personaltools",this.navbar_content);e?i||this.personal_tools.detach().appendTo(this.navbar_content):(i&&this.personal_tools.detach().prependTo(this.header_content),t(".dropdown-menu.show").removeClass("show"))}}class ${static initialize(e){const t=s.query_elem("#navtree",e);t&&new $(t)}constructor(e){this.elem=e,this.dropdown_elem=t("#navigation-collapse",e),this.dropdown_elem.hasClass("no-collapse")||(localStorage.getItem("cone.app.navtree.open")&&this.dropdown_elem.addClass("show"),this.set_menu_open=this.set_menu_open.bind(this),this.set_menu_closed=this.set_menu_closed.bind(this),this.dropdown_elem.on("shown.bs.collapse",this.set_menu_open),this.dropdown_elem.on("hidden.bs.collapse",this.set_menu_closed),s.ajax.attach(this,e))}set_menu_open(e){localStorage.setItem("cone.app.navtree.open","true")}set_menu_closed(e){localStorage.removeItem("cone.app.navtree.open")}destroy(){this.dropdown_elem.off(),this.elem.off()}}class A{constructor(e){this.options=e,this.selected=[],this.select_direction=0,this.firstclick=!0}reset(){this.selected=[]}add(e){this.remove(e),this.selected.push(e)}remove(e){let s=t.grep(this.selected,function(t,s){return t!==e});this.selected=s}select_no_key(e,t){e.children().removeClass("selected"),t.addClass("selected"),this.reset(),this.add(t.get(0))}select_ctrl_down(e){e.toggleClass("selected"),e.hasClass("selected")?this.add(e.get(0)):this.remove(e.get(0))}get_nearest(e,s){let i,o,l=e.children(".selected"),r=-1;return t(l).each(function(){o=t(this),i=o.index(),-1==r?r=i:s>i?this.select_direction>0?ir&&(r=i):sNo search results\n ',this.result)}render_suggestion(e){s.compile_template(this,`\n \n
\n
\n ${void 0===e.description?"":e.description}\n
\n
\n `,this.result)}on_result(e,t,i){if(this.content.empty(),s.compile_template(this,`\n \n
\n
Search results for "${this._term}" \n \n
\n `,this.content),e.length){s.compile_template(this,`\n \n ${e.length} Results \n
\n `,this.result);for(const t of e)this.render_suggestion(t)}else this.render_no_results();this.result.tsajax()}on_keydown(e){13!==e.keyCode&&s.clock.schedule_frame(()=>{this._term!==this.elem.val()&&this.elem.trigger("change")})}on_change(e){if(this._in_progress)return;const t=this.elem.val();this._term!==t&&(this._term=t,this._term.length{this._timeout_event=null,this.search()},this._delay)))}}class _{static initialize(e){if(!e)return;let s=e.parents("div.modal");if(!s.length)return;let i=s.data("overlay"),o=i.ref_target;t("a.addreference",e).each(function(){new p(t(this),o,i)}),t("a.removereference",e).each(function(){new m(t(this),o,i)})}constructor(e,t,s){this.elem=e,this.target=t,this.target_tag=t.get(0).tagName,this.overlay=s}single_value(){return"INPUT"==this.target_tag}multi_value(){return"SELECT"==this.target_tag}toggle_enabled(e){t("a",e.parent()).toggleClass("disabled")}reset_selected(e){let s=new Array;this.single_value()&&s.push(e.attr("value")),this.multi_value()&&t("[selected=selected]",e).each(function(){s.push(t(this).attr("value"))}),this.set_selected_on_ajax_target(e.parent(),s);let i=this.overlay,o=this;t("div.referencebrowser a",i.elem).each(function(){let e=t(this);e.attr("ajax:target")&&o.set_selected_on_ajax_target(e,s)})}set_selected_on_ajax_target(e,t){let i=s.ajax.parse_target(e.attr("ajax:target"));i.params.selected=t.join(",");let o=new Array;for(let e in i.params)o.push(e+"="+i.params[e]);e.attr("ajax:target",i.url+"?"+o.join("&"))}}class p extends _{constructor(e,t,s){super(e,t,s),e.off("click").on("click",this.add_reference.bind(this))}add_reference(e){e.preventDefault();let s=this.elem,i=this.target,o=s.attr("id");o=o.substring(4,o.length);let l=t(".reftitle",s.parent()).html();if(this.single_value()){i.attr("value",l);let e='[name="'+i.attr("name")+'.uid"]';return t(e).attr("value",o),this.set_selected_on_ajax_target(i.parent(),[o]),void this.overlay.close()}if(this.multi_value()){if(t('[value="'+o+'"]',i.parent()).length)return;let e=t(" ");e.val(o).html(l).attr("selected","selected"),i.append(e),i.trigger("change")}this.reset_selected(i),this.toggle_enabled(s)}}class m extends _{constructor(e,t,s){super(e,t,s),e.off("click").on("click",this.remove_reference.bind(this))}remove_reference(e){e.preventDefault();let s=this.elem,i=this.target,o=s.attr("id");if(o=o.substring(4,o.length),this.single_value()){i.attr("value","");let e='[name="'+i.attr("name")+'.uid"]';t(e).attr("value","")}if(this.multi_value()){let e='[value="'+o+'"]';if(!t(e,i.parent()).length)return;t(e,i).remove(),i.trigger("change")}this.reset_selected(i),this.toggle_enabled(s)}}class u{static initialize(e){t(".referencebrowser_trigger",e).each(function(){new u(t(this))})}constructor(e){this.wrapper=e.parent();let s=`[name="${e.data("reference-name")}"]`;this.target=t(s,this.wrapper),e.off("click").on("click",this.load_ref_browser.bind(this))}load_ref_browser(e){e.preventDefault(),s.ajax.overlay({action:"referencebrowser",title:"Referencebrowser",css:"modal-lg",target:this.wrapper.attr("ajax:target"),on_complete:this.on_complete.bind(this)}).ref_target=this.target}on_complete(e){let s=this.target;t("a.addreference",e.elem).each(function(){new p(t(this),s,e)}),t("a.removereference",e.elem).each(function(){new m(t(this),s,e)})}}function b(e,t){u.initialize(t)}function g(e,s,i){t(".referencebrowser_trigger",s).each(function(){let s=t(this),o=s.data("reference-name"),l=e.base_id.replace(/\-/g,".");s.data("reference-name",e.set_value_index(o,l,i,"."))})}t(function(){void 0!==window.yafowil_array&&(yafowil_array.on_array_event("on_add",b),yafowil_array.on_array_event("on_index",g))});class f extends s.Events{on_sidebar_left_resize(e){}on_sidebar_right_resize(e){}on_main_area_mode(e){}}const w=new f;class v extends s.Events{static initialize(e){const t=s.query_elem("#main-area",e);t&&new v(t)}constructor(e){super(),this.elem=e,new s.Property(this,"is_compact",null),new s.Property(this,"is_super_compact",null),this.set_mode=this.set_mode.bind(this),w.on("on_sidebar_left_resize",this.set_mode),w.on("on_sidebar_right_resize",this.set_mode),t(window).on("resize",this.set_mode),this.set_mode(),s.ajax.attach(this,e)}destroy(){t(window).off("resize",this.set_mode),w.off("on_sidebar_left_resize",this.set_mode),w.off("on_sidebar_right_resize",this.set_mode)}set_mode(){this.is_compact=this.elem.outerWidth()<992,this.is_super_compact=this.elem.outerWidth()<576}on_is_compact(e){e?(this.elem.removeClass("full"),this.elem.addClass("compact")):(this.elem.removeClass("compact"),this.elem.addClass("full")),w.trigger("on_main_area_mode",this)}on_is_super_compact(e){e?this.elem.addClass("super-compact"):this.elem.removeClass("super-compact"),w.trigger("on_main_area_mode",this)}}class k extends s.Events{constructor(e){super(),this.elem=e,new s.Property(this,"is_compact",null),new s.Property(this,"is_super_compact",null),new s.Property(this,"is_sidebar_collapsed",null),this.set_mode=this.set_mode.bind(this),w.on("on_main_area_mode",this.set_mode),this.on_sidebar_left_resize=this.on_sidebar_left_resize.bind(this),w.on("on_sidebar_left_resize",this.on_sidebar_left_resize),w.on("on_sidebar_right_resize",this.on_sidebar_left_resize),s.ajax.attach(this,e)}destroy(){w.off("on_main_area_mode",this.set_mode),w.off("on_sidebar_left_resize",this.on_sidebar_left_resize),w.off("on_sidebar_right_resize",this.on_sidebar_left_resize)}set_mode(e,t){this.is_compact=t.is_compact,this.is_super_compact=t.is_super_compact}on_is_compact(e){e?(this.elem.removeClass("full"),this.elem.addClass("compact")):(this.elem.removeClass("compact"),this.elem.addClass("full"))}on_is_super_compact(e){e?this.elem.addClass("super-compact"):this.elem.removeClass("super-compact")}on_sidebar_left_resize(e,t){this.is_sidebar_left_collapsed=t.collapsed}on_sidebar_right_resize(e,t){this.is_sidebar_right_collapsed=t.collapsed}on_is_sidebar_left_collapsed(e){}on_is_sidebar_right_collapsed(e){}}const z=e=>class extends e{constructor(...e){super(...e),this.elem&&s.ajax.attach(this,this.elem),this.on_window_resize=this.on_window_resize.bind(this),t(window).on("resize",this.on_window_resize)}on_window_resize(e){super.on_window_resize&&super.on_window_resize(e)}destroy(){try{super.destroy()}catch(e){console.warn(e)}finally{t(window).off("resize",this.on_window_resize)}}};class y extends s.Motion{static initialize(e){t(".scrollable-x",e).each(function(){new x(t(this))}),t(".scrollable-y",e).each(function(){new C(t(this))})}constructor(e){if(super(e),this.elem=e,this.elem.data("scrollbar"))return void console.warn("cone.app: Only one Scrollbar can be bound to each element.");this.elem.data("scrollbar",this),this.content=s.query_elem("> .scrollable-content",e),this.on_scroll=this.on_scroll.bind(this),this.on_click=this.on_click.bind(this),this.on_hover=this.on_hover.bind(this),this.on_window_resize=this.on_window_resize.bind(this),this.compile(),this.position=0,this.scroll_step=50,new s.Property(this,"disabled",!1),s.clock.schedule_frame(()=>this.render());const i=t(window).width()<=768;new s.Property(this,"is_mobile",i)}on_window_resize(e){this.is_mobile=t(window).innerWidth()<=768,this.position=this.safe_position(this.position),this.render()}get position(){return this._position||0}set position(e){this._position=this.safe_position(e),this.update(),this.trigger("on_position",this._position)}get pointer_events(){return"all"===this.elem.css("pointer-events")}set pointer_events(e){this.elem.css("pointer-events",e?"all":"none")}fade_timer(){this.scrollbar.is(":visible")||this.scrollbar.fadeIn("fast"),this.fade_out_timeout&&clearTimeout(this.fade_out_timeout),this.fade_out_timeout=setTimeout(()=>{this.scrollbar.fadeOut("slow")},700)}on_is_mobile(e){e&&this.contentsize>this.scrollsize?(this.scrollbar.stop(!0,!0).show(),this.elem.off("mouseenter mouseleave",this.on_hover)):(this.scrollbar.stop(!0,!0).hide(),this.elem.on("mouseenter mouseleave",this.on_hover))}bind(){this.pointer_events=!0,this.elem.on("mousewheel wheel",this.on_scroll),this.scrollbar.on("click",this.on_click),this.set_scope(this.thumb,t(document),this.elem)}unbind(){this.elem.off("mousewheel wheel",this.on_scroll),this.elem.off("mouseenter mouseleave",this.on_hover),this.scrollbar.off("click",this.on_click),t(this.thumb).off("mousedown",this._down_handle)}destroy(){this.fade_out_timeout&&clearTimeout(this.fade_out_timeout),this.unbind(),this.elem.removeData("scrollbar")}compile(){s.compile_template(this,'\n \n ',this.elem)}render(e){this.scrollbar.css(e,this.scrollsize),this.contentsize<=this.scrollsize?this.thumbsize=this.scrollsize:this.thumbsize=Math.pow(this.scrollsize,2)/this.contentsize,this.thumb.css(e,this.thumbsize),this.update(),this.position=this.safe_position(this.position)}safe_position(e){if("number"!=typeof e)throw new Error(`Scrollbar position must be a Number, position is: "${e}".`);if(this.contentsize<=this.scrollsize)return 0;const t=this.contentsize-this.scrollsize;return e>=t?e=t:e<=0&&(e=0),e}on_disabled(e){e?this.unbind():this.bind()}on_hover(e){e.preventDefault(),e.stopPropagation();const t=this.elem;(t.has(e.target).length>0||t.is(e.target))&&this.contentsize>this.scrollsize&&("mouseenter"===e.type?this.scrollbar.stop(!0,!0).fadeIn():"mouseleave"===e.type&&e.relatedTarget!==t.get(0)&&this.scrollbar.stop(!0,!0).fadeOut())}on_scroll(e){if(this.contentsize<=this.scrollsize)return;let t=e.originalEvent;"number"==typeof t.deltaY&&(t.deltaY>0?this.position+=this.scroll_step:t.deltaY<0&&(this.position-=this.scroll_step))}on_click(e){e.preventDefault(),this.thumb.addClass("active");let t=this.pos_from_evt(e)-this.offset-this.thumbsize/2;this.position=this.contentsize*t/this.scrollsize,this.thumb.removeClass("active")}touchstart(e){const t=e.originalEvent.touches[0];this._touch_pos=this.pos_from_evt(t),this._start_position=this.position}touchmove(e){if(this.contentsize<=this.scrollsize)return;const t=e.originalEvent.touches[0],s=this.pos_from_evt(t)-this._touch_pos;this.position=this._start_position-s,this.fade_timer()}touchend(e){delete this._touch_pos,delete this._start_position}down(e){this._mouse_pos=this.pos_from_evt(e)-this.offset,this._thumb_pos=this.position/(this.contentsize/this.scrollsize),this.elem.off("mouseenter mouseleave",this.on_hover),this.thumb.addClass("active")}move(e){let t=this.pos_from_evt(e)-this.offset,s=this._thumb_pos+t-this._mouse_pos;this.position=this.contentsize*s/this.scrollsize}up(e){delete this._mouse_pos,delete this._thumb_pos,this.elem.on("mouseenter mouseleave",this.on_hover),this.thumb.removeClass("active")}}class x extends(z(y)){get offset(){return this.elem.offset().left}get contentsize(){return this.content.outerWidth()}get scrollsize(){const e=parseFloat(this.elem.css("padding-right")),t=parseFloat(this.elem.css("padding-left"));return this.elem.outerWidth()-t-e}compile(){super.compile(),this.thumb.css("height","6px"),this.scrollbar.css("height","6px").css("width",this.scrollsize),this.thumbsize=this.scrollsize/(this.contentsize/this.scrollsize),this.thumb.css("width",this.thumbsize)}render(){super.render("width")}update(){let e=this.position/(this.contentsize/this.scrollsize);this.content.css("right",this.position+"px"),this.thumb.css("left",e+"px")}pos_from_evt(e){return e.pageX}}class C extends(z(y)){get offset(){return this.elem.offset().top}get contentsize(){return this.content.outerHeight()}get scrollsize(){const e=parseFloat(this.elem.css("padding-top")),t=parseFloat(this.elem.css("padding-bottom"));return this.elem.outerHeight()-e-t}compile(){super.compile(),this.thumb.css("width","6px"),this.scrollbar.css("width","6px").css("top","0px").css("height",this.scrollsize),this.thumbsize=this.scrollsize/(this.contentsize/this.scrollsize),this.thumb.css("height",this.thumbsize)}render(){super.render("height")}update(){let e=this.position/(this.contentsize/this.scrollsize);this.content.css("bottom",this.position+"px"),this.thumb.css("top",e+"px")}pos_from_evt(e){return e.pageY}}class j{static initialize(e){new j(e)}constructor(e){t("input.add_remove_role_for_principal",e).off("change").on("change",this.set_principal_role)}set_principal_role(e){e.preventDefault();let i,o=t(this);i=this.checked?"add_principal_role":"remove_principal_role";let l=o.parent().attr("ajax:target"),a={id:o.attr("name"),role:o.attr("value")};s.ajax.action({name:i,mode:"NONE",selector:"NONE",url:l,params:a})}}class S extends s.Events{constructor(e,i){super(i),this.elem=i;const o=this.target=i.data("target");this.parent=e,this.related_tile=t(`[data-tile="${o}"]`,this.parent.tiles_container),this.on_click=this.on_click.bind(this),this.compile(),s.ajax.attach(this,i)}compile(){this.elem.on("click",this.on_click)}on_click(e){e.preventDefault(),this.parent.activate_tile(this)}activate_tile(){this.elem.addClass("active"),this.related_tile.removeClass("d-none"),this.parent.sidebar.elem.attr("tile",this.target)}deactivate_tile(){this.elem.removeClass("active"),this.related_tile.addClass("d-none")}destroy(){this.elem.off("click",this.on_click)}}class q extends s.Events{constructor(e,s){super(s),this.sidebar=e,this.elem=s,this.mode=e.elem.data("mode")||"stacked",this.tiles=e.elem.data("tiles")??[],this.navigation=t(".sidebar-controls",this.sidebar.elem),this.tiles_container=t(".sidebar-tiles",this.elem),this.controls=[],this.compile(),"toggle"===this.mode&&this.controls.length>0&&this.activate_tile(this.controls[0])}compile(){"toggle"===this.mode&&this.tiles.length>1?t(".sidebar-control",this.navigation).each((e,s)=>{this.controls.push(new S(this,t(s)))}):this.navigation.addClass("d-none")}activate_tile(e){"stacked"!==this.mode&&(this.deactivate_all(),e.activate_tile())}deactivate_all(){for(const e of this.controls){if("stacked"===this.mode)return;e.deactivate_tile()}}}class I extends(z(s.Motion)){constructor(e){super(e),this.elem=e,this.min_width=e.data("min-width")||115;const i=Math.max(this.min_width,this.sidebar_width);e.css("width",i+"px");const o=this.elem.data("static");if(this.static=!0===o||"True"===o,this.moving=!1,this.trigger_event=this.trigger_event.bind(this),this.scrollbar=s.query_elem(".scrollable-y",e).data("scrollbar"),this.on_click=this.on_click.bind(this),this.collapse_elem=s.query_elem("#sidebar_collapse",e),this.collapse_elem.on("click",this.on_click),this.on_lock=this.on_lock.bind(this),this.lock_input=s.query_elem(".lock-state-input",e),this.lock_elem=s.query_elem(".lock-state-btn",e),this.lock_elem.on("click",this.on_lock),this.resizer_elem=s.query_elem("#sidebar_resizer",e),this.set_scope(this.resizer_elem,t(document)),this.responsive_toggle=this.responsive_toggle.bind(this),this.responsive_toggle(),void 0!==this.locked&&null!==this.locked){if(this.lock_input.prop("checked",!0).trigger("change"),this.disable_lock)return;this.locked.collapsed?this.collapse():this.expand()}this.disable_or_enable_interaction=this.disable_or_enable_interaction.bind(this),this.disable_or_enable_interaction(),t("html, body").css("overscroll-behavior","auto");const l=t(".sidebar-content",e);this.sidebar_content=new q(this,l),s.ajax.attach(this,e)}get collapsed(){return this.elem.outerWidth()<=0}on_window_resize(e){this.responsive_toggle()}on_lock(e){!this.lock_input.get(0).checked?this.set_state():(this.unset_state(),this.elem.removeClass("collapsed"),this.elem.removeClass("expanded")),this.collapsed?this.elem.addClass("collapsed"):this.elem.addClass("expanded"),this.disable_or_enable_interaction()}disable_or_enable_interaction(){this.locked&&!this.disable_lock?(t(".collapse_btn",this.collapse_elem).addClass("disabled"),this.resizer_elem.addClass("d-none")):(t(".collapse_btn",this.collapse_elem).removeClass("disabled"),this.resizer_elem.removeClass("d-none"))}responsive_toggle(){this.locked||(this.elem.removeClass("collapsed"),this.elem.removeClass("expanded")),this.static?(this.collapsed||this.collapse(),this.elem.removeClass("responsive-expanded"),this.elem.removeClass("responsive-collapsed")):this.collapsed?(this.elem.removeClass("responsive-expanded"),this.elem.addClass("responsive-collapsed")):(this.elem.addClass("responsive-expanded"),this.elem.removeClass("responsive-collapsed")),this.collapsed!==this.responsive_collapsed&&(this.responsive_collapsed=this.collapsed,this.trigger_event()),t(window).width()<768?(this.disable_lock=!0,this.locked&&!this.locked.collapsed&&this.collapse()):(this.disable_lock=!1,this.locked&&!this.locked.collapsed&&this.collapsed?this.expand():this.locked&&this.locked.collapsed&&!this.collapsed&&this.collapse()),this.locked&&this.disable_or_enable_interaction()}collapse(){t("html, body").css("overscroll-behavior","auto"),this.elem.removeClass("expanded").addClass("collapsed"),this.trigger_event()}expand(){t("html, body").css("overscroll-behavior","none"),this.elem.removeClass("collapsed").addClass("expanded"),this.trigger_event()}on_click(e){this.collapsed?this.expand():this.collapse(),void 0===this.locked||null===this.locked||this.disable_lock||this.set_state()}move(e){this.locked||(this.moving=!0,this.scrollbar.pointer_events=!1,this.sidebar_width=this.get_width_from_event(e),this.elem.css("width",this.sidebar_width),this.trigger_event())}up(){this.scrollbar.pointer_events=!0,this.trigger_event(),this.moving=!1}on_sibling_sidebar_resize(e,s){const i=t(window).width()-this.elem.outerWidth()-300,o=t(window).width()<768,l=this.locked&&!this.locked.collapsed,a=s.elem.outerWidth()>=i;!s.collapsed&&o?(this.collapse(),this.elem.addClass("d-none")):s.collapsed||!a||s.moving&&this.locked?s.collapsed&&l&&this.collapsed&&!o?(this.expand(),this.elem.removeClass("d-none")):s.collapsed&&this.elem.removeClass("d-none"):this.collapse()}destroy(){this.reset_state(),t(window).off("resize",this.on_window_resize),this.collapse_elem.off(),this.scrollbar=null,this.elem.off(),this.lock_elem.off("click",this.on_lock)}}class E extends I{static initialize(e){const t=s.query_elem("#sidebar_left",e);t&&new E(t)}constructor(e){super(e),this.on_sidebar_right_resize=this.on_sidebar_right_resize.bind(this),w.on("on_sidebar_right_resize",this.on_sidebar_right_resize)}get locked(){return JSON.parse(localStorage.getItem("cone.app.sidebar_left.locked"))}get sidebar_width(){return localStorage.getItem("cone-app-sidebar-left-width")||300}set sidebar_width(e){localStorage.setItem("cone-app-sidebar-left-width",e)}trigger_event(){w.trigger("on_sidebar_left_resize",this)}get_width_from_event(e){let s=e.pageX,i=0;t("#sidebar_right").length>0&&(i=t("#sidebar_right").outerWidth());const o=t(window).width()-i-300;return s=Math.max(this.min_width,Math.min(s,o)),parseInt(s)}on_sidebar_right_resize(e,t){this.on_sibling_sidebar_resize(e,t)}set_state(){localStorage.setItem("cone.app.sidebar_left.locked",JSON.stringify({collapsed:this.collapsed}))}unset_state(){localStorage.removeItem("cone.app.sidebar_left.locked")}destroy(){super.destroy(),w.off("on_sidebar_right_resize",this.on_sidebar_right_resize)}}class N extends I{static initialize(e){const t=s.query_elem("#sidebar_right",e);t&&new N(t)}constructor(e){super(e),this.on_sidebar_left_resize=this.on_sidebar_left_resize.bind(this),w.on("on_sidebar_left_resize",this.on_sidebar_left_resize)}get locked(){return JSON.parse(localStorage.getItem("cone.app.sidebar_right.locked"))}get sidebar_width(){return localStorage.getItem("cone-app-sidebar-right-width")||300}set sidebar_width(e){localStorage.setItem("cone-app-sidebar-right-width",e)}trigger_event(){w.trigger("on_sidebar_right_resize",this)}get_width_from_event(e){let s=t(window).outerWidth()-e.pageX,i=0;t("#sidebar_left").length>0&&(i=t("#sidebar_left").outerWidth());const o=t(window).width()-i-300;return s=Math.max(this.min_width,Math.min(s,o)),parseInt(s)}on_sidebar_left_resize(e,t){this.on_sibling_sidebar_resize(e,t)}set_state(){localStorage.setItem("cone.app.sidebar_right.locked",JSON.stringify({collapsed:this.collapsed}))}unset_state(){localStorage.removeItem("cone.app.sidebar_right.locked")}destroy(){super.destroy(),w.off("on_sidebar_left_resize",this.on_sidebar_left_resize)}}class T{static initialize(e){o.initialize(e,".table_length select"),l.initialize(e,".table_filter input")}}class D{static initialize(e){t(".translation-nav",e).each(function(){new D(t(this))})}constructor(e){t("div.invalid-feedback",e.parent()).show(),this.nav_elem=e,this.fields_elem=e.next(),this.show_lang_handle=this.show_lang_handle.bind(this),t("li > a",e).on("click",this.show_lang_handle),t("li.error",e).length?t("li.error:first > a",e).trigger("click"):t("li > a.active",e).trigger("click"),this.fields_elem.show()}show_lang_handle(e){e.preventDefault(),t("li > a",this.nav_elem).removeClass("active"),this.fields_elem.children().hide();let s=t(e.currentTarget);s.addClass("active"),t(s.attr("href"),this.fields_elem).show()}}class M extends k{static initialize(e){const t=s.query_elem("#mainmenu",e);t&&new M(t)}constructor(e){super(e),this.elem=e,this.scrollbar=e.data("scrollbar"),this.elems=t(".nav-link.dropdown-toggle",e),this.open_dropdown=null,this.on_show_dropdown_desktop=this.on_show_dropdown_desktop.bind(this),this.on_hide_dropdown_desktop=this.on_hide_dropdown_desktop.bind(this),this.hide_dropdowns=this.hide_dropdowns.bind(this),this.scrollbar.on("on_position",this.hide_dropdowns)}get height(){return this.elem.outerHeight(!0)}on_sidebar_left_resize(e,t){super.on_sidebar_left_resize(e,t),requestAnimationFrame(()=>{this.scrollbar.render()})}on_is_compact(e){this.hide_dropdowns(),e?(this.scrollbar.off("on_position",this.hide_dropdowns),this.bind_dropdowns_mobile()):(this.bind_dropdowns_desktop(),this.scrollbar.on("on_position",this.hide_dropdowns))}on_show_dropdown_desktop(e){const s=e.target;this.open_dropdown=s;t(s).siblings("ul.dropdown-menu").css({top:this.height-1+"px",left:`${t(s).offset().left}px`})}on_hide_dropdown_desktop(e){const t=e.target;this.open_dropdown===t&&(this.open_dropdown=null)}bind_dropdowns_desktop(){this.elem.on("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.on("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop)}bind_dropdowns_mobile(){this.elem.off("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.off("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop)}hide_dropdowns(){this.elems.each((e,s)=>{t(s).dropdown("hide")})}destroy(){super.destroy(),this.elem.off("shown.bs.dropdown",".nav-link.dropdown-toggle",this.on_show_dropdown_desktop),this.elem.off("hidden.bs.dropdown",".nav-link.dropdown-toggle",this.on_hide_dropdown_desktop),this.scrollbar.off("on_position",this.hide_dropdowns),this.scrollbar.destroy()}}class O extends k{static initialize(e){const t=s.query_elem("#header-main",e);t&&new O(t)}constructor(e){super(e),this.elem=e,this.header_content=s.query_elem("#header-content",e),this.navbar_content_wrapper=s.query_elem("#navbar-content-wrapper",e),this.navbar_content=s.query_elem("#navbar-content",e),this.navbar_toggler=s.query_elem("#navbar-toggler",this.elem),this.personal_tools=s.query_elem("#personaltools",e),this.mainmenu=s.query_elem("#mainmenu",e),this.mainmenu_elems=t(".nav-link.dropdown-toggle",this.mainmenu),this.render_mobile_scrollbar=this.render_mobile_scrollbar.bind(this),this.mainmenu_elems.each((e,s)=>{t(s).on("shown.bs.dropdown",this.render_mobile_scrollbar),t(s).on("hidden.bs.dropdown",this.render_mobile_scrollbar)}),this.set_mobile_menu_open=this.set_mobile_menu_open.bind(this),this.set_mobile_menu_closed=this.set_mobile_menu_closed.bind(this),this.bind()}destroy(){super.destroy(),this.mobile_scrollbar&&(this.mobile_scrollbar.destroy(),this.mobile_scrollbar=null),this.mainmenu_elems.each((e,s)=>{t(s).off("shown.bs.dropdown",this.render_mobile_scrollbar),t(s).off("hidden.bs.dropdown",this.render_mobile_scrollbar)});const e=this.navbar_content_wrapper;e.off("show.bs.collapse shown.bs.collapse",this.set_mobile_menu_open),e.off("hide.bs.collapse hidden.bs.collapse",this.set_mobile_menu_closed)}render_mobile_scrollbar(){this.is_compact&&this.mobile_scrollbar&&this.mobile_scrollbar.render()}bind(){const e=this.navbar_content_wrapper;e.on("show.bs.collapse shown.bs.collapse",this.set_mobile_menu_open),e.on("hidden.bs.collapse",this.set_mobile_menu_closed)}set_mobile_menu_open(){this.elem.addClass("mobile-menu-open")}set_mobile_menu_closed(){this.elem.removeClass("mobile-menu-open")}on_is_compact(e){this.mobile_scrollbar&&(this.navbar_content.removeClass("scrollable-content"),this.mobile_scrollbar.destroy(),this.mobile_scrollbar=null),e?(this.elem.removeClass("full").removeClass("navbar-expand"),this.elem.addClass("compact"),this.navbar_content.addClass("scrollable-content"),this.mobile_scrollbar=new C(this.navbar_content_wrapper),this.navbar_content_wrapper.on("shown.bs.collapse",()=>{t("html, body").css("overscroll-behavior","none"),this.mobile_scrollbar.render()}),this.navbar_content_wrapper.on("hide.bs.collapse",()=>{t("html, body").css("overscroll-behavior","auto"),this.mobile_scrollbar.scrollbar.hide()})):(this.elem.removeClass("compact"),this.elem.addClass("full").addClass("navbar-expand"))}on_is_super_compact(e){const i=null!==s.query_elem("#personaltools",this.navbar_content);e?i||this.personal_tools.detach().appendTo(this.navbar_content):(i&&this.personal_tools.detach().prependTo(this.header_content),t(".dropdown-menu.show").removeClass("show"))}}class ${static initialize(e){const t=s.query_elem("#navtree",e);t&&new $(t)}constructor(e){this.elem=e,this.dropdown_elem=t("#navigation-collapse",e),this.dropdown_elem.hasClass("no-collapse")||(localStorage.getItem("cone.app.navtree.open")&&this.dropdown_elem.addClass("show"),this.set_menu_open=this.set_menu_open.bind(this),this.set_menu_closed=this.set_menu_closed.bind(this),this.dropdown_elem.on("shown.bs.collapse",this.set_menu_open),this.dropdown_elem.on("hidden.bs.collapse",this.set_menu_closed),s.ajax.attach(this,e))}set_menu_open(e){localStorage.setItem("cone.app.navtree.open","true")}set_menu_closed(e){localStorage.removeItem("cone.app.navtree.open")}destroy(){this.dropdown_elem.off(),this.elem.off()}}class A{constructor(e){this.options=e,this.selected=[],this.select_direction=0,this.firstclick=!0}reset(){this.selected=[]}add(e){this.remove(e),this.selected.push(e)}remove(e){let s=t.grep(this.selected,function(t,s){return t!==e});this.selected=s}select_no_key(e,t){e.children().removeClass("selected"),t.addClass("selected"),this.reset(),this.add(t.get(0))}select_ctrl_down(e){e.toggleClass("selected"),e.hasClass("selected")?this.add(e.get(0)):this.remove(e.get(0))}get_nearest(e,s){let i,o,l=e.children(".selected"),a=-1;return t(l).each(function(){o=t(this),i=o.index(),-1==a?a=i:s>i?this.select_direction>0?ia&&(a=i):s