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>
109 lines
2.8 KiB
Python
109 lines
2.8 KiB
Python
"""Document service: retrieval and version management."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.models.document import Document, DocumentItem, ItemVersion
|
|
|
|
|
|
async def get_document_with_items(slug: str, db: AsyncSession) -> Document | None:
|
|
"""Load a document with all its items and their versions, eagerly.
|
|
|
|
Parameters
|
|
----------
|
|
slug:
|
|
Unique slug of the document.
|
|
db:
|
|
Async database session.
|
|
|
|
Returns
|
|
-------
|
|
Document | None
|
|
The document with items and versions loaded, or None if not found.
|
|
"""
|
|
result = await db.execute(
|
|
select(Document)
|
|
.options(
|
|
selectinload(Document.items).selectinload(DocumentItem.versions)
|
|
)
|
|
.where(Document.slug == slug)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def apply_version(
|
|
item_id: uuid.UUID,
|
|
version_id: uuid.UUID,
|
|
db: AsyncSession,
|
|
) -> DocumentItem:
|
|
"""Apply an accepted version to a document item.
|
|
|
|
This replaces the item's current_text with the version's proposed_text
|
|
and marks the version as 'accepted'.
|
|
|
|
Parameters
|
|
----------
|
|
item_id:
|
|
UUID of the DocumentItem to update.
|
|
version_id:
|
|
UUID of the ItemVersion to apply.
|
|
db:
|
|
Async database session.
|
|
|
|
Returns
|
|
-------
|
|
DocumentItem
|
|
The updated document item.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
If the item or version is not found, or the version does not
|
|
belong to the item.
|
|
"""
|
|
# Load item
|
|
item_result = await db.execute(
|
|
select(DocumentItem).where(DocumentItem.id == item_id)
|
|
)
|
|
item = item_result.scalar_one_or_none()
|
|
if item is None:
|
|
raise ValueError(f"Element de document introuvable : {item_id}")
|
|
|
|
# Load version
|
|
version_result = await db.execute(
|
|
select(ItemVersion).where(ItemVersion.id == version_id)
|
|
)
|
|
version = version_result.scalar_one_or_none()
|
|
if version is None:
|
|
raise ValueError(f"Version introuvable : {version_id}")
|
|
|
|
if version.item_id != item.id:
|
|
raise ValueError(
|
|
f"La version {version_id} n'appartient pas a l'element {item_id}"
|
|
)
|
|
|
|
# Apply the version
|
|
item.current_text = version.proposed_text
|
|
version.status = "accepted"
|
|
|
|
# Mark all other pending/voting versions for this item as rejected
|
|
other_versions_result = await db.execute(
|
|
select(ItemVersion).where(
|
|
ItemVersion.item_id == item_id,
|
|
ItemVersion.id != version_id,
|
|
ItemVersion.status.in_(["proposed", "voting"]),
|
|
)
|
|
)
|
|
for other_version in other_versions_result.scalars():
|
|
other_version.status = "rejected"
|
|
|
|
await db.commit()
|
|
await db.refresh(item)
|
|
|
|
return item
|