"""Kleidungsstücke: durchsuchen, verwalten, reservieren. Der Schreibschutz (Anmeldung, Rate-Limit) kommt in Phase 3 dazu; die Berechtigungslogik der Reservierung steckt aber schon hier, weil sie den Datenfluss bestimmt. """ from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app import crud from app.database import get_db from app.models import Geschlecht, Item, Saison, Status from app.schemas import ( ItemAendern, ItemAnlegen, ItemAus, ItemListeAus, ReservierenAus, ReservierenRein, ) router = APIRouter(prefix="/api/v1/items", tags=["Kleidungsstücke"]) def _item_oder_404(db: Session, item_id: int) -> Item: item = crud.item_holen(db, item_id) if item is None: raise HTTPException(status_code=404, detail="Kleidungsstück nicht gefunden.") return item def _als_antwort(item: Item) -> ItemAus: """Baut die Antwort und ergänzt den abgeleiteten Titel. display_title entsteht hier und nicht beim Speichern: sonst bliebe ein automatisch gesetzter Titel stehen, wenn später die Grösse korrigiert wird. """ return ItemAus.model_validate({**item.__dict__, "display_title": item.anzeige_titel, "kategorie": item.kategorie, "bilder": item.bilder}) @router.get("", response_model=ItemListeAus) def items_auflisten( db: Session = Depends(get_db), size: str | None = None, category_id: int | None = None, item_status: Status | None = Query(default=None, alias="status"), gender: Geschlecht | None = None, season: Saison | None = None, limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), ): treffer, gesamt = crud.items_suchen( db, size=size, category_id=category_id, status=item_status.value if item_status else None, gender=gender.value if gender else None, season=season.value if season else None, limit=limit, offset=offset, ) return ItemListeAus( items=[_als_antwort(i) for i in treffer], total=gesamt, limit=limit, offset=offset, ) @router.get("/{item_id}", response_model=ItemAus) def item_anzeigen(item_id: int, db: Session = Depends(get_db)): return _als_antwort(_item_oder_404(db, item_id)) @router.post("", response_model=ItemAus, status_code=status.HTTP_201_CREATED) def item_anlegen(daten: ItemAnlegen, db: Session = Depends(get_db)): if daten.category_id is not None and crud.kategorie_holen(db, daten.category_id) is None: raise HTTPException(status_code=422, detail="Kategorie gibt es nicht.") try: item = crud.item_anlegen(db, daten) except IntegrityError as e: db.rollback() # Greift z.B., wenn ein Eintrag ohne Grösse oder Kategorie # veröffentlicht werden soll (CHECK in models.py). raise HTTPException( status_code=422, detail="Ein sichtbarer Eintrag braucht Grösse und Kategorie.", ) from e return _als_antwort(item) @router.patch("/{item_id}", response_model=ItemAus) def item_aendern(item_id: int, daten: ItemAendern, db: Session = Depends(get_db)): item = _item_oder_404(db, item_id) if daten.category_id is not None and crud.kategorie_holen(db, daten.category_id) is None: raise HTTPException(status_code=422, detail="Kategorie gibt es nicht.") try: geaendert = crud.item_aendern(db, item, daten) except IntegrityError as e: db.rollback() raise HTTPException( status_code=422, detail="Ein sichtbarer Eintrag braucht Grösse und Kategorie.", ) from e return _als_antwort(geaendert) @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) def item_loeschen(item_id: int, db: Session = Depends(get_db)): item = _item_oder_404(db, item_id) # Die Dateien selbst löscht Phase 2 (images.py); hier fallen nur die # Namen an, damit die Aufräumlogik an einer Stelle liegt. crud.item_loeschen(db, item) return Response(status_code=status.HTTP_204_NO_CONTENT) # --------------------------------------------------------- Reservierung --- @router.post("/{item_id}/reserve", response_model=ReservierenAus) def item_reservieren(item_id: int, daten: ReservierenRein, db: Session = Depends(get_db)): item = _item_oder_404(db, item_id) if item.status == Status.draft.value: raise HTTPException(status_code=404, detail="Kleidungsstück nicht gefunden.") if item.status != Status.available.value: raise HTTPException( status_code=409, detail="Dieses Kleidungsstück ist nicht mehr verfügbar." ) token = crud.reservieren(db, item, daten.reserved_by) return ReservierenAus(item=_als_antwort(item), reservation_token=token) @router.post("/{item_id}/release", response_model=ItemAus) def item_freigeben( item_id: int, token: str | None = Query(default=None), db: Session = Depends(get_db), ist_betreiber: bool = False, # wird in Phase 3 durch die Anmeldung ersetzt ): """Hebt eine Reservierung auf. Erlaubt für den Reservierenden (mit Token) und den Betreiber. Ohne Token wäre der Endpunkt offen - dann könnte jeder Besucher fremde Reservierungen löschen, und weil reserved_by mitgeht, bliebe nicht einmal nachvollziehbar, dass jemand reserviert hatte. """ item = _item_oder_404(db, item_id) if item.status != Status.reserved.value: raise HTTPException(status_code=409, detail="Dieses Kleidungsstück ist nicht reserviert.") if not ist_betreiber and not crud.token_gueltig(item, token): raise HTTPException( status_code=403, detail="Zum Aufheben braucht es den Link aus der Reservierung.", ) return _als_antwort(crud.freigeben(db, item)) @router.post("/{item_id}/mark-given", response_model=ItemAus) def item_abgegeben(item_id: int, db: Session = Depends(get_db)): item = _item_oder_404(db, item_id) return _als_antwort(crud.als_abgegeben_markieren(db, item))