groupe-meditation/backend/app/routers/postes.py
Daniel Allaire 393f9243cb Renforcer isolation multi-tenant
- scoper les postes et permissions par groupe

- retirer les lectures district du calendrier, des evenements et anniversaires

- documenter les regles d isolation multi-groupe
2026-05-31 18:12:26 -04:00

766 lines
27 KiB
Python

"""Routes postes, affectations, candidatures et accès aux modules."""
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.core.decisions import prochain_numero_decision
from app.core.security import get_current_membre, require_executif
from app.models.poste import Poste
from app.models.affectation import Affectation
from app.models.membre import Membre
from app.models.candidature import Candidature
from app.models.poste_module import PosteModule
from app.schemas.schemas import PosteOut, PosteCreate, PosteUpdate, AffectationCreate, TerminerMandatRequest
router = APIRouter(prefix="/postes", tags=["Postes et affectations"])
# ── Postes ──
@router.get("/", response_model=list[PosteOut])
async def liste_postes(
inclure_abolis: bool = False,
db: AsyncSession = Depends(get_db),
membre=Depends(get_current_membre),
):
query = select(Poste).where(Poste.groupe_id == membre.groupe_id)
if not inclure_abolis:
query = query.where(Poste.actif == True)
result = await db.execute(query.order_by(Poste.ordre_affichage))
postes = result.scalars().all()
return [PosteOut.model_validate(p) for p in postes]
@router.post("/", response_model=PosteOut)
async def creer_poste(
req: PosteCreate,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Créer un nouveau poste (exécutif)."""
if req.categorie not in ("executif", "service", "physique"):
raise HTTPException(status_code=400, detail="Catégorie invalide")
poste = Poste(
groupe_id=executif.groupe_id,
nom=req.nom.strip(),
categorie=req.categorie,
description=req.description,
prerequis=req.prerequis,
informations=req.informations,
ordre_affichage=req.ordre_affichage or 99,
)
db.add(poste)
await db.flush()
return PosteOut.model_validate(poste)
@router.patch("/{poste_id}", response_model=PosteOut)
async def modifier_poste(
poste_id: str,
req: PosteUpdate,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste:
raise HTTPException(status_code=404, detail="Poste introuvable")
if req.categorie is not None and req.categorie not in ("executif", "service", "physique"):
raise HTTPException(status_code=400, detail="Catégorie invalide")
for attr in ["nom", "categorie", "description", "prerequis", "informations", "ordre_affichage"]:
value = getattr(req, attr)
if value is not None:
setattr(poste, attr, value.strip() if isinstance(value, str) else value)
if not poste.nom:
raise HTTPException(status_code=400, detail="Nom requis")
await db.flush()
return PosteOut.model_validate(poste)
@router.delete("/{poste_id}")
async def abolir_poste(
poste_id: str,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Abolir un poste — termine tous les mandats actifs associés."""
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste:
raise HTTPException(status_code=404, detail="Poste introuvable")
# Terminer les mandats actifs
result = await db.execute(
select(Affectation).where(
Affectation.poste_id == poste.id,
Affectation.groupe_id == executif.groupe_id,
Affectation.statut == "actif",
)
)
mandats_termines = 0
for a in result.scalars().all():
a.statut = "termine"
a.date_fin = date.today()
a.note_transition = f"Poste aboli: {poste.nom}"
a.termine_par = executif.id
mandats_termines += 1
# Refuser les candidatures en attente
result = await db.execute(
select(Candidature).where(
Candidature.poste_id == poste.id,
Candidature.groupe_id == executif.groupe_id,
Candidature.statut == "proposee",
)
)
for c in result.scalars().all():
c.statut = "refusee"
poste.actif = False
await db.flush()
return {
"message": f"Poste «{poste.nom}» aboli",
"mandats_termines": mandats_termines,
}
# ── Postes disponibles (vacants) ──
@router.get("/disponibles")
async def postes_disponibles(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Postes actifs sans titulaire — opportunités d'implication."""
result = await db.execute(
select(Poste).where(Poste.groupe_id == membre.groupe_id, Poste.actif == True).order_by(Poste.ordre_affichage)
)
postes = result.scalars().all()
disponibles = []
for p in postes:
result = await db.execute(
select(Affectation).where(
Affectation.poste_id == p.id,
Affectation.statut == "actif",
Affectation.groupe_id == membre.groupe_id,
)
)
if not result.scalars().first():
# Vérifier les candidatures existantes du membre
result_cand = await db.execute(
select(Candidature).where(
Candidature.poste_id == p.id,
Candidature.membre_id == membre.id,
Candidature.statut == "proposee",
)
)
deja_postule = result_cand.scalars().first() is not None
disponibles.append({
"id": str(p.id),
"nom": p.nom,
"categorie": p.categorie,
"description": p.description,
"prerequis": p.prerequis,
"informations": p.informations,
"deja_postule": deja_postule,
"candidats": await _candidats_poste(p.id, membre.groupe_id, db),
})
return disponibles
async def _candidats_poste(poste_id, groupe_id, db: AsyncSession):
result = await db.execute(
select(Candidature).where(
Candidature.poste_id == poste_id,
Candidature.groupe_id == groupe_id,
Candidature.statut == "proposee",
)
)
candidats = []
for c in result.scalars().all():
membre = await db.get(Membre, c.membre_id)
if membre and membre.groupe_id == groupe_id:
candidats.append({
"id": str(c.id),
"membre_id": str(membre.id),
"prenom": membre.prenom,
"type": c.type,
"motivation": c.motivation,
})
return candidats
# ── Candidatures ──
@router.post("/candidatures")
async def postuler(
poste_id: str,
motivation: str = "",
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Proposer sa candidature — crée automatiquement une proposition de nomination."""
from app.models.proposition import Proposition
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == membre.groupe_id))
poste = result.scalar_one_or_none()
if not poste or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
# Vérifier qu'il n'a pas déjà postulé
result = await db.execute(
select(Candidature).where(
Candidature.poste_id == poste_id,
Candidature.groupe_id == membre.groupe_id,
Candidature.membre_id == membre.id,
Candidature.statut == "proposee",
)
)
if result.scalars().first():
raise HTTPException(status_code=400, detail="Vous avez déjà postulé pour ce poste")
# Créer la candidature
cand = Candidature(
groupe_id=membre.groupe_id,
poste_id=poste_id,
membre_id=membre.id,
type="candidature",
motivation=motivation.strip() or None,
)
db.add(cand)
await db.flush()
# Créer automatiquement la proposition de nomination
prop = Proposition(
groupe_id=membre.groupe_id,
sujet=f"Nomination de {membre.prenom} au poste de {poste.nom}",
description=motivation.strip() or None,
proposeur_id=membre.id,
candidature_id=cand.id,
statut="proposee",
)
db.add(prop)
cand.proposition_id = prop.id
await db.flush()
return {
"message": f"Candidature soumise pour {poste.nom} — proposition créée",
"id": str(cand.id),
"proposition_id": str(prop.id),
}
@router.post("/candidatures/nominer")
async def nominer(
poste_id: str,
membre_id: str,
motivation: str = "",
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Nominer quelqu'un (exécutif). Le nominé devra accepter avant que la proposition soit créée."""
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
nominee = await db.get(Membre, membre_id)
if not nominee or nominee.groupe_id != executif.groupe_id:
raise HTTPException(status_code=404, detail="Membre introuvable")
cand = Candidature(
groupe_id=executif.groupe_id,
poste_id=poste_id,
membre_id=membre_id,
type="nomination",
nomme_par=executif.id,
motivation=motivation.strip() or None,
)
db.add(cand)
await db.flush()
return {
"message": f"{nominee.prenom} nominé(e) pour {poste.nom} — en attente de son acceptation",
"id": str(cand.id),
}
@router.post("/candidatures/proposer")
async def proposer_membre(
poste_id: str,
membre_id: str,
motivation: str = "",
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Proposer un autre membre pour un poste."""
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == membre.groupe_id))
poste = result.scalar_one_or_none()
if not poste or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
cible = await db.get(Membre, membre_id)
if not cible or cible.groupe_id != membre.groupe_id or not cible.actif:
raise HTTPException(status_code=404, detail="Membre introuvable")
if cible.id == membre.id:
raise HTTPException(status_code=400, detail="Utilisez Postuler pour votre propre candidature")
result = await db.execute(
select(Candidature).where(
Candidature.poste_id == poste_id,
Candidature.groupe_id == membre.groupe_id,
Candidature.membre_id == membre_id,
Candidature.statut == "proposee",
)
)
if result.scalars().first():
raise HTTPException(status_code=400, detail="Ce membre est déjà proposé pour ce poste")
cand = Candidature(
groupe_id=membre.groupe_id,
poste_id=poste_id,
membre_id=membre_id,
type="nomination",
nomme_par=membre.id,
motivation=motivation.strip() or None,
)
db.add(cand)
await db.flush()
return {"message": f"{cible.prenom} proposé(e) pour {poste.nom}", "id": str(cand.id)}
@router.post("/candidatures/{candidature_id}/retirer")
async def retirer_candidature(
candidature_id: str,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Retirer sa propre candidature. Retire aussi la proposition associée."""
from app.models.proposition import Proposition
result = await db.execute(
select(Candidature).where(
Candidature.id == candidature_id,
Candidature.groupe_id == membre.groupe_id,
Candidature.membre_id == membre.id,
Candidature.statut == "proposee",
)
)
cand = result.scalar_one_or_none()
if not cand:
raise HTTPException(status_code=404, detail="Candidature introuvable ou déjà traitée")
cand.statut = "retiree"
# Retirer la proposition associée si elle n'est pas encore votée
if cand.proposition_id:
prop = await db.get(Proposition, cand.proposition_id)
if prop and prop.statut in ("proposee", "secondee"):
prop.statut = "rejetee"
prop.note = "Candidature retirée par le membre"
await db.flush()
return {"message": "Candidature retirée"}
@router.post("/candidatures/{candidature_id}/accepter-nomination")
async def accepter_nomination(
candidature_id: str,
accepter: bool = True,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Le nominé accepte/refuse. Si accepté, la proposition est créée automatiquement."""
from app.models.proposition import Proposition
result = await db.execute(
select(Candidature).where(
Candidature.id == candidature_id,
Candidature.groupe_id == membre.groupe_id,
Candidature.membre_id == membre.id,
Candidature.type == "nomination",
Candidature.statut == "proposee",
)
)
cand = result.scalar_one_or_none()
if not cand:
raise HTTPException(status_code=404, detail="Nomination introuvable")
cand.acceptee_par_membre = accepter
if not accepter:
cand.statut = "refusee"
await db.flush()
return {"message": "Nomination refusée"}
# Acceptée → créer la proposition de nomination
poste = await db.get(Poste, cand.poste_id)
if not poste or poste.groupe_id != membre.groupe_id:
raise HTTPException(status_code=404, detail="Poste introuvable")
nomme_par = await db.get(Membre, cand.nomme_par) if cand.nomme_par else None
prop = Proposition(
groupe_id=cand.groupe_id,
sujet=f"Nomination de {membre.prenom} au poste de {poste.nom if poste else '?'}",
description=f"Nominé par {nomme_par.prenom if nomme_par else '?'}. {cand.motivation or ''}".strip(),
proposeur_id=cand.nomme_par or membre.id,
candidature_id=cand.id,
statut="proposee",
)
db.add(prop)
cand.proposition_id = prop.id
await db.flush()
return {
"message": "Nomination acceptée — proposition créée, en attente d'être secondée",
"proposition_id": str(prop.id),
}
@router.get("/candidatures")
async def liste_candidatures(
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Liste des candidatures du groupe (exécutif)."""
result = await db.execute(
select(Candidature).where(
Candidature.groupe_id == executif.groupe_id,
Candidature.statut == "proposee",
)
)
candidatures = result.scalars().all()
out = []
for c in candidatures:
mbr = await db.get(Membre, c.membre_id)
poste = await db.get(Poste, c.poste_id)
nomme = await db.get(Membre, c.nomme_par) if c.nomme_par else None
if poste and poste.groupe_id != executif.groupe_id:
continue
out.append({
"id": str(c.id),
"membre_prenom": mbr.prenom if mbr else "?",
"poste_nom": poste.nom if poste else "?",
"type": c.type,
"motivation": c.motivation,
"nomme_par_prenom": nomme.prenom if nomme else None,
"acceptee_par_membre": c.acceptee_par_membre,
"cree_le": str(c.cree_le),
})
return out
@router.get("/candidatures/mes")
async def mes_candidatures(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Mes candidatures et nominations en attente."""
result = await db.execute(
select(Candidature).where(
Candidature.membre_id == membre.id,
Candidature.groupe_id == membre.groupe_id,
Candidature.statut == "proposee",
)
)
out = []
for c in result.scalars().all():
poste = await db.get(Poste, c.poste_id)
nomme = await db.get(Membre, c.nomme_par) if c.nomme_par else None
if poste and poste.groupe_id != membre.groupe_id:
continue
out.append({
"id": str(c.id),
"poste_nom": poste.nom if poste else "?",
"type": c.type,
"nomme_par_prenom": nomme.prenom if nomme else None,
"acceptee_par_membre": c.acceptee_par_membre,
})
return out
@router.patch("/candidatures/{candidature_id}")
async def traiter_candidature(
candidature_id: str,
statut: str,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Accepter ou refuser une candidature (exécutif)."""
if statut not in ("acceptee", "refusee"):
raise HTTPException(status_code=400, detail="Statut: acceptee ou refusee")
result = await db.execute(
select(Candidature).where(
Candidature.id == candidature_id,
Candidature.groupe_id == executif.groupe_id,
)
)
cand = result.scalar_one_or_none()
if not cand:
raise HTTPException(status_code=404, detail="Candidature introuvable")
cand.statut = statut
# Si acceptée, créer l'affectation et la décision de nomination automatiquement.
if statut == "acceptee":
if str(executif.id) == str(cand.membre_id):
raise HTTPException(
status_code=400,
detail="Un second membre est requis pour accepter cette candidature comme nomination",
)
membre_cible = await db.get(Membre, cand.membre_id)
poste = await db.get(Poste, cand.poste_id)
if not membre_cible or not membre_cible.actif or membre_cible.groupe_id != executif.groupe_id:
raise HTTPException(status_code=404, detail="Membre à affecter introuvable ou inactif")
if not poste or poste.groupe_id != executif.groupe_id or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
result = await db.execute(
select(Affectation).where(
Affectation.groupe_id == executif.groupe_id,
Affectation.poste_id == cand.poste_id,
Affectation.statut == "actif",
)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Ce poste est déjà affecté")
numero_resolution = await prochain_numero_decision(executif.groupe_id, "nomination", date.today().year, db)
resolution = Proposition(
groupe_id=executif.groupe_id,
sujet=f"Nomination de {membre_cible.prenom} au poste de {poste.nom}",
description=cand.motivation,
proposeur_id=executif.id,
secondeur_id=cand.membre_id,
statut="adoptee",
numero_resolution=numero_resolution,
date_vote=date.today(),
date_adoption=date.today(),
groupe_a_decide_de=f"Nommer {membre_cible.prenom} au poste de {poste.nom}.",
candidature_id=cand.id,
)
db.add(resolution)
await db.flush()
cand.proposition_id = resolution.id
affectation = Affectation(
groupe_id=executif.groupe_id,
membre_id=cand.membre_id,
poste_id=cand.poste_id,
date_debut=date.today(),
statut="actif",
)
db.add(affectation)
await db.flush()
return {"message": "Candidature " + ("acceptée" if statut == "acceptee" else "refusée")}
# ── Affectations ──
@router.get("/affectations")
async def mes_affectations(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.groupe_id == membre.groupe_id,
Affectation.statut == "actif",
)
)
out = []
for a in result.scalars().all():
poste = await db.get(Poste, a.poste_id)
out.append({
"id": str(a.id),
"poste_nom": poste.nom if poste else "?",
"poste_categorie": poste.categorie if poste else "?",
"date_debut": str(a.date_debut),
"statut": a.statut,
})
return out
@router.get("/affectations/groupe")
async def affectations_groupe(executif=Depends(require_executif), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Affectation).where(
Affectation.groupe_id == executif.groupe_id,
Affectation.statut == "actif",
)
)
out = []
for a in result.scalars().all():
poste = await db.get(Poste, a.poste_id)
mbr = await db.get(Membre, a.membre_id)
out.append({
"id": str(a.id),
"membre_id": str(a.membre_id),
"membre_prenom": mbr.prenom if mbr else "?",
"poste_nom": poste.nom if poste else "?",
"poste_categorie": poste.categorie if poste else "?",
"date_debut": str(a.date_debut),
})
return out
@router.post("/affectations")
async def creer_affectation(req: AffectationCreate, executif=Depends(require_executif), db: AsyncSession = Depends(get_db)):
from app.models.proposition import Proposition
if req.proposeur_id == req.secondeur_id:
raise HTTPException(status_code=400, detail="Le proposeur et le secondeur doivent être différents")
cible = await db.get(Membre, req.membre_id)
proposeur = await db.get(Membre, req.proposeur_id)
secondeur = await db.get(Membre, req.secondeur_id)
poste = await db.get(Poste, req.poste_id)
if not cible or cible.groupe_id != executif.groupe_id or not cible.actif:
raise HTTPException(status_code=404, detail="Membre à affecter introuvable ou inactif")
if not proposeur or proposeur.groupe_id != executif.groupe_id or not proposeur.actif:
raise HTTPException(status_code=404, detail="Proposeur introuvable ou inactif")
if not secondeur or secondeur.groupe_id != executif.groupe_id or not secondeur.actif:
raise HTTPException(status_code=404, detail="Secondeur introuvable ou inactif")
if not poste or poste.groupe_id != executif.groupe_id or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
result = await db.execute(
select(Affectation).where(
Affectation.groupe_id == executif.groupe_id,
Affectation.poste_id == req.poste_id,
Affectation.statut == "actif",
)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Ce poste est déjà affecté")
numero_resolution = await prochain_numero_decision(executif.groupe_id, "nomination", req.date_debut.year, db)
resolution = Proposition(
groupe_id=executif.groupe_id,
sujet=f"Nomination de {cible.prenom} au poste de {poste.nom}",
description=f"Affectation directe au poste de {poste.nom} à partir du {req.date_debut}.",
proposeur_id=req.proposeur_id,
secondeur_id=req.secondeur_id,
statut="adoptee",
numero_resolution=numero_resolution,
date_vote=req.date_debut,
date_adoption=req.date_debut,
groupe_a_decide_de=f"Nommer {cible.prenom} au poste de {poste.nom}.",
)
db.add(resolution)
await db.flush()
affectation = Affectation(
groupe_id=executif.groupe_id,
membre_id=req.membre_id,
poste_id=req.poste_id,
date_debut=req.date_debut,
statut="actif",
)
db.add(affectation)
await db.flush()
return {
"message": "Affectation créée",
"id": str(affectation.id),
"resolution_id": str(resolution.id),
"numero_resolution": resolution.numero_resolution,
}
@router.post("/affectations/{affectation_id}/terminer")
async def terminer_mandat(
affectation_id: str, req: TerminerMandatRequest,
executif=Depends(require_executif), db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Affectation).where(
Affectation.id == affectation_id,
Affectation.groupe_id == executif.groupe_id,
Affectation.statut == "actif",
)
)
affectation = result.scalar_one_or_none()
if not affectation:
raise HTTPException(status_code=404, detail="Affectation introuvable")
affectation.statut = "termine"
affectation.date_fin = date.today()
affectation.note_transition = req.note_transition
affectation.termine_par = executif.id
await db.flush()
return {"message": "Mandat terminé"}
# ── Réactivation / suppression définitive de postes ──
@router.post("/{poste_id}/reactiver")
async def reactiver_poste(
poste_id: str,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Réactiver un poste aboli."""
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste:
raise HTTPException(status_code=404, detail="Poste introuvable")
if poste.actif:
raise HTTPException(status_code=400, detail="Ce poste est déjà actif")
poste.actif = True
await db.flush()
return {"message": f"Poste «{poste.nom}» réactivé"}
@router.delete("/{poste_id}/permanent")
async def supprimer_poste_permanent(
poste_id: str,
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
"""Supprimer définitivement un poste aboli (irréversible)."""
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste:
raise HTTPException(status_code=404, detail="Poste introuvable")
if poste.actif:
raise HTTPException(status_code=400, detail="Abolissez d'abord le poste avant de le supprimer")
await db.delete(poste)
await db.flush()
return {"message": f"Poste «{poste.nom}» supprimé définitivement"}
# ── Acces aux modules ──
@router.get("/modules")
async def modules_par_poste(
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(PosteModule)
.join(Poste, PosteModule.poste_id == Poste.id)
.where(Poste.groupe_id == executif.groupe_id)
)
modules = result.scalars().all()
par_poste: dict[str, list[str]] = {}
for m in modules:
par_poste.setdefault(str(m.poste_id), []).append(m.module_code)
return par_poste
@router.put("/{poste_id}/modules")
async def definir_modules_poste(
poste_id: str,
codes: list[str],
executif=Depends(require_executif),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(Poste).where(Poste.id == poste_id, Poste.groupe_id == executif.groupe_id))
poste = result.scalar_one_or_none()
if not poste or not poste.actif:
raise HTTPException(status_code=404, detail="Poste introuvable ou aboli")
result = await db.execute(select(PosteModule).where(PosteModule.poste_id == poste.id))
for item in result.scalars().all():
await db.delete(item)
for code in sorted(set(codes)):
db.add(PosteModule(poste_id=poste.id, module_code=code))
await db.flush()
return {"message": "Accès aux modules mis à jour"}