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:
parent
d6206d378e
commit
aa4849d947
10 changed files with 1322 additions and 22 deletions
190
backend/routes/gassi_zeiten.py
Normal file
190
backend/routes/gassi_zeiten.py
Normal 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,))
|
||||
Loading…
Add table
Add a link
Reference in a new issue