4 parallele Review-Agenten (Reuse/Simplification/Efficiency/Altitude) gegen den Diff dieser Session (c7dd47f...HEAD) laufen lassen, echte Funde angewendet: - lager.py: 5x wiederholtes "db.get(...) or 404 raisen" durch _get_or_404()- Helper ersetzt - akte.py: zwei Queries für Geräte-Instanzen (erst Positions-IDs, dann Geräte) zu einer Query mit Subquery zusammengefasst - akte.py: manuelle Feld-für-Feld-Rekonstruktion von ObjektRead/HistorieRead durch model_validate()+model_copy() ersetzt (HistorieRead.benutzer_name bekommt dafür einen Default, harmlos für den bestehenden Endpunkt) - vorlagen.py: doppelte "Positionen löschen + flush"-Logik (Vorlage-Löschen und Positionen-Ersetzen) in loesche_alle_positionen() zusammengeführt Bewusst nicht angewendet: - Zentrale Session-Rollback-Vereinheitlichung für die drei Pre-Check-Stellen (objekte.py Selbstbezug, geraet_instanz.py SN-Duplikat, lager.py Eltern- Selbstbezug) - der Altitude-Review schlug das vor, aber get_db() rollt in Produktion bei jeder Exception bereits korrekt zurück (jede Anfrage hat eine eigene Session); das PendingRollbackError-Problem trat nur in der geteilten Test-Session auf und wurde bereits gezielt per Pre-Check vermieden - eine zusätzliche zentrale Rollback-Schicht würde nichts mehr reparieren, was nicht schon repariert ist. - Postgres-Cluster-Encoding (template1) auf beiden Hosts fixen, nicht nur in CI/für die eine migrierte DB - echter Infra-Eingriff, braucht Rücksprache. - LagerSection.tsx "inkonsistente" Formular-Resets - beim genaueren Hinsehen gewolltes UX (Ort/Typ bleiben ausgewählt für mehrere Anlagen hintereinander). 140 Tests weiterhin grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
128 lines
5.2 KiB
Python
128 lines
5.2 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_user, require_roles
|
|
from app.db.session import get_db
|
|
from app.models.auth import RolleTyp
|
|
from app.models.objekt import Objekt
|
|
from app.models.vorlage import Beladungsvorlage
|
|
from app.schemas.vorlage import BeladungsvorlageCreate, BeladungsvorlageRead, VorlagenAenderung
|
|
from app.services.vorlagen import (
|
|
aktualisiere_positionen,
|
|
erstelle_vorlage,
|
|
hole_positionen,
|
|
loesche_alle_positionen,
|
|
neue_version,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
_ersteller = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher)
|
|
|
|
|
|
async def _mit_positionen(db: AsyncSession, vorlage: Beladungsvorlage) -> BeladungsvorlageRead:
|
|
positionen = await hole_positionen(db, vorlage.id)
|
|
return BeladungsvorlageRead(
|
|
id=vorlage.id,
|
|
objekttyp_id=vorlage.objekttyp_id,
|
|
name=vorlage.name,
|
|
version=vorlage.version,
|
|
status=vorlage.status,
|
|
positionen=positionen,
|
|
)
|
|
|
|
|
|
@router.get("/vorlagen", response_model=list[BeladungsvorlageRead])
|
|
async def liste_vorlagen(
|
|
objekttyp_id: int | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
_=Depends(get_current_user),
|
|
) -> list[BeladungsvorlageRead]:
|
|
stmt = select(Beladungsvorlage)
|
|
if objekttyp_id is not None:
|
|
stmt = stmt.where(Beladungsvorlage.objekttyp_id == objekttyp_id)
|
|
result = await db.execute(stmt)
|
|
return [await _mit_positionen(db, v) for v in result.scalars().all()]
|
|
|
|
|
|
@router.get("/vorlagen/{vorlage_id}", response_model=BeladungsvorlageRead)
|
|
async def hole_vorlage(
|
|
vorlage_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
|
) -> BeladungsvorlageRead:
|
|
vorlage = await db.get(Beladungsvorlage, vorlage_id)
|
|
if vorlage is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
|
|
return await _mit_positionen(db, vorlage)
|
|
|
|
|
|
@router.post("/vorlagen", response_model=BeladungsvorlageRead, status_code=status.HTTP_201_CREATED)
|
|
async def erstelle_neue_vorlage(
|
|
payload: BeladungsvorlageCreate, db: AsyncSession = Depends(get_db), _=Depends(_ersteller)
|
|
) -> BeladungsvorlageRead:
|
|
vorlage = await erstelle_vorlage(
|
|
db, objekttyp_id=payload.objekttyp_id, name=payload.name, positionen=payload.positionen
|
|
)
|
|
return await _mit_positionen(db, vorlage)
|
|
|
|
|
|
@router.post(
|
|
"/vorlagen/{vorlage_id}/neue-version",
|
|
response_model=BeladungsvorlageRead,
|
|
status_code=status.HTTP_201_CREATED,
|
|
)
|
|
async def erzeuge_neue_version(
|
|
vorlage_id: int,
|
|
payload: VorlagenAenderung,
|
|
db: AsyncSession = Depends(get_db),
|
|
_=Depends(_ersteller),
|
|
) -> BeladungsvorlageRead:
|
|
"""Prompt 08 Punkt 4: erzeugt neue Version, alte bleibt unverändert/lesbar."""
|
|
aktuelle_vorlage = await db.get(Beladungsvorlage, vorlage_id)
|
|
if aktuelle_vorlage is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
|
|
neue = await neue_version(db, aktuelle_vorlage=aktuelle_vorlage, positionen=payload.positionen)
|
|
return await _mit_positionen(db, neue)
|
|
|
|
|
|
@router.put("/vorlagen/{vorlage_id}/positionen", response_model=BeladungsvorlageRead)
|
|
async def aktualisiere_vorlage_positionen(
|
|
vorlage_id: int,
|
|
payload: VorlagenAenderung,
|
|
db: AsyncSession = Depends(get_db),
|
|
_=Depends(_ersteller),
|
|
) -> BeladungsvorlageRead:
|
|
"""Nutzer-Vorgabe 2026-09-05: Vorlage muss keine Versionen haben - ersetzt
|
|
die Positionen der bestehenden Vorlage direkt, ohne neue Version anzulegen.
|
|
Bereits angelegte Objekte sind unbetroffen (sollmenge_vorlage ist an der
|
|
Objektposition fest kopiert, siehe app/services/objekte.py)."""
|
|
vorlage = await db.get(Beladungsvorlage, vorlage_id)
|
|
if vorlage is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
|
|
aktualisiert = await aktualisiere_positionen(db, vorlage=vorlage, positionen=payload.positionen)
|
|
return await _mit_positionen(db, aktualisiert)
|
|
|
|
|
|
@router.delete("/vorlagen/{vorlage_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def loesche_vorlage(
|
|
vorlage_id: int, db: AsyncSession = Depends(get_db), _=Depends(_ersteller)
|
|
) -> None:
|
|
"""Vorlagen sind bewusst versioniert (Prompt 08) - normalerweise wird eine
|
|
Version nie gelöscht, nur durch eine neue ersetzt (status=veraltet). Echtes
|
|
Löschen ist nur erlaubt, wenn KEIN Objekt diese Version referenziert (sonst
|
|
FK-Verletzung bzw. Datenverlust an echten Objekten) - z.B. versehentlich
|
|
angelegte/Test-Versionen entfernen."""
|
|
vorlage = await db.get(Beladungsvorlage, vorlage_id)
|
|
if vorlage is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
|
|
|
|
result = await db.execute(select(Objekt.id).where(Objekt.vorlage_id == vorlage_id).limit(1))
|
|
if result.scalar_one_or_none() is not None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Vorlage wird von mindestens einem Objekt verwendet und kann nicht gelöscht werden",
|
|
)
|
|
|
|
await loesche_alle_positionen(db, vorlage_id)
|
|
await db.delete(vorlage)
|