diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py index 481b80b..9f0701c 100644 --- a/backend/app/routers/products.py +++ b/backend/app/routers/products.py @@ -251,19 +251,21 @@ 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. + if "image_url" in data: + # Massgeblich ist, woher die vorhandene Kopie stammt - nicht, ob sich + # die Adresse am Artikel geaendert hat. Sonst passierte genau dann + # nichts, wenn die Adresse schon stimmte, der Abruf damals aber + # fehlschlug: Der Artikel bliebe dauerhaft ohne Bild. alt = db.get(ProductImage, product.id) - if alt is not None: + if alt is not None and alt.source_url != product.image_url: db.delete(alt) db.flush() - if product.image_url: + alt = None + if alt is None and product.image_url: images.store(db, product, product.image_url) db.commit() db.refresh(product) diff --git a/backend/tests/test_product_images.py b/backend/tests/test_product_images.py index 211a5b4..4f3b155 100644 --- a/backend/tests/test_product_images.py +++ b/backend/tests/test_product_images.py @@ -99,6 +99,28 @@ def test_import_ueberschreibt_kein_vorhandenes_bild(db, user): assert vorhanden.image_url == "https://eigenes.example/bild.jpg" +def test_speichern_holt_nach_wenn_die_kopie_fehlt(db, user, monkeypatch): + """Der haeufigste Fall: Adresse stimmt laengst, der Abruf schlug nur einmal fehl. + + Wuerde nur auf eine geaenderte Adresse geachtet, bliebe so ein Artikel + dauerhaft ohne Bild - "Uebernehmen" waere dann folgenlos. + """ + from app.routers.products import update_product + from app.schemas import ProductUpdate + + monkeypatch.setattr(images, "fetch", lambda url: (EIN_PIXEL, "image/png")) + artikel = _artikel(db, "https://example.org/pesto.png") + assert db.get(ProductImage, artikel.id) is None + + update_product( + artikel.id, + ProductUpdate(image_url="https://example.org/pesto.png"), + db=db, + _=user, + ) + assert db.get(ProductImage, artikel.id) is not None + + def test_fetch_geht_nur_an_http_adressen(monkeypatch): """Ohne diese Schranke waere eine 'file://'-Adresse ein Weg ins Dateisystem.""" monkeypatch.setattr( diff --git a/web/src/components/OffVergleich.jsx b/web/src/components/OffVergleich.jsx index f4f223c..8d6f9d8 100644 --- a/web/src/components/OffVergleich.jsx +++ b/web/src/components/OffVergleich.jsx @@ -11,7 +11,8 @@ import ProduktBild from "./ProduktBild"; * felder: [{ schluessel, titel, eigen, fremd, anzeigeEigen?, anzeigeFremd? }] */ export default function OffVergleich({ - felder, bildEigenId, bildFremdUrl, onUebernehmen, onAlle, onSchliessen, + felder, bildEigenId, bildFremdUrl, bildUebernommen, + onUebernehmen, onAlle, onSchliessen, }) { const leer = (wert) => wert === null || wert === undefined || wert === ""; const abweichend = felder.filter((f) => !leer(f.fremd) && String(f.fremd) !== String(f.eigen)); @@ -62,7 +63,19 @@ export default function OffVergleich({ Bild - + + {/* Nach dem Übernehmen steht hier schon das neue Bild. Ohne das + sah der Knopf wirkungslos aus: Das Artikelbild wechselt erst + beim Speichern, weil der Server es dann erst holt. */} + {bildUebernommen + ? ( + + + neu + + ) + : } + {/* Vorschau kommt hier direkt von OFF – lokal gibt es das Bild ja gerade noch nicht. Übernommen und gespeichert, holt der @@ -74,7 +87,7 @@ export default function OffVergleich({ diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index c79dbc7..9e443cf 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -176,6 +176,9 @@ export default function ProductForm() { if (!offDaten) return; if (schluessel === "image_url") { set("image_url", offDaten.image_url || ""); + // Das Bild selbst wechselt erst beim Speichern - ohne Hinweis wirkt der + // Knopf folgenlos. + toast("Bild übernommen. Mit „Speichern“ wird es geholt."); return; } if (schluessel === "package_size") { @@ -384,6 +387,7 @@ export default function ProductForm() { felder={offFelder()} bildEigenId={id} bildFremdUrl={offDaten.image_url || ""} + bildUebernommen={Boolean(offDaten.image_url) && form.image_url === offDaten.image_url} onUebernehmen={uebernehmen} onAlle={alleUebernehmen} onSchliessen={() => setOffDaten(null)} diff --git a/web/src/styles.css b/web/src/styles.css index 5b2d9f0..fdcfe73 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -230,6 +230,7 @@ input::placeholder { color: var(--muted); opacity: 0.7; } width: 56px; aspect-ratio: 3 / 4; object-fit: contain; background: var(--surface-2); border-radius: 4px; display: block; } +.off-neu { display: flex; align-items: center; gap: var(--sp-2); } .field-inline { display: flex; gap: var(--sp-2); align-items: flex-end; margin-bottom: var(--sp-4); } /* Labels in einer Inline-Reihe duerfen keinen eigenen Abstand haben, sonst rutschen daneben stehende Buttons optisch nach unten. */