Ergebnisansicht: Sidebar + Header, Kategorie-Chip, manuelle Korrektur
- Layout: Grid sidebar|main. Linke Seitenleiste mit den Produktgruppen
(Counts + unsicher-Badges, Klick springt zur Gruppe), sticky Header (Ort,
Anzahl, Cache-Statistik, Modell, unsicher, Filter).
- Kategorie pro Angebot als Chip sichtbar; Chip ist zugleich Korrektur-Anker.
- POST /api/korrektur {titel,marke,gruppe,plz}: schreibt die manuelle Gruppe
(modell='manuell') in den Produkt-Cache -- die hochwertigste Cache-Quelle;
patcht den UI-Ergebnis-Cache der PLZ (Angebot wandert, unsicher-Flag weg).
Kein LLM, kein Fetch; Whitelist erzwungen. Frontend hängt das Angebot
client-seitig um, ohne neuen Lauf.
- +6 Tests (gültig/400/400, modell=manuell, manuelle Zuordnung -> Cache-Hit
kein LLM, Ergebnis-Cache-Patch). 76 Tests grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
077a877480
commit
e1c7afef7e
3 changed files with 319 additions and 17 deletions
|
|
@ -225,6 +225,76 @@ def api_status(job_id: str) -> dict:
|
|||
return job
|
||||
|
||||
|
||||
# === Manuelle Korrektur -> Produkt-Cache (die hochwertigste Cache-Quelle) =====
|
||||
|
||||
|
||||
@app.post("/api/korrektur")
|
||||
def api_korrektur(req: dict) -> dict:
|
||||
"""Setzt die Produktgruppe eines Produkts manuell und dauerhaft.
|
||||
|
||||
Schnitt-konform: betrifft NUR die Kategorie (Cache-Seite), nie Angebotsdaten;
|
||||
ruft KEIN LLM und fetcht nicht. Geschlossene Liste wird erzwungen. Die
|
||||
Zuordnung (modell='manuell') überschreibt im Produkt-Cache jeden früheren
|
||||
LLM-Wert -- dieses Produkt ist damit künftig sicher und LLM-frei.
|
||||
"""
|
||||
from .config import PRODUKTGRUPPEN
|
||||
from .produktcache import ProduktCache, produkt_schluessel
|
||||
|
||||
titel = (req.get("titel") or "").strip()
|
||||
marke = req.get("marke") # darf None/"" sein -- Teil der Produkt-Identität
|
||||
gruppe = (req.get("gruppe") or "").strip()
|
||||
|
||||
if not titel:
|
||||
raise HTTPException(status_code=400, detail="Titel fehlt")
|
||||
if gruppe not in PRODUKTGRUPPEN:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unbekannte Produktgruppe '{gruppe}' -- nur die geschlossene "
|
||||
"Liste ist erlaubt.",
|
||||
)
|
||||
|
||||
schluessel = produkt_schluessel(titel, marke)
|
||||
geschrieben = ProduktCache().schreibe_viele([(schluessel, gruppe, "manuell")])
|
||||
|
||||
plz = (req.get("plz") or "").strip()
|
||||
if plz:
|
||||
_patche_ergebnis_cache(plz, schluessel, gruppe)
|
||||
|
||||
return {"schluessel": schluessel, "gruppe": gruppe, "gespeichert": bool(geschrieben)}
|
||||
|
||||
|
||||
def _patche_ergebnis_cache(plz: str, schluessel: str, ziel_gruppe: str) -> None:
|
||||
"""Hält die zwischengespeicherte UI-Struktur konsistent mit der Korrektur:
|
||||
hängt alle Angebote mit passendem Produkt-Schlüssel in die Zielgruppe um,
|
||||
setzt unsicher=False und aktualisiert Counts. Verschiebt nur die Kategorie --
|
||||
keine Neukategorisierung, kein Fetch."""
|
||||
from .produktcache import produkt_schluessel
|
||||
|
||||
for key, erg in _ergebnis_cache.items():
|
||||
if key[0] != plz:
|
||||
continue
|
||||
gruppen = erg.get("gruppen", [])
|
||||
zielblock = next((g for g in gruppen if g["name"] == ziel_gruppe), None)
|
||||
if zielblock is None:
|
||||
continue
|
||||
bewegt = []
|
||||
for block in gruppen:
|
||||
bleibt = []
|
||||
for a in block["angebote"]:
|
||||
if produkt_schluessel(a["titel"], a.get("marke")) == schluessel:
|
||||
a["unsicher"] = False
|
||||
bewegt.append(a)
|
||||
else:
|
||||
bleibt.append(a)
|
||||
block["angebote"] = bleibt
|
||||
zielblock["angebote"].extend(bewegt)
|
||||
for g in gruppen:
|
||||
g["anzahl"] = len(g["angebote"])
|
||||
erg["unsicher"] = sum(
|
||||
1 for g in gruppen for a in g["angebote"] if a.get("unsicher")
|
||||
)
|
||||
|
||||
|
||||
def _run_kategorisieren(job_id, plz, fetch, modell, anbieter, key) -> None:
|
||||
"""LLM-Schritt im Hintergrund. Arbeitet auf den geladenen Rohdaten.
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,43 @@
|
|||
footer.note .hinweis { display:block; margin-top:6px; font-style:italic; }
|
||||
|
||||
.sep { color:var(--faint); }
|
||||
|
||||
/* --- Ergebnis-Layout: Sidebar + sticky Header --- */
|
||||
.ergebnis-layout { display:grid; grid-template-columns:248px minmax(0,1fr); gap:var(--s6); align-items:start; }
|
||||
aside.sidebar {
|
||||
position:sticky; top:var(--s4); align-self:start; max-height:calc(100vh - var(--s8)); overflow:auto;
|
||||
background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-sm); padding:var(--s3);
|
||||
}
|
||||
.sidebar-head { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); font-weight:700; padding:var(--s2) var(--s3) var(--s3); }
|
||||
nav#gruppennav { display:flex; flex-direction:column; gap:2px; }
|
||||
.navitem {
|
||||
display:flex; justify-content:space-between; align-items:center; gap:var(--s3);
|
||||
width:100%; text-align:left; padding:8px var(--s3); border-radius:var(--radius-sm);
|
||||
background:transparent; color:var(--ink-2); border:1px solid transparent; font-weight:500; font-size:14px; min-height:0; cursor:pointer;
|
||||
transition:background .15s, color .15s;
|
||||
}
|
||||
.navitem:hover { background:var(--panel-2); transform:none; box-shadow:none; color:var(--ink); }
|
||||
.navitem.aktiv { background:var(--brand-weak); color:var(--brand-d); border-color:#cfe3d9; font-weight:600; }
|
||||
.navitem .nav-cnt { font-variant-numeric:tabular-nums; color:var(--muted); font-size:13px; }
|
||||
.navitem .nav-uns { font-size:11px; color:var(--warn); border:1px solid var(--warn-line); background:var(--warn-bg); border-radius:5px; padding:0 6px; margin-left:6px; }
|
||||
header.ergebnis-header { position:sticky; top:0; z-index:5; background:var(--bg); padding-top:var(--s2); padding-bottom:var(--s3); margin-bottom:var(--s4); border-bottom:1px solid var(--line); }
|
||||
header.ergebnis-header .summary { margin-bottom:var(--s3); }
|
||||
header.ergebnis-header .filters { margin-bottom:0; }
|
||||
.kfeedback { font-size:13px; color:var(--brand-d); font-weight:600; height:0; overflow:hidden; transition:height .2s; }
|
||||
.kfeedback.an { height:22px; margin-top:var(--s2); }
|
||||
|
||||
/* Kategorie-Chip pro Angebot (zugleich Korrektur-Anker) */
|
||||
.ang .gchip { display:inline-block; font-size:11px; color:var(--muted); background:var(--panel-2); border:1px solid var(--line); border-radius:5px; padding:0 7px; margin-left:7px; cursor:pointer; vertical-align:middle; }
|
||||
.ang .gchip:hover { border-color:var(--brand); color:var(--brand-d); }
|
||||
.ang.unsicher .gchip { border-color:var(--warn-line); background:var(--warn-bg); color:var(--warn); }
|
||||
.ang .ksel { font-size:12px; padding:2px 6px; border:1px solid var(--brand); border-radius:6px; margin-left:7px; vertical-align:middle; }
|
||||
|
||||
@media (max-width:860px){
|
||||
.ergebnis-layout { grid-template-columns:1fr; }
|
||||
aside.sidebar { position:static; max-height:none; order:-1; }
|
||||
nav#gruppennav { flex-direction:row; flex-wrap:wrap; }
|
||||
.navitem { width:auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -311,14 +348,25 @@
|
|||
|
||||
<!-- ============ ERGEBNIS ============ -->
|
||||
<div id="result" hidden>
|
||||
<div class="summary" id="summary"></div>
|
||||
<div class="filters">
|
||||
<div class="field"><label for="fhaendler">Händler</label><select id="fhaendler"><option value="">alle</option></select></div>
|
||||
<div class="field"><label for="ftext">Angebot suchen</label><input id="ftext" type="text" placeholder="Titel…" /></div>
|
||||
<div class="field check"><input id="fsicher" type="checkbox" /><label for="fsicher">nur sichere</label></div>
|
||||
<div class="ergebnis-layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-head">Produktgruppen</div>
|
||||
<nav id="gruppennav"></nav>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<header class="ergebnis-header">
|
||||
<div class="summary" id="summary"></div>
|
||||
<div class="filters">
|
||||
<div class="field"><label for="fhaendler">Händler</label><select id="fhaendler"><option value="">alle</option></select></div>
|
||||
<div class="field"><label for="ftext">Angebot suchen</label><input id="ftext" type="text" placeholder="Titel…" /></div>
|
||||
<div class="field check"><input id="fsicher" type="checkbox" /><label for="fsicher">nur sichere</label></div>
|
||||
</div>
|
||||
<div id="korrektur-feedback" class="kfeedback"></div>
|
||||
</header>
|
||||
<div id="gruppen"></div>
|
||||
<footer class="note" id="footer"></footer>
|
||||
</div>
|
||||
</div>
|
||||
<div id="gruppen"></div>
|
||||
<footer class="note" id="footer"></footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -553,22 +601,30 @@ function render(){
|
|||
const modellTxt = d.modell
|
||||
? ` <span class="sep">·</span> kategorisiert mit <b>${esc(d.modell)}</b>${d.anbieter?` <span class="t-h">(${esc(d.anbieter)})</span>`:""}`
|
||||
: "";
|
||||
const cacheTxt = (d.aus_cache != null)
|
||||
? ` <span class="sep">·</span> <b>${d.aus_cache}</b> aus Cache · <b>${d.neu}</b> neu` : "";
|
||||
$("#summary").innerHTML =
|
||||
`Ort <b>${esc(d.ort_name || d.ort_plz)}</b> (PLZ ${esc(d.ort_plz)}) <span class="sep">·</span> <b>${d.anzahl}</b> Angebote <span class="sep">·</span>
|
||||
Quellen: ${d.quellen.map(esc).join(", ") || "—"} <span class="sep">·</span> <b>${d.unsicher}</b> unsicher${modellTxt}`;
|
||||
Quellen: ${d.quellen.map(esc).join(", ") || "—"} <span class="sep">·</span> <b>${d.unsicher}</b> unsicher${cacheTxt}${modellTxt}`;
|
||||
|
||||
const fh = $("#fhaendler");
|
||||
const aktuell = fh.value;
|
||||
fh.innerHTML = "<option value=''>alle</option>" + d.haendler.map(h => `<option>${esc(h)}</option>`).join("");
|
||||
fh.value = aktuell;
|
||||
|
||||
zeichneGruppen();
|
||||
zeichneGruppen(); // baut Sektionen + ruft baueSidebar mit den gefilterten Counts
|
||||
|
||||
$("#footer").innerHTML =
|
||||
`<span class="haendler"><b>Beobachtete Händler (belegt):</b> ${d.haendler.map(esc).join(", ")}.</span>` +
|
||||
d.hinweise.map(h => `<span class="hinweis">${esc(h)}</span>`).join("");
|
||||
}
|
||||
|
||||
function passt(a, fH, fT, nurSicher){
|
||||
return (!fH || a.haendler === fH) &&
|
||||
(!nurSicher || !a.unsicher) &&
|
||||
(!fT || (a.titel + " " + (a.marke||"")).toLowerCase().includes(fT));
|
||||
}
|
||||
|
||||
function zeichneGruppen(){
|
||||
const d = DATEN;
|
||||
const fH = $("#fhaendler").value;
|
||||
|
|
@ -576,15 +632,13 @@ function zeichneGruppen(){
|
|||
const nurSicher = $("#fsicher").checked;
|
||||
const box = $("#gruppen");
|
||||
box.innerHTML = "";
|
||||
const navDaten = [];
|
||||
|
||||
for (const g of d.gruppen){
|
||||
const items = g.angebote.filter(a =>
|
||||
(!fH || a.haendler === fH) &&
|
||||
(!nurSicher || !a.unsicher) &&
|
||||
(!fT || (a.titel + " " + (a.marke||"")).toLowerCase().includes(fT))
|
||||
);
|
||||
const items = g.angebote.filter(a => passt(a, fH, fT, nurSicher));
|
||||
const sec = document.createElement("section");
|
||||
sec.className = "grp";
|
||||
sec.dataset.grp = g.name;
|
||||
const h = document.createElement("h3");
|
||||
h.innerHTML = `<span>${esc(g.name)}</span><span class="cnt">${items.length}</span>`;
|
||||
h.onclick = () => sec.classList.toggle("zu");
|
||||
|
|
@ -595,14 +649,39 @@ function zeichneGruppen(){
|
|||
sec.appendChild(p);
|
||||
} else {
|
||||
const ul = document.createElement("ul");
|
||||
for (const a of items) ul.appendChild(zeile(a));
|
||||
for (const a of items) ul.appendChild(zeile(a, g.name));
|
||||
sec.appendChild(ul);
|
||||
navDaten.push({ name:g.name, count:items.length, uns:items.filter(a=>a.unsicher).length });
|
||||
}
|
||||
box.appendChild(sec);
|
||||
}
|
||||
baueSidebar(navDaten);
|
||||
}
|
||||
|
||||
function zeile(a){
|
||||
function baueSidebar(navDaten){
|
||||
const nav = $("#gruppennav");
|
||||
nav.innerHTML = "";
|
||||
if (!navDaten.length){ nav.innerHTML = `<div class="navitem" style="cursor:default;color:var(--muted)">keine Treffer</div>`; return; }
|
||||
for (const g of navDaten){
|
||||
const b = document.createElement("button");
|
||||
b.className = "navitem"; b.dataset.grp = g.name;
|
||||
const uns = g.uns ? `<span class="nav-uns">${g.uns}</span>` : "";
|
||||
b.innerHTML = `<span>${esc(g.name)}</span><span><span class="nav-cnt">${g.count}</span>${uns}</span>`;
|
||||
b.onclick = () => springeZu(g.name);
|
||||
nav.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function springeZu(name){
|
||||
let ziel = null;
|
||||
for (const sec of document.querySelectorAll("#gruppen section.grp")){
|
||||
if (sec.dataset.grp === name){ ziel = sec; break; }
|
||||
}
|
||||
if (ziel){ ziel.classList.remove("zu"); ziel.scrollIntoView({behavior:"smooth", block:"start"}); }
|
||||
for (const b of document.querySelectorAll(".navitem")) b.classList.toggle("aktiv", b.dataset.grp === name);
|
||||
}
|
||||
|
||||
function zeile(a, gruppe){
|
||||
const li = document.createElement("li");
|
||||
li.className = "ang" + (a.unsicher ? " unsicher" : "");
|
||||
const marke = a.marke ? `<span class="marke">${esc(a.marke)}</span> ` : "";
|
||||
|
|
@ -611,14 +690,64 @@ function zeile(a){
|
|||
? ` <span class="sep">·</span> gültig ${a.gueltig_von||"?"}–${a.gueltig_bis||"?"}` : "";
|
||||
const badge = a.unsicher ? `<span class="badge">unsicher</span>` : "";
|
||||
li.innerHTML =
|
||||
`<div class="name">${marke}${esc(a.titel)}${badge}</div>` +
|
||||
`<div class="name">${marke}${esc(a.titel)}${badge}<span class="gchip" title="Produktgruppe ändern">${esc(gruppe)}</span></div>` +
|
||||
`<div class="preis">${preisFmt(a.preis)}</div>` +
|
||||
`<div class="meta"><span class="haendler">${esc(a.haendler)}</span>${menge}${gueltig}
|
||||
<span class="quelle" title="${esc(a.quelle)}">${esc((a.quelle||"").slice(0,42))}</span></div>` +
|
||||
`<div class="grund">${a.grundpreis ? esc(a.grundpreis) : ""}</div>`;
|
||||
const chip = li.querySelector(".gchip");
|
||||
chip.onclick = () => oeffneKorrektur(chip, a, gruppe);
|
||||
return li;
|
||||
}
|
||||
|
||||
// ----- Korrektur: Produktgruppe manuell setzen -> Produkt-Cache --------------
|
||||
function oeffneKorrektur(chip, a, vonGruppe){
|
||||
const sel = document.createElement("select");
|
||||
sel.className = "ksel";
|
||||
for (const g of DATEN.gruppen.map(x => x.name)){
|
||||
const o = document.createElement("option");
|
||||
o.value = g; o.textContent = g;
|
||||
if (g === vonGruppe) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
chip.replaceWith(sel);
|
||||
sel.focus();
|
||||
let fertig = false;
|
||||
sel.onchange = () => { fertig = true; korrigiere(a, vonGruppe, sel.value); };
|
||||
sel.onblur = () => { setTimeout(() => { if (!fertig && DATEN) render(); }, 150); };
|
||||
}
|
||||
|
||||
async function korrigiere(a, vonGruppe, zielGruppe){
|
||||
if (zielGruppe === vonGruppe){ render(); return; }
|
||||
try {
|
||||
const r = await fetch("/api/korrektur", {
|
||||
method:"POST", headers:{"Content-Type":"application/json"},
|
||||
body: JSON.stringify({ titel:a.titel, marke:a.marke, gruppe:zielGruppe, plz:$("#plz").value.trim() })
|
||||
});
|
||||
if (!r.ok){ throw new Error((await r.json()).detail || r.status); }
|
||||
const von = DATEN.gruppen.find(g => g.name === vonGruppe);
|
||||
const ziel = DATEN.gruppen.find(g => g.name === zielGruppe);
|
||||
if (von){ von.angebote = von.angebote.filter(x => x !== a); von.anzahl = von.angebote.length; }
|
||||
a.unsicher = false;
|
||||
if (ziel){ ziel.angebote.push(a); ziel.anzahl = ziel.angebote.length; }
|
||||
DATEN.unsicher = DATEN.gruppen.reduce((s,g) => s + g.angebote.filter(x=>x.unsicher).length, 0);
|
||||
render();
|
||||
zeigeFeedback(`✓ „${a.titel}" → ${zielGruppe} gemerkt (auch im Cache)`);
|
||||
} catch(e){
|
||||
render();
|
||||
zeigeFeedback(`Korrektur fehlgeschlagen: ${e.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeFeedback(txt, fehler){
|
||||
const el = $("#korrektur-feedback");
|
||||
el.textContent = txt;
|
||||
el.style.color = fehler ? "var(--err)" : "var(--brand-d)";
|
||||
el.classList.add("an");
|
||||
clearTimeout(zeigeFeedback._t);
|
||||
zeigeFeedback._t = setTimeout(() => el.classList.remove("an"), 2800);
|
||||
}
|
||||
|
||||
// ----- Events ----------------------------------------------------------------
|
||||
$("#rohholen").onclick = holeRohdaten;
|
||||
$("#kategorisieren").onclick = kategorisiere;
|
||||
|
|
|
|||
103
tests/test_korrektur.py
Normal file
103
tests/test_korrektur.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Tests für die manuelle Korrektur (POST /api/korrektur) + Cache-Wirkung."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
import angebote.web as web # noqa: E402
|
||||
from angebote.produktcache import ProduktCache, produkt_schluessel # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path, monkeypatch):
|
||||
"""Isolierte Cache-DB; der Endpoint nutzt ProduktCache() ohne Pfad -> STANDARD_DB."""
|
||||
p = tmp_path / "kat.sqlite"
|
||||
monkeypatch.setattr("angebote.produktcache.STANDARD_DB", p)
|
||||
return p
|
||||
|
||||
|
||||
def test_korrektur_gueltig_schreibt_cache(db):
|
||||
client = TestClient(web.app)
|
||||
r = client.post(
|
||||
"/api/korrektur",
|
||||
json={"titel": "Hafer-Drink", "marke": "Oatly", "gruppe": "Getränke (alkoholfrei)"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["gespeichert"] is True
|
||||
cache = ProduktCache(db_pfad=db)
|
||||
assert cache.hole(produkt_schluessel("Hafer-Drink", "Oatly")) == "Getränke (alkoholfrei)"
|
||||
|
||||
|
||||
def test_korrektur_off_list_ist_400(db):
|
||||
client = TestClient(web.app)
|
||||
r = client.post("/api/korrektur", json={"titel": "X", "gruppe": "Weltraumzeug"})
|
||||
assert r.status_code == 400
|
||||
assert ProduktCache(db_pfad=db).groesse() == 0
|
||||
|
||||
|
||||
def test_korrektur_ohne_titel_ist_400(db):
|
||||
client = TestClient(web.app)
|
||||
r = client.post("/api/korrektur", json={"titel": " ", "gruppe": "Fisch"})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_korrektur_modell_ist_manuell(db):
|
||||
client = TestClient(web.app)
|
||||
client.post("/api/korrektur", json={"titel": "Lachs", "marke": None, "gruppe": "Fisch"})
|
||||
with sqlite3.connect(str(db)) as con:
|
||||
row = con.execute("SELECT modell FROM produkt_kategorie").fetchone()
|
||||
assert row[0] == "manuell"
|
||||
|
||||
|
||||
def test_manuelle_zuordnung_ist_cache_hit_kein_llm(db):
|
||||
from angebote.kategorisieren import kategorisiere
|
||||
from tests.fakes import CountingFakeKategorisierer, beispiel_angebot
|
||||
|
||||
cache = ProduktCache(db_pfad=db)
|
||||
cache.schreibe_viele(
|
||||
[(produkt_schluessel("Hafer-Drink", None), "Getränke (alkoholfrei)", "manuell")]
|
||||
)
|
||||
a = beispiel_angebot("Hafer-Drink", marke=None)
|
||||
fake = CountingFakeKategorisierer("Sonstiges") # würde falsch raten
|
||||
stat = {}
|
||||
erg = kategorisiere([a], fake, cache=cache, statistik=stat)
|
||||
assert fake.gesehen == 0 # kein LLM-Posten -- die manuelle Zuordnung gewinnt
|
||||
assert stat == {"aus_cache": 1, "neu": 0}
|
||||
assert erg[0].gruppe == "Getränke (alkoholfrei)" and not erg[0].unsicher
|
||||
|
||||
|
||||
def test_korrektur_patcht_ergebnis_cache(db):
|
||||
client = TestClient(web.app)
|
||||
schluessel = produkt_schluessel("Toffifee", "Storck")
|
||||
key = ("99999", "openrouter", "x")
|
||||
web._ergebnis_cache[key] = {
|
||||
"gruppen": [
|
||||
{
|
||||
"name": "Sonstiges",
|
||||
"anzahl": 1,
|
||||
"angebote": [{"titel": "Toffifee", "marke": "Storck", "unsicher": True}],
|
||||
},
|
||||
{"name": "Süßwaren & Snacks", "anzahl": 0, "angebote": []},
|
||||
],
|
||||
"unsicher": 1,
|
||||
}
|
||||
try:
|
||||
r = client.post(
|
||||
"/api/korrektur",
|
||||
json={
|
||||
"titel": "Toffifee", "marke": "Storck",
|
||||
"gruppe": "Süßwaren & Snacks", "plz": "99999",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
erg = web._ergebnis_cache[key]
|
||||
suess = next(g for g in erg["gruppen"] if g["name"] == "Süßwaren & Snacks")
|
||||
sonst = next(g for g in erg["gruppen"] if g["name"] == "Sonstiges")
|
||||
assert len(suess["angebote"]) == 1 and not suess["angebote"][0]["unsicher"]
|
||||
assert len(sonst["angebote"]) == 0
|
||||
assert erg["unsicher"] == 0
|
||||
finally:
|
||||
web._ergebnis_cache.pop(key, None)
|
||||
Loading…
Reference in a new issue