145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
"""Service de gestion des événements et statistiques.
|
|
|
|
Chaque événement (soirée, mariage, fête...) a ses propres compteurs :
|
|
- photos_taken : incrémenté par le webhook 'finished' de photobooth-app
|
|
- print_requests : incrémenté à chaque demande d'impression dans la file
|
|
- prints_done : incrémenté à chaque impression réussie
|
|
- downloads : incrémenté à chaque téléchargement via /api/gallery/download/
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import unicodedata
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import aiosqlite
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def slugify(name: str) -> str:
|
|
"""Transforme un nom d'événement en slug ASCII sans accents ni espaces."""
|
|
# Normalise les caractères Unicode (enlève les accents)
|
|
nfkd = unicodedata.normalize("NFD", name)
|
|
ascii_str = "".join(c for c in nfkd if unicodedata.category(c) != "Mn")
|
|
ascii_str = ascii_str.lower()
|
|
# Garde lettres, chiffres, espaces, tirets
|
|
ascii_str = re.sub(r"[^\w\s-]", "", ascii_str)
|
|
# Remplace espaces/tirets multiples par un underscore
|
|
ascii_str = re.sub(r"[\s_-]+", "_", ascii_str)
|
|
return ascii_str.strip("_") or "evenement"
|
|
|
|
|
|
_SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
started_at REAL NOT NULL,
|
|
ended_at REAL,
|
|
photos_taken INTEGER NOT NULL DEFAULT 0,
|
|
print_requests INTEGER NOT NULL DEFAULT 0,
|
|
prints_done INTEGER NOT NULL DEFAULT 0,
|
|
downloads INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
"""
|
|
|
|
_VALID_COUNTERS = {"photos_taken", "print_requests", "prints_done", "downloads"}
|
|
|
|
|
|
class EventService:
|
|
def __init__(self):
|
|
self._db: aiosqlite.Connection | None = None
|
|
|
|
async def init_db(self, db_path: Path):
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._db = await aiosqlite.connect(str(db_path))
|
|
self._db.row_factory = aiosqlite.Row
|
|
await self._db.execute(_SCHEMA)
|
|
await self._db.commit()
|
|
logger.info("EventService initialisé : %s", db_path)
|
|
|
|
async def ensure_event(self, slug: str, name: str, started_at: float):
|
|
"""Crée la ligne pour cet événement si elle n'existe pas encore."""
|
|
await self._db.execute(
|
|
"""INSERT INTO events (name, slug, started_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(slug) DO UPDATE SET name = excluded.name""",
|
|
(name, slug, started_at),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def archive_event(self, slug: str):
|
|
"""Marque un événement comme terminé (ended_at = maintenant)."""
|
|
await self._db.execute(
|
|
"UPDATE events SET ended_at = ? WHERE slug = ? AND ended_at IS NULL",
|
|
(datetime.now().timestamp(), slug),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def archive_all_open(self):
|
|
"""Marque TOUS les événements ouverts comme terminés."""
|
|
await self._db.execute(
|
|
"UPDATE events SET ended_at = ? WHERE ended_at IS NULL",
|
|
(datetime.now().timestamp(),),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def increment(self, slug: str, counter: str):
|
|
"""Incrémente un compteur de l'événement identifié par son slug.
|
|
|
|
Crée automatiquement une ligne si elle n'existe pas.
|
|
"""
|
|
if counter not in _VALID_COUNTERS:
|
|
logger.warning("Compteur inconnu : %s", counter)
|
|
return
|
|
# Upsert : insère si absent, sinon incrémente
|
|
await self._db.execute(
|
|
f"""INSERT INTO events (name, slug, started_at, {counter})
|
|
VALUES (?, ?, unixepoch(), 1)
|
|
ON CONFLICT(slug) DO UPDATE SET {counter} = {counter} + 1""",
|
|
(slug, slug),
|
|
)
|
|
await self._db.commit()
|
|
|
|
async def get_event_by_slug(self, slug: str) -> dict | None:
|
|
"""Retourne un événement complet par son slug."""
|
|
async with self._db.execute(
|
|
"""SELECT id, name, slug, started_at, ended_at,
|
|
photos_taken, print_requests, prints_done, downloads
|
|
FROM events WHERE slug = ?""",
|
|
(slug,),
|
|
) as cur:
|
|
row = await cur.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
async def get_stats(self, slug: str) -> dict:
|
|
"""Retourne les stats pour un slug donné."""
|
|
async with self._db.execute(
|
|
"""SELECT photos_taken, print_requests, prints_done, downloads
|
|
FROM events WHERE slug = ?""",
|
|
(slug,),
|
|
) as cur:
|
|
row = await cur.fetchone()
|
|
if not row:
|
|
return {"photos_taken": 0, "print_requests": 0, "prints_done": 0, "downloads": 0}
|
|
return dict(row)
|
|
|
|
async def get_history(self) -> list[dict]:
|
|
"""Retourne tous les événements triés du plus récent au plus ancien."""
|
|
async with self._db.execute(
|
|
"""SELECT id, name, slug, started_at, ended_at,
|
|
photos_taken, print_requests, prints_done, downloads
|
|
FROM events
|
|
ORDER BY started_at DESC"""
|
|
) as cur:
|
|
rows = await cur.fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
async def close(self):
|
|
if self._db:
|
|
await self._db.close()
|