Files
Vorrania/backend/app/routers/locations.py
Scarriffle b49546b297 Lagerorte umhängen (Backend + Web)
Bestehende Lagerorte lassen sich jetzt nachträglich einem anderen Elternort
zuordnen oder auf die oberste Ebene holen.
- Backend: LocationUpdate um parent_id (Name jetzt optional); update_location
  haengt um, mit Schutz gegen Ringe (weder auf sich selbst noch auf einen
  eigenen Unterort) und Existenzpruefung des Ziels. 6 Tests.
- Web: Lagerorte-Seite bekommt je Ort Bearbeiten (Name + Elternort ueber den
  CategorySelect-Baum, eigene Unterorte ausgeschlossen); api.updateLocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 15:58:54 +02:00

105 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Location, User
from ..schemas import LocationCreate, LocationOut, LocationUpdate
router = APIRouter(prefix="/locations", tags=["locations"])
def _descendant_ids(db: Session, location_id: int) -> set[int]:
"""Alle Unterorte (rekursiv) als Ziel beim Umhängen ausgeschlossen, sonst
entstünde ein Ring."""
result: set[int] = set()
stack = [location_id]
while stack:
cur = stack.pop()
for kid in db.query(Location).filter(Location.parent_id == cur).all():
if kid.id not in result:
result.add(kid.id)
stack.append(kid.id)
return result
@router.get("", response_model=list[LocationOut])
def list_locations(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[Location]:
return db.query(Location).order_by(Location.name).all()
@router.post("", response_model=LocationOut, status_code=status.HTTP_201_CREATED)
def create_location(
payload: LocationCreate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Location:
loc = Location(name=payload.name, parent_id=payload.parent_id)
db.add(loc)
db.commit()
db.refresh(loc)
return loc
@router.patch("/{location_id}", response_model=LocationOut)
def update_location(
location_id: int,
payload: LocationUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Location:
"""Umbenennen und/oder umhängen. Chargen haengen an der ID, behalten ihren
Lagerort also."""
loc = db.get(Location, location_id)
if loc is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
data = payload.model_dump(exclude_unset=True)
if "name" in data and data["name"]:
name = data["name"].strip()
doppelt = (
db.query(Location)
.filter(func.lower(Location.name) == name.lower(), Location.id != location_id)
.first()
)
if doppelt is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "Diesen Lagerort gibt es bereits")
loc.name = name
# parent_id nur anfassen, wenn ausdrücklich mitgeschickt (None = oberste Ebene).
if "parent_id" in data:
neu = data["parent_id"]
if neu is not None:
if db.get(Location, neu) is None:
raise HTTPException(
status.HTTP_404_NOT_FOUND, "Übergeordneter Lagerort nicht gefunden"
)
if neu == location_id or neu in _descendant_ids(db, location_id):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Ein Lagerort kann nicht sich selbst oder einem seiner Unterorte "
"untergeordnet werden.",
)
loc.parent_id = neu
db.commit()
db.refresh(loc)
return loc
@router.delete("/{location_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_location(
location_id: int,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> None:
loc = db.get(Location, location_id)
if loc is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
db.delete(loc)
db.commit()