banyaro/backend/routes/challenges.py
rene aa4849d947 Feature: 3 Community-Features — Foto-Challenge, Stamm-Gassis, Rassen-Chip (SW by-v700)
- Foto-Challenge der Woche: DB-Tabellen, routes/challenges.py (current/submit/vote/winners),
  Scheduler-Job jeden Montag 08:00, walks.js Challenge-Tab mit Banner, Galerie, Voting-Herz
- Gassi-Zeiten-Pool: DB-Tabelle gassi_zeiten, routes/gassi_zeiten.py (CRUD + Umkreis),
  walks.js Stamm-Gassis-Tab mit Karten, Wochentag-Selector, Mitmachen→Chat
- Rassen-Treffen-Chip: GET /api/friends/same-breed, dog-profile.js zeigt Chip
  wenn andere User gleiche Rasse haben, Klick → Forum mit Rassen-Suche vorausgefüllt
2026-05-04 21:09:35 +02:00

304 lines
10 KiB
Python

"""BAN YARO — Foto-Challenge der Woche"""
import os
import uuid
import logging
from datetime import date, timedelta
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user, get_current_user_optional
from media_utils import convert_media, generate_preview
logger = logging.getLogger(__name__)
router = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
CHALLENGE_DIR = os.path.join(MEDIA_DIR, "challenges")
_CHALLENGE_THEMEN = [
"Bestes Schnüffel-Foto 👃",
"Action-Aufnahme 🏃",
"Schlafendes Tier 😴",
"Gassi im Regen 🌧️",
"Hundeblick in die Kamera 👀",
"Spielzeit mit Freunden 🐕",
"Herbstspaziergang 🍂",
"Beste Sprung-Aufnahme 🦘",
"Hund am Wasser 🌊",
"Erstes Mal im Schnee ❄️",
"Genuss-Moment 🦴",
"Versteckt im Gebüsch 🌿",
"Tierisches Selfie 🤳",
"Hund & Kind 👶",
"Hund & Katze zusammen 🐱",
"Der beste Buddel-Moment 🐾",
"Freude beim Apportieren 🎾",
"Hund in seiner Lieblingshöhle 🛋️",
"Sonnenuntergangs-Gassi 🌅",
"Hundebegegnung auf dem Spaziergang 🐕🐕",
"Ausdrucksstarker Hundeblick 😍",
"Hund im Herbstlaub 🍁",
"Welpenfoto 🍼",
"Seniorenhund im Porträt 👴",
"Lustigste Schlafposition 💤",
"Hund trägt etwas 🎀",
"Hund + Besitzer Spiegelfoto 🪞",
"Hund auf Abenteuer 🏕️",
"Beste Lauf-Action 💨",
"Hund im Café ☕",
]
def _current_week_monday() -> str:
today = date.today()
monday = today - timedelta(days=today.weekday())
return monday.isoformat()
def _current_week_sunday() -> str:
monday = date.fromisoformat(_current_week_monday())
return (monday + timedelta(days=6)).isoformat()
def _ensure_current_challenge(conn) -> int:
"""Stellt sicher dass eine Challenge für die aktuelle Woche existiert. Gibt die ID zurück."""
monday = _current_week_monday()
sunday = _current_week_sunday()
existing = conn.execute(
"SELECT id FROM foto_challenge WHERE start_date = ?", (monday,)
).fetchone()
if existing:
return existing["id"]
# Thema aus Rotation wählen (Wochennummer % Anzahl Themen)
week_num = date.today().isocalendar()[1]
thema = _CHALLENGE_THEMEN[week_num % len(_CHALLENGE_THEMEN)]
cur = conn.execute(
"INSERT INTO foto_challenge (thema, beschreibung, start_date, end_date, created_by) "
"VALUES (?, ?, ?, ?, NULL)",
(thema, f"Diese Woche: {thema}", monday, sunday)
)
return cur.lastrowid
# ------------------------------------------------------------------
# GET /api/challenges/current
# ------------------------------------------------------------------
@router.get("/current")
async def get_current_challenge(user=Depends(get_current_user_optional)):
with db() as conn:
challenge_id = _ensure_current_challenge(conn)
challenge = conn.execute(
"SELECT * FROM foto_challenge WHERE id = ?", (challenge_id,)
).fetchone()
submissions = conn.execute("""
SELECT cs.id, cs.user_id, cs.dog_id, cs.foto_url, cs.caption, cs.votes, cs.created_at,
u.name AS user_name, u.avatar_url,
d.name AS dog_name, d.foto_url AS dog_foto_url
FROM challenge_submissions cs
LEFT JOIN users u ON u.id = cs.user_id
LEFT JOIN dogs d ON d.id = cs.dog_id
WHERE cs.challenge_id = ?
ORDER BY cs.votes DESC, cs.created_at ASC
""", (challenge_id,)).fetchall()
my_submission = None
my_votes = set()
if user:
mine = conn.execute(
"SELECT id FROM challenge_submissions WHERE challenge_id=? AND user_id=?",
(challenge_id, user["id"])
).fetchone()
if mine:
my_submission = mine["id"]
voted_rows = conn.execute(
"SELECT cv.submission_id FROM challenge_votes cv "
"JOIN challenge_submissions cs ON cs.id = cv.submission_id "
"WHERE cv.user_id = ? AND cs.challenge_id = ?",
(user["id"], challenge_id)
).fetchall()
my_votes = {r["submission_id"] for r in voted_rows}
# Countdown bis Sonntag
end = date.fromisoformat(challenge["end_date"])
days_left = (end - date.today()).days + 1
result_subs = []
for s in submissions:
sd = dict(s)
sd["i_voted"] = (sd["id"] in my_votes) if user else False
result_subs.append(sd)
return {
"challenge": dict(challenge),
"submissions": result_subs,
"my_submission_id": my_submission,
"days_left": max(0, days_left),
}
# ------------------------------------------------------------------
# POST /api/challenges/{id}/submit
# ------------------------------------------------------------------
@router.post("/{challenge_id}/submit", status_code=201)
async def submit_photo(
challenge_id: int,
caption: Optional[str] = Form(None),
dog_id: Optional[int] = Form(None),
foto: UploadFile = File(...),
user=Depends(get_current_user),
):
with db() as conn:
challenge = conn.execute(
"SELECT * FROM foto_challenge WHERE id = ?", (challenge_id,)
).fetchone()
if not challenge:
raise HTTPException(404, "Challenge nicht gefunden.")
today = date.today().isoformat()
if today > challenge["end_date"]:
raise HTTPException(400, "Die Challenge ist bereits beendet.")
# Doppelt-Check
existing = conn.execute(
"SELECT id FROM challenge_submissions WHERE challenge_id=? AND user_id=?",
(challenge_id, user["id"])
).fetchone()
if existing:
raise HTTPException(409, "Du hast bereits ein Foto eingereicht.")
# Foto speichern
os.makedirs(CHALLENGE_DIR, exist_ok=True)
orig_filename = foto.filename or "foto.jpg"
ext = os.path.splitext(orig_filename)[1] or ".jpg"
base = uuid.uuid4().hex
raw = await foto.read()
# HEIC→JPEG Konvertierung falls nötig
try:
converted, out_ext = convert_media(raw, orig_filename)
except Exception:
converted, out_ext = raw, ext
save_filename = f"{base}{out_ext}"
save_path = os.path.join(CHALLENGE_DIR, save_filename)
with open(save_path, "wb") as f:
f.write(converted)
foto_url = f"/media/challenges/{save_filename}"
# Preview
try:
preview = generate_preview(converted, out_ext)
if preview:
prev_path = os.path.join(CHALLENGE_DIR, f"{base}_preview.webp")
with open(prev_path, "wb") as f:
f.write(preview)
except Exception:
pass
with db() as conn:
cur = conn.execute(
"INSERT INTO challenge_submissions (challenge_id, user_id, dog_id, foto_url, caption) "
"VALUES (?, ?, ?, ?, ?)",
(challenge_id, user["id"], dog_id, foto_url, caption)
)
row = conn.execute("""
SELECT cs.*, u.name AS user_name, d.name AS dog_name
FROM challenge_submissions cs
LEFT JOIN users u ON u.id = cs.user_id
LEFT JOIN dogs d ON d.id = cs.dog_id
WHERE cs.id = ?
""", (cur.lastrowid,)).fetchone()
return dict(row)
# ------------------------------------------------------------------
# POST /api/challenges/submissions/{id}/vote — Toggle-Vote
# ------------------------------------------------------------------
@router.post("/submissions/{submission_id}/vote")
async def vote_submission(submission_id: int, user=Depends(get_current_user)):
with db() as conn:
sub = conn.execute(
"SELECT * FROM challenge_submissions WHERE id = ?", (submission_id,)
).fetchone()
if not sub:
raise HTTPException(404, "Einreichung nicht gefunden.")
if sub["user_id"] == user["id"]:
raise HTTPException(400, "Du kannst nicht für dein eigenes Foto abstimmen.")
existing = conn.execute(
"SELECT id FROM challenge_votes WHERE submission_id=? AND user_id=?",
(submission_id, user["id"])
).fetchone()
if existing:
# Toggle: Vote entfernen
conn.execute(
"DELETE FROM challenge_votes WHERE submission_id=? AND user_id=?",
(submission_id, user["id"])
)
conn.execute(
"UPDATE challenge_submissions SET votes = MAX(0, votes - 1) WHERE id=?",
(submission_id,)
)
voted = False
else:
conn.execute(
"INSERT INTO challenge_votes (submission_id, user_id) VALUES (?, ?)",
(submission_id, user["id"])
)
conn.execute(
"UPDATE challenge_submissions SET votes = votes + 1 WHERE id=?",
(submission_id,)
)
voted = True
votes = conn.execute(
"SELECT votes FROM challenge_submissions WHERE id=?", (submission_id,)
).fetchone()["votes"]
return {"voted": voted, "votes": votes}
# ------------------------------------------------------------------
# GET /api/challenges/winners — letzte 4 Gewinner
# ------------------------------------------------------------------
@router.get("/winners")
async def get_winners():
with db() as conn:
# Vergangene Challenges (ohne aktuelle Woche)
monday = _current_week_monday()
challenges = conn.execute(
"SELECT id, thema, start_date, end_date FROM foto_challenge "
"WHERE end_date < ? ORDER BY end_date DESC LIMIT 4",
(monday,)
).fetchall()
winners = []
for ch in challenges:
winner = conn.execute("""
SELECT cs.id, cs.user_id, cs.foto_url, cs.caption, cs.votes,
u.name AS user_name, u.avatar_url,
d.name AS dog_name
FROM challenge_submissions cs
LEFT JOIN users u ON u.id = cs.user_id
LEFT JOIN dogs d ON d.id = cs.dog_id
WHERE cs.challenge_id = ?
ORDER BY cs.votes DESC, cs.created_at ASC
LIMIT 1
""", (ch["id"],)).fetchone()
winners.append({
"challenge": dict(ch),
"winner": dict(winner) if winner else None,
})
return winners