Files
decision/backend/app/routers/groups.py
Yvv 9a8f10efdf Groupes d'identités : modèle DB, router, store, UI cercles + Protocoles
- Group + GroupMember : modèle SQLAlchemy + migration + router CRUD
- /api/v1/groups : liste, création, suppression, membres (add/remove)
- groups.ts : store Pinia (fetchAll, getGroup, create, remove, addMember, removeMember)
- decisions/new.vue : cercles 1 & 2 en mode texte libre OU groupe prédéfini
  (affected_count calculé depuis le member_count du groupe)
- protocols/index.vue : section Groupes avec expand/collapse, ajout/suppression membres
- lang="fr" + spellcheck sur tous les textareas ; placeholder cercle 2 corrigé
- n8n channels : prévu sprint futur (texte libre → webhook appel à contribution)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 20:25:44 +02:00

127 lines
4.1 KiB
Python

"""Groups router — predefined sets of Duniter identities used in decision circles."""
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models.group import Group, GroupMember
from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary
from app.services.auth_service import get_current_identity
router = APIRouter()
def _org_id_from_header(request_headers) -> uuid.UUID | None:
raw = request_headers.get("x-organization")
if not raw:
return None
try:
return uuid.UUID(raw)
except ValueError:
return None
@router.get("/", response_model=list[GroupSummary])
async def list_groups(
db: AsyncSession = Depends(get_db),
) -> list[GroupSummary]:
"""List all groups. No auth required — groups are public within the workspace."""
result = await db.execute(
select(Group).options(selectinload(Group.members)).order_by(Group.name)
)
groups = result.scalars().all()
return [
GroupSummary(
id=g.id,
name=g.name,
description=g.description,
organization_id=g.organization_id,
member_count=len(g.members),
)
for g in groups
]
@router.post("/", response_model=GroupOut, status_code=201)
async def create_group(
payload: GroupCreate,
db: AsyncSession = Depends(get_db),
_identity=Depends(get_current_identity),
) -> GroupOut:
group = Group(name=payload.name, description=payload.description)
db.add(group)
await db.commit()
await db.refresh(group)
await db.execute(select(Group).where(Group.id == group.id).options(selectinload(Group.members)))
return GroupOut.model_validate(group)
@router.get("/{group_id}", response_model=GroupOut)
async def get_group(group_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> GroupOut:
result = await db.execute(
select(Group).where(Group.id == group_id).options(selectinload(Group.members))
)
group = result.scalar_one_or_none()
if group is None:
raise HTTPException(status_code=404, detail="Group not found")
return GroupOut.model_validate(group)
@router.delete("/{group_id}", status_code=204, response_class=Response, response_model=None)
async def delete_group(
group_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
_identity=Depends(get_current_identity),
) -> None:
result = await db.execute(select(Group).where(Group.id == group_id))
group = result.scalar_one_or_none()
if group is None:
raise HTTPException(status_code=404, detail="Group not found")
await db.delete(group)
await db.commit()
@router.post("/{group_id}/members", response_model=GroupMemberOut, status_code=201)
async def add_member(
group_id: uuid.UUID,
payload: GroupMemberCreate,
db: AsyncSession = Depends(get_db),
_identity=Depends(get_current_identity),
) -> GroupMemberOut:
result = await db.execute(select(Group).where(Group.id == group_id))
if result.scalar_one_or_none() is None:
raise HTTPException(status_code=404, detail="Group not found")
member = GroupMember(
group_id=group_id,
display_name=payload.display_name,
identity_id=payload.identity_id,
)
db.add(member)
await db.commit()
await db.refresh(member)
return GroupMemberOut.model_validate(member)
@router.delete("/{group_id}/members/{member_id}", status_code=204, response_class=Response, response_model=None)
async def remove_member(
group_id: uuid.UUID,
member_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
_identity=Depends(get_current_identity),
) -> None:
result = await db.execute(
select(GroupMember).where(GroupMember.id == member_id, GroupMember.group_id == group_id)
)
member = result.scalar_one_or_none()
if member is None:
raise HTTPException(status_code=404, detail="Member not found")
await db.delete(member)
await db.commit()