2026-04-01 10:36:22 +02:00
|
|
|
from __future__ import annotations
|
2026-03-30 16:37:58 +02:00
|
|
|
"""API routes for Ketten (chains)."""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
|
|
|
|
|
from tracker.api.models import (
|
|
|
|
|
KetteDetail,
|
|
|
|
|
KetteKurz,
|
|
|
|
|
KettenGliedOut,
|
|
|
|
|
PaginatedKetten,
|
|
|
|
|
ParteiOut,
|
|
|
|
|
VorlageKurz,
|
|
|
|
|
)
|
2026-04-01 18:30:24 +02:00
|
|
|
from tracker.core.ampel import get_ampel, get_ampel_kompakt
|
2026-03-30 16:37:58 +02:00
|
|
|
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,
|
2026-04-01 10:36:22 +02:00
|
|
|
partei: str | None = None,
|
2026-04-01 14:58:10 +02:00
|
|
|
periode: str | None = None,
|
|
|
|
|
parteien: str | None = None,
|
2026-03-30 16:37:58 +02:00
|
|
|
conn=Depends(_db),
|
|
|
|
|
):
|
|
|
|
|
"""List Ketten with optional filters."""
|
2026-04-01 14:58:10 +02:00
|
|
|
from tracker.core.perioden import periode_date_filter, parteien_kuerzel_filter
|
|
|
|
|
|
2026-03-30 16:37:58 +02:00
|
|
|
where_clauses = []
|
|
|
|
|
params: list = []
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
where_clauses.append("k.status = ?")
|
|
|
|
|
params.append(status)
|
|
|
|
|
|
|
|
|
|
if typ:
|
|
|
|
|
where_clauses.append("k.typ = ?")
|
|
|
|
|
params.append(typ)
|
|
|
|
|
|
2026-04-01 10:36:22 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-03-30 16:37:58 +02:00
|
|
|
if suche:
|
|
|
|
|
where_clauses.append("k.thema LIKE ?")
|
|
|
|
|
params.append(f"%{suche}%")
|
|
|
|
|
|
2026-04-01 14:58:10 +02:00
|
|
|
# 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)
|
|
|
|
|
|
2026-03-30 16:37:58 +02:00
|
|
|
where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else ""
|
|
|
|
|
|
|
|
|
|
total = conn.execute(
|
|
|
|
|
f"SELECT COUNT(*) as cnt FROM ketten k {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,
|
2026-04-01 18:30:24 +02:00
|
|
|
k.strang,
|
2026-03-30 16:37:58 +02:00
|
|
|
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"],
|
2026-04-01 18:30:24 +02:00
|
|
|
strang=r["strang"],
|
|
|
|
|
ampel=get_ampel_kompakt(r["strang"] or "", r["status"] or ""),
|
2026-03-30 16:37:58 +02:00
|
|
|
)
|
|
|
|
|
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,
|
2026-04-01 10:36:22 +02:00
|
|
|
k.letzte_aktivitaet, k.vertagungen_count, k.begruendung, k.ursprung_id,
|
2026-04-01 18:30:24 +02:00
|
|
|
k.strang,
|
2026-03-30 16:37:58 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-04-01 18:30:24 +02:00
|
|
|
strang = row["strang"]
|
|
|
|
|
ampel_data = get_ampel(strang or "", row["status"] or "")
|
|
|
|
|
|
2026-03-30 16:37:58 +02:00
|
|
|
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"],
|
2026-04-01 10:36:22 +02:00
|
|
|
begruendung=row["begruendung"],
|
2026-03-30 16:37:58 +02:00
|
|
|
glieder=glieder,
|
|
|
|
|
antragsteller=antragsteller,
|
|
|
|
|
graph=graph,
|
2026-04-01 18:30:24 +02:00
|
|
|
strang=strang,
|
|
|
|
|
ampel=ampel_data,
|
2026-03-30 16:37:58 +02:00
|
|
|
)
|