banyaro/backend/routes/breeder.py
rene 5f01abc590 Züchter-Editor: Wurfnamen sichtbar, 'undefined Medien' gefixt, Mitgliedschaften & Zertifikate
Rene: 'Züchter sollten mehr Einfluss haben — Wurfnamen (B-Wurf), Mitglied-
schaften und Zertifikate fürs Profil.'

- Wurfnamen: Infrastruktur existierte komplett (wurf_rang/wurf_name in DB,
  Backend, Wurfverwaltungs-Formular) — war nur im Editor und auf der
  öffentlichen Seite unsichtbar. Editor-Karten zeigen jetzt 'B-Wurf · Name'
  (Feldname-Bug geburtsdatum→geburt_datum), öffentliche Wurf-Karten bekommen
  den Rang/Namen als Badge, Hinweis im Editor verlinkt zur Vergabe.
- 'undefined Medien': my-editor lieferte kein foto_count (+photos fürs
  Profil-Grid) — ergänzt.
- NEU Mitgliedschaften & Zertifikate: entity_type 'certificate' im Foto-
  System (Ownership wie breeder), Editor-Sektion mit Upload (Bezeichnung als
  Caption) + Löschen, öffentliche Profilseite zeigt eigene Sektion mit
  Logos/Urkunden (klickbar, lazy). Public-Endpoint liefert result.zertifikate.

Tests: my-editor inkl. Wurfname/foto_count, Zertifikat-Roundtrip bis zur
öffentlichen Seite. Suite: 61 passed.
2026-06-07 21:00:14 +02:00

591 lines
26 KiB
Python

"""BAN YARO — Züchter-Verwaltung (Antrag, Admin-Prüfung)"""
import os
import logging
from datetime import datetime
from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from typing import Optional
from database import db
from auth import get_current_user, require_premium
from mailer import send_email, email_html
router = APIRouter()
logger = logging.getLogger(__name__)
_TZ = ZoneInfo("Europe/Berlin")
BREEDER_DOCS_DIR = os.getenv("BREEDER_DOCS_DIR", "/data/breeder_docs")
os.makedirs(BREEDER_DOCS_DIR, exist_ok=True)
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin@banyaro.app")
APP_URL = os.getenv("APP_URL", "https://banyaro.app")
# ------------------------------------------------------------------
# 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 verifizierte Züchter.")
return user
def require_admin(user=Depends(get_current_user)):
if user["rolle"] != "admin":
raise HTTPException(403, "Nur für Admins.")
return user
# ------------------------------------------------------------------
# GET /api/breeder/status — eigener Antragsstatus
# ------------------------------------------------------------------
@router.get("/breeder/status")
async def breeder_status(user=Depends(get_current_user)):
with db() as conn:
row = conn.execute(
"SELECT rolle, breeder_status FROM users WHERE id=?",
(user["id"],)
).fetchone()
if not row:
raise HTTPException(404, "User nicht gefunden.")
profile = None
if row["rolle"] in ("breeder", "admin"):
profile = conn.execute(
"SELECT id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung, verified_at "
"FROM breeder_profiles WHERE user_id=?",
(user["id"],)
).fetchone()
if profile:
logo = conn.execute(
"""SELECT file_path FROM breeder_photos
WHERE breeder_id=? AND entity_type='breeder'
ORDER BY is_primary DESC, id LIMIT 1""",
(profile["id"],)
).fetchone()
result = {
"rolle": row["rolle"],
"breeder_status": row["breeder_status"],
"profile": dict(profile) if profile else None,
}
if profile:
result["profile"]["logo_url"] = f"/media/{logo['file_path']}" if logo else None
return result
# ------------------------------------------------------------------
# POST /api/breeder/apply — Antrag stellen
# ------------------------------------------------------------------
@router.post("/breeder/apply")
async def breeder_apply(
zwingername: str = Form(...),
rasse_text: str = Form(...),
verein: str = Form(...),
vdh_mitglied: int = Form(0),
stadt: str = Form(...),
website: str = Form(""),
beschreibung: str = Form(""),
dokument: UploadFile = File(None),
user=Depends(get_current_user),
):
with db() as conn:
row = conn.execute(
"SELECT rolle, breeder_status FROM users WHERE id=?",
(user["id"],)
).fetchone()
if not row:
raise HTTPException(404, "User nicht gefunden.")
if row["rolle"] == "breeder":
raise HTTPException(400, "Du bist bereits verifizierter Züchter.")
if row["breeder_status"] == "pending":
raise HTTPException(400, "Du hast bereits einen offenen Antrag.")
# Dokument optional speichern
filepath = None
if dokument and dokument.filename:
data = await dokument.read()
if len(data) > 10 * 1024 * 1024:
raise HTTPException(400, "Dokument zu groß (max. 10 MB).")
ext = os.path.splitext(dokument.filename)[1].lower()
if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"):
raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.")
user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"]))
os.makedirs(user_doc_dir, exist_ok=True)
filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}"
filepath = os.path.join(user_doc_dir, filename)
with open(filepath, "wb") as f:
f.write(data)
with db() as conn:
conn.execute(
"UPDATE users SET breeder_status='pending' WHERE id=?",
(user["id"],)
)
conn.execute(
"INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) "
"VALUES (?,?,?,?,?,?,?,?) "
"ON CONFLICT(user_id) DO UPDATE SET "
"zwingername=excluded.zwingername, rasse_text=excluded.rasse_text, "
"verein=excluded.verein, vdh_mitglied=excluded.vdh_mitglied, "
"stadt=excluded.stadt, website=excluded.website, beschreibung=excluded.beschreibung, "
"verified_at=NULL",
(user["id"], zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung)
)
if filepath:
conn.execute(
"INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)",
(user["id"], "antrag", filepath)
)
# Admin benachrichtigen
admin_body = f"""
<p style="margin:0 0 12px"><b>Neuer Züchter-Antrag eingegangen:</b></p>
<table style="font-size:14px;border-collapse:collapse;width:100%">
<tr><td style="padding:5px 12px 5px 0;color:#888;white-space:nowrap">Von</td><td style="padding:5px 0"><b>{user['name']}</b> ({user['email']})</td></tr>
<tr><td style="padding:5px 12px 5px 0;color:#888">Zwingername</td><td style="padding:5px 0">{zwingername}</td></tr>
<tr><td style="padding:5px 12px 5px 0;color:#888">Rasse</td><td style="padding:5px 0">{rasse_text}</td></tr>
<tr><td style="padding:5px 12px 5px 0;color:#888">Verein</td><td style="padding:5px 0">{verein}</td></tr>
<tr><td style="padding:5px 12px 5px 0;color:#888">VDH</td><td style="padding:5px 0">{'Ja' if vdh_mitglied else 'Nein'}</td></tr>
<tr><td style="padding:5px 12px 5px 0;color:#888">Stadt</td><td style="padding:5px 0">{stadt}</td></tr>
</table>"""
try:
await send_email(
ADMIN_EMAIL,
f"[Banyaro] Neuer Züchter-Antrag — {zwingername}",
email_html(admin_body, cta_url=f"{APP_URL}/#admin", cta_label="Im Admin-Bereich prüfen"),
f"Neuer Züchter-Antrag von {user['name']} ({user['email']}): {zwingername}, {rasse_text}, {verein}",
)
except Exception as e:
logger.warning(f"Admin-Mail nicht gesendet: {e}")
return {"message": "Antrag eingereicht. Du wirst per E-Mail benachrichtigt."}
# ------------------------------------------------------------------
# GET /api/admin/breeders/pending — offene Anträge
# ------------------------------------------------------------------
@router.get("/admin/breeders/pending")
async def admin_pending_breeders(admin=Depends(require_admin)):
with db() as conn:
rows = conn.execute("""
SELECT u.id, u.name, u.email, u.created_at, u.breeder_status,
bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied,
bp.stadt, bp.website, bp.beschreibung, bp.created_at AS antrag_at,
(SELECT COUNT(*) FROM breeder_documents WHERE user_id=u.id) AS dok_count
FROM users u
JOIN breeder_profiles bp ON bp.user_id = u.id
WHERE u.breeder_status = 'pending'
ORDER BY bp.created_at ASC
""").fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# GET /api/admin/breeders — alle aktiven Züchter
# ------------------------------------------------------------------
@router.get("/admin/breeders")
async def admin_all_breeders(admin=Depends(require_admin)):
with db() as conn:
rows = conn.execute("""
SELECT u.id, u.name, u.email, u.created_at, u.subscription_tier,
u.breeder_status, u.last_login,
bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied,
bp.stadt, bp.website, bp.verified_at,
(SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count,
(SELECT COUNT(*) FROM dogs WHERE user_id=u.id) AS hunde_count
FROM users u
LEFT JOIN breeder_profiles bp ON bp.user_id = u.id
WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved'
ORDER BY CASE WHEN bp.verified_at IS NULL THEN 1 ELSE 0 END,
bp.verified_at DESC, u.created_at DESC
""").fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags
# ------------------------------------------------------------------
@router.get("/admin/breeder/{user_id}/documents")
async def admin_breeder_documents(user_id: int, admin=Depends(require_admin)):
with db() as conn:
docs = conn.execute(
"SELECT id, dokument_typ, file_path, uploaded_at FROM breeder_documents WHERE user_id=?",
(user_id,)
).fetchall()
return [dict(d) for d in docs]
# ------------------------------------------------------------------
# GET /api/admin/breeder/{user_id}/document/{doc_id} — Datei herunterladen
# ------------------------------------------------------------------
@router.get("/admin/breeder/{user_id}/document/{doc_id}")
async def admin_download_document(user_id: int, doc_id: int, admin=Depends(require_admin)):
with db() as conn:
row = conn.execute(
"SELECT file_path FROM breeder_documents WHERE id=? AND user_id=?",
(doc_id, user_id)
).fetchone()
if not row:
raise HTTPException(404, "Dokument nicht gefunden.")
path = row["file_path"]
if not os.path.exists(path):
raise HTTPException(404, "Datei nicht auf Datenträger.")
return FileResponse(path)
class RejectBody(BaseModel):
grund: str = Field(..., min_length=3, max_length=2000)
# ------------------------------------------------------------------
# POST /api/admin/breeder/{user_id}/approve — Freischalten
# ------------------------------------------------------------------
@router.post("/admin/breeder/{user_id}/approve")
async def admin_approve_breeder(user_id: int, admin=Depends(require_admin)):
with db() as conn:
user = conn.execute(
"SELECT id, name, email, breeder_status FROM users WHERE id=?",
(user_id,)
).fetchone()
if not user:
raise HTTPException(404, "User nicht gefunden.")
if user["breeder_status"] != "pending":
raise HTTPException(400, "Kein offener Antrag.")
conn.execute(
"UPDATE users SET rolle='breeder', breeder_status='approved' WHERE id=?",
(user_id,)
)
conn.execute(
"UPDATE breeder_profiles SET verified_at=datetime('now') WHERE user_id=?",
(user_id,)
)
# Bestätigungs-Mail
approve_body = f"""
<p style="margin:0 0 16px">Hallo <b>{user['name']}</b>,</p>
<p style="margin:0 0 16px">
dein Züchter-Profil bei Ban Yaro wurde erfolgreich verifiziert. 🎉<br>
Ab sofort hast du Zugang zu allen Züchter-Features.
</p>"""
try:
await send_email(
user["email"],
"Willkommen als Züchter bei Ban Yaro!",
email_html(approve_body, cta_url=APP_URL, cta_label="Zur App"),
f"Hallo {user['name']}, dein Züchter-Profil wurde verifiziert.",
)
except Exception as e:
logger.warning(f"Bestätigungs-Mail nicht gesendet: {e}")
return {"message": f"{user['name']} als Züchter freigeschaltet."}
# ------------------------------------------------------------------
# POST /api/admin/breeder/{user_id}/reject — Ablehnen
# ------------------------------------------------------------------
@router.post("/admin/breeder/{user_id}/reject")
async def admin_reject_breeder(user_id: int, body: RejectBody, admin=Depends(require_admin)):
with db() as conn:
user = conn.execute(
"SELECT id, name, email, breeder_status FROM users WHERE id=?",
(user_id,)
).fetchone()
if not user:
raise HTTPException(404, "User nicht gefunden.")
if user["breeder_status"] != "pending":
raise HTTPException(400, "Kein offener Antrag.")
conn.execute(
"UPDATE users SET breeder_status='rejected' WHERE id=?",
(user_id,)
)
# Ablehnungs-Mail
import html as _h
reject_body = f"""
<p style="margin:0 0 16px">Hallo <b>{user['name']}</b>,</p>
<p style="margin:0 0 16px">
leider konnten wir deinen Züchter-Antrag aktuell nicht bestätigen.
</p>
<div style="background:#fdf6ef;border-left:3px solid #C4843A;padding:12px 16px;
border-radius:0 8px 8px 0;margin:0 0 16px;font-size:14px">
<b>Grund:</b> {_h.escape(body.grund)}
</div>
<p style="margin:0;color:#666;font-size:14px">
Du kannst jederzeit einen neuen Antrag stellen. Bei Fragen erreichst du uns unter
<a href="mailto:{ADMIN_EMAIL}" style="color:#C4843A">{ADMIN_EMAIL}</a>.
</p>"""
try:
await send_email(
user["email"],
"Dein Züchter-Antrag bei Ban Yaro",
email_html(reject_body),
f"Hallo {user['name']}, dein Antrag wurde abgelehnt. Grund: {body.grund}",
)
except Exception as e:
logger.warning(f"Ablehnungs-Mail nicht gesendet: {e}")
return {"message": f"Antrag von {user['name']} abgelehnt."}
# ------------------------------------------------------------------
# GET /api/breeder/profil/{zwingername} — öffentliches Profil (angereichert)
# ------------------------------------------------------------------
@router.get("/breeder/profil/{zwingername}")
async def breeder_public_profile(zwingername: str):
with db() as conn:
row = conn.execute("""
SELECT bp.id, bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied,
bp.stadt, bp.website, bp.beschreibung,
bp.location_lat, bp.location_lng, bp.verified_at, bp.created_at,
u.id AS zuechter_user_id,
u.name AS zuechter_name
FROM breeder_profiles bp
JOIN users u ON u.id = bp.user_id
WHERE LOWER(bp.zwingername) = LOWER(?)
AND u.rolle IN ('breeder', 'admin')
AND (u.breeder_status = 'approved' OR u.rolle = 'admin')
""", (zwingername,)).fetchone()
if not row:
raise HTTPException(404, "Züchter nicht gefunden.")
breeder_id = row["id"]
result = dict(row)
# Öffentliche Zuchthunde + ihre wichtigsten Gesundheitstests + Titel
hunde_rows = conn.execute("""
SELECT id, name, rufname, geschlecht, geburtsdatum, farbe, zuchtbuchnummer, foto_url
FROM zucht_hunde
WHERE breeder_id=? AND is_public=1 AND (sterbedatum IS NULL OR sterbedatum='')
ORDER BY geschlecht, name
""", (breeder_id,)).fetchall()
hunde = []
for h in hunde_rows:
hund = dict(h)
# Gesundheitstests (nur öffentliche, nur HD/ED/Augen/Herz)
tests = conn.execute("""
SELECT test_typ, ergebnis, test_name, untersuch_am
FROM dog_health_tests
WHERE hund_id=? AND is_public=1
AND test_typ IN ('HD','ED','augen','herz','OCD','patella','ZTP')
ORDER BY test_typ, untersuch_am DESC
""", (h["id"],)).fetchall()
seen = set()
hund["health_tests"] = []
for t in tests:
if t["test_typ"] not in seen:
seen.add(t["test_typ"])
hund["health_tests"].append(dict(t))
# Gentests (nur öffentliche, Zusammenfassung)
gentests = conn.execute("""
SELECT COUNT(*) as total,
SUM(CASE WHEN ergebnis_klasse='clear' THEN 1 ELSE 0 END) as clear_cnt
FROM dog_genetic_tests WHERE hund_id=? AND is_public=1
""", (h["id"],)).fetchone()
hund["gentests_total"] = gentests["total"] or 0
hund["gentests_clear"] = gentests["clear_cnt"] or 0
# Auszeichnungen (nur Zucht/Champion)
titles = conn.execute("""
SELECT titel_name FROM dog_titles
WHERE hund_id=? AND titel_typ IN ('champion','zucht','ausstellung')
ORDER BY verliehen_am DESC LIMIT 3
""", (h["id"],)).fetchall()
hund["titel"] = [t["titel_name"] for t in titles]
hunde.append(hund)
result["hunde"] = hunde
# Sichtbare Würfe
wuerfe = conn.execute("""
SELECT id, wurf_rang, wurf_name, vater_name, mutter_name,
geburt_datum, erwartetes_datum,
status, welpen_gesamt, welpen_verfuegbar, preis_spanne, beschreibung
FROM litters
WHERE breeder_id=? AND sichtbar=1 AND status != 'abgeschlossen'
ORDER BY COALESCE(geburt_datum, erwartetes_datum) DESC
""", (breeder_id,)).fetchall()
result["wuerfe"] = [dict(w) for w in wuerfe]
# Mitgliedschaften & Zertifikate (öffentliche Logos/Badges mit Caption)
certs = conn.execute("""
SELECT id, file_path, thumbnail_path, caption FROM breeder_photos
WHERE breeder_id=? AND entity_type='certificate' AND visibility='public'
ORDER BY sort_order
""", (breeder_id,)).fetchall()
result["zertifikate"] = [{
"id": c["id"],
"url": f"/media/{c['file_path']}",
"thumbnail_url": f"/media/{c['thumbnail_path']}" if c["thumbnail_path"] else f"/media/{c['file_path']}",
"caption": c["caption"],
} for c in certs]
# Gesundheits-Statistik (aggregiert über alle öffentlichen Hunde)
hd_stats = conn.execute("""
SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests
WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1)
AND test_typ='HD' AND is_public=1
GROUP BY ergebnis
""", (breeder_id,)).fetchall()
result["hd_stats"] = [dict(r) for r in hd_stats]
ed_stats = conn.execute("""
SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests
WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1)
AND test_typ='ED' AND is_public=1
GROUP BY ergebnis
""", (breeder_id,)).fetchall()
result["ed_stats"] = [dict(r) for r in ed_stats]
# Logo = primäres Bild der entity_type='breeder' Fotos
logo = conn.execute("""
SELECT file_path FROM breeder_photos
WHERE breeder_id=? AND entity_type='breeder' AND is_primary=1
LIMIT 1
""", (breeder_id,)).fetchone()
if not logo:
logo = conn.execute("""
SELECT file_path FROM breeder_photos
WHERE breeder_id=? AND entity_type='breeder'
ORDER BY sort_order, id LIMIT 1
""", (breeder_id,)).fetchone()
result["logo_url"] = f"/media/{logo['file_path']}" if logo else None
# Öffentliche Fotos für die Gallery (alle entity_type='breeder', max. 12)
photos = conn.execute("""
SELECT file_path, thumbnail_path, caption, is_primary FROM breeder_photos
WHERE breeder_id=? AND entity_type='breeder' AND visibility IN ('public','inquiry')
ORDER BY is_primary DESC, sort_order, id LIMIT 12
""", (breeder_id,)).fetchall()
result["fotos"] = [{
"url": f"/media/{p['file_path']}",
"thumb": f"/media/{p['thumbnail_path']}" if p['thumbnail_path'] else f"/media/{p['file_path']}",
"caption": p["caption"] or "",
"primary": bool(p["is_primary"]),
} for p in photos]
return result
# ------------------------------------------------------------------
# POST /api/admin/breeder/create-profile — Admin-Schnellprofil
# ------------------------------------------------------------------
@router.post("/admin/breeder/create-profile")
async def admin_create_profile(admin=Depends(require_admin)):
with db() as conn:
existing = conn.execute(
"SELECT id FROM breeder_profiles WHERE user_id=?", (admin["id"],)
).fetchone()
if existing:
return {"message": "Profil existiert bereits.", "profile_id": existing["id"]}
cur = conn.execute(
"INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, stadt, verified_at) "
"VALUES (?, ?, ?, ?, ?, datetime('now'))",
(admin["id"], "Admin-Zwinger", "Alle Rassen", "Admin", "Überall")
)
conn.execute(
"UPDATE users SET breeder_status='approved' WHERE id=?", (admin["id"],)
)
return {"message": "Admin-Züchterprofil angelegt.", "profile_id": cur.lastrowid}
# ------------------------------------------------------------------
# PUT /api/breeder/profile — eigenes Profil bearbeiten
# ------------------------------------------------------------------
class BreederProfileUpdate(BaseModel):
zwingername: Optional[str] = Field(None, max_length=200)
rasse_text: Optional[str] = Field(None, max_length=200)
verein: Optional[str] = Field(None, max_length=200)
vdh_mitglied: Optional[int] = None
stadt: Optional[str] = Field(None, max_length=200)
website: Optional[str] = Field(None, max_length=500)
beschreibung: Optional[str] = Field(None, max_length=10000)
@router.get("/breeder/my-editor")
async def breeder_my_editor(user=Depends(require_breeder)):
"""Daten für den Profil-Editor: Profil + eigene Würfe + Speicherverbrauch.
(Frontend breeder-editor.js stammt aus 459cd42 — dieser Lese-Endpoint
ging damals im Worktree-Merge verloren, wie /partner/my-profile.)"""
from routes.breeder_photos import _photo_dict
with db() as conn:
profile = conn.execute(
"SELECT * FROM breeder_profiles WHERE user_id=?", (user["id"],)
).fetchone()
if not profile:
raise HTTPException(404, "Noch kein Züchter-Profil angelegt.")
profile = dict(profile)
profile["photos"] = [_photo_dict(r) for r in conn.execute(
"SELECT * FROM breeder_photos WHERE breeder_id=? AND entity_type='breeder' ORDER BY sort_order",
(profile["id"],)
).fetchall()]
# Mitgliedschaften & Zertifikate (Logos/Badges fürs öffentliche Profil)
profile["certificates"] = [_photo_dict(r) for r in conn.execute(
"SELECT * FROM breeder_photos WHERE breeder_id=? AND entity_type='certificate' ORDER BY sort_order",
(profile["id"],)
).fetchall()]
litters = [dict(r) for r in conn.execute(
"""SELECT l.*,
(SELECT COUNT(*) FROM breeder_photos p
WHERE p.entity_type='litter' AND p.entity_id=l.id) AS foto_count
FROM litters l WHERE l.breeder_id=? ORDER BY l.created_at DESC""",
(profile["id"],)
).fetchall()]
# Speicherverbrauch der Züchter-Medien (MEDIA_DIR/breeders/{breeder_id}/**)
media_dir = os.getenv("MEDIA_DIR", "/data/media")
base = os.path.join(media_dir, "breeders", str(profile["id"]))
total = 0
if os.path.isdir(base):
for root, _dirs, files in os.walk(base):
for f in files:
try:
total += os.path.getsize(os.path.join(root, f))
except OSError:
pass
return {
"profile": profile,
"litters": litters,
"storage_mb": round(total / (1024 * 1024), 4),
"storage_limit_mb": 200,
}
@router.put("/breeder/profile")
async def update_breeder_profile(body: BreederProfileUpdate, user=Depends(require_breeder)):
with db() as conn:
profile = conn.execute(
"SELECT id FROM breeder_profiles WHERE user_id=?", (user["id"],)
).fetchone()
if not profile:
raise HTTPException(404, "Kein Züchter-Profil vorhanden.")
fields = {k: v for k, v in body.model_dump().items() if v is not None}
if not fields:
return {"message": "Keine Änderungen."}
set_clause = ", ".join(f"{k}=?" for k in fields)
conn.execute(
f"UPDATE breeder_profiles SET {set_clause} WHERE id=?",
(*fields.values(), profile["id"])
)
return {"message": "Profil aktualisiert."}
# ------------------------------------------------------------------
# GET /api/breeder/map — alle Züchter für Karte
# ------------------------------------------------------------------
@router.get("/breeder/map")
async def breeder_map_markers():
with db() as conn:
rows = conn.execute("""
SELECT bp.id, bp.zwingername, bp.rasse_text, bp.stadt,
bp.location_lat, bp.location_lng
FROM breeder_profiles bp
JOIN users u ON u.id = bp.user_id
WHERE bp.verified_at IS NOT NULL
AND u.rolle = 'breeder'
""").fetchall()
return [dict(r) for r in rows]