Sprint 2: Giftköder-Alarm (poison.py + poison.js)
Backend: vollständiges CRUD (list/report/confirm/resolve/photo), Haversine-Radius-Filter, Auto-Expiry 7 Tage, Foto-Upload. Frontend: Leaflet-Karte + Meldungsliste + GPS-Formular. main.py: /media StaticFiles-Mount für Foto-Serving (auch Diary). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
44b1451966
commit
cc36ead720
3 changed files with 780 additions and 3 deletions
|
|
@ -1,3 +1,161 @@
|
|||
"""BAN YARO — poison Routes (Stub, wird ausgebaut)"""
|
||||
from fastapi import APIRouter
|
||||
router = APIRouter()
|
||||
"""BAN YARO — Giftköder-Alarm Routes"""
|
||||
|
||||
import os, uuid, math
|
||||
from datetime import datetime, timedelta
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Haversine-Distanz in Metern
|
||||
# ------------------------------------------------------------------
|
||||
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
R = 6_371_000
|
||||
p1 = math.radians(lat1)
|
||||
p2 = 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))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schemas
|
||||
# ------------------------------------------------------------------
|
||||
class PoisonCreate(BaseModel):
|
||||
lat: float
|
||||
lon: float
|
||||
beschreibung: Optional[str] = None
|
||||
typ: str = "unbekannt"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/poison — aktive Meldungen im Umkreis (kein Login nötig)
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("")
|
||||
async def list_poison(lat: float, lon: float, radius: int = 5000):
|
||||
now = datetime.utcnow().isoformat()
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT p.*, u.name AS melder_name
|
||||
FROM poison p
|
||||
LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE p.geloest = 0
|
||||
AND p.expires_at > ?
|
||||
ORDER BY p.created_at DESC""",
|
||||
(now,)
|
||||
).fetchall()
|
||||
|
||||
results = []
|
||||
for r in rows:
|
||||
entry = dict(r)
|
||||
dist = _haversine(lat, lon, entry["lat"], entry["lon"])
|
||||
if dist <= radius:
|
||||
entry["distanz_m"] = round(dist)
|
||||
results.append(entry)
|
||||
|
||||
results.sort(key=lambda x: x["distanz_m"])
|
||||
return results
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/poison — neue Meldung (Login erforderlich)
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("", status_code=201)
|
||||
async def report_poison(data: PoisonCreate, user=Depends(get_current_user)):
|
||||
expires_at = (datetime.utcnow() + timedelta(days=7)).isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO poison (user_id, lat, lon, beschreibung, typ, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(user["id"], data.lat, data.lon, data.beschreibung, data.typ, expires_at)
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM poison WHERE user_id=? ORDER BY id DESC LIMIT 1",
|
||||
(user["id"],)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/poison/{id}/confirm — Community-Bestätigung
|
||||
# Verlängert die Laufzeit um 7 weitere Tage ab jetzt
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{poison_id}/confirm")
|
||||
async def confirm_poison(poison_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
entry = conn.execute(
|
||||
"SELECT * FROM poison WHERE id=?", (poison_id,)
|
||||
).fetchone()
|
||||
if not entry:
|
||||
raise HTTPException(404, "Meldung nicht gefunden.")
|
||||
if dict(entry)["user_id"] == user["id"]:
|
||||
raise HTTPException(400, "Eigene Meldung kann nicht bestätigt werden.")
|
||||
|
||||
new_expires = (datetime.utcnow() + timedelta(days=7)).isoformat()
|
||||
conn.execute(
|
||||
"""UPDATE poison
|
||||
SET bestaetigt=1, bestaetigt_von=?, expires_at=?
|
||||
WHERE id=?""",
|
||||
(user["id"], new_expires, poison_id)
|
||||
)
|
||||
row = conn.execute("SELECT * FROM poison WHERE id=?", (poison_id,)).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/poison/{id}/resolve — als erledigt markieren
|
||||
# Nur der Melder selbst oder ein Admin
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{poison_id}/resolve")
|
||||
async def resolve_poison(poison_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
entry = conn.execute(
|
||||
"SELECT * FROM poison WHERE id=?", (poison_id,)
|
||||
).fetchone()
|
||||
if not entry:
|
||||
raise HTTPException(404, "Meldung nicht gefunden.")
|
||||
e = dict(entry)
|
||||
if e["user_id"] != user["id"] and user.get("rolle") != "admin":
|
||||
raise HTTPException(403, "Keine Berechtigung.")
|
||||
conn.execute("UPDATE poison SET geloest=1 WHERE id=?", (poison_id,))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/poison/{id}/photo — Foto nachträglich anhängen
|
||||
# Nur vom Melder selbst
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{poison_id}/photo")
|
||||
async def upload_photo(
|
||||
poison_id: int,
|
||||
file: UploadFile = File(...),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
with db() as conn:
|
||||
entry = conn.execute(
|
||||
"SELECT id FROM poison WHERE id=? AND user_id=?",
|
||||
(poison_id, user["id"])
|
||||
).fetchone()
|
||||
if not entry:
|
||||
raise HTTPException(404, "Meldung nicht gefunden oder keine Berechtigung.")
|
||||
|
||||
ext = os.path.splitext(file.filename or "")[1] or ".jpg"
|
||||
filename = f"poison_{poison_id}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
path = os.path.join(MEDIA_DIR, "poison", filename)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(await file.read())
|
||||
|
||||
foto_url = f"/media/poison/{filename}"
|
||||
with db() as conn:
|
||||
conn.execute("UPDATE poison SET foto_url=? WHERE id=?", (foto_url, poison_id))
|
||||
|
||||
return {"foto_url": foto_url}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue