Source code for org.boardlets

from __future__ import annotations

import re
from datetime import timedelta
from functools import cached_property

from sedate import utcnow
from typing import TYPE_CHECKING

from onegov.org import _
from onegov.org import OrgApp
from onegov.org.layout import DefaultLayout
from onegov.org.models import Boardlet, BoardletFact, News, Topic
from onegov.plausible.plausible_api import PlausibleAPI
from onegov.ticket import Ticket

if TYPE_CHECKING:
    from collections.abc import Iterator
    from sqlalchemy.orm import Session

    from onegov.org.request import OrgRequest


[docs] class OrgBoardlet(Boardlet):
[docs] request: OrgRequest
@cached_property
[docs] def session(self) -> Session: return self.request.session
@cached_property
[docs] def layout(self) -> DefaultLayout: return DefaultLayout(None, self.request)
@cached_property
[docs] def plausible_api(self) -> PlausibleAPI | None: analytics_code = self.request.app.org.analytics_code if analytics_code: if 'analytics.seantis.ch' in analytics_code: match = re.search(r'data-domain="(.+?)"', analytics_code) if match: site_id = match.group(1) return PlausibleAPI(site_id) return None
@OrgApp.boardlet(name='ticket', order=(1, 1), icon='fa-ticket-alt')
[docs] class TicketBoardlet(OrgBoardlet): @property
[docs] def title(self) -> str: return _('Tickets')
@property
[docs] def facts(self) -> Iterator[BoardletFact]: time_30d_ago = utcnow() - timedelta(days=30) yield BoardletFact( text=_('Open Tickets'), number=self.session.query(Ticket).filter_by( state='open').count(), icon='fas fa-hourglass' ) yield BoardletFact( text=_('Pending Tickets'), number=self.session.query(Ticket).filter_by( state='pending').count(), icon='fas fa-hourglass-half' ) yield BoardletFact( text=_('New Tickets in the Last Month'), number=self.session.query(Ticket).filter( Ticket.created > time_30d_ago).count(), icon='fas fa-plus-circle' ) closed_ticket_query = ( self.session.query(Ticket). filter(Ticket.closed_on.isnot(None)). filter(Ticket.closed_on >= time_30d_ago)) closed_tickets_count = closed_ticket_query.count() yield BoardletFact( text=_('Closed Tickets in the Last Month'), number=closed_tickets_count, icon='fas fa-check-circle' ) # average lead time from opening to closing average_lead_time_s: float | None = None if closed_tickets_count: total_lead_time_s = sum( (t.reaction_time or 0) + (t.process_time or 0) for t in closed_ticket_query) average_lead_time_s = total_lead_time_s / closed_tickets_count average_lead_time_s = round(average_lead_time_s / 86400, 1) yield BoardletFact( text=_('Lead Time from opening to closing in Days ' 'over the Last Month'), number=average_lead_time_s or '-', icon='fas fa-clock' ) # average lead time from pending to closing average_lead_time_s = None if closed_tickets_count: total_lead_time_s = sum( t.process_time or 1 for t in closed_ticket_query) average_lead_time_s = total_lead_time_s / closed_tickets_count average_lead_time_s = round(average_lead_time_s / 86400, 1) yield BoardletFact( text=_('Lead Time from pending to closing in Days ' 'over the Last Month'), number=average_lead_time_s or '-', icon='fas fa-clock' )
[docs] def get_icon_for_access_level(access: str) -> str: access_icons = { 'public': 'fas fa-eye', 'secret': 'fas fa-user-secret', 'private': 'fas fa-lock', 'member': 'fas fa-users' } if access not in access_icons: raise ValueError(f'Invalid access: {access}') return access_icons[access]
[docs] def get_icon_title(request: OrgRequest, access: str) -> str: access_texts = { 'public': 'Public', 'secret': 'Through URL only (not listed)', 'private': 'Only by privileged users', 'member': 'Only by privileged users and members' } if access not in ['public', 'secret', 'private', 'member']: raise ValueError(f'Invalid access: {access}') a = request.translate(_('Access')) b = request.translate(_(access_texts[access])) return f'{a}: {b}'
@OrgApp.boardlet(name='pages', order=(1, 2), icon='fa-edit')
[docs] class EditedTopicsBoardlet(OrgBoardlet): @property
[docs] def title(self) -> str: return _('Last Edited Topics')
@property
[docs] def facts(self) -> Iterator[BoardletFact]: last_edited_pages = self.session.query(Topic).order_by( Topic.last_change.desc()).limit(8) for p in last_edited_pages: yield BoardletFact( text='', link=(self.layout.request.link(p), p.title), icon='fas fa-file', visibility_icon=get_icon_for_access_level(p.access), icon_title=get_icon_title(self.request, p.access) )
@OrgApp.boardlet(name='news', order=(1, 3), icon='fa-edit')
[docs] class EditedNewsBoardlet(OrgBoardlet): @property
[docs] def title(self) -> str: return _('Last Edited News')
@property
[docs] def facts(self) -> Iterator[BoardletFact]: last_edited_news = self.session.query(News).order_by( News.last_change.desc()).limit(8) for n in last_edited_news: yield BoardletFact( text='', link=(self.layout.request.link(n), n.title), icon='fas fa-newspaper', visibility_icon=get_icon_for_access_level(n.access), icon_title=get_icon_title(self.request, n.access) )
@OrgApp.boardlet(name='web stats', order=(2, 1))
[docs] class PlausibleStats(OrgBoardlet): @property
[docs] def title(self) -> str: return _('Web Statistics')
@property
[docs] def is_available(self) -> bool: return self.plausible_api is not None
@property
[docs] def facts(self) -> Iterator[BoardletFact]: if not self.plausible_api: return None texts = [ _('Unique Visitors in the Last Month'), _('Total Visits in the Last Month'), _('Total Page Views in the Last Month'), _('Number of Page Views per Visit'), _('Average Visit Duration in Minutes') ] results = self.plausible_api.get_stats() if not results: yield BoardletFact( text=_('No data available'), number=None ) for text, value in zip(texts, results): yield BoardletFact( text=text, number=value )
@OrgApp.boardlet(name='most popular pages', order=(2, 2))
[docs] class PlausibleTopPages(OrgBoardlet): @property
[docs] def title(self) -> str: return _('Most Popular Pages')
@property
[docs] def is_available(self) -> bool: return self.plausible_api is not None
@property
[docs] def facts(self) -> Iterator[BoardletFact]: if not self.plausible_api: return None results = self.plausible_api.get_top_pages(limit=10) if not results: yield BoardletFact( text=_('No data available'), number=None ) for text, number in results.items(): yield BoardletFact( text=text, number=number )