feat: gestion événements — historique, export ZIP, filtre galerie
This commit is contained in:
@@ -120,6 +120,15 @@ async def admin_settings(request: Request):
|
||||
})
|
||||
|
||||
|
||||
@router.get("/admin/events", response_class=HTMLResponse)
|
||||
async def admin_events(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
cfg = request.app.state.config
|
||||
return _templates.TemplateResponse(request, "admin/events.html", {"config": cfg})
|
||||
|
||||
|
||||
@router.get("/admin/print", response_class=HTMLResponse)
|
||||
async def admin_print(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.services.event_service import slugify
|
||||
@@ -81,6 +85,55 @@ async def update_event(request: Request, payload: EventUpdate):
|
||||
}
|
||||
|
||||
|
||||
@router.get("/event/{slug}/export")
|
||||
async def export_event_zip(request: Request, slug: str):
|
||||
"""Exporte toutes les photos d'un événement dans un fichier ZIP.
|
||||
|
||||
Les photos sont sélectionnées par timestamp de fichier (mtime) entre
|
||||
started_at et ended_at de l'événement. Pour l'événement en cours,
|
||||
ended_at = maintenant.
|
||||
"""
|
||||
event_svc = request.app.state.event_service
|
||||
cfg = request.app.state.config
|
||||
|
||||
ev = await event_svc.get_event_by_slug(slug)
|
||||
if not ev:
|
||||
return JSONResponse({"error": f"Événement '{slug}' introuvable"}, status_code=404)
|
||||
|
||||
started_at = ev["started_at"] or 0.0
|
||||
ended_at = ev["ended_at"] or datetime.now().timestamp()
|
||||
|
||||
media_dir = Path(cfg.photobooth.media_dir)
|
||||
if not media_dir.exists():
|
||||
return JSONResponse({"error": "Dossier média introuvable"}, status_code=500)
|
||||
|
||||
# Sélectionner les photos dans la plage de l'événement
|
||||
photos = []
|
||||
for f in sorted(media_dir.iterdir()):
|
||||
if f.suffix.lower() not in (".jpg", ".jpeg", ".png"):
|
||||
continue
|
||||
mtime = f.stat().st_mtime
|
||||
if started_at <= mtime <= ended_at:
|
||||
photos.append(f)
|
||||
|
||||
if not photos:
|
||||
return JSONResponse({"error": "Aucune photo pour cet événement"}, status_code=404)
|
||||
|
||||
# Créer le ZIP en mémoire
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for photo in photos:
|
||||
zf.write(photo, photo.name)
|
||||
buf.seek(0)
|
||||
|
||||
filename = f"{slug}_By_LSDW_{datetime.fromtimestamp(started_at).strftime('%Y-%m-%d')}.zip"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/event/history")
|
||||
async def get_event_history(request: Request):
|
||||
"""Retourne tous les événements passés avec leurs statistiques."""
|
||||
|
||||
@@ -34,12 +34,37 @@ async def api_gallery_photos(
|
||||
request: Request,
|
||||
page: int = Query(default=1, ge=1),
|
||||
limit: int = Query(default=24, ge=1, le=100),
|
||||
event_slug: str = Query(default=None, description="Filtrer par slug d'événement"),
|
||||
):
|
||||
pb = request.app.state.photobooth_service
|
||||
cfg = request.app.state.config
|
||||
|
||||
all_photos = await pb.get_media_collection(limit=500)
|
||||
photos = [p for p in all_photos if _is_image(p)]
|
||||
|
||||
# Filtre par événement si demandé
|
||||
if event_slug:
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
event_svc = getattr(request.app.state, "event_service", None)
|
||||
if event_svc:
|
||||
ev = await event_svc.get_event_by_slug(event_slug)
|
||||
if ev:
|
||||
started_at = ev["started_at"] or 0.0
|
||||
ended_at = ev["ended_at"] or datetime.now().timestamp()
|
||||
media_dir = Path(cfg.photobooth.media_dir)
|
||||
# Construire un index mtime par nom de fichier
|
||||
mtime_index: dict[str, float] = {}
|
||||
if media_dir.exists():
|
||||
for f in media_dir.iterdir():
|
||||
mtime_index[f.name] = f.stat().st_mtime
|
||||
# Filtrer les photos par mtime
|
||||
def _in_event(p: dict) -> bool:
|
||||
pid = _get_id(p)
|
||||
mtime = mtime_index.get(pid, mtime_index.get(pid + ".jpg", 0.0))
|
||||
return started_at <= mtime <= ended_at
|
||||
photos = [p for p in photos if _in_event(p)]
|
||||
|
||||
total = len(photos)
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
@@ -56,6 +81,7 @@ async def api_gallery_photos(
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": max(1, (total + limit - 1) // limit),
|
||||
"event_slug": event_slug,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -97,6 +97,17 @@ class EventService:
|
||||
)
|
||||
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(
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/actions" class="active">Actions</a>
|
||||
<a href="/admin/print">Impression</a>
|
||||
<a href="/admin/settings">Réglages</a>
|
||||
<a href="/admin/events">Événements</a>
|
||||
<a href="/admin/logout">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<a href="/admin/actions">Actions</a>
|
||||
<a href="/admin/print">Impression</a>
|
||||
<a href="/admin/settings">Réglages</a>
|
||||
<a href="/admin/events">Événements</a>
|
||||
<a href="/admin/logout">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -330,3 +331,4 @@ setInterval(loadQueue, 15000);
|
||||
setInterval(loadEvent, 60000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
| ||||