- User können Fotos für Rassen vorschlagen (Upload-Modal in Rassen-Detail) - Mod/Admin-Review-Tab im Wiki mit Freischalten/Ablehnen + Push-Notification - wikipedia_photos.py: holt Fotos über Wikidata-QID → Wikipedia-API - Foto-Status: 578 lokal, 186 extern, 238 ohne Foto - DB: wiki_foto_submissions Tabelle - SW by-v90
439 lines
15 KiB
Python
439 lines
15 KiB
Python
"""BAN YARO — Hunde-Wiki Routes"""
|
|
|
|
import os
|
|
import shutil
|
|
import time
|
|
import logging
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
|
from pydantic import BaseModel
|
|
from database import db
|
|
from auth import get_current_user
|
|
|
|
logger = logging.getLogger(__name__)
|
|
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|
BREEDS_DIR = os.path.join(MEDIA_DIR, "breeds")
|
|
SUBMIT_DIR = os.path.join(BREEDS_DIR, "submissions")
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class BerichtCreate(BaseModel):
|
|
rasse: str
|
|
titel: str
|
|
text: str
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Hilfsfunktion Quiz-Scoring
|
|
# ------------------------------------------------------------------
|
|
def _quiz_score(rasse: dict, params: dict) -> int:
|
|
score = 0
|
|
if params.get("groesse") and rasse["groesse"] == params["groesse"]:
|
|
score += 2
|
|
# Aktivität: exakt = 2, eine Stufe daneben = 1
|
|
aktiv_map = {"niedrig": 0, "mittel": 1, "hoch": 2, "sehr_hoch": 3}
|
|
if params.get("aktivitaet"):
|
|
a_user = aktiv_map.get(params["aktivitaet"], -1)
|
|
a_rasse = aktiv_map.get(rasse["aktivitaet"], -1)
|
|
diff = abs(a_user - a_rasse)
|
|
if diff == 0:
|
|
score += 2
|
|
elif diff == 1:
|
|
score += 1
|
|
# Erfahrung: anfaenger bekommt Bonus für einfache Rassen
|
|
erf_map = {"anfaenger": 0, "fortgeschritten": 1, "experte": 2}
|
|
if params.get("erfahrung"):
|
|
e_user = erf_map.get(params["erfahrung"], -1)
|
|
e_rasse = erf_map.get(rasse["erfahrung"], -1)
|
|
if e_user >= e_rasse:
|
|
score += 2
|
|
elif e_user == e_rasse - 1:
|
|
score += 1
|
|
# Kinder
|
|
if params.get("kinder") in ("true", "True", "1"):
|
|
if rasse["kinder_geeignet"]:
|
|
score += 1
|
|
# Wohnung
|
|
if params.get("wohnung") in ("true", "True", "1"):
|
|
if rasse["wohnung_geeignet"]:
|
|
score += 2
|
|
elif params.get("wohnung") in ("false", "False", "0"):
|
|
if not rasse["wohnung_geeignet"]:
|
|
score += 1
|
|
return score
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/wiki/stats — Seed-Status
|
|
# ------------------------------------------------------------------
|
|
@router.get("/stats")
|
|
async def get_stats():
|
|
with db() as conn:
|
|
row = conn.execute("SELECT COUNT(*) as total FROM wiki_rassen").fetchone()
|
|
total = row["total"] if row else 0
|
|
return {"total_breeds": total, "seeded": total > 0}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/wiki/rassen — alle Rassen (Übersicht, paginiert)
|
|
# ------------------------------------------------------------------
|
|
@router.get("/rassen")
|
|
async def get_rassen(
|
|
search: str = Query(""),
|
|
gruppe: str = Query(""),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
conditions = []
|
|
args = []
|
|
|
|
if search:
|
|
conditions.append("(LOWER(name) LIKE ? OR LOWER(gruppe) LIKE ? OR LOWER(temperament) LIKE ?)")
|
|
like = f"%{search.lower()}%"
|
|
args += [like, like, like]
|
|
if gruppe:
|
|
conditions.append("gruppe = ?")
|
|
args.append(gruppe)
|
|
|
|
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
|
args_paged = args + [limit, offset]
|
|
|
|
with db() as conn:
|
|
rows = conn.execute(f"""
|
|
SELECT id, name, gruppe, groesse, aktivitaet, erfahrung,
|
|
foto_url, slug, kinder_geeignet, wohnung_geeignet
|
|
FROM wiki_rassen
|
|
{where}
|
|
ORDER BY name ASC
|
|
LIMIT ? OFFSET ?
|
|
""", args_paged).fetchall()
|
|
|
|
count_row = conn.execute(f"""
|
|
SELECT COUNT(*) as total FROM wiki_rassen {where}
|
|
""", args).fetchone()
|
|
|
|
# Alle Gruppen für Filter-Dropdown
|
|
gruppen_rows = conn.execute(
|
|
"SELECT DISTINCT gruppe FROM wiki_rassen WHERE gruppe IS NOT NULL ORDER BY gruppe"
|
|
).fetchall()
|
|
|
|
return {
|
|
"breeds": [dict(r) for r in rows],
|
|
"total": count_row["total"] if count_row else 0,
|
|
"gruppen": [r["gruppe"] for r in gruppen_rows],
|
|
}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/wiki/rassen/{slug} — Rasse-Detail + Community-Berichte
|
|
# ------------------------------------------------------------------
|
|
@router.get("/rassen/{rasse_slug}")
|
|
async def get_rasse(rasse_slug: str):
|
|
with db() as conn:
|
|
rasse = conn.execute(
|
|
"SELECT * FROM wiki_rassen WHERE slug = ?", (rasse_slug,)
|
|
).fetchone()
|
|
|
|
if not rasse:
|
|
raise HTTPException(404, "Rasse nicht gefunden.")
|
|
|
|
rows = conn.execute(
|
|
"""SELECT wb.id, wb.titel, wb.text, wb.created_at, u.name as autor
|
|
FROM wiki_berichte wb
|
|
JOIN users u ON u.id = wb.user_id
|
|
WHERE wb.rasse = ?
|
|
ORDER BY wb.created_at DESC
|
|
LIMIT 50""",
|
|
(rasse_slug,),
|
|
).fetchall()
|
|
|
|
result = dict(rasse)
|
|
result["berichte"] = [dict(r) for r in rows]
|
|
return result
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/wiki/berichte — Community-Bericht hinzufügen
|
|
# ------------------------------------------------------------------
|
|
@router.post("/berichte")
|
|
async def create_bericht(data: BerichtCreate, user=Depends(get_current_user)):
|
|
# Prüfen ob die Rasse in der DB existiert
|
|
with db() as conn:
|
|
rasse_row = conn.execute(
|
|
"SELECT slug FROM wiki_rassen WHERE slug = ?", (data.rasse,)
|
|
).fetchone()
|
|
|
|
if not rasse_row:
|
|
raise HTTPException(400, "Ungültige Rasse.")
|
|
if not data.titel.strip():
|
|
raise HTTPException(400, "Titel darf nicht leer sein.")
|
|
if not data.text.strip():
|
|
raise HTTPException(400, "Text darf nicht leer sein.")
|
|
|
|
with db() as conn:
|
|
cur = conn.execute(
|
|
"""INSERT INTO wiki_berichte (user_id, rasse, titel, text)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(user["id"], data.rasse, data.titel.strip(), data.text.strip()),
|
|
)
|
|
row = conn.execute(
|
|
"SELECT wb.id, wb.titel, wb.text, wb.created_at, u.name as autor "
|
|
"FROM wiki_berichte wb JOIN users u ON u.id = wb.user_id "
|
|
"WHERE wb.id = ?",
|
|
(cur.lastrowid,),
|
|
).fetchone()
|
|
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/wiki/berichte/{id} — Bericht löschen (nur eigene)
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/berichte/{bericht_id}")
|
|
async def delete_bericht(bericht_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"SELECT id, user_id FROM wiki_berichte WHERE id = ?",
|
|
(bericht_id,),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Bericht nicht gefunden.")
|
|
if row["user_id"] != user["id"]:
|
|
raise HTTPException(403, "Nicht erlaubt.")
|
|
conn.execute("DELETE FROM wiki_berichte WHERE id = ?", (bericht_id,))
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/wiki/quiz/result — Quiz-Ergebnis berechnen
|
|
# ------------------------------------------------------------------
|
|
@router.get("/quiz/result")
|
|
async def quiz_result(
|
|
groesse: str = Query(""),
|
|
aktivitaet: str = Query(""),
|
|
erfahrung: str = Query(""),
|
|
kinder: str = Query(""),
|
|
wohnung: str = Query(""),
|
|
):
|
|
params = {
|
|
"groesse": groesse,
|
|
"aktivitaet": aktivitaet,
|
|
"erfahrung": erfahrung,
|
|
"kinder": kinder,
|
|
"wohnung": wohnung,
|
|
}
|
|
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"""SELECT id, name, gruppe, groesse, aktivitaet, erfahrung,
|
|
foto_url, slug, kinder_geeignet, wohnung_geeignet,
|
|
temperament, bred_for
|
|
FROM wiki_rassen
|
|
ORDER BY name ASC"""
|
|
).fetchall()
|
|
|
|
rassen = [dict(r) for r in rows]
|
|
|
|
if not rassen:
|
|
return {"results": []}
|
|
|
|
scored = sorted(
|
|
rassen,
|
|
key=lambda r: _quiz_score(r, params),
|
|
reverse=True,
|
|
)
|
|
|
|
top3 = [
|
|
{
|
|
"slug": r["slug"],
|
|
"name": r["name"],
|
|
"gruppe": r["gruppe"],
|
|
"groesse": r["groesse"],
|
|
"aktivitaet": r["aktivitaet"],
|
|
"erfahrung": r["erfahrung"],
|
|
"foto_url": r["foto_url"],
|
|
"kinder_geeignet": r["kinder_geeignet"],
|
|
"wohnung_geeignet":r["wohnung_geeignet"],
|
|
"temperament": r["temperament"],
|
|
"score": _quiz_score(r, params),
|
|
}
|
|
for r in scored[:3]
|
|
]
|
|
|
|
return {"results": top3}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/wiki/rassen/{slug}/foto — User reicht Foto ein
|
|
# ------------------------------------------------------------------
|
|
@router.post("/rassen/{slug}/foto", status_code=201)
|
|
async def submit_foto(
|
|
slug: str,
|
|
file: UploadFile = File(...),
|
|
user = Depends(get_current_user),
|
|
):
|
|
with db() as conn:
|
|
rasse = conn.execute(
|
|
"SELECT id, name, external_id FROM wiki_rassen WHERE slug=?", (slug,)
|
|
).fetchone()
|
|
if not rasse:
|
|
raise HTTPException(404, "Rasse nicht gefunden.")
|
|
|
|
# Dateiformat prüfen
|
|
ct = file.content_type or ""
|
|
if not ct.startswith("image/"):
|
|
raise HTTPException(400, "Nur Bilddateien erlaubt.")
|
|
|
|
os.makedirs(SUBMIT_DIR, exist_ok=True)
|
|
ts = int(time.time())
|
|
filename = f"{slug}_{user['id']}_{ts}.jpg"
|
|
path = os.path.join(SUBMIT_DIR, filename)
|
|
|
|
content = await file.read()
|
|
if len(content) > 8 * 1024 * 1024:
|
|
raise HTTPException(400, "Datei zu groß (max. 8 MB).")
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
|
|
local_url = f"/media/breeds/submissions/{filename}"
|
|
|
|
with db() as conn:
|
|
# Bestehende pending-Einreichung des Users für diese Rasse ersetzen
|
|
old = conn.execute(
|
|
"SELECT foto_url FROM wiki_foto_submissions WHERE rasse_id=? AND user_id=? AND status='pending'",
|
|
(rasse["id"], user["id"])
|
|
).fetchone()
|
|
if old:
|
|
try:
|
|
old_path = old["foto_url"].replace("/media/", MEDIA_DIR + "/", 1)
|
|
if os.path.exists(old_path):
|
|
os.remove(old_path)
|
|
except Exception:
|
|
pass
|
|
conn.execute(
|
|
"DELETE FROM wiki_foto_submissions WHERE rasse_id=? AND user_id=? AND status='pending'",
|
|
(rasse["id"], user["id"])
|
|
)
|
|
|
|
conn.execute("""
|
|
INSERT INTO wiki_foto_submissions (user_id, rasse_id, foto_url)
|
|
VALUES (?,?,?)
|
|
""", (user["id"], rasse["id"], local_url))
|
|
|
|
logger.info(f"Foto-Einreichung: {rasse['name']} von User {user['id']}")
|
|
return {"ok": True, "foto_url": local_url}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/wiki/foto-submissions — offene Einreichungen (Mod/Admin)
|
|
# ------------------------------------------------------------------
|
|
@router.get("/foto-submissions")
|
|
async def list_submissions(user=Depends(get_current_user)):
|
|
if not (user.get("is_moderator") or user.get("rolle") == "admin"):
|
|
raise HTTPException(403, "Nur Moderatoren.")
|
|
|
|
with db() as conn:
|
|
rows = conn.execute("""
|
|
SELECT s.id, s.foto_url, s.status, s.created_at,
|
|
u.name AS user_name,
|
|
r.name AS rasse_name, r.slug AS rasse_slug,
|
|
r.foto_url AS aktuell_foto
|
|
FROM wiki_foto_submissions s
|
|
JOIN users u ON u.id = s.user_id
|
|
JOIN wiki_rassen r ON r.id = s.rasse_id
|
|
WHERE s.status = 'pending'
|
|
ORDER BY s.created_at ASC
|
|
""").fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PATCH /api/wiki/foto-submissions/{id} — genehmigen oder ablehnen
|
|
# ------------------------------------------------------------------
|
|
class ReviewModel(BaseModel):
|
|
action: str # "approve" | "reject"
|
|
reject_reason: str = ""
|
|
|
|
|
|
@router.patch("/foto-submissions/{sub_id}")
|
|
async def review_submission(sub_id: int, data: ReviewModel, user=Depends(get_current_user)):
|
|
if not (user.get("is_moderator") or user.get("rolle") == "admin"):
|
|
raise HTTPException(403, "Nur Moderatoren.")
|
|
if data.action not in ("approve", "reject"):
|
|
raise HTTPException(400, "action muss 'approve' oder 'reject' sein.")
|
|
|
|
with db() as conn:
|
|
sub = conn.execute(
|
|
"SELECT * FROM wiki_foto_submissions WHERE id=? AND status='pending'",
|
|
(sub_id,)
|
|
).fetchone()
|
|
if not sub:
|
|
raise HTTPException(404, "Einreichung nicht gefunden.")
|
|
|
|
rasse = conn.execute(
|
|
"SELECT id, external_id, slug FROM wiki_rassen WHERE id=?",
|
|
(sub["rasse_id"],)
|
|
).fetchone()
|
|
|
|
if data.action == "approve":
|
|
# Ziel-Dateiname aus external_id ableiten
|
|
ext_id = rasse["external_id"] if rasse else None
|
|
if ext_id and str(ext_id).startswith("wd_"):
|
|
qid = str(ext_id).replace("wd_", "")
|
|
dest_name = f"{qid}.jpg"
|
|
elif ext_id:
|
|
dest_name = f"{ext_id}.jpg"
|
|
else:
|
|
dest_name = f"{rasse['slug']}.jpg"
|
|
|
|
os.makedirs(BREEDS_DIR, exist_ok=True)
|
|
src = sub["foto_url"].replace("/media/", MEDIA_DIR + "/", 1)
|
|
dest = os.path.join(BREEDS_DIR, dest_name)
|
|
try:
|
|
shutil.copy2(src, dest)
|
|
except Exception as e:
|
|
raise HTTPException(500, f"Datei konnte nicht kopiert werden: {e}")
|
|
|
|
new_url = f"/media/breeds/{dest_name}"
|
|
conn.execute(
|
|
"UPDATE wiki_rassen SET foto_url=? WHERE id=?",
|
|
(new_url, rasse["id"])
|
|
)
|
|
conn.execute("""
|
|
UPDATE wiki_foto_submissions
|
|
SET status='approved', reviewed_by=?, reviewed_at=datetime('now')
|
|
WHERE id=?
|
|
""", (user["id"], sub_id))
|
|
|
|
# Push-Notification an Einreicher
|
|
try:
|
|
from routes.push import send_push_to_user
|
|
send_push_to_user(sub["user_id"], {
|
|
"title": "Foto freigeschalten!",
|
|
"body": f"Dein Foto wurde im Wiki veröffentlicht.",
|
|
"type": "wiki_foto_approved",
|
|
"data": {"page": "wiki"},
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
else: # reject
|
|
conn.execute("""
|
|
UPDATE wiki_foto_submissions
|
|
SET status='rejected', reviewed_by=?, reviewed_at=datetime('now'),
|
|
reject_reason=?
|
|
WHERE id=?
|
|
""", (user["id"], data.reject_reason or "Nicht geeignet.", sub_id))
|
|
# Temp-Datei löschen
|
|
try:
|
|
path = sub["foto_url"].replace("/media/", MEDIA_DIR + "/", 1)
|
|
if os.path.exists(path):
|
|
os.remove(path)
|
|
except Exception:
|
|
pass
|
|
|
|
return {"ok": True}
|