From bf5c795f02fe9568e9dd9c1811bff345bd4d92d9 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Mon, 28 Sep 2026 13:10:57 +0200 Subject: [PATCH] fix(magazine): keep approval when auto-linking already-approved text Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 +- core/magazine.py | 9 +++++++-- tests/test_magazine.py | 8 +++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 498e805..47e6b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi - **Text flow indicator.** Review draws a green arrow where an article's text carries on to the next box below or the next column, and chips where it continues on another page (*continues on page 111 ›*, *‹ from page 110*) plus *article starts / ends*; a *Show text flow* switch hides them. The reading text shows small markers (`↓`, `col ›`, `p.111 ›`) at the exact word where a paragraph crosses a box, column or page. Built from the same fusion the narration uses (`GET .../articles/{id}/flow`), so the arrows show what is read as one paragraph. On the measured issue: 97 column, 29 page and 21 below continuations. - **The whole-magazine button is now named *Auto-fix all pages*** (it fixes boxes, adds pull quotes, finds missed text, re-reads words and fixes provable misspellings on every page) and keeps a layout backup first. -- **Continuation pages are linked into their articles automatically.** Detection often found an article's headline page and left the pages that follow as one-page stand-ins ("Page 61") that had to be approved one by one (79 of 132 articles on the measured issue). Text no article owns is now attached to the article running from the page before, unless that article already ended (a signed end such as `(hag)`), it is more than two pages back, or a headline with a byline/deck opens a new article there. Approved articles that gain text go back to *detected* (nothing unreviewed is ever narrated); stand-ins whose text moved are removed. Runs at import, in **Auto-fix all pages**, and on its own (`POST .../articles/auto-link`, undoable). +- **Continuation pages are linked into their articles automatically.** Detection often found an article's headline page and left the pages that follow as one-page stand-ins ("Page 61") that had to be approved one by one (79 of 132 articles on the measured issue). Text no article owns is now attached to the article running from the page before, unless that article already ended (a signed end such as `(hag)`), it is more than two pages back, or a headline with a byline/deck opens a new article there. Text you already approved (as part of a stand-in page) stays approved when it is merged; an approved article that gains text nobody approved goes back to *detected*, so nothing unreviewed is ever narrated. Stand-ins whose text moved are removed. Runs at import, in **Auto-fix all pages**, and on its own (`POST .../articles/auto-link`, undoable). - **Link an article to a page by hand:** *Link to page N — Article continues there / starts there* in the review panel picks the text that belongs (the paragraph that carries the sentence on, up to the next headline) and adds it ticked; undoable. - **Flow arrows follow the reading order:** a green arrow for every jump to another column and for a sentence running across boxes (dashed when a new paragraph starts, solid when a sentence continues), chips for page jumps. - **Much less noise:** the *Auto-fix all pages* report now lists what was fixed and only the pages that need a decision about their boxes (with a *Start with page N* button and compact page pills); pages with only words to check are collapsed and optional. The per-box "words look mis-read" marks are gone (spelling keeps its red wavy underline). The spellchecker knows ~130 company/product names, treats a word printed twice anywhere in the issue as vocabulary (missing-umlaut words such as `fur` stay flagged) and treats two unknown capitalised words in a row as a person's name: 871 → 625 findings on the measured issue. diff --git a/core/magazine.py b/core/magazine.py index 8c6fc47..1701021 100644 --- a/core/magazine.py +++ b/core/magazine.py @@ -4695,6 +4695,9 @@ def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]], articles = [dict(a, blocks=list(a.get("blocks") or []), pages=list(a.get("pages") or [])) for a in articles] real = [a for a in articles if not _is_placeholder_article(a)] owner = {b: a for a in real for b in a["blocks"]} + # Regions the reviewer already approved (as part of any article, stand-ins included): moving + # them into another article does not make them unreviewed. + approved_ids = {b for a in articles if a.get("status") == "approved" for b in a["blocks"]} blocks = {b["id"]: b for b in layout.get("blocks", [])} by_page: dict[int, list[dict[str, Any]]] = {} for b in layout.get("blocks", []): @@ -4742,6 +4745,8 @@ def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]], author=re.sub(r"^(von|by)\s+", "", (byline or {}).get("text", ""), flags=re.I).strip()[:120], pages=sorted({x["page"] for x in part}), blocks=[x["id"] for x in part], confidence=0.55).to_json() + if all(x["id"] in approved_ids for x in part): + fresh["status"] = "approved" articles.append(fresh) real.append(fresh) owner.update({b: fresh for b in fresh["blocks"]}) @@ -4752,8 +4757,8 @@ def auto_link_pages(layout: dict[str, Any], articles: list[dict[str, Any]], and not _ARTICLE_END_RE.search(running_tail.strip())): running["blocks"].extend(x["id"] for x in part) running["pages"] = sorted(set(running["pages"]) | {x["page"] for x in part}) - if running.get("status") == "approved": - running["status"] = "detected" + if running.get("status") == "approved" and not all(x["id"] in approved_ids for x in part): + running["status"] = "detected" # gained text nobody has approved yet owner.update({x["id"]: running for x in part}) moved.update(x["id"] for x in part) running_page, running_tail = page, part[-1]["text"] diff --git a/tests/test_magazine.py b/tests/test_magazine.py index e19656c..524ab58 100644 --- a/tests/test_magazine.py +++ b/tests/test_magazine.py @@ -2999,9 +2999,15 @@ class AutoLinkPagesTests(unittest.TestCase): self.art("article-002", "Page 2", [2], ["b"], "approved"), self.art("article-003", "Page 3", [3], ["c"], "approved")] out, report = self.link(blocks, arts) self.assertEqual([(a["id"], a["pages"], a["blocks"]) for a in out], [("article-001", [1, 2, 3], ["t", "a", "b", "c"])]) - self.assertEqual(out[0]["status"], "detected") # gained unreviewed text: back to review + self.assertEqual(out[0]["status"], "approved") # the stand-ins were approved: still reviewed self.assertEqual((report["linked"], report["dropped"]), (2, 2)) + def test_text_nobody_approved_sends_the_article_back_to_review(self): + blocks = [self.blk("t", 1, "title", "Der Butler", 0), self.blk("a", 1, "body", "Der Text beginnt und", 1), + self.blk("b", 2, "body", "geht weiter.", 0)] + arts = [self.art("article-001", "Der Butler", [1], ["t", "a"], "approved"), self.art("article-002", "Page 2", [2], ["b"], "detected")] + self.assertEqual(self.link(blocks, arts)[0][0]["status"], "detected") + def test_a_signed_end_stops_the_article_and_leaves_the_next_page_alone(self): blocks = [self.blk("t", 1, "title", "Kurz", 0), self.blk("a", 1, "body", "Fertig. (atr)", 1), self.blk("b", 2, "body", "Ein ganz anderer Text.", 0)]