Feature: Gesundheit Multi-Media — health_media Tabelle + Multi-Upload UI
- Neue Tabelle health_media (Migration in database.py) analog zu diary_media
- GET-Endpoints geben media_items:[{id,url,media_type}] zurück (datei_url bleibt für Rückwärtskompatibilität)
- POST /health/{id}/media und DELETE /health/{id}/media/{media_id} Endpoints
- Multi-Upload-Bereich im Formular: Thumbnails für Bilder, PDF-Icon, X-Button zum Entfernen
- Galerie in Detailansicht: Bilder klickbar/zoombar, PDFs als Link
- CSS-Klassen health-media-grid/thumb/gallery in components.css
- SW by-v213, APP_VER 187
This commit is contained in:
parent
fa0fcbf8c9
commit
aa70a838f2
5 changed files with 347 additions and 42 deletions
|
|
@ -11,6 +11,8 @@ from auth import get_current_user
|
|||
router = APIRouter()
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
||||
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
|
||||
|
||||
# Erlaubte Typen
|
||||
TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", "laeufigkeit"}
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ class HealthUpdate(BaseModel):
|
|||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktion: Zugriffscheck Dog → User
|
||||
# Hilfsfunktionen
|
||||
# ------------------------------------------------------------------
|
||||
def _check_dog_owner(conn, dog_id: int, user_id: int):
|
||||
dog = conn.execute(
|
||||
|
|
@ -81,6 +83,30 @@ def _check_dog_owner(conn, dog_id: int, user_id: int):
|
|||
return dog
|
||||
|
||||
|
||||
def _fetch_media_items(conn, entry_ids: list) -> dict:
|
||||
"""Gibt {health_id: [{id, url, media_type}, ...]} zurück."""
|
||||
if not entry_ids:
|
||||
return {}
|
||||
ph = ",".join("?" * len(entry_ids))
|
||||
rows = conn.execute(
|
||||
f"SELECT id, health_id, url, media_type FROM health_media "
|
||||
f"WHERE health_id IN ({ph}) ORDER BY health_id, sort_order",
|
||||
entry_ids
|
||||
).fetchall()
|
||||
result = {}
|
||||
for r in rows:
|
||||
result.setdefault(r["health_id"], []).append({
|
||||
"id": r["id"], "url": r["url"], "media_type": r["media_type"]
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def _entry_with_media(row, media_map: dict) -> dict:
|
||||
e = dict(row)
|
||||
e["media_items"] = media_map.get(e["id"], [])
|
||||
return e
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/dogs/{dog_id}/health
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -99,7 +125,9 @@ async def list_health(dog_id: int, typ: Optional[str] = None,
|
|||
"SELECT * FROM health WHERE dog_id=? ORDER BY datum DESC",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
ids = [r["id"] for r in rows]
|
||||
media_map = _fetch_media_items(conn, ids)
|
||||
return [_entry_with_media(r, media_map) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -130,7 +158,8 @@ async def create_health(dog_id: int, data: HealthCreate,
|
|||
"SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1",
|
||||
(dog_id,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
media_map = _fetch_media_items(conn, [row["id"]])
|
||||
return _entry_with_media(row, media_map)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -155,7 +184,8 @@ async def update_health(dog_id: int, entry_id: int, data: HealthUpdate,
|
|||
values = list(updates.values()) + [entry_id]
|
||||
conn.execute(f"UPDATE health SET {set_clause} WHERE id=?", values)
|
||||
row = conn.execute("SELECT * FROM health WHERE id=?", (entry_id,)).fetchone()
|
||||
return dict(row)
|
||||
media_map = _fetch_media_items(conn, [entry_id])
|
||||
return _entry_with_media(row, media_map)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -243,6 +273,77 @@ async def upload_dokument(
|
|||
return {"datei_url": datei_url, "datei_typ": datei_typ}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/dogs/{dog_id}/health/{entry_id}/media — Datei-Upload (Multi)
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/{dog_id}/health/{entry_id}/media")
|
||||
async def upload_media(
|
||||
dog_id: int,
|
||||
entry_id: int,
|
||||
file: UploadFile = File(...),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
with db() as conn:
|
||||
_check_dog_owner(conn, dog_id, user["id"])
|
||||
entry = conn.execute(
|
||||
"SELECT id FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
||||
).fetchone()
|
||||
if not entry:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden.")
|
||||
|
||||
ext = os.path.splitext(file.filename or "")[1].lower() or ".jpg"
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(400, "Nur JPG, PNG, WebP und PDF erlaubt.")
|
||||
|
||||
filename = f"health_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
path = os.path.join(MEDIA_DIR, "health", filename)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(await file.read())
|
||||
|
||||
media_url = f"/media/health/{filename}"
|
||||
media_type = "pdf" if ext == ".pdf" else "image"
|
||||
|
||||
with db() as conn:
|
||||
max_order = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order), -1) FROM health_media WHERE health_id=?",
|
||||
(entry_id,)
|
||||
).fetchone()[0]
|
||||
conn.execute(
|
||||
"INSERT INTO health_media (health_id, url, media_type, sort_order) VALUES (?,?,?,?)",
|
||||
(entry_id, media_url, media_type, max_order + 1)
|
||||
)
|
||||
new_id = conn.execute(
|
||||
"SELECT id FROM health_media WHERE health_id=? ORDER BY id DESC LIMIT 1",
|
||||
(entry_id,)
|
||||
).fetchone()["id"]
|
||||
|
||||
return {"id": new_id, "url": media_url, "media_type": media_type, "sort_order": max_order + 1}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/dogs/{dog_id}/health/{entry_id}/media/{media_id}
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/{dog_id}/health/{entry_id}/media/{media_id}", status_code=204)
|
||||
async def delete_media_item(dog_id: int, entry_id: int, media_id: int,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_check_dog_owner(conn, dog_id, user["id"])
|
||||
row = conn.execute(
|
||||
"SELECT hm.id, hm.url FROM health_media hm "
|
||||
"JOIN health h ON h.id = hm.health_id "
|
||||
"WHERE hm.id=? AND hm.health_id=? AND h.dog_id=?",
|
||||
(media_id, entry_id, dog_id)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Medium nicht gefunden.")
|
||||
file_path = os.path.join(MEDIA_DIR, row["url"].lstrip("/media/"))
|
||||
try: os.remove(file_path)
|
||||
except OSError: pass
|
||||
conn.execute("DELETE FROM health_media WHERE id=?", (media_id,))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/dogs/{dog_id}/health/gewicht — Gewichtsverlauf
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue