Sprint 11b: Wiki-Foto-Einreichungen + Wikipedia-Foto-Scraper
- 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
This commit is contained in:
parent
097295c628
commit
32d630d5a1
6 changed files with 598 additions and 3 deletions
|
|
@ -1,10 +1,19 @@
|
|||
"""BAN YARO — Hunde-Wiki Routes"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -256,3 +265,175 @@ async def quiz_result(
|
|||
]
|
||||
|
||||
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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue