antragstracker/backend/src/tracker/api/routes/ketten.py

254 lines
8.3 KiB
Python
Raw Normal View History

from __future__ import annotations
"""API routes for Ketten (chains)."""
from fastapi import APIRouter, Depends, HTTPException, Query
from tracker.api.models import (
KetteDetail,
KetteKurz,
KettenGliedOut,
PaginatedKetten,
ParteiOut,
VorlageKurz,
)
from tracker.core.ampel import get_ampel, get_ampel_kompakt
from tracker.core.graph import get_kette_graph
from tracker.db.session import get_connection
router = APIRouter(prefix="/ketten", tags=["Ketten"])
def _db():
conn = get_connection()
try:
yield conn
finally:
conn.close()
@router.get("", response_model=PaginatedKetten)
def list_ketten(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
status: str | None = None,
typ: str | None = None,
suche: str | None = None,
partei: str | None = None,
periode: str | None = None,
parteien: str | None = None,
conn=Depends(_db),
):
"""List Ketten with optional filters."""
from tracker.core.perioden import periode_date_filter, parteien_kuerzel_filter
where_clauses = []
params: list = []
if status:
where_clauses.append("k.status = ?")
params.append(status)
if typ:
where_clauses.append("k.typ = ?")
params.append(typ)
if partei:
where_clauses.append(
"k.ursprung_id IN (SELECT a.vorlage_id FROM antragsteller a "
"JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel = ?)"
)
params.append(partei)
if suche:
where_clauses.append(
"(k.thema LIKE ? OR v.aktenzeichen LIKE ? OR v.betreff LIKE ?)"
)
like = f"%{suche}%"
params.extend([like, like, like])
# Global filter: Ratsperiode (filter on letzte_aktivitaet)
per_clause, per_params = periode_date_filter(periode, "k.letzte_aktivitaet")
if per_clause:
where_clauses.append(per_clause)
params.extend(per_params)
# Global filter: Parteien (multi-select on Ursprung-Antragsteller)
p_kuerzel = parteien_kuerzel_filter(parteien)
if p_kuerzel:
placeholders = ",".join("?" * len(p_kuerzel))
where_clauses.append(
f"k.ursprung_id IN (SELECT a.vorlage_id FROM antragsteller a "
f"JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel IN ({placeholders}))"
)
params.extend(p_kuerzel)
where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else ""
total = conn.execute(
f"SELECT COUNT(*) as cnt FROM ketten k LEFT JOIN vorlagen v ON k.ursprung_id = v.id {where_sql}", params
).fetchone()["cnt"]
offset = (page - 1) * page_size
rows = conn.execute(
f"""SELECT k.id, k.typ, k.thema, k.status, k.status_seit,
k.letzte_aktivitaet, k.vertagungen_count, k.ursprung_id,
k.strang,
v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang,
v.ist_verwaltungsvorlage,
(SELECT COUNT(*) FROM ketten_glieder kg WHERE kg.kette_id = k.id) as glieder_count
FROM ketten k
LEFT JOIN vorlagen v ON k.ursprung_id = v.id
{where_sql}
ORDER BY k.letzte_aktivitaet DESC NULLS LAST, k.id DESC
LIMIT ? OFFSET ?""",
params + [page_size, offset],
).fetchall()
items = [
KetteKurz(
id=r["id"],
ursprung=VorlageKurz(
id=r["ursprung_id"],
aktenzeichen=r["aktenzeichen"],
typ=r["v_typ"],
betreff=r["betreff"],
datum_eingang=r["datum_eingang"],
ist_verwaltungsvorlage=bool(r["ist_verwaltungsvorlage"]),
) if r["ursprung_id"] else None,
typ=r["typ"],
thema=r["thema"],
status=r["status"],
status_seit=r["status_seit"],
letzte_aktivitaet=r["letzte_aktivitaet"],
vertagungen_count=r["vertagungen_count"],
glieder_count=r["glieder_count"],
strang=r["strang"],
ampel=get_ampel_kompakt(r["strang"] or "", r["status"] or ""),
)
for r in rows
]
return PaginatedKetten(items=items, total=total, page=page, page_size=page_size)
@router.get("/{kette_id}", response_model=KetteDetail)
def get_kette(kette_id: int, conn=Depends(_db)):
"""Get a single Kette with all Glieder."""
row = conn.execute(
"""SELECT k.id, k.typ, k.thema, k.status, k.status_seit,
k.letzte_aktivitaet, k.vertagungen_count, k.begruendung, k.ursprung_id,
k.strang,
v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang,
v.ist_verwaltungsvorlage
FROM ketten k
LEFT JOIN vorlagen v ON k.ursprung_id = v.id
WHERE k.id = ?""",
(kette_id,),
).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Kette nicht gefunden")
# Get Glieder
glieder_rows = conn.execute(
"""SELECT kg.position, kg.rolle,
v.id, v.aktenzeichen, v.typ, v.betreff, v.datum_eingang,
v.ist_verwaltungsvorlage
FROM ketten_glieder kg
JOIN vorlagen v ON kg.vorlage_id = v.id
WHERE kg.kette_id = ?
ORDER BY kg.position""",
(kette_id,),
).fetchall()
glieder = [
KettenGliedOut(
vorlage=VorlageKurz(
id=g["id"],
aktenzeichen=g["aktenzeichen"],
typ=g["typ"],
betreff=g["betreff"],
datum_eingang=g["datum_eingang"],
ist_verwaltungsvorlage=bool(g["ist_verwaltungsvorlage"]),
),
position=g["position"],
rolle=g["rolle"],
)
for g in glieder_rows
]
# Antragsteller des Ursprungs
antragsteller = []
if row["ursprung_id"]:
antragsteller_rows = conn.execute("""
SELECT p.id, p.kuerzel, p.name, p.farbe
FROM antragsteller a
JOIN parteien p ON a.partei_id = p.id
WHERE a.vorlage_id = ?
""", (row["ursprung_id"],)).fetchall()
antragsteller = [ParteiOut(**dict(a)) for a in antragsteller_rows]
# Graph/Perlenschnur data
graph = get_kette_graph(conn, kette_id)
# Umsetzungsbewertung (alle Versionen für diese Kette, neueste zuerst)
umsetzung = None
umsetzung_versionen = []
import json as _json
ub_rows = conn.execute(
"""SELECT score, begruendung, anmerkungen, erstellt_at, prompt_version
FROM ki_bewertungen
WHERE kette_id = ? AND typ = 'umsetzung_match'
ORDER BY id DESC""",
(kette_id,),
).fetchall()
for i, ub_row in enumerate(ub_rows):
details = {}
if ub_row["anmerkungen"]:
try:
details = _json.loads(ub_row["anmerkungen"])
except Exception:
pass
entry = {
"score": ub_row["score"],
"bewertung": details.get("bewertung", ""),
"begruendung": ub_row["begruendung"],
"kernpunkt_erfuellt": details.get("kernpunkt_erfuellt"),
"details": details.get("details", ""),
"erstellt_at": ub_row["erstellt_at"],
"prompt_version": ub_row["prompt_version"],
}
if i == 0:
umsetzung = entry
else:
umsetzung_versionen.append(entry)
strang = row["strang"]
ampel_data = get_ampel(strang or "", row["status"] or "")
return KetteDetail(
id=row["id"],
ursprung=VorlageKurz(
id=row["ursprung_id"],
aktenzeichen=row["aktenzeichen"],
typ=row["v_typ"],
betreff=row["betreff"],
datum_eingang=row["datum_eingang"],
ist_verwaltungsvorlage=bool(row["ist_verwaltungsvorlage"]),
) if row["ursprung_id"] else None,
typ=row["typ"],
thema=row["thema"],
status=row["status"],
status_seit=row["status_seit"],
letzte_aktivitaet=row["letzte_aktivitaet"],
vertagungen_count=row["vertagungen_count"],
begruendung=row["begruendung"],
glieder=glieder,
antragsteller=antragsteller,
graph=graph,
strang=strang,
ampel=ampel_data,
umsetzung=umsetzung,
umsetzung_versionen=umsetzung_versionen if umsetzung_versionen else None,
)