diff --git a/src/onegov/org/app.py b/src/onegov/org/app.py
index 4cbf7b6dd3..00dff4796f 100644
--- a/src/onegov/org/app.py
+++ b/src/onegov/org/app.py
@@ -69,6 +69,7 @@ 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 93b4f2aa6f..b41090fd6a 100644
--- a/src/onegov/org/directives.py
+++ b/src/onegov/org/directives.py
@@ -293,3 +293,30 @@ 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/org/exports/base.py b/src/onegov/org/exports/base.py
index e2446bcc52..b76fc3fb87 100644
--- a/src/onegov/org/exports/base.py
+++ b/src/onegov/org/exports/base.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from onegov.org.models import Export
-from onegov.town6 import _
+from onegov.org import _
from typing import Any, TYPE_CHECKING
diff --git a/src/onegov/org/layout.py b/src/onegov/org/layout.py
index 14aed066b9..a78382fa55 100644
--- a/src/onegov/org/layout.py
+++ b/src/onegov/org/layout.py
@@ -36,7 +36,7 @@
from onegov.org import _
from onegov.org import utils
from onegov.org.exports.base import OrgExport
-from onegov.org.models import CitizenDashboard
+from onegov.org.models import CitizenDashboard, GeneralFile
from onegov.org.models import ExportCollection, Editor
from onegov.org.models import GeneralFileCollection
from onegov.org.models import ImageFile
@@ -289,6 +289,21 @@ def get_link(locale: str) -> str:
for locale in sorted(self.app.locales)
]
+ @cached_property
+ def files_url(self) -> str:
+ """ Returns the url to the files view. """
+ url = self.request.link(
+ GeneralFileCollection(self.request.session)
+ )
+ return self.csrf_protected_url(url)
+
+ def files_url_with_anchor(self, file: GeneralFile | None) -> str:
+ """ Returns the url to the files view including anchor. """
+ if file is None:
+ return self.files_url
+
+ return f'{self.files_url}#{file.name}'
+
@cached_property
def file_upload_url(self) -> str:
""" Returns the url to the file upload action. """
@@ -1329,6 +1344,21 @@ def editbar_links(self) -> list[Link | LinkGroup] | None:
return None
+class FormDefinitionLayout(DefaultLayout):
+
+ @property
+ def forms_url(self) -> str:
+ return self.request.class_link(FormCollection)
+
+ @cached_property
+ def breadcrumbs(self) -> list[Link]:
+ return [
+ Link(_('Homepage'), self.homepage_url),
+ Link(_('Forms'), self.forms_url),
+ Link(self.model.title, self.request.link(self.model))
+ ]
+
+
class SurveySubmissionWindowLayout(DefaultLayout):
@cached_property
def breadcrumbs(self) -> list[Link]:
@@ -1761,7 +1791,8 @@ def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
Link(_('Tickets'), get_current_tickets_url(self.request)),
- Link(self.model.number, '#')
+ Link(self.model.number, self.request.link(
+ TicketCollection(self.request.session).by_id(self.model.id)))
]
@cached_property
@@ -3427,7 +3458,9 @@ def og_description(self) -> str:
def breadcrumbs(self) -> list[Link]:
return [
Link(_('Homepage'), self.homepage_url),
- Link(_('Directories'), '#')
+ Link(_('Directories'), self.request.class_link(
+ DirectoryCollection
+ )),
]
@cached_property
@@ -3451,6 +3484,19 @@ def editbar_links(self) -> list[Link | LinkGroup] | None:
return None
+class DirectoryLayout(DefaultLayout):
+
+ @cached_property
+ def breadcrumbs(self) -> list[Link]:
+ return [
+ Link(_('Homepage'), self.homepage_url),
+ Link(_('Directories'), self.request.class_link(
+ DirectoryCollection
+ )),
+ Link(self.model.title, self.request.link(self.model))
+ ]
+
+
class DirectoryEntryMixin:
request: OrgRequest
diff --git a/src/onegov/org/request.py b/src/onegov/org/request.py
index e73c3f8636..1c3e885fc0 100644
--- a/src/onegov/org/request.py
+++ b/src/onegov/org/request.py
@@ -6,6 +6,7 @@
from onegov.core.request import CoreRequest
from onegov.core.security import Private
from onegov.core.utils import normalize_for_url
+from onegov.org.layout import DefaultLayout, Layout
from onegov.org.models import News, TANAccessCollection, Topic
from onegov.page import Page, PageCollection
from onegov.user import User
@@ -283,3 +284,21 @@ def analytics_provider(self) -> AnalyticsProvider | None:
if name := self.app.org.analytics_provider_name:
return self.app.available_analytics_providers.get(name)
return None
+
+ 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
+
+ return layout_class(model, self)
diff --git a/src/onegov/town6/layout.py b/src/onegov/town6/layout.py
index f16bc9e222..bd96ff5f7a 100644
--- a/src/onegov/town6/layout.py
+++ b/src/onegov/town6/layout.py
@@ -8,9 +8,9 @@
from onegov.core.utils import append_query_param, to_html_ul
from onegov.chat.collections import ChatCollection
from onegov.chat.models import Chat
-from onegov.directory import DirectoryCollection
-from onegov.event import OccurrenceCollection
-from onegov.form import FormCollection
+from onegov.directory import DirectoryCollection, DirectoryEntry, Directory
+from onegov.event import Event
+from onegov.event import OccurrenceCollection, Occurrence
from onegov.org.elements import QrCodeLink, IFrameLink
from onegov.org.layout import (
Layout as OrgLayout,
@@ -22,6 +22,7 @@
ArchivedTicketsLayout as OrgArchivedTicketsLayout,
DashboardLayout as OrgDashboardLayout,
DirectoryCollectionLayout as OrgDirectoryCollectionLayout,
+ DirectoryLayout as OrgDirectoryLayout,
DirectoryEntryCollectionLayout as OrgDirectoryEntryCollectionLayout,
DirectoryEntryLayout as OrgDirectoryEntryLayout,
EditorLayout as OrgEditorLayout,
@@ -30,6 +31,7 @@
ExternalLinkLayout as OrgExternalLinkLayout,
FindYourSpotLayout as OrgFindYourSpotLayout,
FormCollectionLayout as OrgFormCollectionLayout,
+ FormDefinitionLayout as OrgFormDefinitionLayout,
SurveyCollectionLayout as OrgSurveyCollectionLayout,
FormEditorLayout as OrgFormEditorLayout,
FormSubmissionLayout as OrgFormSubmissionLayout,
@@ -42,7 +44,7 @@
MessageCollectionLayout as OrgMessageCollectionLayout,
NewsLayout as OrgNewsLayout,
NewsletterLayout as OrgNewsletterLayout,
- PageLayout as OrgPageLayout,
+ PageLayout as OrgTopicLayout,
PaymentCollectionLayout as OrgPaymentCollectionLayout,
PaymentProviderLayout as OrgPaymentProviderLayout,
PersonCollectionLayout as OrgPersonCollectionLayout,
@@ -69,32 +71,46 @@
UserGroupLayout as OrgUserGroupLayout,
UserGroupCollectionLayout as OrgUserGroupCollectionLayout,
UserManagementLayout as OrgUserManagementLayout)
+from onegov.form import FormDefinition
+from onegov.org.models import GeneralFile
+from onegov.org.models import ImageSet
+from onegov.org.models import Meeting
from onegov.org.models import MeetingCollection
+from onegov.org.models import MeetingItem
+from onegov.org.models import News
from onegov.org.models import PageMove
+from onegov.org.models import PoliticalBusiness
from onegov.org.models import PoliticalBusinessCollection
+from onegov.org.models import RISCommission
from onegov.org.models import RISCommissionCollection
+from onegov.org.models import RISParliamentarian
from onegov.org.models import RISParliamentarianCollection
+from onegov.org.models import RISParliamentaryGroup
from onegov.org.models import RISParliamentaryGroupCollection
+from onegov.org.models import Topic
from onegov.org.models.directory import ExtendedDirectoryEntryCollection
from onegov.page import PageCollection
+from onegov.people import Person
+from onegov.reservation import Resource
from onegov.stepsequence import step_sequences
from onegov.stepsequence.extension import StepsLayoutExtension
+from onegov.ticket import Ticket
+from onegov.user import User
from onegov.town6 import _
from onegov.town6.theme import user_options
+from onegov.town6 import TownApp
from typing import Any, NamedTuple, TypeVar, TYPE_CHECKING
+
if TYPE_CHECKING:
- from collections.abc import Iterator
- from onegov.event import Event
- from onegov.form import FormDefinition, FormSubmission
+ from collections.abc import Iterator, Sequence
+ from onegov.form import FormSubmission
from onegov.form.models.definition import SurveyDefinition
from onegov.form.models.submission import SurveySubmission
from onegov.org.models import ExtendedDirectoryEntry
from onegov.org.request import PageMeta
from onegov.page import Page
- from onegov.reservation import Resource
- from onegov.ticket import Ticket
from onegov.town6.app import TownApp
from onegov.town6.request import TownRequest
from typing import TypeAlias
@@ -272,7 +288,8 @@ class SettingsLayout(OrgSettingsLayout, DefaultLayout):
request: TownRequest
-class PageLayout(OrgPageLayout, AdjacencyListLayout):
+@TownApp.layout(model=Topic)
+class PageLayout(OrgTopicLayout, AdjacencyListLayout):
app: TownApp
request: TownRequest
@@ -284,6 +301,7 @@ def contact_html(self) -> str:
)
+@TownApp.layout(model=News)
class NewsLayout(OrgNewsLayout, AdjacencyListLayout):
app: TownApp
@@ -397,9 +415,12 @@ class FormCollectionLayout(OrgFormCollectionLayout, DefaultLayout):
app: TownApp
request: TownRequest
- @property
- def forms_url(self) -> str:
- return self.request.class_link(FormCollection)
+
+@TownApp.layout(model=FormDefinition)
+class FormDefinitionLayout(OrgFormDefinitionLayout, DefaultLayout):
+
+ app: TownApp
+ request: TownRequest
class SurveySubmissionWindowLayout(OrgSurveySubmissionWindowLayout,
@@ -421,6 +442,7 @@ class PersonCollectionLayout(OrgPersonCollectionLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=Person)
class PersonLayout(OrgPersonLayout, DefaultLayout):
app: TownApp
@@ -439,6 +461,7 @@ class ArchivedTicketsLayout(OrgArchivedTicketsLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=Ticket)
class TicketLayout(OrgTicketLayout, DefaultLayout):
app: TownApp
@@ -551,6 +574,7 @@ class ResourceRecipientsFormLayout(
request: TownRequest
+@TownApp.layout(model=Resource)
class ResourceLayout(OrgResourceLayout, DefaultLayout):
app: TownApp
@@ -631,6 +655,7 @@ def editbar_links(self) -> list[Link | LinkGroup]:
return links
+@TownApp.layout(model=Occurrence)
class OccurrenceLayout(OrgOccurrenceLayout, DefaultLayout):
app: TownApp
@@ -661,6 +686,7 @@ def editbar_links(self) -> list[Link | LinkGroup]:
cls_before='EventLayout',
cls_after='TicketChatMessageLayout'
)
+@TownApp.layout(model=Event)
class EventLayout(StepsLayoutExtension, OrgEventLayout, DefaultLayout):
app: TownApp
@@ -701,6 +727,7 @@ class ImageSetCollectionLayout(OrgImageSetCollectionLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=ImageSet)
class ImageSetLayout(OrgImageSetLayout, DefaultLayout):
app: TownApp
@@ -713,6 +740,7 @@ class UserManagementLayout(OrgUserManagementLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=User)
class UserLayout(OrgUserLayout, DefaultLayout):
app: TownApp
@@ -770,6 +798,13 @@ class DirectoryCollectionLayout(OrgDirectoryCollectionLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=Directory)
+class DirectoryLayout(OrgDirectoryLayout, DefaultLayout):
+
+ app: TownApp
+ request: TownRequest
+
+
@step_sequences.registered_step(
1, _('Form'), cls_after='FormSubmissionLayout'
)
@@ -897,6 +932,7 @@ def links() -> Iterator[Link | LinkGroup]:
@step_sequences.registered_step(1, _('Form'), cls_after='FormSubmissionLayout')
+@TownApp.layout(model=DirectoryEntry)
class DirectoryEntryLayout(
StepsLayoutExtension,
OrgDirectoryEntryLayout,
@@ -932,6 +968,7 @@ class DashboardLayout(OrgDashboardLayout, DefaultLayout):
request: TownRequest
+@TownApp.layout(model=GeneralFile)
class GeneralFileCollectionLayout(DefaultLayout):
def __init__(self, model: Any, request: TownRequest) -> None:
@@ -944,6 +981,18 @@ def __init__(self, model: Any, request: TownRequest) -> None:
request.include('upload')
request.include('prompt')
+ @cached_property
+ def breadcrumbs(self) -> Sequence[Link]:
+ name = self.model.name[:40]
+ if len(name) == 40:
+ name = name[:37] + '...'
+
+ return [
+ Link(_('Homepage'), self.homepage_url),
+ Link(_('Files'), self.files_url),
+ Link(name, self.files_url_with_anchor(self.model)),
+ ]
+
class ImageFileCollectionLayout(DefaultLayout):
@@ -1097,6 +1146,7 @@ def editbar_links(self) -> list[LinkGroup] | None:
return None
+@TownApp.layout(model=Meeting)
class MeetingLayout(DefaultLayout):
@cached_property
@@ -1113,11 +1163,18 @@ def og_description(self) -> str:
@cached_property
def breadcrumbs(self) -> list[Link]:
+ title = (
+ self.title + ' - ' +
+ self.format_date(self.model.start_datetime, 'date')
+ if self.model.start_datetime
+ else self.title
+ )
+
return [
Link(_('Homepage'), self.homepage_url),
Link(_('RIS Settings'), self.ris_overview_url),
Link(_('Meetings'), self.request.class_link(MeetingCollection)),
- Link(self.title, self.request.link(self.model)),
+ Link(title, self.request.link(self.model)),
]
@cached_property
@@ -1159,6 +1216,29 @@ def editbar_links(self) -> list[Link | LinkGroup] | None:
return None
+@TownApp.layout(model=MeetingItem)
+class MeetingItemLayout(DefaultLayout):
+
+ @cached_property
+ def breadcrumbs(self) -> list[Link]:
+ title = (
+ self.model.meeting.title + ' - ' +
+ self.format_date(self.model.meeting.start_datetime, 'date')
+ if self.model.meeting.start_datetime
+ else self.model.meeting.title
+ )
+
+ return [
+ Link(_('Homepage'), self.homepage_url),
+ Link(_('RIS Settings'), self.ris_overview_url),
+ Link(
+ _('Meetings'),
+ self.request.class_link(MeetingCollection)
+ ),
+ Link(title, self.request.link(self.model, fragment=self.model.title))
+ ]
+
+
class RISParliamentarianCollectionLayout(DefaultLayout):
@cached_property
@@ -1195,6 +1275,7 @@ def editbar_links(self) -> list[LinkGroup] | None:
return None
+@TownApp.layout(model=RISParliamentarian)
class RISParliamentarianLayout(DefaultLayout):
@cached_property
@@ -1376,6 +1457,7 @@ def editbar_links(self) -> list[LinkGroup] | None:
return None
+@TownApp.layout(model=RISParliamentaryGroup)
class RISParliamentaryGroupLayout(DefaultLayout):
@cached_property
@@ -1511,6 +1593,7 @@ def editbar_links(self) -> list[LinkGroup] | None:
return None
+@TownApp.layout(model=RISCommission)
class RISCommissionLayout(DefaultLayout):
@cached_property
@@ -1603,6 +1686,7 @@ def editbar_links(self) -> list[LinkGroup] | None:
return None
+@TownApp.layout(model=PoliticalBusiness)
class PoliticalBusinessLayout(DefaultLayout):
@cached_property
diff --git a/src/onegov/town6/templates/macros.pt b/src/onegov/town6/templates/macros.pt
index b8fcb8b5ae..583b1b4a8a 100644
--- a/src/onegov/town6/templates/macros.pt
+++ b/src/onegov/town6/templates/macros.pt
@@ -1933,16 +1933,28 @@
+
+
+ ${result.title}
-
@@ -1950,7 +1962,7 @@
-
@@ -1982,6 +1996,9 @@
${result.function}
-People
+
- ${result.lead[:160]}
No agenda items defined yet.