Artikelbilder lokal speichern und auf der Artikelseite anzeigen

Open Food Facts liefert nur eine Bildadresse. Direkt dorthin zu verlinken hiesse:
Das Bild verschwindet, wenn OFF es austauscht, die Installation braucht Internet,
und jeder Seitenaufruf verraet OFF, welche Artikel jemand ansieht.

Das Bild wird daher einmal geholt und in einer eigenen Tabelle product_images
abgelegt - eigene Tabelle, damit Artikellisten die Blobs nicht mitziehen.
Geholt wird beim Anlegen, bei geaenderter Bildadresse und beim ersten Abruf
(so bekommen auch bestehende Artikel ihre Kopie, ohne Wanderung ueber alle
Datensaetze).

GET /products/{id}/image verlangt eine Anmeldung - aus den Bildern liesse sich
sonst ohne Konto ablesen, was im Vorrat liegt. Da ein <img src> keinen
Authorization-Header schickt, laedt die Oberflaeche das Bild ueber fetch und
zeigt es als Objekt-URL.

Angezeigt wird es rechts neben Barcode, Name und Marke. Fehlt ein Bild, rendert
die Komponente nichts und die Felder nehmen die volle Breite ein.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 13:35:10 +02:00
parent dc1750d23a
commit c155116b4e
8 changed files with 321 additions and 19 deletions

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import Response
from sqlalchemy.orm import Session
from ..crud import product_to_out
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Barcode, BaseUnit, Category, Group, Product, User
from ..models import Barcode, BaseUnit, Category, Group, Product, ProductImage, User
from ..off import lookup_barcode
from ..schemas import BarcodeCreate, LookupResult, ProductCreate, ProductOut, ProductUpdate
from ..services import images
from ..services.categories import suggest_category
from .categories import descendant_ids
from ..services.conversion import ConversionError, resolve_product_unit
@@ -90,6 +92,34 @@ def get_product(
return product_to_out(db, product)
@router.get("/{product_id}/image")
def get_product_image(
product_id: int,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> Response:
"""Artikelbild aus der eigenen Datenbank nie von Open Food Facts.
Anders als das Logo verlangt diese Route eine Anmeldung: Aus den Bildern
liesse sich sonst ohne Konto ablesen, was im Vorrat liegt.
"""
product = db.get(Product, product_id)
if product is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Produkt nicht gefunden")
bild = images.ensure(db, product)
if bild is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Kein Bild vorhanden")
return Response(
content=bild.data,
media_type=bild.content_type,
headers={
"Cache-Control": "private, max-age=300",
"ETag": f'"bild-{product_id}-{int(bild.updated_at.timestamp())}"',
},
)
@router.post("", response_model=ProductOut, status_code=status.HTTP_201_CREATED)
def create_product(
payload: ProductCreate,
@@ -129,6 +159,10 @@ def create_product(
db.add(product)
db.flush() # product.id fuer den Gruppen-Code
sync_group_code(db, product)
if product.image_url:
# Gleich beim Anlegen holen. Schlaegt es fehl, entsteht kein Fehler:
# Ein fehlendes Bild darf das Anlegen eines Artikels nicht verhindern.
images.store(db, product, product.image_url)
db.commit()
db.refresh(product)
return product_to_out(db, product)
@@ -173,10 +207,20 @@ def update_product(
data.pop("date_precision", None) # Spalte ist NOT NULL
else:
data["date_precision"] = data["date_precision"].value
alte_bildadresse = product.image_url
for field, value in data.items():
setattr(product, field, value)
# Gruppe oder Barcode koennen sich geaendert haben - Code nachziehen.
sync_group_code(db, product)
if "image_url" in data and data["image_url"] != alte_bildadresse:
# Neue Adresse heisst neues Bild die alte Kopie waere sonst dauerhaft
# falsch, weil ``ensure`` nur nachlaedt, wenn gar keine Kopie da ist.
alt = db.get(ProductImage, product.id)
if alt is not None:
db.delete(alt)
db.flush()
if product.image_url:
images.store(db, product, product.image_url)
db.commit()
db.refresh(product)
return product_to_out(db, product)