groupe-meditation/deploy/seed.py
Daniel Allaire 1946b8741e Ajouter acces sysadmin aux groupes
- bouton Sysadmin public avec PIN

- CRUD sysadmin des groupes

- groupe demo protege et reinitialisable
2026-05-31 22:50:35 -04:00

221 lines
9.1 KiB
Python

"""
Script d'initialisation — Groupe Résilience
Crée les postes, le groupe initial et le premier membre.
Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py
"""
import asyncio
import sys
import os
import uuid
import secrets
import csv
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
# Ajouter le backend au path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
from app.core.database import engine, Base, async_session
from app.core.security import hash_pin
from app.services.admin_database import initialiser_demo
# Importer TOUS les modèles pour que create_all crée toutes les tables
from app.models.groupe import Groupe
from app.models.membre import Membre
from app.models.poste import Poste
from app.models.affectation import Affectation
from app.models.invitation import Invitation
from app.models.collecte import Collecte
from app.models.depense import Depense
from app.models.reunion import Reunion
from app.models.presence import Presence
from app.models.pv_reunion import PvReunion
from app.models.rotation import Rotation
from app.models.rapport_rsg import RapportRsg
from app.models.jeton import Jeton
from app.models.litterature import Litterature
from app.models.vente_litterature import VenteLitterature
from app.models.envoi_contribution import EnvoiContribution
from app.models.config_repartition import ConfigRepartition
from app.models.sync_log import SyncLog
from app.models.proposition import Proposition
from app.models.operation_bancaire import OperationBancaire
from app.models.evenement import Evenement
from app.models.candidature import Candidature
from app.models.poste_module import PosteModule
from app.models.reserve import Reserve
from app.models.transaction_comptable import TransactionComptable
from app.models.mouvement_reserve import MouvementReserve
from app.models.destinataire_contribution import DestinataireContribution
POSTES = [
("RSG", "executif", "Représentant aux Services du Groupe", "730 days", "2 ans sobriété", 1),
("RSG Substitut(e)", "executif", "Remplace le RSG, se prépare à assumer le rôle", "730 days", "1 an sobriété", 2),
("Trésorier(ière)", "executif", "Gestion financière, 7e Tradition", "548 days", "1 an sobriété", 3),
("Secrétaire", "executif", "PV, correspondance, mémoire du groupe", "548 days", "6 mois sobriété", 4),
("Animateur(trice)", "service", "Anime les réunions régulières", None, "6 mois sobriété", 5),
("Responsable accueil", "physique", "Accueil des membres et nouveaux", "365 days", "Sobriété stable", 6),
("Responsable café", "physique", "Café, entretien de la salle", "365 days", "Aucun", 7),
("Responsable littérature", "service", "Inventaire et vente de littérature AA", "365 days", "Connaissance littérature AA", 8),
("Responsable jetons", "service", "Jetons et anniversaires de sobriété", "365 days", "Aucun", 9),
("Responsable clés", "physique", "Ouverture et fermeture du local", None, "Fiabilité", 10),
]
def charger_catalogue_litterature():
chemin = Path(__file__).resolve().parents[1] / "data" / "import" / "Liste-de-prix-rev.-Fevrier-2026.csv"
if not chemin.exists():
return [
{"titre": "Les Alcooliques anonymes (Gros Livre)", "categorie": "livre", "prix_coutant": 12.00, "code_bsg": "B-1"},
{"titre": "Les Douze Étapes et les Douze Traditions", "categorie": "livre", "prix_coutant": 11.00, "code_bsg": "B-2"},
{"titre": "Vivre sans alcool", "categorie": "livre", "prix_coutant": 9.00, "code_bsg": "B-7"},
{"titre": "Les AA sont-ils pour vous?", "categorie": "brochure", "prix_coutant": 0.00, "code_bsg": "P-3"},
{"titre": "Un nouveau arrive", "categorie": "depliant", "prix_coutant": 0.00, "code_bsg": "P-16"},
]
with chemin.open(newline="", encoding="utf-8") as fichier:
return [
{
"code_bsg": row["code_bsg"].strip() or None,
"titre": row["titre"].strip(),
"categorie": row["categorie"].strip(),
"prix_coutant": float(row["prix_coutant"]) if row["prix_coutant"].strip() else None,
}
for row in csv.DictReader(fichier)
if row.get("titre", "").strip()
]
async def seed():
# Créer les tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with async_session() as db:
# ── Créer le groupe ──
print("\n═══ Initialisation du Groupe Résilience ═══\n")
groupe = Groupe(
nom="Groupe Résilience",
district="87-16",
region="87",
adresse_local="Salle communautaire Saint-Louis, Montréal",
jour_reunion="mardi",
heure_reunion=datetime.strptime("19:30", "%H:%M").time(),
format_reunion="Discussion ouverte",
lien_aa87="https://aa87.org",
)
db.add(groupe)
await db.flush()
print(f"✓ Groupe créé: {groupe.nom} (district {groupe.district})")
# ── Créer les postes ──
postes_map = {}
for nom, cat, desc, duree, prereq, ordre in POSTES:
poste = Poste(
groupe_id=groupe.id,
nom=nom,
categorie=cat,
description=desc,
prerequis=prereq,
ordre_affichage=ordre,
)
db.add(poste)
await db.flush()
postes_map[nom] = poste
print(f" ✓ Poste: {nom} ({cat})")
# ── Premier membre ──
print("\n── Premier membre (vous) ──")
prenom = input(" Prénom: ").strip().title()
nom_membre = input(" Nom (optionnel, Entrée pour passer): ").strip()
nom_membre = nom_membre.title() if nom_membre else None
courriel = input(" Courriel: ").strip().lower()
while not courriel or ("@" not in courriel and courriel != "sysadmin"):
courriel = input(" Courriel invalide. Réessayer: ").strip().lower()
telephone = input(" Téléphone: ").strip()
while not telephone:
telephone = input(" Téléphone obligatoire: ").strip()
pin = input(" PIN (4 chiffres): ").strip()
while not pin.isdigit() or len(pin) != 4:
pin = input(" PIN invalide. 4 chiffres: ").strip()
poste_choisi = None
if courriel != "sysadmin":
print("\n Postes disponibles:")
for i, (nom_poste, _, _, _, _, _) in enumerate(POSTES[:4], 1):
print(f" {i}. {nom_poste}")
choix = input(" Votre poste (1-4): ").strip()
poste_idx = int(choix) - 1 if choix.isdigit() and 1 <= int(choix) <= 4 else 0
poste_choisi = POSTES[poste_idx][0]
membre = Membre(
groupe_id=groupe.id,
prenom=prenom,
nom=nom_membre,
courriel=courriel,
telephone=telephone,
pin_hash=hash_pin(pin),
)
db.add(membre)
await db.flush()
if poste_choisi:
affectation = Affectation(
groupe_id=groupe.id,
membre_id=membre.id,
poste_id=postes_map[poste_choisi].id,
date_debut=date.today(),
statut="actif",
)
db.add(affectation)
# ── Première invitation ──
code = secrets.token_hex(6).upper()
invitation = Invitation(
groupe_id=groupe.id,
code=code,
cree_par=membre.id,
expire_le=datetime.now(timezone.utc) + timedelta(days=7),
)
db.add(invitation)
# ── Littérature de base ──
catalogue_litterature = charger_catalogue_litterature()
for item in catalogue_litterature:
db.add(Litterature(
groupe_id=groupe.id,
titre=item["titre"],
categorie=item["categorie"],
prix_coutant=item["prix_coutant"],
quantite_stock=0,
seuil_commande=2,
code_bsg=item["code_bsg"],
))
print(f" ✓ Littérature de base : {len(catalogue_litterature)} titres")
db.add(DestinataireContribution(
groupe_id=groupe.id,
nom="District 87-16",
protege=True,
actif=True,
))
print(" ✓ Destinataire de contribution : District 87-16")
demo = await initialiser_demo(db)
print(f" ✓ Groupe de démonstration : {demo['message']}")
await db.commit()
print(f"\n═══ Initialisation terminée ═══")
print(f" Membre: {prenom} ({courriel})")
print(f" Groupe: Groupe Méditation")
print(f" Poste: {poste_choisi or 'Sysadmin'}")
print(f"\n Code d'invitation pour les prochains membres: {code}")
print(f" (Expire dans 7 jours)")
print(f"\n Connectez-vous sur l'app avec:")
print(f" Courriel: {courriel}")
print(f" PIN: {pin}")
if __name__ == "__main__":
asyncio.run(seed())