Skip to content

Commit

Permalink
Adds dashboard with ticket, page and news figures
Browse files Browse the repository at this point in the history
  • Loading branch information
Tschuppi81 committed Jan 28, 2025
1 parent cffde5f commit acbb71b
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 41 deletions.
10 changes: 9 additions & 1 deletion src/onegov/org/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from onegov.form.collection import FormCollection, SurveyCollection
from onegov.org import _, OrgApp
from onegov.org.models import (
GeneralFileCollection, ImageFileCollection, Organisation)
GeneralFileCollection, ImageFileCollection, Organisation, Dashboard)
from onegov.pay import PaymentProviderCollection, PaymentCollection
from onegov.ticket import TicketCollection
from onegov.ticket.collection import ArchivedTicketCollection
Expand Down Expand Up @@ -66,6 +66,14 @@ def get_global_tools(request: OrgRequest) -> Iterator[Link | LinkGroup]:
if request.is_manager:
links = []

links.append(
Link(
text=_('Dashboard'),
url=request.class_link(Dashboard),
attrs={'class': 'show-dashboard'}
)
)

links.append(
Link(
_('Timeline'), request.class_link(MessageCollection),
Expand Down
13 changes: 11 additions & 2 deletions src/onegov/org/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class SettingsDict(TypedDict):
class BoardletConfig(TypedDict):
cls: type[_Boardlet]
order: tuple[int, int]
icon: str


class HomepageWidgetAction(Action):
Expand Down Expand Up @@ -258,14 +259,21 @@ class Boardlet(Action):
'boardlets_registry': dict
}

def __init__(self, name: str, order: tuple[int, int]):
def __init__(
self,
name: str,
order: tuple[int, int],
icon: str = ''
) -> None:

assert isinstance(order, tuple) and len(order) == 2, """
The order should consist of two values, a group and an order
within the group.
"""

self.name = name
self.order = order
self.icon = icon

def identifier( # type:ignore[override]
self,
Expand All @@ -280,5 +288,6 @@ def perform( # type:ignore[override]
) -> None:
boardlets_registry[self.name] = {
'cls': func,
'order': self.order
'order': self.order,
'icon': self.icon,
}
19 changes: 16 additions & 3 deletions src/onegov/org/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ def boardlets(self) -> list[tuple[Boardlet, ...]]:

instances = []

for name, data in self.request.app.config.boardlets_registry.items():
for name, data in (
self.request.app.config.boardlets_registry.items()):
instances.append(data['cls'](
name=name,
order=data['order'],
icon=data['icon'],
request=self.request
))

Expand All @@ -49,7 +51,7 @@ class Boardlet:
from onegov.app import App
@App.boardlet(name='foo', order=(1, 1))
@App.boardlet(name='foo', order=(1, 1), icon='')
class MyBoardlet(Boardlet):
pass
Expand All @@ -59,10 +61,12 @@ def __init__(
self,
name: str,
order: tuple[int, int],
icon: str,
request: OrgRequest
) -> None:
self.name = name
self.order = order
self.icon = icon or ''
self.request = request

@property
Expand Down Expand Up @@ -95,10 +99,19 @@ def state(self) -> Literal['success', 'warning', 'failure']:
class BoardletFact:
""" A single boardlet fact. """

# the text of the fact (includes the metric)
# the text of the fact (not including the metric)
text: str

# the metric of the fact
number: int | float | str | None = None

# link to be displayed as tuple of link, link text
link: tuple[str, str] | None = None

# the font awesome (fa-*) icon to use, if any
icon: str | None = None

# title of the icon (hover text)
icon_title: str | None = None

css_class: str | None = None
136 changes: 136 additions & 0 deletions src/onegov/town6/boardlets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from __future__ import annotations
from datetime import timedelta
from functools import cached_property
from sedate import utcnow
from typing import TYPE_CHECKING

from onegov.org.layout import DefaultLayout
from onegov.org.models import Boardlet, BoardletFact, News
from onegov.page import Page
from onegov.ticket import Ticket
from onegov.town6 import TownApp, _

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

from onegov.town6.request import TownRequest


class TownBoardlet(Boardlet):

request: TownRequest

@cached_property
def session(self) -> Session:
return self.request.session

@cached_property
def layout(self) -> DefaultLayout:
return DefaultLayout(None, self.request)


@TownApp.boardlet(name='ticket', order=(1, 1), icon='fa-ticket-alt')
class TicketBoardlet(TownBoardlet):

@property
def title(self) -> str:
return 'Tickets'

@property
def facts(self) -> Iterator[BoardletFact]:

yield BoardletFact(
text=_('Open Tickets'),
number=self.session.query(Ticket).filter_by(state='open').count(),
icon='fa-hourglass'
)

yield BoardletFact(
text=_('Pending Tickets'),
number=self.session.query(Ticket).filter_by(
state='pending').count(),
icon='fa-hourglass-half'
)

time_7d_ago = utcnow() - timedelta(days=7)

new_tickets = self.session.query(Ticket).filter(
Ticket.created <= time_7d_ago).count()
yield BoardletFact(
text=_('New Tickets in the Last Week'),
number=new_tickets,
icon='fa-plus-circle'
)

closed_tickets = self.session.query(Ticket).filter_by(
state='closed').filter(
Ticket.last_change >= time_7d_ago).count()
yield BoardletFact(
text=_('Closed Tickets in the Last Week'),
number=closed_tickets,
icon='fa-check-circle'
)


def get_icon_for_visibility(visibility: str) -> str:
visibility_icons = {
'public': 'fa-eye',
'secret': 'fa-user-secret',
'private': 'fa-lock',
'member': 'fa-users'
}

if visibility not in visibility_icons:
raise ValueError(f'Invalid visibility: {visibility}')

Check warning on line 85 in src/onegov/town6/boardlets.py

View check run for this annotation

Codecov / codecov/patch

src/onegov/town6/boardlets.py#L85

Added line #L85 was not covered by tests

return visibility_icons[visibility]


def get_icon_title(request: TownRequest, visibility: str) -> str:
if visibility not in ['public', 'secret', 'private', 'member']:
raise ValueError(f'Invalid visibility: {visibility}')

Check warning on line 92 in src/onegov/town6/boardlets.py

View check run for this annotation

Codecov / codecov/patch

src/onegov/town6/boardlets.py#L92

Added line #L92 was not covered by tests

return request.translate(_('Visibility ${visibility}',
mapping={'visibility': visibility}))


@TownApp.boardlet(name='pages', order=(1, 2), icon='fa-edit')
class EditedPagesBoardlet(TownBoardlet):

@property
def title(self) -> str:
return 'Last Edited Pages'

@property
def facts(self) -> Iterator[BoardletFact]:

last_edited_pages = self.session.query(Page).order_by(
Page.last_change.desc()).limit(8)
for p in last_edited_pages:
yield BoardletFact(
text='',
link=(self.layout.request.link(p), p.title),
icon=get_icon_for_visibility(p.access), # type:ignore[attr-defined]
icon_title=get_icon_title(self.request, p.access) # type:ignore[attr-defined]
)


@TownApp.boardlet(name='news', order=(1, 3), icon='fa-edit')
class EditedNewsBoardlet(TownBoardlet):

@property
def title(self) -> str:
return 'Last Edited News'

@property
def facts(self) -> Iterator[BoardletFact]:
last_edited_news = self.session.query(News).order_by(
Page.last_change.desc()).limit(8)
for n in last_edited_news:
yield BoardletFact(
text='',
link=(self.layout.request.link(n), n.title),
icon=get_icon_for_visibility(n.access),
icon_title=get_icon_title(self.request, n.access)
)
36 changes: 23 additions & 13 deletions src/onegov/town6/templates/dashboard.pt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,29 @@
${title}
</tal:b>
<tal:b metal:fill-slot="content">
<div class="boardlet-group" tal:repeat="group model.boardlets()">
<div class="boardlet boardlet-${boardlet.state}" tal:repeat="boardlet group">
<h2>${boardlet.title}</h2>
<ul class="dense">
<li tal:repeat="fact boardlet.facts">
<i class="fa fa-fw ${fact.icon}" aria-hidden="true"></i>

<span class="boardlet-fact">
${fact.text}
</span>
</li>
</ul>
<div class="boardlet-group grid-x grid-padding-x columns small-up-1 medium-up-2 large-up-3 align-center" tal:repeat="group model.boardlets()">
<div class=" cell" tal:repeat="boardlet group">
<div class="boardlet boardlet-${boardlet.state} ${boardlet.name}">
<h2 class="h4">
<span>${boardlet.number|''}</span><br tal:condition="exists:boardlet.number"><span class="title">${boardlet.title}</span></h2>
<table class="facts">
<tr tal:repeat="fact boardlet.facts" class="${fact.css_class} small ${'none' if not (fact.number or fact.icon) else ''}">
<!--? use far instead of fas, although icons seem not to be available-->
<td>
<i tal:condition="fact.icon" class="fas ${fact.icon} fact-icon" tal:attributes="title fact.icon_title|None"></i>
<span tal:condition="fact.number is not None" class="fact-number">${fact.number}</span>
<span tal:condition="not:(fact.number is not None or fact.icon)" class="fact-number">-</span>
</td>
<td>
<span class="fact-text">${fact.text}</span>
<span tal:condition="fact.link" class="boardlet-fact">
<a href="${fact.link[0]}">${fact.link[1]}</a>
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</tal:b>
</div>
Loading

0 comments on commit acbb71b

Please sign in to comment.