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
This commit is contained in:
rene 2026-05-04 21:09:35 +02:00
parent d6206d378e
commit aa4849d947
10 changed files with 1322 additions and 22 deletions

View file

@ -0,0 +1,190 @@
"""BAN YARO — Gassi-Zeiten-Pool (regelmäßige Gassi-Zeiten mit Gleichgesinnten)"""
import json
import math
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional, List
from database import db
from auth import get_current_user
logger = logging.getLogger(__name__)
router = APIRouter()
def _haversine(lat1, lon1, lat2, lon2):
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
class GassiZeitCreate(BaseModel):
dog_id: Optional[int] = None
wochentage: List[str] # ["mo", "mi", "fr"]
uhrzeit: str # "17:00"
ort_name: Optional[str] = None
lat: Optional[float] = None
lon: Optional[float] = None
radius_m: int = 500
notiz: Optional[str] = None
class GassiZeitUpdate(BaseModel):
aktiv: Optional[int] = None
# ------------------------------------------------------------------
# GET /api/gassi-zeiten — alle in der Nähe (oder eigene)
# ------------------------------------------------------------------
@router.get("")
async def list_gassi_zeiten(
lat: Optional[float] = None,
lon: Optional[float] = None,
radius: int = 5000, # Meter
nur_eigene: bool = False,
user=Depends(get_current_user),
):
with db() as conn:
if nur_eigene:
rows = conn.execute("""
SELECT gz.*, u.name AS user_name, u.avatar_url,
d.name AS dog_name, d.foto_url AS dog_foto_url, d.rasse AS dog_rasse
FROM gassi_zeiten gz
LEFT JOIN users u ON u.id = gz.user_id
LEFT JOIN dogs d ON d.id = gz.dog_id
WHERE gz.user_id = ?
ORDER BY gz.uhrzeit ASC
""", (user["id"],)).fetchall()
else:
rows = conn.execute("""
SELECT gz.*, u.name AS user_name, u.avatar_url,
d.name AS dog_name, d.foto_url AS dog_foto_url, d.rasse AS dog_rasse
FROM gassi_zeiten gz
LEFT JOIN users u ON u.id = gz.user_id
LEFT JOIN dogs d ON d.id = gz.dog_id
WHERE gz.aktiv = 1
ORDER BY gz.uhrzeit ASC
""").fetchall()
result = []
for r in rows:
d = dict(r)
# wochentage JSON parsen
try:
d["wochentage"] = json.loads(d["wochentage"]) if isinstance(d["wochentage"], str) else d["wochentage"]
except Exception:
d["wochentage"] = []
d["is_mine"] = (d["user_id"] == user["id"])
# Distanz-Filter
if lat is not None and lon is not None and d.get("lat") and d.get("lon"):
dist = _haversine(lat, lon, d["lat"], d["lon"])
if not nur_eigene and dist > radius:
continue
d["distance_m"] = int(dist)
else:
d["distance_m"] = None
result.append(d)
# Sortierung: eigene zuerst, dann nach Distanz
result.sort(key=lambda x: (0 if x["is_mine"] else 1, x.get("distance_m") or 99999))
return result
# ------------------------------------------------------------------
# POST /api/gassi-zeiten — eigene Zeit anlegen
# ------------------------------------------------------------------
@router.post("", status_code=201)
async def create_gassi_zeit(data: GassiZeitCreate, user=Depends(get_current_user)):
if not data.wochentage:
raise HTTPException(400, "Mindestens ein Wochentag muss angegeben werden.")
if not data.uhrzeit:
raise HTTPException(400, "Uhrzeit muss angegeben werden.")
wochentage_json = json.dumps(data.wochentage)
with db() as conn:
# Hund-Prüfung
if data.dog_id:
dog = conn.execute(
"SELECT id FROM dogs WHERE id=? AND user_id=?", (data.dog_id, user["id"])
).fetchone()
if not dog:
raise HTTPException(403, "Hund nicht gefunden oder gehört nicht dir.")
cur = conn.execute("""
INSERT INTO gassi_zeiten (user_id, dog_id, wochentage, uhrzeit,
ort_name, lat, lon, radius_m, notiz)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (user["id"], data.dog_id, wochentage_json, data.uhrzeit,
data.ort_name, data.lat, data.lon, data.radius_m, data.notiz))
row = conn.execute("""
SELECT gz.*, u.name AS user_name, d.name AS dog_name, d.foto_url AS dog_foto_url
FROM gassi_zeiten gz
LEFT JOIN users u ON u.id = gz.user_id
LEFT JOIN dogs d ON d.id = gz.dog_id
WHERE gz.id = ?
""", (cur.lastrowid,)).fetchone()
result = dict(row)
try:
result["wochentage"] = json.loads(result["wochentage"])
except Exception:
pass
result["is_mine"] = True
return result
# ------------------------------------------------------------------
# PATCH /api/gassi-zeiten/{id} — pausieren / aktivieren
# ------------------------------------------------------------------
@router.patch("/{gz_id}")
async def update_gassi_zeit(gz_id: int, data: GassiZeitUpdate, user=Depends(get_current_user)):
with db() as conn:
gz = conn.execute(
"SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,)
).fetchone()
if not gz:
raise HTTPException(404, "Gassi-Zeit nicht gefunden.")
if gz["user_id"] != user["id"]:
raise HTTPException(403, "Nicht deine Gassi-Zeit.")
updates = data.model_dump(exclude_none=True)
if updates:
cols = ", ".join(f"{k} = ?" for k in updates)
conn.execute(f"UPDATE gassi_zeiten SET {cols} WHERE id=?", [*updates.values(), gz_id])
row = conn.execute(
"SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,)
).fetchone()
result = dict(row)
try:
result["wochentage"] = json.loads(result["wochentage"])
except Exception:
pass
result["is_mine"] = True
return result
# ------------------------------------------------------------------
# DELETE /api/gassi-zeiten/{id}
# ------------------------------------------------------------------
@router.delete("/{gz_id}", status_code=204)
async def delete_gassi_zeit(gz_id: int, user=Depends(get_current_user)):
with db() as conn:
gz = conn.execute(
"SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,)
).fetchone()
if not gz:
raise HTTPException(404, "Gassi-Zeit nicht gefunden.")
if gz["user_id"] != user["id"]:
raise HTTPException(403, "Nicht deine Gassi-Zeit.")
conn.execute("DELETE FROM gassi_zeiten WHERE id=?", (gz_id,))