Remove-from-catalog: a third durable curation store
Titles that aren't games (misread box art, out-of-scope items) can now be removed outright: a danger button in the catalog's edit panel posts /api/remove-title, which drops the line's matches rows (veto'd ones too — removal is the human explicitly discarding the line), records the decision photo-scoped in data/title_removals.json, and replays titles.json. Every rebuild filters removed sightings after edits and before dedupe, so re-extraction cannot resurrect them; undo by deleting the record from the store. The three stores now share one scoped-record parser and recorder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
@@ -24,6 +24,7 @@ from bggpipe.extract import (
|
||||
replay_titles,
|
||||
run_extract,
|
||||
)
|
||||
from bggpipe.normalize import normalize_title
|
||||
|
||||
|
||||
def _write_image(path, size=(400, 300), fmt="JPEG", color=(200, 30, 30)):
|
||||
@@ -234,6 +235,22 @@ def test_edits_chain_and_target_photos():
|
||||
assert entries[1] == _entry("Wiz-War", "b.jpg") # untargeted copy untouched
|
||||
|
||||
|
||||
def test_removals_filter_rebuilds_photo_scoped():
|
||||
from bggpipe.extract import apply_title_removals
|
||||
|
||||
entries = [
|
||||
_entry("Not A Game", "a.jpg"),
|
||||
_entry("Not A Game", "b.jpg"),
|
||||
_entry("Catan", "a.jpg"),
|
||||
]
|
||||
removals = [{"norm": normalize_title("Not A Game"), "photos": {"a.jpg"}}]
|
||||
kept = apply_title_removals(entries, removals)
|
||||
assert [(e["title_raw"], e["source_photos"]) for e in kept] == [
|
||||
("Not A Game", ["b.jpg"]), # other sighting untouched
|
||||
("Catan", ["a.jpg"]),
|
||||
]
|
||||
|
||||
|
||||
def test_stores_roundtrip_and_replay_from_raw(tmp_path):
|
||||
cfg = Config(data_dir=tmp_path / "data", photos_dir=tmp_path / "photos")
|
||||
raw = cfg.extract_raw_dir
|
||||
|
||||
@@ -726,3 +726,43 @@ def test_single_photo_rowless_entry_is_not_splittable(tmp_path):
|
||||
if c["title_raw"] == "Fresh Off The Shelf"
|
||||
)
|
||||
assert line["can_split"] is False
|
||||
|
||||
|
||||
def test_remove_title_is_durable_and_drops_vetoed_rows(tmp_path):
|
||||
cfg = make_cfg(tmp_path)
|
||||
rows = read_matches(cfg.matches_path)
|
||||
rows.append(
|
||||
_row(
|
||||
title_raw="Citadels",
|
||||
match_status="approved",
|
||||
bgg_id="478",
|
||||
source_photos="shelf.jpg",
|
||||
dedupe_veto="1",
|
||||
)
|
||||
)
|
||||
write_matches(cfg.matches_path, rows)
|
||||
web = TestClient(create_app(cfg, client=unauthorized_client(tmp_path)))
|
||||
res = web.post(
|
||||
"/api/remove-title",
|
||||
json={"title_raw": "Citadels", "source_photos": "shelf.jpg"},
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert "Citadels" not in [c["title_raw"] for c in res.json()["catalog"]]
|
||||
# removal is explicit: the veto'd row goes too (unlike edits)
|
||||
assert not any(r["title_raw"] == "Citadels" for r in read_matches(cfg.matches_path))
|
||||
(record,) = json.loads(cfg.title_removals_path.read_text())
|
||||
assert record == {"title": "Citadels", "photos": ["shelf.jpg"]}
|
||||
# durable: a fresh replay keeps it gone; the sibling title is untouched
|
||||
from bggpipe.extract import replay_titles
|
||||
|
||||
replay_titles(cfg)
|
||||
titles = {e["title_raw"] for e in json.loads(cfg.titles_path.read_text())}
|
||||
assert titles == {"Fresh Off The Shelf"}
|
||||
# unknown titles still 404
|
||||
assert (
|
||||
web.post(
|
||||
"/api/remove-title",
|
||||
json={"title_raw": "No Such Game", "source_photos": "x.jpg"},
|
||||
).status_code
|
||||
== 404
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user