bifroest/app/approvals/routes.py
2026-10-27 08:34:17 +01:00

83 lines
4.2 KiB
Python

"""Freigaben in der App (→ Konzept 4.5, 10.5): Liste, Antrag mit echtem Diff, Entscheidung."""
import uuid
from typing import Annotated
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, Response
from sqlalchemy import select
from app import web
from app.approvals import service as approvals
from app.approvals.models import LABELS, ApprovalRequest
from app.data_sources.models import DataSource
from app.identity.models import User
from app.tasks import engine
from app.tasks.models import TaskRun
from app.workspaces.models import Workspace
def names(session, approvals_: list[ApprovalRequest]) -> dict[str, dict[int, str]]:
"""Anzeigenamen für Nutzer, Workspaces und Quellen der Anträge."""
users = {a.requested_by for a in approvals_} | {a.decided_by for a in approvals_ if a.decided_by}
return {
"users": dict(session.execute(select(User.id, User.display_name).where(User.id.in_(users))).all()),
"workspaces": dict(session.execute(select(Workspace.id, Workspace.name).where(
Workspace.id.in_({a.workspace_id for a in approvals_}))).all()),
"sources": dict(session.execute(select(DataSource.id, DataSource.name).where(
DataSource.id.in_({a.source_id for a in approvals_ if a.source_id}))).all()),
}
def router() -> APIRouter:
r = APIRouter()
@r.get("/approvals")
def overview(request: Request, principal: web.User) -> HTMLResponse:
with web.db(request).session() as session:
open_, recent = approvals.overview(session, principal)
context = {"open": open_, "recent": recent, "labels": LABELS, **names(session, open_ + recent)}
return web.render(request, "approvals.html", context)
@r.get("/approvals/count")
def count(request: Request, principal: web.User) -> HTMLResponse:
with web.db(request).session() as session:
return web.render(request, "_approval_badge.html", {"open_approvals": approvals.count_open(session, principal)})
@r.get("/approvals/{approval_id}")
def detail(request: Request, principal: web.User, approval_id: int) -> HTMLResponse:
with web.db(request).session() as session:
approvals.expire_due_session(session)
approval, may_approve = approvals.get(session, principal, approval_id)
blocked = approvals.blocked(session, principal.user_id, approval)
run = session.get(TaskRun, approval.run_id)
replaced_by = session.scalar(select(ApprovalRequest.id).where(ApprovalRequest.replaces_id == approval.id))
context = {"a": approval, "may_decide": not blocked, "blocked": blocked, "may_approve": may_approve,
"run": run, "labels": LABELS,
"replaced_by": replaced_by, "current_workspace": session.get(Workspace, approval.workspace_id),
**names(session, [approval])}
return web.render(request, "approval.html", context)
@r.post("/approvals/{approval_id}/decide")
def decide(request: Request, principal: web.Checked, approval_id: int, decision: Annotated[str, Form()],
comment: Annotated[str, Form()] = "") -> Response:
with web.db(request).session() as session:
approvals.decide(session, principal, approval_id, decision == "approve", comment)
return web.redirect(f"/approvals/{approval_id}")
@r.post("/approvals/{approval_id}/rerequest")
def rerequest(request: Request, principal: web.Checked, approval_id: int) -> Response:
with web.db(request).session() as session:
run = approvals.rerequest(session, principal, approval_id)
chat_id = run.chat_session_id
return web.redirect(f"/chats/{chat_id}" if chat_id else "/approvals")
@r.post("/runs/{run_id}/reconcile")
def reconcile(request: Request, principal: web.Checked, run_id: uuid.UUID,
give_up: Annotated[bool, Form()] = False) -> Response:
with web.db(request).session() as session:
run = engine.reconcile(session, principal, run_id, give_up)
chat_id = run.chat_session_id
return web.redirect(f"/chats/{chat_id}" if chat_id else "/workspaces")
return r