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}
-

+

${translate(result.role.capitalize())} -

+
+
+ +
-
@@ -1950,7 +1962,7 @@
-

+

Has a digital seal, @@ -1963,11 +1975,13 @@ ${result.stats.get('pages', 0)} pages -

+
+
+ +
-
@@ -1982,6 +1996,9 @@

+
+ +
@@ -2026,6 +2043,9 @@ +
+ +
@@ -2033,8 +2053,11 @@
${result.title}
-

${result.function}

-

People

+
${result.function}
+
People
+
+ +
@@ -2056,10 +2079,13 @@
${result.title}
-

- ${result.lead[:160]} -

+
+ ${result.lead[:260]} +
+
+ +
@@ -2797,7 +2823,7 @@ file_upload file.upload_date|file.created; file_publish_end file.publish_end_date|None; "> - + ${file.name} diff --git a/src/onegov/town6/templates/meeting.pt b/src/onegov/town6/templates/meeting.pt index 5dcb4d3fcc..f50b8464f2 100644 --- a/src/onegov/town6/templates/meeting.pt +++ b/src/onegov/town6/templates/meeting.pt @@ -39,7 +39,7 @@

No agenda items defined yet.