Sprint 1 : scaffolding complet de Glibredecision
Plateforme de decisions collectives pour Duniter/G1. Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services, moteur de vote avec formule d'inertie WoT/Smith/TechComm). Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores). Infrastructure Docker + Woodpecker CI + Traefik. Documentation technique et utilisateur (15 fichiers). Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote. 30 tests unitaires (formules, mode params, vote nuance) -- tous verts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
backend/app/routers/protocols.py
Normal file
139
backend/app/routers/protocols.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Protocols router: voting protocols and formula configurations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.protocol import FormulaConfig, VotingProtocol
|
||||
from app.models.user import DuniterIdentity
|
||||
from app.schemas.protocol import (
|
||||
FormulaConfigCreate,
|
||||
FormulaConfigOut,
|
||||
VotingProtocolCreate,
|
||||
VotingProtocolOut,
|
||||
)
|
||||
from app.services.auth_service import get_current_identity
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _get_protocol(db: AsyncSession, protocol_id: uuid.UUID) -> VotingProtocol:
|
||||
"""Fetch a voting protocol by ID with its formula config, or raise 404."""
|
||||
result = await db.execute(
|
||||
select(VotingProtocol)
|
||||
.options(selectinload(VotingProtocol.formula_config))
|
||||
.where(VotingProtocol.id == protocol_id)
|
||||
)
|
||||
protocol = result.scalar_one_or_none()
|
||||
if protocol is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable")
|
||||
return protocol
|
||||
|
||||
|
||||
# ── Voting Protocol routes ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/", response_model=list[VotingProtocolOut])
|
||||
async def list_protocols(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
vote_type: str | None = Query(default=None, description="Filtrer par type de vote (binary, nuanced)"),
|
||||
skip: int = Query(default=0, ge=0),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
) -> list[VotingProtocolOut]:
|
||||
"""List all voting protocols with their formula configurations."""
|
||||
stmt = select(VotingProtocol).options(selectinload(VotingProtocol.formula_config))
|
||||
|
||||
if vote_type is not None:
|
||||
stmt = stmt.where(VotingProtocol.vote_type == vote_type)
|
||||
|
||||
stmt = stmt.order_by(VotingProtocol.created_at.desc()).offset(skip).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
protocols = result.scalars().unique().all()
|
||||
|
||||
return [VotingProtocolOut.model_validate(p) for p in protocols]
|
||||
|
||||
|
||||
@router.post("/", response_model=VotingProtocolOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_protocol(
|
||||
payload: VotingProtocolCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: DuniterIdentity = Depends(get_current_identity),
|
||||
) -> VotingProtocolOut:
|
||||
"""Create a new voting protocol.
|
||||
|
||||
The formula_config_id must reference an existing FormulaConfig.
|
||||
"""
|
||||
# Verify formula config exists
|
||||
fc_result = await db.execute(
|
||||
select(FormulaConfig).where(FormulaConfig.id == payload.formula_config_id)
|
||||
)
|
||||
if fc_result.scalar_one_or_none() is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Configuration de formule introuvable",
|
||||
)
|
||||
|
||||
protocol = VotingProtocol(**payload.model_dump())
|
||||
db.add(protocol)
|
||||
await db.commit()
|
||||
await db.refresh(protocol)
|
||||
|
||||
# Reload with formula config
|
||||
protocol = await _get_protocol(db, protocol.id)
|
||||
return VotingProtocolOut.model_validate(protocol)
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=VotingProtocolOut)
|
||||
async def get_protocol(
|
||||
id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> VotingProtocolOut:
|
||||
"""Get a single voting protocol with its formula configuration."""
|
||||
protocol = await _get_protocol(db, id)
|
||||
return VotingProtocolOut.model_validate(protocol)
|
||||
|
||||
|
||||
# ── Formula Config routes ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/formulas", response_model=list[FormulaConfigOut])
|
||||
async def list_formulas(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
skip: int = Query(default=0, ge=0),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
) -> list[FormulaConfigOut]:
|
||||
"""List all formula configurations."""
|
||||
stmt = (
|
||||
select(FormulaConfig)
|
||||
.order_by(FormulaConfig.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
formulas = result.scalars().all()
|
||||
|
||||
return [FormulaConfigOut.model_validate(f) for f in formulas]
|
||||
|
||||
|
||||
@router.post("/formulas", response_model=FormulaConfigOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_formula(
|
||||
payload: FormulaConfigCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
identity: DuniterIdentity = Depends(get_current_identity),
|
||||
) -> FormulaConfigOut:
|
||||
"""Create a new formula configuration for WoT threshold computation."""
|
||||
formula = FormulaConfig(**payload.model_dump())
|
||||
db.add(formula)
|
||||
await db.commit()
|
||||
await db.refresh(formula)
|
||||
|
||||
return FormulaConfigOut.model_validate(formula)
|
||||
Reference in New Issue
Block a user