Added stats

This commit is contained in:
marc
2025-03-23 00:19:33 +01:00
parent 2f7c7c2429
commit c097811e40
10 changed files with 144 additions and 44 deletions

View File

@@ -1,6 +1,3 @@
{% if session == None %}
L'últim Dimecres de cada mes
{% else %}
{% set dn = date_names(session.date) %} {% set dn = date_names(session.date) %}
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
de {{ session.start_time.strftime("%H:%M") }} de {{ session.start_time.strftime("%H:%M") }}
@@ -8,4 +5,3 @@
{% if session.venue.name %} {% if session.venue.name %}
a {{ session.venue.name }} a {{ session.venue.name }}
{% endif %} {% endif %}
{% endif %}

View File

@@ -13,5 +13,6 @@
{% include "fragments/tema/lyrics.html" %} {% include "fragments/tema/lyrics.html" %}
{% include "fragments/tema/links.html" %} {% include "fragments/tema/links.html" %}
{% include "fragments/tema/properties.html" %} {% include "fragments/tema/properties.html" %}
{% include "fragments/tema/stats.html" %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,42 @@
{% if tema.stats %}
<h4 class="pt-4 text-xl mt-3 text-beige">Estadístiques</h4>
<hr class="h-px mt-1 mb-3 bg-beige border-0">
<div>
<p>
Aquest tema ha sigut tocat en
{% if tema.stats.times_played == 1%}
una sessió.
{% else %}
{{ tema.stats.times_played }} sessions.
{% endif %}
</p>
<p class="py-2">
S'ha tocat a les sessions següents:
<ol class="flex flex-col items-center justify-center">
{% for session in tema.stats.sessions_played %}
<li class="border rounded border-beige
flex flex-row grow
p-2 m-2 w-full max-w-xl
relative">
<a href="/session/{{session.id}}">
<div class="flex flex-row grow items-center">
<div class="flex-1">
<a href="/sessio/{{session.id}}/">
{% set dn = date_names(session.date) %}
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
</a>
</div>
<div class="ml-auto">
<a title="Més informació"
class="text-beige mx-1"
href="/sessio/{{session.id}}/">
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</a>
</div>
</div>
</li>
{% endfor %}
</ol>
</p>
</div>
{% endif %}

View File

@@ -4,6 +4,7 @@
<tr class="border-b border-beige"> <tr class="border-b border-beige">
<td class="font-bold py-2 px-4">Nom</td> <td class="font-bold py-2 px-4">Nom</td>
<td class="font-bold py-2 px-4">Enllaços</td> <td class="font-bold py-2 px-4">Enllaços</td>
<td class="font-bold py-2 px-4">Cops tocat</td>
</tr> </tr>
{% for tema in temes %} {% for tema in temes %}
<tr class="border-b border-beige"> <tr class="border-b border-beige">
@@ -15,6 +16,13 @@
<td class="py-2 px-4"> <td class="py-2 px-4">
{% include "fragments/temes/result_links.html" %} {% include "fragments/temes/result_links.html" %}
</td> </td>
<td class="py-2 px-4">
{% if tema.stats is none %}
-
{% else %}
{{ tema.stats.times_played }}
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@@ -1,10 +1,11 @@
from collections.abc import Iterator from collections.abc import Iterable, Iterator
from typing import TypedDict from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.dal.sql.sessions import conversion as sessions_conversion from folkugat_web.dal.sql.sessions import conversion as sessions_conversion
from folkugat_web.model import playlists as model from folkugat_web.model import playlists as model
from folkugat_web.model.sessions import Session from folkugat_web.model.sessions import Session
from folkugat_web.utils import groupby
from . import conversion from . import conversion
@@ -55,14 +56,22 @@ def get_playlist_entries(
return map(conversion.row_to_playlist_entry, cur.fetchall()) return map(conversion.row_to_playlist_entry, cur.fetchall())
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> Iterator[Session]: GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, bool]
query = """
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]:
placeholders = ", ".join(["?" for _ in tema_ids])
query = f"""
SELECT p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name, s.venue_url, s.is_live SELECT p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name, s.venue_url, s.is_live
FROM playlists p JOIN sessions s ON p.session_id = s.id FROM playlists p JOIN sessions s ON p.session_id = s.id
WHERE p.tema_id IN :tema_ids WHERE p.tema_id IN ({placeholders})
""" """
data = dict(tema_ids=tuple(tema_ids))
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, tema_ids)
return map(sessions_conversion.row_to_session, cur.fetchall()) result_rows: Iterable[GetTuneSessionsRow] = cur.fetchall()
return dict(groupby(
result_rows,
key_fn=lambda row: row[0],
group_fn=lambda rows: list(sessions_conversion.row_to_session(row[1:]) for row in rows)
))

View File

@@ -1,6 +1,4 @@
from typing import Optional from fastapi import HTTPException, Request
from fastapi import Request
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes.links import guess_link_type from folkugat_web.services.temes.links import guess_link_type
@@ -12,6 +10,8 @@ def title(request: Request, logged_in: bool, tema: model.Tema | None = None, tem
if tema_id is None: if tema_id is None:
raise ValueError("Either 'tema' or 'tema_id' must be given!") raise ValueError("Either 'tema' or 'tema_id' must be given!")
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/title.html", "fragments/tema/title.html",
{ {
@@ -24,6 +24,8 @@ def title(request: Request, logged_in: bool, tema: model.Tema | None = None, tem
def title_editor(request: Request, logged_in: bool, tema_id: int): def title_editor(request: Request, logged_in: bool, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/editor/title.html", "fragments/tema/editor/title.html",
{ {
@@ -36,8 +38,8 @@ def title_editor(request: Request, logged_in: bool, tema_id: int):
def lyric(request: Request, logged_in: bool, tema_id: int, lyric_id: int): def lyric(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
lyric = tema.lyrics.get(lyric_id) lyric = tema.lyrics.get(lyric_id)
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -54,8 +56,8 @@ def lyric(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int): def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
lyric = tema.lyrics.get(lyric_id) lyric = tema.lyrics.get(lyric_id)
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -72,8 +74,8 @@ def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int)
def link(request: Request, logged_in: bool, tema_id: int, link_id: int): def link(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id) link = tema.links.get(link_id)
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -95,8 +97,8 @@ def link(request: Request, logged_in: bool, tema_id: int, link_id: int):
def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int): def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id) link = tema.links.get(link_id)
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -115,8 +117,8 @@ def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int):
def link_editor_url(request: Request, logged_in: bool, tema_id: int, link_id: int): def link_editor_url(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id) link = tema.links.get(link_id)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/editor/link_url.html", "fragments/tema/editor/link_url.html",
@@ -134,8 +136,8 @@ def link_editor_url(request: Request, logged_in: bool, tema_id: int, link_id: in
def link_editor_file(request: Request, logged_in: bool, tema_id: int, link_id: int): def link_editor_file(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id) link = tema.links.get(link_id)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/editor/link_file.html", "fragments/tema/editor/link_file.html",
@@ -175,8 +177,8 @@ def link_icon(request: Request, logged_in: bool, tema_id: int, link_id: int, url
def score(request: Request, logged_in: bool, tema_id: int): def score(request: Request, logged_in: bool, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/score.html", "fragments/tema/score.html",
{ {
@@ -190,8 +192,8 @@ def score(request: Request, logged_in: bool, tema_id: int):
def property_(request: Request, logged_in: bool, tema_id: int, property_id: int): def property_(request: Request, logged_in: bool, tema_id: int, property_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
prop = tema.properties.get(property_id) prop = tema.properties.get(property_id)
return templates.TemplateResponse( return templates.TemplateResponse(
@@ -209,8 +211,8 @@ def property_(request: Request, logged_in: bool, tema_id: int, property_id: int)
def property_editor(request: Request, logged_in: bool, tema_id: int, property_id: int): def property_editor(request: Request, logged_in: bool, tema_id: int, property_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if not tema:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise HTTPException(status_code=404, detail="Could not find tune")
prop = tema.properties.get(property_id) prop = tema.properties.get(property_id)
return templates.TemplateResponse( return templates.TemplateResponse(

View File

@@ -1,6 +1,7 @@
from fastapi import Request from fastapi import HTTPException, Request
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.model.pagines import Pages from folkugat_web.model.pagines import Pages
from folkugat_web.services import sessions as sessions_service
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes import search as temes_s from folkugat_web.services.temes import search as temes_s
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -34,6 +35,8 @@ def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0,
if offset > 0: if offset > 0:
prev_offset = max(offset - limit, 0) prev_offset = max(offset - limit, 0)
temes = temes_q.temes_compute_stats(temes)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/temes/results.html", "fragments/temes/results.html",
{ {
@@ -51,6 +54,9 @@ def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0,
def tema(request: Request, tema_id: int, logged_in: bool): def tema(request: Request, tema_id: int, logged_in: bool):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
tema = temes_q.tema_compute_stats(tema)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/pagina.html", "fragments/tema/pagina.html",
{ {
@@ -60,5 +66,6 @@ def tema(request: Request, tema_id: int, logged_in: bool):
"LinkType": model.LinkType, "LinkType": model.LinkType,
"ContentType": model.ContentType, "ContentType": model.ContentType,
"tema": tema, "tema": tema,
"date_names": sessions_service.get_date_names,
} }
) )

View File

@@ -101,6 +101,12 @@ class Lyrics(WithId):
) )
@dataclasses.dataclass
class Stats:
times_played: int
sessions_played: list[Session]
@dataclasses.dataclass @dataclasses.dataclass
class Tema: class Tema:
id: int | None = None id: int | None = None
@@ -117,7 +123,7 @@ class Tema:
modification_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now) modification_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
creation_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now) creation_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
# Stats # Stats
played: list[Session] = dataclasses.field(default_factory=list) stats: Stats | None = None
def compute_ngrams(self): def compute_ngrams(self):
self.ngrams = ngrams.get_text_ngrams(self.title, *self.alternatives) self.ngrams = ngrams.get_text_ngrams(self.title, *self.alternatives)
@@ -126,5 +132,13 @@ class Tema:
self.compute_ngrams() self.compute_ngrams()
return self return self
@staticmethod
def _is_score(link: Link) -> bool:
if link.content_type is not ContentType.PARTITURA:
return False
if link.link_type is LinkType.PDF:
return link.url.startswith("/")
return link.link_type is LinkType.IMAGE
def score(self) -> Link | None: def score(self) -> Link | None:
return next(filter(lambda l: l.content_type is ContentType.PARTITURA and l.url.startswith('/'), self.links), None) return next(filter(self._is_score, self.links), None)

View File

@@ -1,8 +1,29 @@
from typing import Optional from folkugat_web.dal.sql.playlists import query as playlists_q
from folkugat_web.dal.sql.temes import query as temes_q from folkugat_web.dal.sql.temes import query as temes_q
from folkugat_web.model import sessions as sessions_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
def get_tema_by_id(tema_id: int) -> model.Tema | None: def get_tema_by_id(tema_id: int) -> model.Tema | None:
return temes_q.get_tema_by_id(tema_id) return temes_q.get_tema_by_id(tema_id)
def tema_compute_stats(
tema: model.Tema,
tune_sessions_dict: dict[int, list[sessions_model.Session]] | None = None,
) -> model.Tema:
if tema.id:
if tune_sessions_dict is None:
tune_sessions_dict = playlists_q.get_tune_sessions(tema_ids=[tema.id])
if tema.id and (tune_sessions := tune_sessions_dict.get(tema.id)):
tema.stats = model.Stats(
times_played=len(tune_sessions),
sessions_played=list(reversed(sorted(tune_sessions, key=lambda s: s.date))),
)
return tema
def temes_compute_stats(temes: list[model.Tema]) -> list[model.Tema]:
tema_ids = [tema.id for tema in temes if tema.id is not None]
tune_sessions_dict = playlists_q.get_tune_sessions(tema_ids=tema_ids)
return [tema_compute_stats(tema=tema, tune_sessions_dict=tune_sessions_dict) for tema in temes]

View File

@@ -4,7 +4,7 @@ from typing import Protocol, Self, TypeVar
class SupportsLessThan(Protocol): class SupportsLessThan(Protocol):
def __lt__(self, other: Self) -> bool: def __lt__(self, other: Self, /) -> bool:
raise NotImplementedError() raise NotImplementedError()