From b22812ea85fb6c9e29b309a9e56e4c9eccb54f59 Mon Sep 17 00:00:00 2001 From: Reto Tschuppert Date: Fri, 16 Jan 2026 10:58:39 +0100 Subject: [PATCH 1/2] Revert layout directive --- src/onegov/org/app.py | 1 - src/onegov/org/directives.py | 27 --------------------------- src/onegov/town6/layout.py | 18 ------------------ 3 files changed, 46 deletions(-) diff --git a/src/onegov/org/app.py b/src/onegov/org/app.py index 00dff4796f..4cbf7b6dd3 100644 --- a/src/onegov/org/app.py +++ b/src/onegov/org/app.py @@ -69,7 +69,6 @@ class OrgApp(Framework, LibresIntegration, SearchApp, MapboxApp, event_search_widget = directive(directives.EventSearchWidgetAction) settings_view = directive(directives.SettingsView) boardlet = directive(directives.Boardlet) - layout = directive(directives.Layout) #: cronjob settings send_ticket_statistics = True diff --git a/src/onegov/org/directives.py b/src/onegov/org/directives.py index b41090fd6a..93b4f2aa6f 100644 --- a/src/onegov/org/directives.py +++ b/src/onegov/org/directives.py @@ -293,30 +293,3 @@ def perform( # type:ignore[override] 'order': self.order, 'icon': self.icon, } - - -class Layout(Action): - """ - Registers a layout for a model. This is used to show breadcrumbs - for search results. - """ - - config = { - 'layout_registry': dict - } - - def __init__(self, model: type) -> None: - self.model = model - - def identifier( # type:ignore[override] - self, - layout_registry: dict[type, Layout] - ) -> str: - return str(self.model) - - def perform( # type:ignore[override] - self, - layout: Layout, - layout_registry: dict[type, Layout] - ) -> None: - layout_registry[self.model] = layout diff --git a/src/onegov/town6/layout.py b/src/onegov/town6/layout.py index bd96ff5f7a..df153496e3 100644 --- a/src/onegov/town6/layout.py +++ b/src/onegov/town6/layout.py @@ -288,7 +288,6 @@ class SettingsLayout(OrgSettingsLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=Topic) class PageLayout(OrgTopicLayout, AdjacencyListLayout): app: TownApp @@ -301,7 +300,6 @@ def contact_html(self) -> str: ) -@TownApp.layout(model=News) class NewsLayout(OrgNewsLayout, AdjacencyListLayout): app: TownApp @@ -416,7 +414,6 @@ class FormCollectionLayout(OrgFormCollectionLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=FormDefinition) class FormDefinitionLayout(OrgFormDefinitionLayout, DefaultLayout): app: TownApp @@ -442,7 +439,6 @@ class PersonCollectionLayout(OrgPersonCollectionLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=Person) class PersonLayout(OrgPersonLayout, DefaultLayout): app: TownApp @@ -461,7 +457,6 @@ class ArchivedTicketsLayout(OrgArchivedTicketsLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=Ticket) class TicketLayout(OrgTicketLayout, DefaultLayout): app: TownApp @@ -574,7 +569,6 @@ class ResourceRecipientsFormLayout( request: TownRequest -@TownApp.layout(model=Resource) class ResourceLayout(OrgResourceLayout, DefaultLayout): app: TownApp @@ -655,7 +649,6 @@ def editbar_links(self) -> list[Link | LinkGroup]: return links -@TownApp.layout(model=Occurrence) class OccurrenceLayout(OrgOccurrenceLayout, DefaultLayout): app: TownApp @@ -686,7 +679,6 @@ def editbar_links(self) -> list[Link | LinkGroup]: cls_before='EventLayout', cls_after='TicketChatMessageLayout' ) -@TownApp.layout(model=Event) class EventLayout(StepsLayoutExtension, OrgEventLayout, DefaultLayout): app: TownApp @@ -727,7 +719,6 @@ class ImageSetCollectionLayout(OrgImageSetCollectionLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=ImageSet) class ImageSetLayout(OrgImageSetLayout, DefaultLayout): app: TownApp @@ -740,7 +731,6 @@ class UserManagementLayout(OrgUserManagementLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=User) class UserLayout(OrgUserLayout, DefaultLayout): app: TownApp @@ -798,7 +788,6 @@ class DirectoryCollectionLayout(OrgDirectoryCollectionLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=Directory) class DirectoryLayout(OrgDirectoryLayout, DefaultLayout): app: TownApp @@ -932,7 +921,6 @@ def links() -> Iterator[Link | LinkGroup]: @step_sequences.registered_step(1, _('Form'), cls_after='FormSubmissionLayout') -@TownApp.layout(model=DirectoryEntry) class DirectoryEntryLayout( StepsLayoutExtension, OrgDirectoryEntryLayout, @@ -968,7 +956,6 @@ class DashboardLayout(OrgDashboardLayout, DefaultLayout): request: TownRequest -@TownApp.layout(model=GeneralFile) class GeneralFileCollectionLayout(DefaultLayout): def __init__(self, model: Any, request: TownRequest) -> None: @@ -1146,7 +1133,6 @@ def editbar_links(self) -> list[LinkGroup] | None: return None -@TownApp.layout(model=Meeting) class MeetingLayout(DefaultLayout): @cached_property @@ -1216,7 +1202,6 @@ def editbar_links(self) -> list[Link | LinkGroup] | None: return None -@TownApp.layout(model=MeetingItem) class MeetingItemLayout(DefaultLayout): @cached_property @@ -1275,7 +1260,6 @@ def editbar_links(self) -> list[LinkGroup] | None: return None -@TownApp.layout(model=RISParliamentarian) class RISParliamentarianLayout(DefaultLayout): @cached_property @@ -1457,7 +1441,6 @@ def editbar_links(self) -> list[LinkGroup] | None: return None -@TownApp.layout(model=RISParliamentaryGroup) class RISParliamentaryGroupLayout(DefaultLayout): @cached_property @@ -1593,7 +1576,6 @@ def editbar_links(self) -> list[LinkGroup] | None: return None -@TownApp.layout(model=RISCommission) class RISCommissionLayout(DefaultLayout): @cached_property From 3b6a01865b99d249804afdfb18279b1f33208127 Mon Sep 17 00:00:00 2001 From: Reto Tschuppert Date: Fri, 16 Jan 2026 11:01:43 +0100 Subject: [PATCH 2/2] Switching to dispatch method (no success) --- src/onegov/core/framework.py | 16 ++++++++++++++++ src/onegov/org/app.py | 9 ++++++++- src/onegov/org/request.py | 14 ++------------ src/onegov/town6/app.py | 11 ++++++++++- src/onegov/town6/layout.py | 17 ++++++++++++++++- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/onegov/core/framework.py b/src/onegov/core/framework.py index 085a5c5d47..79fa25c97d 100644 --- a/src/onegov/core/framework.py +++ b/src/onegov/core/framework.py @@ -37,6 +37,7 @@ class MyApplication(Framework): from functools import cached_property, wraps from itsdangerous import BadSignature, Signer from libres.db.models import ORMBase +from morepath import dispatch_method from morepath.publish import resolve_model, get_view_name from more.content_security import ContentSecurityApp from more.content_security import ContentSecurityPolicy @@ -46,6 +47,8 @@ class MyApplication(Framework): from more.webassets import WebassetsApp from more.webassets.core import webassets_injector_tween from more.webassets.tweens import METHODS, CONTENT_TYPES +from reg import ClassIndex + from onegov.core import cache, log, utils from onegov.core import directives from onegov.core.crypto import stored_random_token @@ -68,6 +71,7 @@ class MyApplication(Framework): from typing import overload, Any, Literal, TypeVar, TYPE_CHECKING + if TYPE_CHECKING: from _typeshed import StrPath from _typeshed.wsgi import WSGIApplication, WSGIEnvironment, StartResponse @@ -1552,6 +1556,18 @@ def decrypt(self, cyphertext: bytes) -> str: self.hashed_identity_key ).decrypt(cyphertext).decode('utf-8') + @dispatch_method('model') + def get_layout_class(self, model: object) -> type | None: + return None + + +@Framework.predicate(Framework.get_layout_class, name='model', default=None, index=ClassIndex) +def model_predicate(self, model: object) -> type: + # return model if isinstance(model, type) else model.__class__ + retval = model if isinstance(model, type) else model.__class__ + print('*** tschupre model predicate called for object:', model, 'retval:', retval) + return retval + @Framework.webasset_url() def get_webasset_url() -> str: diff --git a/src/onegov/org/app.py b/src/onegov/org/app.py index 4cbf7b6dd3..252931b58a 100644 --- a/src/onegov/org/app.py +++ b/src/onegov/org/app.py @@ -12,7 +12,7 @@ from more.content_security import NONE from more.content_security.core import content_security_policy_tween_factory from onegov.core import Framework, utils -from onegov.core.framework import default_content_security_policy +from onegov.core.framework import default_content_security_policy, model_predicate from onegov.core.i18n import default_locale_negotiator from onegov.core.orm.cache import orm_cached, request_cached from onegov.core.templates import PageTemplate, render_template @@ -24,6 +24,7 @@ from onegov.org.auth import MTANAuth from onegov.org.exceptions import MTANAccessLimitExceeded from onegov.org.initial_content import create_new_organisation +from onegov.org.layout import Layout, DefaultLayout from onegov.org.models import Dashboard, Organisation, PublicationCollection from onegov.org.request import OrgRequest from onegov.org.theme import OrgTheme @@ -491,6 +492,12 @@ def redirect_after_login( return URL(request.link(dashboard)).path() +@OrgApp.predicate_fallback(OrgApp.get_layout_class, model_predicate) +def model_not_found(self, model: object) -> type[Layout]: + print('*** tschupre using DEFAULTLAYOUT ORG for model', model) + return DefaultLayout + + @OrgApp.webasset_path() def get_shared_assets_path() -> str: return utils.module_path('onegov.shared', 'assets/js') diff --git a/src/onegov/org/request.py b/src/onegov/org/request.py index 1c3e885fc0..a03914ccf9 100644 --- a/src/onegov/org/request.py +++ b/src/onegov/org/request.py @@ -289,16 +289,6 @@ def get_layout(self, model: object) -> Layout | DefaultLayout: """ Get the registered layout for a model instance. """ - layout_registry = self.app.config.layout_registry - model_type = model if isinstance(model, type) else type(model) - - layout_class = None - for cls in model_type.mro(): - layout_class = layout_registry.get(cls) - if layout_class: - break - - if layout_class is None: - layout_class = DefaultLayout - + layout_class = self.app.get_layout_class(model) + assert layout_class is not None return layout_class(model, self) diff --git a/src/onegov/town6/app.py b/src/onegov/town6/app.py index 4c7b8888a6..80ac5d3187 100644 --- a/src/onegov/town6/app.py +++ b/src/onegov/town6/app.py @@ -6,7 +6,8 @@ from sedate import replace_timezone from onegov.api import ApiApp -from onegov.core import utils +from onegov.core import utils, Framework +from onegov.core.framework import model_predicate from onegov.core.i18n import default_locale_negotiator from onegov.core.templates import render_template from onegov.core.utils import module_path @@ -30,6 +31,7 @@ from onegov.core.types import RenderData from onegov.org.exceptions import MTANAccessLimitExceeded from onegov.org.models import Organisation + from onegov.town6.layout import Layout, DefaultLayout from onegov.town6.request import TownRequest @@ -76,6 +78,13 @@ def chat_open(self, request: TownRequest) -> bool: return False +@TownApp.predicate_fallback(TownApp.get_layout_class, model_predicate) +def model_not_found(self, model: object) -> type[Layout]: + print('*** tschupre using DEFAULTLAYOUT TOWN6 for model', model) + from onegov.town6.layout import DefaultLayout + return DefaultLayout + + @TownApp.webasset_path() def get_shared_assets_path() -> str: return utils.module_path('onegov.shared', 'assets/js') diff --git a/src/onegov/town6/layout.py b/src/onegov/town6/layout.py index df153496e3..dea54f79ac 100644 --- a/src/onegov/town6/layout.py +++ b/src/onegov/town6/layout.py @@ -3,6 +3,7 @@ import secrets from functools import cached_property +from onegov.core import Framework from onegov.core.elements import Confirm, Intercooler, Link, LinkGroup from onegov.core.static import StaticFile from onegov.core.utils import append_query_param, to_html_ul @@ -1668,7 +1669,21 @@ def editbar_links(self) -> list[LinkGroup] | None: return None -@TownApp.layout(model=PoliticalBusiness) +def register_layout_for(model): + def decorator(cls): + @TownApp.get_layout_class.register(model=model) + def _factory(self, model: object): + print(f'*** tschupre matched! Returning {cls.__name} for {model}') + return cls + + # Debug: verify registration happened + print(f'*** tschupre registered {cls.__name__} for {model}') + + return cls + return decorator + + +@register_layout_for(PoliticalBusiness) class PoliticalBusinessLayout(DefaultLayout): @cached_property