forked from yvv/decision
Compartimentation : isolation stricte des données par espace de travail
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Ajout clause else IS NULL sur tous les endpoints list (protocols, decisions, mandates, documents, groups, votes) — sans X-Organization → données globales seulement, jamais le contenu d'un autre espace - _get_protocol/_get_decision/_get_mandate : org_id propagé à tous les endpoints GET/PUT/DELETE/advance/assign/revoke/steps → 404 si UUID d'un autre espace - votes.py : list_vote_sessions filtre via JOIN VotingProtocol.organization_id - groups.py : suppression _org_id_from_header() mort, create_group assigne organization_id correctement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,13 +31,20 @@ router = APIRouter()
|
|||||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def _get_decision(db: AsyncSession, decision_id: uuid.UUID) -> Decision:
|
async def _get_decision(
|
||||||
"""Fetch a decision by ID with its steps eagerly loaded, or raise 404."""
|
db: AsyncSession, decision_id: uuid.UUID, org_id: uuid.UUID | None = None
|
||||||
result = await db.execute(
|
) -> Decision:
|
||||||
|
"""Fetch a decision by ID within the active org scope, or raise 404."""
|
||||||
|
stmt = (
|
||||||
select(Decision)
|
select(Decision)
|
||||||
.options(selectinload(Decision.steps))
|
.options(selectinload(Decision.steps))
|
||||||
.where(Decision.id == decision_id)
|
.where(Decision.id == decision_id)
|
||||||
)
|
)
|
||||||
|
if org_id is not None:
|
||||||
|
stmt = stmt.where(Decision.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Decision.organization_id.is_(None))
|
||||||
|
result = await db.execute(stmt)
|
||||||
decision = result.scalar_one_or_none()
|
decision = result.scalar_one_or_none()
|
||||||
if decision is None:
|
if decision is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Decision introuvable")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Decision introuvable")
|
||||||
@@ -61,6 +68,8 @@ async def list_decisions(
|
|||||||
|
|
||||||
if org_id is not None:
|
if org_id is not None:
|
||||||
stmt = stmt.where(Decision.organization_id == org_id)
|
stmt = stmt.where(Decision.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Decision.organization_id.is_(None))
|
||||||
if decision_type is not None:
|
if decision_type is not None:
|
||||||
stmt = stmt.where(Decision.decision_type == decision_type)
|
stmt = stmt.where(Decision.decision_type == decision_type)
|
||||||
if status_filter is not None:
|
if status_filter is not None:
|
||||||
@@ -91,7 +100,7 @@ async def create_decision(
|
|||||||
await db.refresh(decision)
|
await db.refresh(decision)
|
||||||
|
|
||||||
# Reload with steps (empty at creation)
|
# Reload with steps (empty at creation)
|
||||||
decision = await _get_decision(db, decision.id)
|
decision = await _get_decision(db, decision.id, org_id)
|
||||||
return DecisionOut.model_validate(decision)
|
return DecisionOut.model_validate(decision)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,9 +108,10 @@ async def create_decision(
|
|||||||
async def get_decision(
|
async def get_decision(
|
||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> DecisionOut:
|
) -> DecisionOut:
|
||||||
"""Get a single decision with all its steps."""
|
"""Get a single decision with all its steps."""
|
||||||
decision = await _get_decision(db, id)
|
decision = await _get_decision(db, id, org_id)
|
||||||
return DecisionOut.model_validate(decision)
|
return DecisionOut.model_validate(decision)
|
||||||
|
|
||||||
|
|
||||||
@@ -111,9 +121,10 @@ async def update_decision(
|
|||||||
payload: DecisionUpdate,
|
payload: DecisionUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> DecisionOut:
|
) -> DecisionOut:
|
||||||
"""Update a decision's metadata (title, description, status, protocol)."""
|
"""Update a decision's metadata (title, description, status, protocol)."""
|
||||||
decision = await _get_decision(db, id)
|
decision = await _get_decision(db, id, org_id)
|
||||||
|
|
||||||
update_data = payload.model_dump(exclude_unset=True)
|
update_data = payload.model_dump(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
@@ -123,7 +134,7 @@ async def update_decision(
|
|||||||
await db.refresh(decision)
|
await db.refresh(decision)
|
||||||
|
|
||||||
# Reload with steps
|
# Reload with steps
|
||||||
decision = await _get_decision(db, decision.id)
|
decision = await _get_decision(db, decision.id, org_id)
|
||||||
return DecisionOut.model_validate(decision)
|
return DecisionOut.model_validate(decision)
|
||||||
|
|
||||||
|
|
||||||
@@ -136,10 +147,11 @@ async def add_step(
|
|||||||
payload: DecisionStepCreate,
|
payload: DecisionStepCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> DecisionStepOut:
|
) -> DecisionStepOut:
|
||||||
"""Add a step to a decision process."""
|
"""Add a step to a decision process."""
|
||||||
# Verify decision exists
|
# Verify decision exists
|
||||||
decision = await _get_decision(db, id)
|
decision = await _get_decision(db, id, org_id)
|
||||||
|
|
||||||
step = DecisionStep(
|
step = DecisionStep(
|
||||||
decision_id=decision.id,
|
decision_id=decision.id,
|
||||||
@@ -160,6 +172,7 @@ async def advance_decision_endpoint(
|
|||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> DecisionAdvanceOut:
|
) -> DecisionAdvanceOut:
|
||||||
"""Advance a decision to its next step or status."""
|
"""Advance a decision to its next step or status."""
|
||||||
try:
|
try:
|
||||||
@@ -168,7 +181,7 @@ async def advance_decision_endpoint(
|
|||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
|
||||||
# Reload with steps for complete output
|
# Reload with steps for complete output
|
||||||
decision = await _get_decision(db, decision.id)
|
decision = await _get_decision(db, decision.id, org_id)
|
||||||
data = DecisionOut.model_validate(decision).model_dump()
|
data = DecisionOut.model_validate(decision).model_dump()
|
||||||
data["message"] = f"Decision avancee au statut : {decision.status}"
|
data["message"] = f"Decision avancee au statut : {decision.status}"
|
||||||
return DecisionAdvanceOut(**data)
|
return DecisionAdvanceOut(**data)
|
||||||
@@ -184,6 +197,7 @@ async def create_vote_session_for_step_endpoint(
|
|||||||
step_id: uuid.UUID,
|
step_id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> VoteSessionOut:
|
) -> VoteSessionOut:
|
||||||
"""Create a vote session linked to a decision step."""
|
"""Create a vote session linked to a decision step."""
|
||||||
try:
|
try:
|
||||||
@@ -199,9 +213,10 @@ async def delete_decision(
|
|||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete a decision (only if in draft status)."""
|
"""Delete a decision (only if in draft status)."""
|
||||||
decision = await _get_decision(db, id)
|
decision = await _get_decision(db, id, org_id)
|
||||||
|
|
||||||
if decision.status != "draft":
|
if decision.status != "draft":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ async def list_documents(
|
|||||||
|
|
||||||
if org_id is not None:
|
if org_id is not None:
|
||||||
stmt = stmt.where(Document.organization_id == org_id)
|
stmt = stmt.where(Document.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Document.organization_id.is_(None))
|
||||||
if doc_type is not None:
|
if doc_type is not None:
|
||||||
stmt = stmt.where(Document.doc_type == doc_type)
|
stmt = stmt.where(Document.doc_type == doc_type)
|
||||||
if status_filter is not None:
|
if status_filter is not None:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
from app.dependencies.org import get_active_org_id
|
||||||
from app.models.group import Group, GroupMember
|
from app.models.group import Group, GroupMember
|
||||||
from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary
|
from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary
|
||||||
from app.services.auth_service import get_current_identity
|
from app.services.auth_service import get_current_identity
|
||||||
@@ -18,24 +19,18 @@ from app.services.auth_service import get_current_identity
|
|||||||
router = APIRouter()
|
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])
|
@router.get("/", response_model=list[GroupSummary])
|
||||||
async def list_groups(
|
async def list_groups(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> list[GroupSummary]:
|
) -> list[GroupSummary]:
|
||||||
"""List all groups. No auth required — groups are public within the workspace."""
|
"""List groups within the active workspace."""
|
||||||
result = await db.execute(
|
stmt = select(Group).options(selectinload(Group.members)).order_by(Group.name)
|
||||||
select(Group).options(selectinload(Group.members)).order_by(Group.name)
|
if org_id is not None:
|
||||||
)
|
stmt = stmt.where(Group.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Group.organization_id.is_(None))
|
||||||
|
result = await db.execute(stmt)
|
||||||
groups = result.scalars().all()
|
groups = result.scalars().all()
|
||||||
return [
|
return [
|
||||||
GroupSummary(
|
GroupSummary(
|
||||||
@@ -54,8 +49,9 @@ async def create_group(
|
|||||||
payload: GroupCreate,
|
payload: GroupCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_identity=Depends(get_current_identity),
|
_identity=Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> GroupOut:
|
) -> GroupOut:
|
||||||
group = Group(name=payload.name, description=payload.description)
|
group = Group(name=payload.name, description=payload.description, organization_id=org_id)
|
||||||
db.add(group)
|
db.add(group)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(group)
|
await db.refresh(group)
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ router = APIRouter()
|
|||||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def _get_mandate(db: AsyncSession, mandate_id: uuid.UUID) -> Mandate:
|
async def _get_mandate(
|
||||||
result = await db.execute(
|
db: AsyncSession, mandate_id: uuid.UUID, org_id: uuid.UUID | None = None
|
||||||
|
) -> Mandate:
|
||||||
|
stmt = (
|
||||||
select(Mandate)
|
select(Mandate)
|
||||||
.options(
|
.options(
|
||||||
selectinload(Mandate.steps),
|
selectinload(Mandate.steps),
|
||||||
@@ -47,6 +49,11 @@ async def _get_mandate(db: AsyncSession, mandate_id: uuid.UUID) -> Mandate:
|
|||||||
)
|
)
|
||||||
.where(Mandate.id == mandate_id)
|
.where(Mandate.id == mandate_id)
|
||||||
)
|
)
|
||||||
|
if org_id is not None:
|
||||||
|
stmt = stmt.where(Mandate.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Mandate.organization_id.is_(None))
|
||||||
|
result = await db.execute(stmt)
|
||||||
mandate = result.scalar_one_or_none()
|
mandate = result.scalar_one_or_none()
|
||||||
if mandate is None:
|
if mandate is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable")
|
||||||
@@ -80,6 +87,8 @@ async def list_mandates(
|
|||||||
|
|
||||||
if org_id is not None:
|
if org_id is not None:
|
||||||
stmt = stmt.where(Mandate.organization_id == org_id)
|
stmt = stmt.where(Mandate.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(Mandate.organization_id.is_(None))
|
||||||
if mandate_type is not None:
|
if mandate_type is not None:
|
||||||
stmt = stmt.where(Mandate.mandate_type == mandate_type)
|
stmt = stmt.where(Mandate.mandate_type == mandate_type)
|
||||||
if status_filter is not None:
|
if status_filter is not None:
|
||||||
@@ -111,7 +120,7 @@ async def create_mandate(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(mandate)
|
await db.refresh(mandate)
|
||||||
|
|
||||||
mandate = await _get_mandate(db, mandate.id)
|
mandate = await _get_mandate(db, mandate.id, org_id)
|
||||||
return _mandate_out(mandate)
|
return _mandate_out(mandate)
|
||||||
|
|
||||||
|
|
||||||
@@ -119,8 +128,9 @@ async def create_mandate(
|
|||||||
async def get_mandate(
|
async def get_mandate(
|
||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateOut:
|
) -> MandateOut:
|
||||||
mandate = await _get_mandate(db, id)
|
mandate = await _get_mandate(db, id, org_id)
|
||||||
return _mandate_out(mandate)
|
return _mandate_out(mandate)
|
||||||
|
|
||||||
|
|
||||||
@@ -130,14 +140,15 @@ async def update_mandate(
|
|||||||
payload: MandateUpdate,
|
payload: MandateUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateOut:
|
) -> MandateOut:
|
||||||
mandate = await _get_mandate(db, id)
|
mandate = await _get_mandate(db, id, org_id)
|
||||||
|
|
||||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||||
setattr(mandate, field, value)
|
setattr(mandate, field, value)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
mandate = await _get_mandate(db, mandate.id)
|
mandate = await _get_mandate(db, mandate.id, org_id)
|
||||||
return _mandate_out(mandate)
|
return _mandate_out(mandate)
|
||||||
|
|
||||||
|
|
||||||
@@ -146,8 +157,9 @@ async def delete_mandate(
|
|||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> None:
|
) -> None:
|
||||||
mandate = await _get_mandate(db, id)
|
mandate = await _get_mandate(db, id, org_id)
|
||||||
|
|
||||||
if mandate.status != "draft":
|
if mandate.status != "draft":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -168,8 +180,9 @@ async def add_step(
|
|||||||
payload: MandateStepCreate,
|
payload: MandateStepCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateStepOut:
|
) -> MandateStepOut:
|
||||||
mandate = await _get_mandate(db, id)
|
mandate = await _get_mandate(db, id, org_id)
|
||||||
|
|
||||||
step = MandateStep(mandate_id=mandate.id, **payload.model_dump())
|
step = MandateStep(mandate_id=mandate.id, **payload.model_dump())
|
||||||
db.add(step)
|
db.add(step)
|
||||||
@@ -183,8 +196,9 @@ async def add_step(
|
|||||||
async def list_steps(
|
async def list_steps(
|
||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> list[MandateStepOut]:
|
) -> list[MandateStepOut]:
|
||||||
mandate = await _get_mandate(db, id)
|
mandate = await _get_mandate(db, id, org_id)
|
||||||
return [MandateStepOut.model_validate(s) for s in mandate.steps]
|
return [MandateStepOut.model_validate(s) for s in mandate.steps]
|
||||||
|
|
||||||
|
|
||||||
@@ -196,13 +210,14 @@ async def advance_mandate_endpoint(
|
|||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateAdvanceOut:
|
) -> MandateAdvanceOut:
|
||||||
try:
|
try:
|
||||||
mandate = await advance_mandate(id, db)
|
mandate = await advance_mandate(id, db)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
|
||||||
mandate = await _get_mandate(db, mandate.id)
|
mandate = await _get_mandate(db, mandate.id, org_id)
|
||||||
out = _mandate_out(mandate)
|
out = _mandate_out(mandate)
|
||||||
return MandateAdvanceOut(
|
return MandateAdvanceOut(
|
||||||
**out.model_dump(),
|
**out.model_dump(),
|
||||||
@@ -216,13 +231,14 @@ async def assign_mandatee_endpoint(
|
|||||||
payload: MandateAssignRequest,
|
payload: MandateAssignRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateOut:
|
) -> MandateOut:
|
||||||
try:
|
try:
|
||||||
mandate = await assign_mandatee(id, payload.mandatee_id, db)
|
mandate = await assign_mandatee(id, payload.mandatee_id, db)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
|
||||||
mandate = await _get_mandate(db, mandate.id)
|
mandate = await _get_mandate(db, mandate.id, org_id)
|
||||||
return _mandate_out(mandate)
|
return _mandate_out(mandate)
|
||||||
|
|
||||||
|
|
||||||
@@ -231,13 +247,14 @@ async def revoke_mandate_endpoint(
|
|||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> MandateOut:
|
) -> MandateOut:
|
||||||
try:
|
try:
|
||||||
mandate = await revoke_mandate(id, db)
|
mandate = await revoke_mandate(id, db)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
|
||||||
mandate = await _get_mandate(db, mandate.id)
|
mandate = await _get_mandate(db, mandate.id, org_id)
|
||||||
return _mandate_out(mandate)
|
return _mandate_out(mandate)
|
||||||
|
|
||||||
|
|
||||||
@@ -251,6 +268,7 @@ async def create_vote_session_for_step_endpoint(
|
|||||||
step_id: uuid.UUID,
|
step_id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> VoteSessionOut:
|
) -> VoteSessionOut:
|
||||||
try:
|
try:
|
||||||
session = await create_vote_session_for_step(id, step_id, db)
|
session = await create_vote_session_for_step(id, step_id, db)
|
||||||
|
|||||||
@@ -34,13 +34,20 @@ router = APIRouter()
|
|||||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def _get_protocol(db: AsyncSession, protocol_id: uuid.UUID) -> VotingProtocol:
|
async def _get_protocol(
|
||||||
"""Fetch a voting protocol by ID with its formula config, or raise 404."""
|
db: AsyncSession, protocol_id: uuid.UUID, org_id: uuid.UUID | None = None
|
||||||
result = await db.execute(
|
) -> VotingProtocol:
|
||||||
|
"""Fetch a voting protocol by ID within the active org scope, or raise 404."""
|
||||||
|
stmt = (
|
||||||
select(VotingProtocol)
|
select(VotingProtocol)
|
||||||
.options(selectinload(VotingProtocol.formula_config))
|
.options(selectinload(VotingProtocol.formula_config))
|
||||||
.where(VotingProtocol.id == protocol_id)
|
.where(VotingProtocol.id == protocol_id)
|
||||||
)
|
)
|
||||||
|
if org_id is not None:
|
||||||
|
stmt = stmt.where(VotingProtocol.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
|
||||||
|
result = await db.execute(stmt)
|
||||||
protocol = result.scalar_one_or_none()
|
protocol = result.scalar_one_or_none()
|
||||||
if protocol is None:
|
if protocol is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Protocole introuvable")
|
||||||
@@ -74,6 +81,8 @@ async def list_protocols(
|
|||||||
|
|
||||||
if org_id is not None:
|
if org_id is not None:
|
||||||
stmt = stmt.where(VotingProtocol.organization_id == org_id)
|
stmt = stmt.where(VotingProtocol.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
|
||||||
if vote_type is not None:
|
if vote_type is not None:
|
||||||
stmt = stmt.where(VotingProtocol.vote_type == vote_type)
|
stmt = stmt.where(VotingProtocol.vote_type == vote_type)
|
||||||
|
|
||||||
@@ -111,7 +120,7 @@ async def create_protocol(
|
|||||||
await db.refresh(protocol)
|
await db.refresh(protocol)
|
||||||
|
|
||||||
# Reload with formula config
|
# Reload with formula config
|
||||||
protocol = await _get_protocol(db, protocol.id)
|
protocol = await _get_protocol(db, protocol.id, org_id)
|
||||||
return VotingProtocolOut.model_validate(protocol)
|
return VotingProtocolOut.model_validate(protocol)
|
||||||
|
|
||||||
|
|
||||||
@@ -119,9 +128,10 @@ async def create_protocol(
|
|||||||
async def get_protocol(
|
async def get_protocol(
|
||||||
id: uuid.UUID,
|
id: uuid.UUID,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> VotingProtocolOut:
|
) -> VotingProtocolOut:
|
||||||
"""Get a single voting protocol with its formula configuration."""
|
"""Get a single voting protocol with its formula configuration."""
|
||||||
protocol = await _get_protocol(db, id)
|
protocol = await _get_protocol(db, id, org_id)
|
||||||
return VotingProtocolOut.model_validate(protocol)
|
return VotingProtocolOut.model_validate(protocol)
|
||||||
|
|
||||||
|
|
||||||
@@ -131,12 +141,13 @@ async def update_protocol(
|
|||||||
payload: VotingProtocolUpdate,
|
payload: VotingProtocolUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
identity: DuniterIdentity = Depends(get_current_identity),
|
identity: DuniterIdentity = Depends(get_current_identity),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
) -> VotingProtocolOut:
|
) -> VotingProtocolOut:
|
||||||
"""Update a voting protocol (meta-governance).
|
"""Update a voting protocol (meta-governance).
|
||||||
|
|
||||||
Only provided fields will be updated. Requires authentication.
|
Only provided fields will be updated. Requires authentication.
|
||||||
"""
|
"""
|
||||||
protocol = await _get_protocol(db, id)
|
protocol = await _get_protocol(db, id, org_id)
|
||||||
|
|
||||||
update_data = payload.model_dump(exclude_unset=True)
|
update_data = payload.model_dump(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
@@ -145,7 +156,7 @@ async def update_protocol(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Reload with formula config
|
# Reload with formula config
|
||||||
protocol = await _get_protocol(db, protocol.id)
|
protocol = await _get_protocol(db, protocol.id, org_id)
|
||||||
return VotingProtocolOut.model_validate(protocol)
|
return VotingProtocolOut.model_validate(protocol)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from app.schemas.vote import (
|
|||||||
VoteSessionListOut,
|
VoteSessionListOut,
|
||||||
VoteSessionOut,
|
VoteSessionOut,
|
||||||
)
|
)
|
||||||
|
from app.dependencies.org import get_active_org_id
|
||||||
from app.services.auth_service import get_current_identity
|
from app.services.auth_service import get_current_identity
|
||||||
from app.services.vote_service import (
|
from app.services.vote_service import (
|
||||||
close_session as svc_close_session,
|
close_session as svc_close_session,
|
||||||
@@ -170,14 +171,22 @@ def _compute_result(
|
|||||||
@router.get("/sessions", response_model=list[VoteSessionListOut])
|
@router.get("/sessions", response_model=list[VoteSessionListOut])
|
||||||
async def list_vote_sessions(
|
async def list_vote_sessions(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
|
org_id: uuid.UUID | None = Depends(get_active_org_id),
|
||||||
session_status: str | None = Query(default=None, alias="status", description="Filtrer par statut (open, closed, tallied)"),
|
session_status: str | None = Query(default=None, alias="status", description="Filtrer par statut (open, closed, tallied)"),
|
||||||
decision_id: uuid.UUID | None = Query(default=None, description="Filtrer par decision_id"),
|
decision_id: uuid.UUID | None = Query(default=None, description="Filtrer par decision_id"),
|
||||||
skip: int = Query(default=0, ge=0),
|
skip: int = Query(default=0, ge=0),
|
||||||
limit: int = Query(default=50, ge=1, le=200),
|
limit: int = Query(default=50, ge=1, le=200),
|
||||||
) -> list[VoteSessionListOut]:
|
) -> list[VoteSessionListOut]:
|
||||||
"""List all vote sessions with optional filters by status and decision_id."""
|
"""List all vote sessions with optional filters by status and decision_id."""
|
||||||
stmt = select(VoteSession)
|
stmt = (
|
||||||
|
select(VoteSession)
|
||||||
|
.join(VotingProtocol, VoteSession.voting_protocol_id == VotingProtocol.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if org_id is not None:
|
||||||
|
stmt = stmt.where(VotingProtocol.organization_id == org_id)
|
||||||
|
else:
|
||||||
|
stmt = stmt.where(VotingProtocol.organization_id.is_(None))
|
||||||
if session_status is not None:
|
if session_status is not None:
|
||||||
stmt = stmt.where(VoteSession.status == session_status)
|
stmt = stmt.where(VoteSession.status == session_status)
|
||||||
if decision_id is not None:
|
if decision_id is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user