Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration
- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push - Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push - Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge, forum, wiki, walks) vollständig auf Phosphor-Icons migriert - Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos) - TheDogAPI lokal gespiegelt (169 Rassen + Fotos) - Quiz-Result-Cards horizontal (korrekte Bildproportionen) - SW by-v89
This commit is contained in:
parent
96bd57f0ad
commit
097295c628
44 changed files with 9980 additions and 300 deletions
258
backend/routes/wiki.py
Normal file
258
backend/routes/wiki.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
"""BAN YARO — Hunde-Wiki Routes"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
|
||||
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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue