PYDANTIC max_length (38 Routen, ~400 Field-Constraints): Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.). Pragmatische Limits: - Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000 - Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100 - Hund-Name/Rasse: 80 · Hund-Bio: 2000 Top-betroffen: forum.py, diary.py, health.py, dogs.py, expenses.py, notes.py, auth.py, profile.py. Manuelle len()-Checks in profile, chat, ki entfernt (jetzt durch Field abgedeckt). PYTEST COVERAGE (+19 Tests, 37 grün + 1 xfail): - test_security.py: require_owner (Places GET/PATCH/DELETE mit Fremduser → 403), JWT-Blacklist (Logout invalidiert Token), Login-Lockout (5 Fehlversuche → 429 + Retry-After Header) - test_race.py: Invoice-Counter (20 parallele Threads, alle unique), Founder-Number (atomare Vergabe, voll bei 100) - test_validation.py: Forum-Titel 30k Zeichen → 422, Diary-Text 50k → 422 (verifiziert Pydantic max_length-Sweep) A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast): - #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44 - dog-profile Wrapped-Slider Prev/Next 40→44 - forum-Lightbox Close 40→44 - --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS) - --c-text-muted Dark: #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS) - Branding-Farben unangetastet
506 lines
19 KiB
Python
506 lines
19 KiB
Python
"""BAN YARO — KI-Features für Züchter (5 Endpunkte)"""
|
|
|
|
import logging
|
|
from datetime import date, timedelta
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, Literal
|
|
|
|
from database import db
|
|
from auth import get_current_user
|
|
import ki
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_FALLBACK = "KI-Analyse momentan nicht verfügbar. Bitte versuche es später erneut."
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Dependency: nur verifizierte Züchter + Admins
|
|
# ------------------------------------------------------------------
|
|
def _require_breeder(user=Depends(get_current_user)):
|
|
if user["rolle"] not in ("breeder", "admin"):
|
|
raise HTTPException(403, "Nur für Züchter.")
|
|
return user
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class WurfankuendigungBody(BaseModel):
|
|
litter_id: int
|
|
|
|
|
|
class GenetikErklaerungBody(BaseModel):
|
|
litter_id: int
|
|
zielgruppe: Literal["kaeufer", "zuechter"] = "kaeufer"
|
|
|
|
|
|
class PaarungAnalyseBody(BaseModel):
|
|
vater_id: int
|
|
mutter_id: int
|
|
ik_prozent: Optional[float] = None
|
|
welfare_level: Optional[str] = Field(None, max_length=50)
|
|
|
|
|
|
class HundBeschreibungBody(BaseModel):
|
|
hund_id: int
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Hilfsfunktionen: DB-Daten laden
|
|
# ------------------------------------------------------------------
|
|
def _load_hund(conn, hund_id: int) -> dict:
|
|
row = conn.execute("SELECT * FROM zucht_hunde WHERE id=?", (hund_id,)).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, f"Hund {hund_id} nicht gefunden.")
|
|
return dict(row)
|
|
|
|
|
|
def _load_gesundheitstests(conn, hund_id: int) -> list[dict]:
|
|
rows = conn.execute(
|
|
"SELECT test_typ, test_name, ergebnis, untersuch_am FROM dog_health_tests WHERE hund_id=?",
|
|
(hund_id,),
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def _load_gentests(conn, hund_id: int) -> list[dict]:
|
|
rows = conn.execute(
|
|
"SELECT marker_name, marker_kategorie, genotyp, ergebnis_klasse FROM dog_genetic_tests WHERE hund_id=?",
|
|
(hund_id,),
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def _load_titel(conn, hund_id: int) -> list[dict]:
|
|
rows = conn.execute(
|
|
"SELECT titel_typ, titel_name, verliehen_am FROM dog_titles WHERE hund_id=?",
|
|
(hund_id,),
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def _fmt_gesundheit(tests: list[dict]) -> str:
|
|
if not tests:
|
|
return " (keine Einträge)"
|
|
return "\n".join(f" - {t['test_name'] or t['test_typ']}: {t['ergebnis']} ({t['untersuch_am']})" for t in tests)
|
|
|
|
|
|
def _fmt_gentests(tests: list[dict]) -> str:
|
|
if not tests:
|
|
return " (keine Einträge)"
|
|
return "\n".join(f" - {t['marker_name']} ({t['marker_kategorie'] or ''}): {t['genotyp']} — {t['ergebnis_klasse'] or ''}" for t in tests)
|
|
|
|
|
|
def _fmt_titel(titel: list[dict]) -> str:
|
|
if not titel:
|
|
return " (keine Titel)"
|
|
return "\n".join(f" - {t['titel_name']} ({t['titel_typ']}, {t['verliehen_am']})" for t in titel)
|
|
|
|
|
|
def _hund_block(hund: dict, gesundheit: list, gentests: list, titel: list, label: str) -> str:
|
|
return (
|
|
f"=== {label} ===\n"
|
|
f"Name: {hund.get('name')} (Rufname: {hund.get('rufname') or '—'})\n"
|
|
f"Geschlecht: {hund.get('geschlecht') or '—'}\n"
|
|
f"Zuchtbuchnummer: {hund.get('zuchtbuchnummer') or '—'}\n"
|
|
f"Geburtsdatum: {hund.get('geburtsdatum') or '—'}\n"
|
|
f"Farbe: {hund.get('farbe') or '—'}\n"
|
|
f"\nGesundheitstests:\n{_fmt_gesundheit(gesundheit)}\n"
|
|
f"\nGentests:\n{_fmt_gentests(gentests)}\n"
|
|
f"\nTitel:\n{_fmt_titel(titel)}"
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 1. POST /api/zucht-ki/wurfankuendigung
|
|
# ------------------------------------------------------------------
|
|
@router.post("/zucht-ki/wurfankuendigung")
|
|
async def wurfankuendigung(body: WurfankuendigungBody, user=Depends(_require_breeder)):
|
|
if not user.get("ki_zucht_wurfankuendigung", 1):
|
|
raise HTTPException(403, "KI-Feature 'Wurfankündigung' ist für diesen Account deaktiviert.")
|
|
|
|
with db() as conn:
|
|
wurf = conn.execute("SELECT * FROM litters WHERE id=?", (body.litter_id,)).fetchone()
|
|
if not wurf:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
wurf = dict(wurf)
|
|
|
|
vater = _load_hund(conn, wurf["vater_id"]) if wurf.get("vater_id") else None
|
|
mutter = _load_hund(conn, wurf["mutter_id"]) if wurf.get("mutter_id") else None
|
|
|
|
vater_gesundheit = _load_gesundheitstests(conn, vater["id"]) if vater else []
|
|
vater_gentests = _load_gentests(conn, vater["id"]) if vater else []
|
|
vater_titel = _load_titel(conn, vater["id"]) if vater else []
|
|
|
|
mutter_gesundheit = _load_gesundheitstests(conn, mutter["id"]) if mutter else []
|
|
mutter_gentests = _load_gentests(conn, mutter["id"]) if mutter else []
|
|
mutter_titel = _load_titel(conn, mutter["id"]) if mutter else []
|
|
|
|
# Elternteil-Blöcke aufbauen
|
|
vater_block = (
|
|
_hund_block(vater, vater_gesundheit, vater_gentests, vater_titel, "VATER")
|
|
if vater
|
|
else f"=== VATER ===\nName: {wurf.get('vater_name') or '—'} (nicht in Zuchtkartei)"
|
|
)
|
|
mutter_block = (
|
|
_hund_block(mutter, mutter_gesundheit, mutter_gentests, mutter_titel, "MUTTER")
|
|
if mutter
|
|
else f"=== MUTTER ===\nName: {wurf.get('mutter_name') or '—'} (nicht in Zuchtkartei)"
|
|
)
|
|
|
|
system = (
|
|
"Du bist ein Experte für Hundezucht und hilfst Züchtern dabei, "
|
|
"professionelle Texte für ihre Wurfbörse zu erstellen. "
|
|
"Antworte ausschließlich auf Deutsch."
|
|
)
|
|
prompt = f"""
|
|
Schreibe eine professionelle, einladende Wurfankündigung für die Wurfbörse einer Hunde-App.
|
|
Basiere dich NUR auf den gegebenen Daten. Max. 4 kurze Absätze.
|
|
Ton: sachlich und herzlich. Keine Übertreibungen. Auf Deutsch.
|
|
|
|
=== WURF-DATEN ===
|
|
Geburtsdatum: {wurf.get('geburt_datum') or wurf.get('erwartetes_datum') or '—'}
|
|
Welpen gesamt: {wurf.get('welpen_gesamt') or '—'}
|
|
Preis: {wurf.get('preis_spanne') or '—'}
|
|
Beschreibung: {wurf.get('beschreibung') or '—'}
|
|
|
|
{vater_block}
|
|
|
|
{mutter_block}
|
|
"""
|
|
try:
|
|
text = await ki.complete(
|
|
prompt=prompt,
|
|
system=system,
|
|
max_tokens=600,
|
|
requires_premium=False,
|
|
user_id=user["id"],
|
|
)
|
|
return {"text": text}
|
|
except Exception as e:
|
|
logger.warning(f"KI nicht verfügbar: {e}")
|
|
return {"text": _FALLBACK}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 2. POST /api/zucht-ki/genetik-erklaerung
|
|
# ------------------------------------------------------------------
|
|
@router.post("/zucht-ki/genetik-erklaerung")
|
|
async def genetik_erklaerung(body: GenetikErklaerungBody, user=Depends(_require_breeder)):
|
|
if not user.get("ki_zucht_genetik", 1):
|
|
raise HTTPException(403, "KI-Feature 'Genetik-Erklärung' ist für diesen Account deaktiviert.")
|
|
|
|
with db() as conn:
|
|
wurf = conn.execute("SELECT * FROM litters WHERE id=?", (body.litter_id,)).fetchone()
|
|
if not wurf:
|
|
raise HTTPException(404, "Wurf nicht gefunden.")
|
|
wurf = dict(wurf)
|
|
|
|
vater = _load_hund(conn, wurf["vater_id"]) if wurf.get("vater_id") else None
|
|
mutter = _load_hund(conn, wurf["mutter_id"]) if wurf.get("mutter_id") else None
|
|
|
|
vater_gentests = _load_gentests(conn, vater["id"]) if vater else []
|
|
mutter_gentests = _load_gentests(conn, mutter["id"]) if mutter else []
|
|
|
|
# Risiko-Marker sammeln (vereinfacht: alles was nicht "frei" ist)
|
|
risks = []
|
|
for t in vater_gentests + mutter_gentests:
|
|
klasse = (t.get("ergebnis_klasse") or "").lower()
|
|
if klasse and klasse not in ("frei", "clear", "negativ", ""):
|
|
risks.append(f"{t['marker_name']}: {t['ergebnis_klasse']}")
|
|
|
|
vater_gt_block = _fmt_gentests(vater_gentests)
|
|
mutter_gt_block = _fmt_gentests(mutter_gentests)
|
|
|
|
if body.zielgruppe == "kaeufer":
|
|
anweisung = (
|
|
"Erkläre diese Gentestergebnisse für einen Welpen-Käufer ohne Fachkenntnisse. "
|
|
"Beantworte konkret: Was bedeutet das für meinen Welpen im Alltag? "
|
|
"Welche Vorsichtsmaßnahmen gibt es? Max. 200 Wörter, sehr verständlich."
|
|
)
|
|
else:
|
|
anweisung = (
|
|
"Analysiere diese Gentestergebnisse aus züchterischer Sicht. "
|
|
"Bewerte mögliche genetische Kombinationen beim Nachwuchs. "
|
|
"Welche Marker sind kritisch? Was bedeutet das für künftige Paarungen? "
|
|
"Max. 200 Wörter, fachlich präzise."
|
|
)
|
|
|
|
system = (
|
|
"Du bist ein Experte für Hundegenetik und Tierschutz. "
|
|
"Antworte ausschließlich auf Deutsch."
|
|
)
|
|
prompt = f"""
|
|
{anweisung}
|
|
|
|
=== GENTESTS VATER ({(vater or {}).get('name', wurf.get('vater_name', '—'))}) ===
|
|
{vater_gt_block}
|
|
|
|
=== GENTESTS MUTTER ({(mutter or {}).get('name', wurf.get('mutter_name', '—'))}) ===
|
|
{mutter_gt_block}
|
|
"""
|
|
try:
|
|
text = await ki.complete(
|
|
prompt=prompt,
|
|
system=system,
|
|
max_tokens=500,
|
|
requires_premium=False,
|
|
user_id=user["id"],
|
|
)
|
|
return {"text": text, "risks": risks}
|
|
except Exception as e:
|
|
logger.warning(f"KI nicht verfügbar: {e}")
|
|
return {"text": _FALLBACK, "risks": risks}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 3. POST /api/zucht-ki/paarung-analyse
|
|
# ------------------------------------------------------------------
|
|
@router.post("/zucht-ki/paarung-analyse")
|
|
async def paarung_analyse(body: PaarungAnalyseBody, user=Depends(_require_breeder)):
|
|
if not user.get("ki_zucht_paarung", 1):
|
|
raise HTTPException(403, "KI-Feature 'Paarungs-Analyse' ist für diesen Account deaktiviert.")
|
|
|
|
with db() as conn:
|
|
vater = _load_hund(conn, body.vater_id)
|
|
mutter = _load_hund(conn, body.mutter_id)
|
|
|
|
vater_gesundheit = _load_gesundheitstests(conn, body.vater_id)
|
|
vater_gentests = _load_gentests(conn, body.vater_id)
|
|
vater_titel = _load_titel(conn, body.vater_id)
|
|
|
|
mutter_gesundheit = _load_gesundheitstests(conn, body.mutter_id)
|
|
mutter_gentests = _load_gentests(conn, body.mutter_id)
|
|
mutter_titel = _load_titel(conn, body.mutter_id)
|
|
|
|
ik_info = f"{body.ik_prozent:.1f}%" if body.ik_prozent is not None else "nicht berechnet"
|
|
welfare_info = body.welfare_level or "nicht angegeben"
|
|
|
|
system = (
|
|
"Du bist ein erfahrener Zuchtwart und Experte für verantwortungsvolle Hundezucht. "
|
|
"Antworte ausschließlich auf Deutsch."
|
|
)
|
|
prompt = f"""
|
|
Bewerte diese Hundepaarung aus züchterischer Sicht.
|
|
Berücksichtige: Inzuchtkoeffizient, Gesundheitsprofil, Ausstellungserfolge, Stärken und Schwächen.
|
|
Gib eine konkrete Empfehlung. Max. 150 Wörter. Sachlich und direkt.
|
|
|
|
Inzuchtkoeffizient (IK): {ik_info}
|
|
Welfare-Level: {welfare_info}
|
|
|
|
{_hund_block(vater, vater_gesundheit, vater_gentests, vater_titel, "VATER")}
|
|
|
|
{_hund_block(mutter, mutter_gesundheit, mutter_gentests, mutter_titel, "MUTTER")}
|
|
"""
|
|
try:
|
|
text = await ki.complete(
|
|
prompt=prompt,
|
|
system=system,
|
|
max_tokens=400,
|
|
requires_premium=False,
|
|
user_id=user["id"],
|
|
)
|
|
|
|
# Empfehlung aus Text ableiten
|
|
text_lower = text.lower()
|
|
if any(w in text_lower for w in ("nicht empfohlen", "abraten", "nicht zu empfehlen", "nicht empfehle")):
|
|
empfehlung = "nicht_empfohlen"
|
|
elif any(w in text_lower for w in ("bedingt", "vorbehalt", "einschränkung", "vorsicht")):
|
|
empfehlung = "bedingt"
|
|
else:
|
|
empfehlung = "empfohlen"
|
|
|
|
return {"text": text, "empfehlung": empfehlung}
|
|
except Exception as e:
|
|
logger.warning(f"KI nicht verfügbar: {e}")
|
|
return {"text": _FALLBACK, "empfehlung": "bedingt"}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 4. POST /api/zucht-ki/hund-beschreibung
|
|
# ------------------------------------------------------------------
|
|
@router.post("/zucht-ki/hund-beschreibung")
|
|
async def hund_beschreibung(body: HundBeschreibungBody, user=Depends(_require_breeder)):
|
|
if not user.get("ki_zucht_beschreibung", 1):
|
|
raise HTTPException(403, "KI-Feature 'Hund-Beschreibung' ist für diesen Account deaktiviert.")
|
|
|
|
with db() as conn:
|
|
hund = _load_hund(conn, body.hund_id)
|
|
gesundheit = _load_gesundheitstests(conn, body.hund_id)
|
|
gentests = _load_gentests(conn, body.hund_id)
|
|
titel = _load_titel(conn, body.hund_id)
|
|
|
|
system = (
|
|
"Du bist ein Experte für Hundezucht und hilfst Züchtern, "
|
|
"ansprechende Profilbeschreibungen für ihre Hunde zu schreiben. "
|
|
"Antworte ausschließlich auf Deutsch."
|
|
)
|
|
prompt = f"""
|
|
Schreibe eine ansprechende Beschreibung für das öffentliche Hunde-Profil.
|
|
Hebe Stärken hervor (Gesundheit, Titel, Charakter falls beschrieben).
|
|
Max. 3 kurze Absätze. Professionell aber warmherzig. Keine Erfindungen.
|
|
|
|
{_hund_block(hund, gesundheit, gentests, titel, "HUND")}
|
|
Notiz des Züchters: {hund.get('notiz') or '—'}
|
|
"""
|
|
try:
|
|
text = await ki.complete(
|
|
prompt=prompt,
|
|
system=system,
|
|
max_tokens=500,
|
|
requires_premium=False,
|
|
user_id=user["id"],
|
|
)
|
|
return {"text": text}
|
|
except Exception as e:
|
|
logger.warning(f"KI nicht verfügbar: {e}")
|
|
return {"text": _FALLBACK}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# 5. POST /api/zucht-ki/jahresbericht
|
|
# ------------------------------------------------------------------
|
|
@router.post("/zucht-ki/jahresbericht")
|
|
async def jahresbericht(user=Depends(_require_breeder)):
|
|
if not user.get("ki_zucht_jahresbericht", 1):
|
|
raise HTTPException(403, "KI-Feature 'Jahresbericht' ist für diesen Account deaktiviert.")
|
|
|
|
zwei_jahre_ago = (date.today() - timedelta(days=730)).isoformat()
|
|
|
|
with db() as conn:
|
|
# Breeder-Profil
|
|
bp = conn.execute(
|
|
"SELECT id, zwingername, rasse_text FROM breeder_profiles WHERE user_id=?",
|
|
(user["id"],),
|
|
).fetchone()
|
|
if not bp:
|
|
raise HTTPException(404, "Kein Züchter-Profil gefunden.")
|
|
breeder_id = bp["id"]
|
|
zwingername = bp["zwingername"] or "Unbekannt"
|
|
rasse_text = bp["rasse_text"] or "—"
|
|
|
|
# Würfe der letzten 2 Jahre
|
|
wuerfe = conn.execute(
|
|
"""SELECT geburt_datum, welpen_gesamt, welpen_verfuegbar, status, welfare_level
|
|
FROM litters
|
|
WHERE breeder_id=? AND geburt_datum >= ?
|
|
ORDER BY geburt_datum DESC""",
|
|
(breeder_id, zwei_jahre_ago),
|
|
).fetchall()
|
|
wuerfe = [dict(w) for w in wuerfe]
|
|
|
|
# Eigene Zuchthunde
|
|
hunde = conn.execute(
|
|
"SELECT id, name, geschlecht FROM zucht_hunde WHERE breeder_id=?",
|
|
(breeder_id,),
|
|
).fetchall()
|
|
hunde = [dict(h) for h in hunde]
|
|
hund_ids = [h["id"] for h in hunde]
|
|
|
|
# Gesundheitstests aller Hunde aggregieren
|
|
alle_gesundheitstests: list[dict] = []
|
|
for hid in hund_ids:
|
|
tests = _load_gesundheitstests(conn, hid)
|
|
for t in tests:
|
|
t["hund_id"] = hid
|
|
alle_gesundheitstests.extend(tests)
|
|
|
|
# Zusammenfassung für den Prompt aufbauen
|
|
wurf_zeilen = []
|
|
for w in wuerfe:
|
|
zeile = f" - {w['geburt_datum'] or '?'}: {w['welpen_gesamt'] or '?'} Welpen, Status: {w['status']}"
|
|
if w.get("welfare_level"):
|
|
zeile += f", Welfare: {w['welfare_level']}"
|
|
wurf_zeilen.append(zeile)
|
|
wuerfe_text = "\n".join(wurf_zeilen) if wurf_zeilen else " (keine Würfe im Zeitraum)"
|
|
|
|
# Gesundheitstrend: welche Tests wurden gemacht?
|
|
test_typen: dict[str, int] = {}
|
|
for t in alle_gesundheitstests:
|
|
key = t.get("test_name") or t.get("test_typ") or "Unbekannt"
|
|
test_typen[key] = test_typen.get(key, 0) + 1
|
|
gesundheit_text = (
|
|
"\n".join(f" - {name}: {anz}x" for name, anz in sorted(test_typen.items(), key=lambda x: -x[1]))
|
|
if test_typen else " (keine Gesundheitstests erfasst)"
|
|
)
|
|
|
|
hunde_text = (
|
|
"\n".join(f" - {h['name']} ({h['geschlecht'] or '—'})" for h in hunde)
|
|
if hunde else " (keine Hunde in der Zuchtkartei)"
|
|
)
|
|
|
|
system = (
|
|
"Du bist ein erfahrener Zuchtwart und Berater für verantwortungsvolle Hundezucht. "
|
|
"Erstelle konstruktive, sachliche Auswertungen für Züchter auf Deutsch."
|
|
)
|
|
prompt = f"""
|
|
Erstelle eine kurze züchterische Jahresauswertung.
|
|
Analysiere: Anzahl Würfe, Gesundheitstrends, Stärken und Verbesserungspotenziale.
|
|
Max. 200 Wörter. Konstruktiv und sachlich. Mit 2-3 konkreten Empfehlungen.
|
|
|
|
Zwingername: {zwingername}
|
|
Rasse: {rasse_text}
|
|
Zeitraum: letzte 2 Jahre (bis {date.today().isoformat()})
|
|
|
|
=== WÜRFE ===
|
|
{wuerfe_text}
|
|
|
|
=== ZUCHTHUNDE ===
|
|
{hunde_text}
|
|
|
|
=== GESUNDHEITSTESTS (Häufigkeit) ===
|
|
{gesundheit_text}
|
|
"""
|
|
try:
|
|
text = await ki.complete(
|
|
prompt=prompt,
|
|
system=system,
|
|
max_tokens=600,
|
|
requires_premium=False,
|
|
user_id=user["id"],
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"KI nicht verfügbar: {e}")
|
|
text = _FALLBACK
|
|
|
|
# Bericht speichern
|
|
jahr = date.today().year
|
|
with db() as conn:
|
|
bp2 = conn.execute("SELECT id FROM breeder_profiles WHERE user_id=?", (user["id"],)).fetchone()
|
|
if bp2:
|
|
conn.execute(
|
|
"INSERT INTO breeder_jahresberichte (user_id, breeder_id, jahr, text) VALUES (?,?,?,?)",
|
|
(user["id"], bp2["id"], jahr, text)
|
|
)
|
|
saved_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
|
else:
|
|
saved_id = None
|
|
|
|
return {"text": text, "saved_id": saved_id, "jahr": jahr}
|
|
|
|
|
|
# GET gespeicherte Berichte
|
|
# ------------------------------------------------------------------
|
|
@router.get("/zucht-ki/jahresbericht")
|
|
async def jahresbericht_list(user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"SELECT id, jahr, created_at FROM breeder_jahresberichte WHERE user_id=? ORDER BY created_at DESC LIMIT 20",
|
|
(user["id"],)
|
|
).fetchall()
|
|
return [{"id": r["id"], "jahr": r["jahr"], "created_at": r["created_at"]} for r in rows]
|
|
|
|
|
|
@router.get("/zucht-ki/jahresbericht/{bericht_id}")
|
|
async def jahresbericht_get(bericht_id: int, user=Depends(_require_breeder)):
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"SELECT id, jahr, text, created_at FROM breeder_jahresberichte WHERE id=? AND user_id=?",
|
|
(bericht_id, user["id"])
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Bericht nicht gefunden.")
|
|
return dict(row)
|