groupe-meditation/backend/app/services/historique.py
Daniel Allaire 6677b0f57b Ajouter historique métier et protection concurrente
Ajoute modifie_le aux objets sensibles et fait transmettre la version affichée par le frontend pour refuser les écritures périmées avec un message HTTP 409.

Ajoute une table historique_modifications et un service d'historique métier afin de journaliser automatiquement les raisons des changements importants sans intervention de l'utilisateur.

Expose l'historique métier dans le module Journal, distinct du journal technique, et documente les processus utilisateurs ainsi qu'une présentation sobre aux membres.
2026-05-31 10:59:41 -04:00

82 lines
2.4 KiB
Python

"""Service d'historique métier."""
from fastapi import HTTPException
from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.concurrency import version_iso
from app.models.historique_modification import HistoriqueModification
from app.models.membre import Membre
async def enregistrer_historique(
db: AsyncSession,
*,
groupe_id,
table_cible: str,
objet_id,
action: str,
raison: str,
membre_id=None,
admin_id=None,
chemin_api: str | None = None,
avant: dict | None = None,
apres: dict | None = None,
) -> HistoriqueModification:
entree = HistoriqueModification(
groupe_id=groupe_id,
table_cible=table_cible,
objet_id=objet_id,
action=action,
raison=raison,
membre_id=membre_id,
admin_id=admin_id,
chemin_api=chemin_api,
avant=avant,
apres=apres,
)
db.add(entree)
return entree
async def dernier_historique(db: AsyncSession, table_cible: str, objet_id) -> HistoriqueModification | None:
result = await db.execute(
select(HistoriqueModification)
.where(
HistoriqueModification.table_cible == table_cible,
HistoriqueModification.objet_id == objet_id,
)
.order_by(desc(HistoriqueModification.cree_le))
.limit(1)
)
return result.scalar_one_or_none()
async def verifier_version_historisee(
db: AsyncSession,
obj,
version_attendue: str | None,
*,
table_cible: str,
objet_id=None,
):
if not version_attendue:
return
actuelle = version_iso(obj)
if not actuelle or actuelle.replace("+00:00", "Z") == version_attendue.replace("+00:00", "Z"):
return
objet = objet_id or getattr(obj, "id", None)
entree = await dernier_historique(db, table_cible, objet) if objet else None
if not entree:
raise HTTPException(
status_code=409,
detail="Cette information a été modifiée depuis son affichage. Rechargez les données avant d'enregistrer.",
)
membre = await db.get(Membre, entree.membre_id) if entree.membre_id else None
par = f" par {membre.prenom}" if membre else ""
quand = entree.cree_le.astimezone().strftime("%Y-%m-%d %H:%M")
raise HTTPException(
status_code=409,
detail=f"Cette information a été modifiée{par} le {quand} : {entree.raison}. Rechargez les données avant d'enregistrer.",
)