groupe-meditation/backend/app/main.py

181 lines
8.2 KiB
Python

"""
App — API Backend
District 87-16, Région 87
100% logiciel libre · Hébergé au Québec
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from app.core.config import settings
from app.core.audit import AuditMiddleware
from app.core.database import engine, Base
from app.services.tenant_schemas import ensure_all_tenant_schemas
from app.routers import admin, auth, membres, postes, accueil, invitations, collectes, reunions, depenses, calendrier, rapports, rotations, parametres, presences, pv, rapports_rsg, jetons, anniversaires, commandes_jetons, litterature, ventes_litterature, contributions, config_repartition, propositions, operations_bancaires, evenements, notifications, journal
# Import models pour que create_all crée toutes les tables
from app.models.sync_log import SyncLog # noqa: F401
from app.models.proposition import Proposition # noqa: F401
from app.models.operation_bancaire import OperationBancaire # noqa: F401
from app.models.evenement import Evenement # noqa: F401
from app.models.candidature import Candidature # noqa: F401
from app.models.poste_module import PosteModule # noqa: F401
from app.models.reserve import Reserve # noqa: F401
from app.models.transaction_comptable import TransactionComptable # noqa: F401
from app.models.mouvement_reserve import MouvementReserve # noqa: F401
from app.models.journal_action import JournalAction # noqa: F401
from app.models.push_subscription import PushSubscription # noqa: F401
from app.models.destinataire_contribution import DestinataireContribution # noqa: F401
from app.models.rapport_adopte import RapportAdopte # noqa: F401
from app.models.vente_jeton import VenteJeton # noqa: F401
from app.models.historique_modification import HistoriqueModification # noqa: F401
from app.models.instance_admin import InstanceAdmin # noqa: F401
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.execute(text("ALTER TABLE groupes ADD COLUMN IF NOT EXISTS tenant_schema VARCHAR(80) UNIQUE"))
await conn.execute(text(
"UPDATE groupes SET tenant_schema = 'grp_' || replace(id::text, '-', '') "
"WHERE tenant_schema IS NULL"
))
await conn.execute(text(
"ALTER TABLE groupes "
"ADD COLUMN IF NOT EXISTS assemblee_affaires_ordre VARCHAR(20) DEFAULT 'premiere'"
))
await conn.execute(text("ALTER TABLE postes ADD COLUMN IF NOT EXISTS groupe_id UUID REFERENCES groupes(id)"))
await conn.execute(text(
"UPDATE postes SET groupe_id = (SELECT id FROM groupes ORDER BY cree_le LIMIT 1) "
"WHERE groupe_id IS NULL AND EXISTS (SELECT 1 FROM groupes)"
))
await conn.execute(text("ALTER TABLE postes ALTER COLUMN groupe_id SET NOT NULL"))
await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS date_adoption DATE"))
await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS etant_donne_que TEXT"))
await conn.execute(text("ALTER TABLE propositions ADD COLUMN IF NOT EXISTS groupe_a_decide_de TEXT"))
await conn.execute(text("ALTER TABLE propositions DROP COLUMN IF EXISTS type_proposition"))
await conn.execute(text("ALTER TABLE membres ADD COLUMN IF NOT EXISTS notification_preferences JSON"))
await conn.execute(text("ALTER TABLE jetons ADD COLUMN IF NOT EXISTS reunion_id UUID REFERENCES reunions(id)"))
await conn.execute(text("ALTER TABLE ventes_litterature ADD COLUMN IF NOT EXISTS reunion_id UUID REFERENCES reunions(id)"))
await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_type VARCHAR(40)"))
await conn.execute(text("ALTER TABLE invitations ADD COLUMN IF NOT EXISTS cible_id UUID"))
for table in (
"propositions",
"depenses",
"collectes",
"reunions",
"pv_reunions",
"reserves",
"litterature",
"destinataires_contributions",
"envois_contributions",
):
await conn.execute(text(
f"ALTER TABLE {table} "
"ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMPTZ DEFAULT now() NOT NULL"
))
await conn.execute(text(
"ALTER TABLE envois_contributions "
"ALTER COLUMN destination TYPE VARCHAR(120) USING destination::text"
))
await conn.execute(text("DROP TABLE IF EXISTS votes"))
await conn.execute(text("DROP TABLE IF EXISTS options_sondage"))
await conn.execute(text("DROP TABLE IF EXISTS sondages"))
await conn.execute(text("DROP TABLE IF EXISTS decisions"))
await conn.execute(text(
"INSERT INTO destinataires_contributions (id, groupe_id, nom, protege, actif, cree_le) "
"SELECT CAST(md5(random()::text || clock_timestamp()::text) AS uuid), g.id, 'District 87-16', true, true, now() "
"FROM groupes g "
"WHERE NOT EXISTS ("
" SELECT 1 FROM destinataires_contributions d "
" WHERE d.groupe_id = g.id AND d.nom = 'District 87-16'"
")"
))
for table in (
"affectations",
"candidatures",
"collectes",
"config_repartition",
"depenses",
"destinataires_contributions",
"envois_contributions",
"evenements",
"historique_modifications",
"invitations",
"jetons",
"journal_actions",
"litterature",
"membres",
"mouvements_reserves",
"operations_bancaires",
"postes",
"propositions",
"push_subscriptions",
"rapports_adoptes",
"rapports_rsg",
"reserves",
"reunions",
"rotations",
"sync_log",
"transactions_comptables",
"ventes_jetons",
"ventes_litterature",
):
await conn.execute(text(f"CREATE INDEX IF NOT EXISTS idx_{table}_groupe_id ON {table} (groupe_id)"))
await ensure_all_tenant_schemas(conn)
yield
await engine.dispose()
app = FastAPI(
title="App API",
description="API du App — District 87-16, Région 87",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(AuditMiddleware)
# Montage des routes
app.include_router(auth.router, prefix="/api")
app.include_router(admin.router, prefix="/api")
app.include_router(membres.router, prefix="/api")
app.include_router(postes.router, prefix="/api")
app.include_router(accueil.router, prefix="/api")
app.include_router(invitations.router, prefix="/api")
app.include_router(collectes.router, prefix="/api")
app.include_router(reunions.router, prefix="/api")
app.include_router(depenses.router, prefix="/api")
app.include_router(calendrier.router, prefix="/api")
app.include_router(rapports.router, prefix="/api")
app.include_router(rotations.router, prefix="/api")
app.include_router(parametres.router, prefix="/api")
app.include_router(presences.router, prefix="/api")
app.include_router(pv.router, prefix="/api")
app.include_router(rapports_rsg.router, prefix="/api")
app.include_router(jetons.router, prefix="/api")
app.include_router(anniversaires.router, prefix="/api")
app.include_router(commandes_jetons.router, prefix="/api")
app.include_router(litterature.router, prefix="/api")
app.include_router(ventes_litterature.router, prefix="/api")
app.include_router(contributions.router, prefix="/api")
app.include_router(config_repartition.router, prefix="/api")
app.include_router(propositions.router, prefix="/api")
app.include_router(operations_bancaires.router, prefix="/api")
app.include_router(evenements.router, prefix="/api")
app.include_router(notifications.router, prefix="/api")
app.include_router(journal.router, prefix="/api")
@app.get("/api/health")
async def root_health():
return {"status": "ok", "app": "App", "version": "0.1.0"}