Files
Vorrania/backend/app/routers/field_definitions.py
Scarriffle 5b952524d7 Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln
Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:33:56 +02:00

144 lines
5.1 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.
"""Verwaltung der selbst definierten Felder je Kategorie."""
import json
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Category, FieldDefinition, FieldType, ProductFieldValue, User
from ..schemas import FieldDefinitionCreate, FieldDefinitionOut, FieldDefinitionUpdate
from ..services.fields import options_list, slugify
router = APIRouter(prefix="/field-definitions", tags=["field-definitions"])
def to_out(fd: FieldDefinition, inherited: bool = False) -> FieldDefinitionOut:
return FieldDefinitionOut(
id=fd.id,
category_id=fd.category_id,
label=fd.label,
key=fd.key,
field_type=FieldType(fd.field_type),
unit=fd.unit,
options=options_list(fd),
required=fd.required,
position=fd.position,
is_builtin=fd.is_builtin,
inherited=inherited,
)
def _unique_key(db: Session, category_id: int, label: str, exclude_id: int | None = None) -> str:
"""Eindeutigen Schlüssel je Kategorie erzeugen (kapazitaet, kapazitaet_2, …)."""
basis = slugify(label)
kandidat = basis
n = 1
while True:
query = db.query(FieldDefinition).filter(
FieldDefinition.category_id == category_id, FieldDefinition.key == kandidat
)
if exclude_id is not None:
query = query.filter(FieldDefinition.id != exclude_id)
if query.first() is None:
return kandidat
n += 1
kandidat = f"{basis}_{n}"
def _options_json(field_type: FieldType, options: list[str] | None) -> str | None:
if field_type != FieldType.select:
return None
return json.dumps([o.strip() for o in (options or []) if o.strip()])
@router.get("", response_model=list[FieldDefinitionOut])
def list_field_definitions(
category_id: int | None = None,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[FieldDefinitionOut]:
"""Felder auflisten ohne category_id alle, sonst nur die dieser Kategorie."""
query = db.query(FieldDefinition)
if category_id is not None:
query = query.filter(FieldDefinition.category_id == category_id)
rows = query.order_by(
FieldDefinition.category_id, FieldDefinition.position, FieldDefinition.id
).all()
return [to_out(fd) for fd in rows]
@router.post("", response_model=FieldDefinitionOut, status_code=status.HTTP_201_CREATED)
def create_field_definition(
payload: FieldDefinitionCreate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> FieldDefinitionOut:
if db.get(Category, payload.category_id) is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Kategorie nicht gefunden")
fd = FieldDefinition(
category_id=payload.category_id,
label=payload.label.strip(),
key=_unique_key(db, payload.category_id, payload.label),
field_type=payload.field_type.value,
unit=(payload.unit or None),
options=_options_json(payload.field_type, payload.options),
required=bool(payload.required),
position=payload.position,
)
db.add(fd)
db.commit()
db.refresh(fd)
return to_out(fd)
@router.patch("/{field_id}", response_model=FieldDefinitionOut)
def update_field_definition(
field_id: int,
payload: FieldDefinitionUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> FieldDefinitionOut:
fd = db.get(FieldDefinition, field_id)
if fd is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Feld nicht gefunden")
data = payload.model_dump(exclude_unset=True)
if "label" in data and data["label"]:
fd.label = data["label"].strip()
fd.key = _unique_key(db, fd.category_id, fd.label, exclude_id=fd.id)
if "field_type" in data and data["field_type"] is not None:
fd.field_type = data["field_type"].value
if "unit" in data:
fd.unit = data["unit"] or None
if "required" in data and data["required"] is not None:
fd.required = bool(data["required"])
if "position" in data and data["position"] is not None:
fd.position = data["position"]
# Optionen immer passend zum (ggf. neuen) Typ ablegen.
if "options" in data or "field_type" in data:
neuer_typ = FieldType(fd.field_type)
optionen = data["options"] if "options" in data else options_list(fd)
fd.options = _options_json(neuer_typ, optionen)
db.commit()
db.refresh(fd)
return to_out(fd)
@router.delete("/{field_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_field_definition(
field_id: int,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> None:
"""Löscht das Feld samt aller dazu erfassten Werte (ON DELETE CASCADE)."""
fd = db.get(FieldDefinition, field_id)
if fd is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Feld nicht gefunden")
db.query(ProductFieldValue).filter(
ProductFieldValue.field_definition_id == field_id
).delete()
db.delete(fd)
db.commit()