Source code for town6.homepage_widgets

import lxml.etree
import requests
from datetime import datetime

from onegov.event import OccurrenceCollection
from onegov.form import FormCollection
from onegov.org.elements import Link, LinkGroup

from onegov.org.homepage_widgets import (
    NewsWidget as OrgNewsWidget,
    DirectoriesWidget as OrgDirectoriesWidget,
    get_lead)
from onegov.org.models import PublicationCollection
from onegov.people import PersonCollection
from onegov.reservation import ResourceCollection

from onegov.town6 import TownApp
from onegov.town6 import _


from typing import NamedTuple, TYPE_CHECKING
if TYPE_CHECKING:
    from collections.abc import Iterator
    from lxml.etree import _Element
    from onegov.core.types import RenderData
    from onegov.town6.layout import DefaultLayout


@TownApp.homepage_widget(tag='row')
[docs] class RowWidget:
[docs] template = """ <xsl:template match="row"> <div class="grid-container"> <div class="grid-x grid-padding-x {@class}"> <xsl:apply-templates select="node()"/> </div> </div> </xsl:template> """
@TownApp.homepage_widget(tag='row-wide')
[docs] class RowWidgetWide:
[docs] template = """ <xsl:template match="row-wide"> <div class="grid-container full {@bgcolor}"> <div class="grid-x {@class}"> <xsl:apply-templates select="node()"/> </div> </div> </xsl:template> """
@TownApp.homepage_widget(tag='column')
[docs] class ColumnWidget:
[docs] template = """ <xsl:template match="column"> <div class="small-12 medium-{@span} cell"> <xsl:apply-templates select="node()"/> </div> </xsl:template> """
@TownApp.homepage_widget(tag='title')
[docs] class TitleWidget:
[docs] template = """ <xsl:template match="title"> <h3 class="{@class}"> <xsl:apply-templates select="node()"/> </h3> </xsl:template> """
@TownApp.homepage_widget(tag='autoplay_video')
[docs] class AutoplayVideoWidget:
[docs] template = """ <xsl:template match="autoplay_video"> <div metal:use-macro="layout.macros.autoplay_video" tal:define="max_height '{@max-height}'; link_mp4 '{@link_mp4}'; link_mp4_low_res '{@link_mp4_low_res}'; link_webm '{@link_webm}'; link_webm_low_res '{@link_webm_low_res}'; text '{@text}' " /> </xsl:template> """
@TownApp.homepage_widget(tag='random_videos')
[docs] class RandomVideosWidget:
[docs] template = """ <xsl:template match="random_videos"> <div id="random-video"> <xsl:apply-templates select="node()" /> </div> </xsl:template> """
@TownApp.homepage_widget(tag='icon_link')
[docs] class IconLinksWidget:
[docs] template = """ <xsl:template match="icon_link"> <div metal:use-macro="layout.macros.icon_link" tal:define=" title '{@title}'; invert '{@invert}'; icon '{@icon}'; text '{@text}'; link '{@link}'; " /> </xsl:template> """
@TownApp.homepage_widget(tag='text')
[docs] class TextWidget:
[docs] template = """ <xsl:template match="text"> <p class="homepage-text"> <xsl:apply-templates select="node()"/> </p> </xsl:template> """
@TownApp.homepage_widget(tag='links')
[docs] class LinksWidget:
[docs] template = """ <xsl:template match="links"> <ul class="panel-links"> <xsl:for-each select="link"> <li> <a> <xsl:attribute name="href"> <xsl:value-of select="@url" /> </xsl:attribute> <xsl:value-of select="node()" /> </a> <xsl:if test="@description"> <small> <xsl:value-of select="@description" /> </small> </xsl:if> </li> </xsl:for-each> </ul> </xsl:template> """
@TownApp.homepage_widget(tag='news')
[docs] class NewsWidget(OrgNewsWidget):
[docs] news_limit = 3
[docs] template = """ <xsl:template match="news"> <div metal:use-macro="layout.macros.newslist" tal:define="heading 'h5'; show_all_news_link True; hide_date False" /> </xsl:template> """
[docs] class EventCard(NamedTuple):
[docs] text: str
[docs] url: str
[docs] subtitle: str
[docs] image_url: str | None
[docs] location: str | None
[docs] lead: str
@TownApp.homepage_widget(tag='events')
[docs] class EventsWidget:
[docs] template = """ <xsl:template match="events"> <metal:block use-macro="layout.macros['event-cards']" tal:define="with_lead True" /> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': occurrences = OccurrenceCollection(layout.app.session()).query() occurrences = occurrences.limit(layout.org.event_limit_homepage) event_links = [ EventCard( text=o.title, url=layout.request.link(o), subtitle=( layout.format_date( o.localized_start, 'event_short').title() + ', ' + layout.format_time_range( o.localized_start, o.localized_end).title()), image_url=( layout.request.link(o.event.image) if o.event.image else None ), location=o.location, lead=get_lead(o.event.title) ) for o in occurrences ] latest_events = LinkGroup( title=_('Events'), links=event_links, # type:ignore[arg-type] ) if event_links else None return { 'event_panel': latest_events, 'all_events_link': Link( text=_('All events'), url=layout.events_url, classes=('more-link', ) ), }
@TownApp.homepage_widget(tag='partners')
[docs] class PartnerWidget:
[docs] template = """ <xsl:template match="partners"> <xsl:variable name="apos">'</xsl:variable> <xsl:variable name="show_title"> <xsl:choose> <xsl:when test="@hide-title"> <xsl:value-of select="'False'" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="'True'" /> </xsl:otherwise> </xsl:choose> </xsl:variable> <metal:block use-macro="layout.macros['partner-cards']"> <xsl:attribute name="tal:define"> <xsl:value-of select="concat( 'title ', $apos, @title, $apos, '; ', 'show_title ', $show_title, ';')" /> </xsl:attribute> </metal:block> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': return {'partners': layout.partners}
@TownApp.homepage_widget(tag='services')
[docs] class ServicesWidget:
[docs] template = """ <xsl:template match="services"> <div class="services-panel"> <ul class="panel-links callout"> <li tal:repeat="link services_panel.links"> <tal:b content="structure link(layout)" /> </li> <xsl:for-each select="link"> <li tal:define="icon '{@icon}'"> <a tal:attributes=" class ('h5 fa fa-' + icon) if icon else 'generic h5' "> <xsl:attribute name="href"> <xsl:value-of select="@url" /> </xsl:attribute> <xsl:value-of select="node()" /> </a> </li> </xsl:for-each> </ul> </div> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': return { 'services_panel': LinkGroup(_('Services'), links=tuple( self.get_service_links(layout) )) }
@TownApp.homepage_widget(tag='contacts_and_albums')
[docs] class ContactsAndAlbumsWidget:
[docs] template = """ <xsl:template match="contacts_and_albums"> <div class="contacts-albums-panel"> <metal:block use-macro="layout.macros['panel-links']" tal:define="panel contacts_and_albums_panel; classes ['more-list']" /> </div> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': request = layout.request return { 'contacts_and_albums_panel': LinkGroup( title=_('Contacts'), links=[ Link( text=_('People'), url=request.class_link(PersonCollection), subtitle=_('All contacts'), classes=('list-link list-title',) ) ] ) }
@TownApp.homepage_widget(tag='directories')
[docs] class DirectoriesWidget(OrgDirectoriesWidget):
[docs] template = """ <xsl:template match="directories"> <metal:block use-macro="layout.macros['directories-list']" /> </xsl:template> """
@TownApp.homepage_widget(tag='focus')
[docs] class FocusWidget:
[docs] template = """ <xsl:template match="focus"> <a href="{@focus-url}" class="focus-link"> <div class="focus-widget card" data-aos="fade"> <xsl:if test="@text-on-image = 'True'"> <xsl:attribute name="class">focus-widget card only-title </xsl:attribute> </xsl:if> <xsl:variable name="apos">'</xsl:variable> <xsl:variable name="image_src"> <xsl:choose> <xsl:when test="@image-src"> <xsl:value-of select="concat($apos, @image-src, $apos)" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="'None'" /> </xsl:otherwise> </xsl:choose> </xsl:variable> <metal:block use-macro="layout.macros['focus-panel']" tal:define="image_src '{@image-src}'; title '{@title}'; text_on_image '{@text-on-image}'; lead '{@lead}';" /> <xsl:choose> <xsl:when test="@text-on-image"> <xsl:if test="text"> <div class="card-section"> <xsl:for-each select="text"> <p class="homepage-text"> <xsl:apply-templates select="node()"/> </p> </xsl:for-each> </div> </xsl:if> </xsl:when> <xsl:otherwise> <div class="card-section"> <h5> <xsl:choose> <xsl:when test="@title"> <xsl:value-of select="@title" /> </xsl:when> <xsl:otherwise> <metal:block use-macro="layout.macros['focus-title']" /> </xsl:otherwise> </xsl:choose> </h5> <xsl:choose> <xsl:when test="@lead"> <p><b> <xsl:value-of select="@lead" /> </b></p> </xsl:when> <xsl:otherwise> </xsl:otherwise> </xsl:choose> <xsl:for-each select="text"> <p class="homepage-text"> <xsl:apply-templates select="node()"/> </p> </xsl:for-each> </div> </xsl:otherwise> </xsl:choose> </div> </a> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': return {}
@TownApp.homepage_widget(tag='testimonial')
[docs] class TestimonialsWidget:
[docs] template = """ <xsl:template match="testimonial"> <div metal:use-macro="layout.macros.testimonial" tal:define="description '{@description}'; quote '{@quote}'; image '{@image}'; " /> </xsl:template> """
@TownApp.homepage_widget(tag='testimonial_slider')
[docs] class TestimonialSliderWidget:
[docs] template = """ <xsl:template match="testimonial_slider"> <div metal:use-macro="layout.macros.testimonial_slider" tal:define="color '{@color}'; description_1 '{@description_1}'; quote_1 '{@quote_1}'; image_1 '{@image_1}'; description_2 '{@description_2}'; quote_2 '{@quote_2}'; image_2 '{@image_2}'; description_3 '{@description_3}'; quote_3 '{@quote_3}'; image_3 '{@image_3}'; " /> </xsl:template> """
@TownApp.homepage_widget(tag='jobs')
[docs] class JobsWidget:
[docs] template = """ <xsl:template match="jobs"> <div metal:use-macro="layout.macros['jobs-cards']" tal:define="jobs_card_title '{@jobs_card_title}'; rss_feed '{@rss_feed}'; " /> </xsl:template> """
[docs] def get_variables(self, layout: 'DefaultLayout') -> 'RenderData': def rss_widget_builder(rss_feed_url: str) -> 'RSSChannel | None': """ Builds and caches widget data from the given RSS URL. Note that this is called within the <?python> tag in the macro. This is done so we can get the ``rss_feed_url`` which itself is a dependency to build the actual widget. """ try: response = layout.app.cache.get_or_create( 'jobs_rss_feed', creator=lambda: requests.get(rss_feed_url, timeout=4), expiration_time=3600, should_cache_fn=lambda respon: respon.status_code == 200, ) parsed = parsed_rss(response.content) return parsed except Exception: return None return {'rss_widget_builder': rss_widget_builder}
[docs] class RSSItem(NamedTuple): """ The elements inside <item> """
[docs] title: str
[docs] description: str
[docs] guid: str
[docs] pubDate: datetime | None # noqa: N815
[docs] class RSSChannel(NamedTuple): """ The elements inside <channel> """
[docs] title: str
[docs] description: str
[docs] language: str
[docs] copyright: str
[docs] items: 'Iterator[RSSItem]'
[docs] def parsed_rss(rss: bytes) -> RSSChannel: def parse_date(date_str: str) -> datetime | None: try: return datetime.strptime(date_str, '%a, %d %b %Y %H:%M:%S %z') except ValueError: return None def get_text(element: '_Element | None') -> str: return element.text or '' if element is not None else '' def extract_channel_info( channel: '_Element', ) -> tuple[str, str, str, str, str]: return ( # type:ignore[return-value] get_text(channel.find(field)) for field in RSSChannel._fields if field != 'items' ) def extract_items(channel: '_Element') -> 'Iterator[RSSItem]': for item in channel.findall('item'): yield RSSItem( title=get_text(item.find('title')), description=get_text(item.find('description')), guid=get_text(item.find('guid')), pubDate=parse_date(get_text(item.find('pubDate'))) ) root = lxml.etree.fromstring(rss) channel = root.find('.//channel') assert channel is not None return RSSChannel( *extract_channel_info(channel), extract_items(channel) )