"""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