Wiki-Foto-System: Gallery-Flow, Community-Fotos, Wiki-Fotos-Badge

- review_submission: Fotos ins gallery/-Verzeichnis statt breeds/ kopieren;
  foto_url der Rasse nur überschreiben wenn noch keins vorhanden (Erstbild)
- Rassen-Detail-API: user_fotos (approved submissions) mitliefern
- Rassen-Listen-API: user_foto-Subquery als Fallback wenn foto_url leer
- achievements: neue Badge-Kategorie "Wiki-Fotos" (bronze 1, silber 3, gold 10)
  mit wiki_fotos-Metrik in check_and_award und my_achievements
- Badge-Check + Push nach Foto-Approval
- wiki.js: Karten-Bild nutzt r.foto_url || r.user_foto
- wiki.js: Detail-Ansicht zeigt Community-Foto-Galerie (scrollbar, clickable)
- Dockerfile: breeds/gallery + breeds/submissions im Image anlegen
- SW by-v366, APP_VER 351
This commit is contained in:
rene 2026-04-25 09:53:24 +02:00
parent b608d5635f
commit 6064a1d750
6 changed files with 111 additions and 43 deletions

View file

@ -12,9 +12,10 @@ from auth import get_current_user, get_current_user_optional
from ratelimit import check as rl_check, block_ip
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")
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
BREEDS_DIR = os.path.join(MEDIA_DIR, "breeds")
SUBMIT_DIR = os.path.join(BREEDS_DIR, "submissions")
GALLERY_DIR = os.path.join(BREEDS_DIR, "gallery")
router = APIRouter()
@ -119,7 +120,10 @@ async def get_rassen(
with db() as conn:
rows = conn.execute(f"""
SELECT id, name, gruppe, groesse, aktivitaet, erfahrung,
foto_url, slug, kinder_geeignet, wohnung_geeignet
foto_url, slug, kinder_geeignet, wohnung_geeignet,
(SELECT s.foto_url FROM wiki_foto_submissions s
WHERE s.rasse_id = wiki_rassen.id AND s.status='approved'
ORDER BY s.reviewed_at DESC LIMIT 1) AS user_foto
FROM wiki_rassen
{where}
ORDER BY name ASC
@ -166,8 +170,18 @@ async def get_rasse(rasse_slug: str, request: Request):
(rasse_slug,),
).fetchall()
user_fotos = conn.execute("""
SELECT s.foto_url, u.name AS user_name, s.created_at
FROM wiki_foto_submissions s
JOIN users u ON u.id = s.user_id
WHERE s.rasse_id = ? AND s.status = 'approved'
ORDER BY s.reviewed_at DESC
LIMIT 10
""", (rasse["id"],)).fetchall()
result = dict(rasse)
result["berichte"] = [dict(r) for r in rows]
result["berichte"] = [dict(r) for r in rows]
result["user_fotos"] = [dict(r) for r in user_fotos]
return result
@ -400,47 +414,61 @@ async def review_submission(sub_id: int, data: ReviewModel, user=Depends(get_cur
).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)
# Ins gallery-Verzeichnis verschieben
os.makedirs(GALLERY_DIR, exist_ok=True)
src = sub["foto_url"].replace("/media/", MEDIA_DIR + "/", 1)
dest_name = f"{rasse['slug']}_{sub_id}.jpg"
dest = os.path.join(GALLERY_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"])
)
new_url = f"/media/breeds/gallery/{dest_name}"
# Nur als Hauptbild setzen wenn noch keins vorhanden
if not rasse["foto_url"]:
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')
SET status='approved', foto_url=?, reviewed_by=?, reviewed_at=datetime('now')
WHERE id=?
""", (user["id"], sub_id))
""", (new_url, 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.",
"body": "Dein Foto wurde im Wiki veröffentlicht.",
"type": "wiki_foto_approved",
"data": {"page": "wiki"},
})
except Exception:
pass
# Badge-Check
try:
from routes.achievements import check_and_award
with db() as conn2:
new_badges = check_and_award(sub["user_id"], conn2)
if new_badges:
try:
send_push_to_user(sub["user_id"], {
"title": "\U0001f3c5 Neues Badge!",
"body": f"Du hast '{new_badges[0]['name']}' verdient!",
"type": "badge_earned",
"data": {"page": "achievements"},
})
except Exception:
pass
except Exception:
pass
else: # reject
conn.execute("""
UPDATE wiki_foto_submissions