Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe

This commit is contained in:
rene 2026-04-17 23:53:50 +02:00
parent 10d30bf565
commit 89d87030a2
18 changed files with 930 additions and 74 deletions

View file

@ -25,6 +25,9 @@ def _haversine(lat1, lon1, lat2, lon2):
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
class RsvpCreate(BaseModel):
status: str = 'going' # 'going' | 'maybe'
class EventCreate(BaseModel):
titel: str
datum: str # YYYY-MM-DD
@ -65,7 +68,8 @@ async def list_events(
q = """
SELECT e.*,
CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name,
e.quelle
e.quelle,
(SELECT COUNT(*) FROM event_rsvp r WHERE r.event_id = e.id AND r.status = 'going') AS rsvp_count
FROM events e
LEFT JOIN users u ON u.id = e.user_id AND e.user_id != 0
WHERE e.status = 'aktiv'
@ -101,7 +105,8 @@ async def create_event(data: EventCreate, user=Depends(get_current_user)):
data.lat, data.lon, data.ort_name,
data.typ, data.beschreibung, data.link))
row = conn.execute(
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle "
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle, "
"0 AS rsvp_count "
"FROM events e LEFT JOIN users u ON u.id = e.user_id AND e.user_id != 0 WHERE e.id = ?",
(cur.lastrowid,)
).fetchone()
@ -115,7 +120,8 @@ async def create_event(data: EventCreate, user=Depends(get_current_user)):
async def get_event(event_id: int):
with db() as conn:
row = conn.execute(
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle "
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle, "
"(SELECT COUNT(*) FROM event_rsvp r WHERE r.event_id = e.id AND r.status = 'going') AS rsvp_count "
"FROM events e LEFT JOIN users u ON u.id = e.user_id AND e.user_id != 0 WHERE e.id = ?",
(event_id,)
).fetchone()
@ -142,7 +148,8 @@ async def update_event(event_id: int, data: EventUpdate, user=Depends(get_curren
cols = ', '.join(f"{k} = ?" for k in updates)
conn.execute(f"UPDATE events SET {cols} WHERE id = ?", [*updates.values(), event_id])
row = conn.execute(
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle "
"SELECT e.*, CASE WHEN e.user_id = 0 THEN 'VDH' ELSE u.name END AS veranstalter_name, e.quelle, "
"(SELECT COUNT(*) FROM event_rsvp r WHERE r.event_id = e.id AND r.status = 'going') AS rsvp_count "
"FROM events e LEFT JOIN users u ON u.id = e.user_id AND e.user_id != 0 WHERE e.id = ?",
(event_id,)
).fetchone()
@ -161,3 +168,53 @@ async def delete_event(event_id: int, user=Depends(get_current_user)):
if ev['user_id'] == 0 or ev['user_id'] != user['id']:
raise HTTPException(403, "Nur der Veranstalter kann das Event löschen.")
conn.execute("UPDATE events SET status = 'geloescht' WHERE id = ?", (event_id,))
# ------------------------------------------------------------------
# POST /api/events/{id}/rsvp
# ------------------------------------------------------------------
@router.post("/{event_id}/rsvp", status_code=201)
async def rsvp_event(event_id: int, data: RsvpCreate, user=Depends(get_current_user)):
if data.status not in ('going', 'maybe'):
raise HTTPException(400, "Status muss 'going' oder 'maybe' sein.")
with db() as conn:
ev = conn.execute("SELECT id FROM events WHERE id = ? AND status = 'aktiv'", (event_id,)).fetchone()
if not ev:
raise HTTPException(404, "Event nicht gefunden.")
conn.execute(
"INSERT OR REPLACE INTO event_rsvp (event_id, user_id, status) VALUES (?, ?, ?)",
(event_id, user['id'], data.status)
)
count = conn.execute(
"SELECT COUNT(*) FROM event_rsvp WHERE event_id = ? AND status = 'going'", (event_id,)
).fetchone()[0]
return {"event_id": event_id, "status": data.status, "rsvp_count": count}
# ------------------------------------------------------------------
# DELETE /api/events/{id}/rsvp
# ------------------------------------------------------------------
@router.delete("/{event_id}/rsvp", status_code=204)
async def cancel_rsvp(event_id: int, user=Depends(get_current_user)):
with db() as conn:
conn.execute(
"DELETE FROM event_rsvp WHERE event_id = ? AND user_id = ?",
(event_id, user['id'])
)
# ------------------------------------------------------------------
# GET /api/events/{id}/rsvp
# ------------------------------------------------------------------
@router.get("/{event_id}/rsvp")
async def list_rsvp(event_id: int):
with db() as conn:
rows = conn.execute(
"SELECT r.user_id, u.name, r.status "
"FROM event_rsvp r "
"JOIN users u ON u.id = r.user_id "
"WHERE r.event_id = ? "
"ORDER BY r.created_at ASC",
(event_id,)
).fetchall()
return [dict(r) for r in rows]

View file

@ -1,11 +1,14 @@
"""BAN YARO — Forum (Sprint 11)"""
import os, uuid, json
import os, uuid, json, logging
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user, get_current_user_optional
from routes.push import send_push_to_user
logger = logging.getLogger(__name__)
router = APIRouter()
@ -295,9 +298,30 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current
WHERE p.id = ?""",
(cur.lastrowid,)
).fetchone()
# Thread-Owner ermitteln für Push-Notification
owner_row = conn.execute(
"SELECT user_id FROM forum_threads WHERE id = ?", (thread_id,)
).fetchone()
owner_id = owner_row['user_id'] if owner_row else None
pd = dict(row)
pd['foto_urls'] = []
pd['user_liked'] = False
# Push-Notification an Thread-Owner (nicht an sich selbst)
if owner_id and owner_id != user['id']:
try:
commenter_name = pd.get('autor_name') or 'Jemand'
send_push_to_user(owner_id, {
"type": "forum_reply",
"title": "Neue Antwort auf deinen Beitrag",
"body": f"{commenter_name} hat auf deinen Beitrag geantwortet",
"tag": f"forum-{thread_id}",
"data": {"page": "forum", "id": thread_id},
})
except Exception:
logger.exception("Push-Notification für Forum-Reply fehlgeschlagen (nicht kritisch)")
return pd

View file

@ -243,6 +243,22 @@ async def upload_dokument(
return {"datei_url": datei_url, "datei_typ": datei_typ}
# ------------------------------------------------------------------
# GET /api/dogs/{dog_id}/health/gewicht — Gewichtsverlauf
# ------------------------------------------------------------------
@router.get("/{dog_id}/health/gewicht")
async def list_gewicht(dog_id: int, user=Depends(get_current_user)):
with db() as conn:
_check_dog_owner(conn, dog_id, user["id"])
rows = conn.execute(
"""SELECT datum, wert AS gewicht FROM health
WHERE dog_id=? AND typ='gewicht' AND wert IS NOT NULL
ORDER BY datum ASC""",
(dog_id,)
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# POST /api/dogs/{dog_id}/health/symptom-check — KI-Symptomprüfung
# ------------------------------------------------------------------