IT-Stock/app/routers/destinataires.py
maxsoch 11da706573 Planification du résumé de stock modifiable depuis l'interface
L'horaire de l'email récapitulatif se configure maintenant sur
/destinataires (raccourcis courants + expression cron personnalisée) au
lieu de .env, avec reprogrammation immédiate sans redémarrage. Stocké en
base (ParametrePlanification) plutôt que dans un fichier de config.
Défaut changé pour tous les lundis à 9h.

Les exemples et le code n'utilisent que des noms de jours (mon, tue, ...)
dans les expressions cron : vérifié empiriquement qu'APScheduler ne suit
pas la convention Unix pour les jours numériques (0=lundi chez lui, pas
0=dimanche), ce qui rendrait "0 9 * * 1" trompeur.
2026-07-14 19:37:36 +02:00

104 lines
3.8 KiB
Python

"""
Routes pour gérer la liste des emails qui reçoivent les alertes de stock
bas, et la planification de l'email récapitulatif périodique.
"""
from fastapi import APIRouter, Depends, Form
from fastapi.requests import Request
from fastapi.responses import RedirectResponse
from sqlmodel import Session, select
from app.database import get_session
from app.models import DestinataireAlerte
from app.scheduler import (
EXEMPLES_CRON,
definir_expression_cron,
obtenir_expression_cron,
prochaine_execution,
valider_expression_cron,
)
from app.templates_engine import templates
router = APIRouter()
def _contexte_planification(session: Session, **kwargs) -> dict:
contexte = {
"expression_cron": obtenir_expression_cron(session),
"exemples_cron": EXEMPLES_CRON,
"prochaine_execution": prochaine_execution(),
"erreur_cron": None,
}
contexte.update(kwargs)
return contexte
@router.get("/destinataires")
def lister_destinataires(request: Request, q: str = "", session: Session = Depends(get_session)):
"""Affiche la liste des emails configurés pour recevoir les alertes,
filtrée par ?q=... si fourni (même comportement que /materiels, pour
rester cohérent si la liste grandit), ainsi que la planification de
l'email récapitulatif périodique."""
tous = session.exec(select(DestinataireAlerte)).all()
destinataires = [d for d in tous if q.lower() in d.email.lower()] if q else tous
return templates.TemplateResponse(
request,
"destinataires.html",
{"destinataires": destinataires, "q": q, **_contexte_planification(session)},
)
@router.post("/destinataires")
def ajouter_destinataire(email: str = Form(...), session: Session = Depends(get_session)):
"""Ajoute un email à la liste des destinataires des alertes."""
session.add(DestinataireAlerte(email=email))
session.commit()
return RedirectResponse(url="/destinataires", status_code=303)
@router.post("/destinataires/{destinataire_id}/supprimer")
def supprimer_destinataire(destinataire_id: int, session: Session = Depends(get_session)):
"""Retire un email de la liste des destinataires."""
destinataire = session.get(DestinataireAlerte, destinataire_id)
if destinataire:
session.delete(destinataire)
session.commit()
return RedirectResponse(url="/destinataires", status_code=303)
@router.post("/destinataires/planification")
def modifier_planification(
request: Request, expression_cron: str = Form(...), session: Session = Depends(get_session)
):
"""Change l'horaire de l'email récapitulatif périodique et reprogramme
la tâche immédiatement (pas besoin de redémarrer le serveur).
Pas de redirection en cas d'expression cron invalide : on réaffiche la
page avec un message d'erreur et la valeur saisie, pour ne pas la
perdre (contrairement au reste du formulaire, elle n'est pas
enregistrée tant qu'elle n'est pas valide).
"""
expression_cron = expression_cron.strip()
if not valider_expression_cron(expression_cron):
tous = session.exec(select(DestinataireAlerte)).all()
return templates.TemplateResponse(
request,
"destinataires.html",
{
"destinataires": tous,
"q": "",
**_contexte_planification(
session,
expression_cron=expression_cron,
erreur_cron=(
f"Expression cron invalide : « {expression_cron} ». "
"Format attendu : minute heure jour mois jour_semaine "
"(ex: 0 9 * * mon)."
),
),
},
)
definir_expression_cron(session, expression_cron)
return RedirectResponse(url="/destinataires", status_code=303)