Inline-Anlage (Kategorien/Felder), Artikelfoto und CSV-Spalte "art"
- Web: Kategorien/Unterkategorien und eigene Felder direkt im Artikelformular
anlegen (ohne Umweg über die Kategorien-Seite).
- Artikelfoto per Kamera oder Galerie hochladen – Backend-Endpunkt
(PUT/DELETE /products/{id}/image), Web (Foto machen / Galerie) und iOS
(Kamera + PhotosPicker, inline in der Produktansicht angezeigt).
- CSV-Import: neue Spalte "art" (Gegenstand/Lebensmittel) steuert den Modus
automatisch angelegter Kategorien; Export schreibt sie mit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -240,6 +240,63 @@ def get_product_image(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{product_id}/image", response_model=ProductOut)
|
||||
async def upload_product_image(
|
||||
product_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> ProductOut:
|
||||
"""Eigenes Foto hochladen (Kamera oder Galerie) und beim Artikel ablegen.
|
||||
|
||||
``source_url = None`` markiert das Bild als selbst hochgeladen – so ersetzt
|
||||
es der Bildabgleich beim Speichern nicht durch ein Bild aus einer Adresse
|
||||
(solange keine Bildadresse gesetzt ist).
|
||||
"""
|
||||
product = db.get(Product, product_id)
|
||||
if product is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Produkt nicht gefunden")
|
||||
if file.content_type not in images.ALLOWED_TYPES:
|
||||
erlaubt = ", ".join(sorted(t.split("/")[-1] for t in images.ALLOWED_TYPES))
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
f"Dieses Bildformat wird nicht unterstützt. Erlaubt sind: {erlaubt}.",
|
||||
)
|
||||
data = await file.read()
|
||||
if not data:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Die Datei ist leer.")
|
||||
if len(data) > images.MAX_BYTES:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
f"Das Bild ist zu groß ({len(data) // 1024} KB). "
|
||||
f"Erlaubt sind höchstens {images.MAX_BYTES // 1024} KB.",
|
||||
)
|
||||
bild = db.get(ProductImage, product.id)
|
||||
if bild is None:
|
||||
bild = ProductImage(
|
||||
product_id=product.id, content_type=file.content_type, data=data, source_url=None
|
||||
)
|
||||
db.add(bild)
|
||||
else:
|
||||
bild.content_type, bild.data, bild.source_url = file.content_type, data, None
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
return product_to_out(db, product)
|
||||
|
||||
|
||||
@router.delete("/{product_id}/image", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_product_image(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> None:
|
||||
"""Foto entfernen. Eine gesetzte Bildadresse bleibt bestehen."""
|
||||
bild = db.get(ProductImage, product_id)
|
||||
if bild is not None:
|
||||
db.delete(bild)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("", response_model=ProductOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_product(
|
||||
payload: ProductCreate,
|
||||
|
||||
Reference in New Issue
Block a user