Sprint 1: Tagebuch — Backend-Routes + Frontend-Modul
diary.py: CRUD, KI-Auto-Tags, Medien-Upload, Ownership-Check diary.js: Timeline (nach Monat gruppiert), Erstellen/Bearbeiten/Löschen, Foto-Upload, Meilenstein-Hervorhebung, Tags, Detail-Modal components.css: Diary-Card-Styles (Timeline, Milestone, Foto, Tags)
This commit is contained in:
parent
70f5eedafa
commit
44b1451966
3 changed files with 710 additions and 3 deletions
|
|
@ -1,3 +1,162 @@
|
|||
"""BAN YARO — Tagebuch Routes (Stub, wird in Sprint 1 ausgebaut)"""
|
||||
from fastapi import APIRouter
|
||||
router = APIRouter()
|
||||
"""BAN YARO — Tagebuch Routes"""
|
||||
|
||||
import os, uuid, json
|
||||
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
|
||||
import ki as KI
|
||||
|
||||
router = APIRouter()
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
||||
|
||||
class DiaryCreate(BaseModel):
|
||||
datum: Optional[str] = None # ISO date, default heute
|
||||
typ: str = "eintrag"
|
||||
titel: Optional[str] = None
|
||||
text: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
gps_lat: Optional[float] = None
|
||||
gps_lon: Optional[float] = None
|
||||
is_milestone: bool = False
|
||||
|
||||
class DiaryUpdate(BaseModel):
|
||||
titel: Optional[str] = None
|
||||
text: Optional[str] = None
|
||||
tags: Optional[list] = None
|
||||
is_milestone: Optional[bool] = None
|
||||
|
||||
|
||||
def _own_dog(dog_id: int, user_id: int, conn):
|
||||
dog = conn.execute(
|
||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
return dog
|
||||
|
||||
|
||||
@router.get("/{dog_id}/diary")
|
||||
async def list_diary(dog_id: int, limit: int = 20, offset: int = 0,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
rows = conn.execute(
|
||||
"""SELECT * FROM diary WHERE dog_id=?
|
||||
ORDER BY datum DESC, created_at DESC
|
||||
LIMIT ? OFFSET ?""",
|
||||
(dog_id, limit, offset)
|
||||
).fetchall()
|
||||
entries = []
|
||||
for r in rows:
|
||||
e = dict(r)
|
||||
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
||||
entries.append(e)
|
||||
return entries
|
||||
|
||||
|
||||
@router.post("/{dog_id}/diary", status_code=201)
|
||||
async def create_diary(dog_id: int, data: DiaryCreate,
|
||||
user=Depends(get_current_user)):
|
||||
tags = data.tags or []
|
||||
|
||||
# KI: Auto-Tags wenn Text vorhanden (lokal, kostenlos)
|
||||
if data.text and len(data.text) > 10:
|
||||
try:
|
||||
ai_tags = await KI.diary_tags(data.text)
|
||||
tags = list(set(tags + ai_tags))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
conn.execute(
|
||||
"""INSERT INTO diary
|
||||
(dog_id, datum, typ, titel, text, tags, gps_lat, gps_lon, is_milestone)
|
||||
VALUES (?,
|
||||
COALESCE(?, date('now')),
|
||||
?,?,?,?,?,?,?)""",
|
||||
(dog_id, data.datum, data.typ, data.titel, data.text,
|
||||
json.dumps(tags), data.gps_lat, data.gps_lon, int(data.is_milestone))
|
||||
)
|
||||
entry = conn.execute(
|
||||
"SELECT * FROM diary WHERE dog_id=? ORDER BY id DESC LIMIT 1",
|
||||
(dog_id,)
|
||||
).fetchone()
|
||||
|
||||
e = dict(entry)
|
||||
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
||||
return e
|
||||
|
||||
|
||||
@router.get("/{dog_id}/diary/{entry_id}")
|
||||
async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden.")
|
||||
e = dict(row)
|
||||
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
||||
return e
|
||||
|
||||
|
||||
@router.patch("/{dog_id}/diary/{entry_id}")
|
||||
async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
fields = {k: v for k, v in data.model_dump().items() if v is not None}
|
||||
if "tags" in fields:
|
||||
fields["tags"] = json.dumps(fields["tags"])
|
||||
if not fields:
|
||||
raise HTTPException(400, "Keine Änderungen.")
|
||||
set_clause = ", ".join(f"{k}=?" for k in fields)
|
||||
conn.execute(
|
||||
f"UPDATE diary SET {set_clause} WHERE id=? AND dog_id=?",
|
||||
list(fields.values()) + [entry_id, dog_id]
|
||||
)
|
||||
row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone()
|
||||
e = dict(row)
|
||||
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
||||
return e
|
||||
|
||||
|
||||
@router.delete("/{dog_id}/diary/{entry_id}", status_code=204)
|
||||
async def delete_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
conn.execute(
|
||||
"DELETE FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{dog_id}/diary/{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:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
entry = conn.execute(
|
||||
"SELECT id FROM diary 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] or ".jpg"
|
||||
filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
|
||||
path = os.path.join(MEDIA_DIR, "diary", 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/diary/{filename}"
|
||||
with db() as conn:
|
||||
conn.execute("UPDATE diary SET media_url=? WHERE id=?", (media_url, entry_id))
|
||||
|
||||
return {"media_url": media_url}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue