Der Vergleich zweier Berichte zur selben Lage (Ceuta, 01.08.2026) zeigte in beiden Fassungen Textverweise, die im gedruckten Verzeichnis nicht auftauchten. Die EU-Fassung zitierte bis [28] bei 25 aufgefuehrten Quellen, die Anthropic-Fassung bis [29] bei 22. Ursache war nicht die Analyse. In der Datenbank passen Text und Liste in beiden Laeufen exakt zusammen, keine einzige Referenz ohne Eintrag. Das PDF-Template druckte jedoch die laufende Zeilennummer (loop.index) statt der Quellennummer aus dem Lagebild (src.nr). Sobald die Nummern Luecken haben, und das ist der Normalfall, weil nicht jede gesammelte Meldung zitiert wird, verschiebt sich ab der ersten Luecke jeder Beleg. Lage 53 hat die Nummern 1 bis 20, 22, 24, 26, 27, 28: gedruckt wurde 1 bis 25, aus Quelle 28 wurde Zeile 25. Behoben: 1. Das Verzeichnis druckt src.nr. Gegenprobe an den echten Daten beider Laeufe: vorher fehlten in Lage 53 die Verweise [22] bis [28], jetzt loest jede der 25 Referenzen auf, in Lage 52 alle 22. 2. Keine Kappung des Verzeichnisses mehr. Im Umfang "Bericht" wurde bei 30 Eintraegen abgeschnitten, hoehere Referenzen im Text waren damit grundsaetzlich unaufloesbar. 3. Die Word-Ausgabe bekommt ueberhaupt erst ein Quellenverzeichnis. Bisher verwies der Text dort auf [1], [2], ohne dass die Datei die Nummern irgendwo aufloeste. 4. Die Quellenstatistik bezieht sich jetzt auf dieselbe Grundmenge wie das Verzeichnis. Vorher zaehlte sie alle gesammelten Artikel (49) und stand damit neben einem Verzeichnis mit 25 Eintraegen, was wie eine Aufwertung der Quellenbasis wirkte. Die Summe der Spalte Belege entspricht jetzt der Zahl der Eintraege, ein Hinweis nennt zusaetzlich die insgesamt ausgewerteten Meldungen. 5. Ein Verlag wird unter einem Namen gefuehrt. Bisher trennte der Bericht "Guardian UK", "Guardian World" und "The Guardian" oder "tagesschau" und "tagesschau.de" in eigene Zeilen und blaehte damit die Vielfaltsstatistik auf. Die neue services/media_registry.py bestimmt die Identitaet ueber die registrierbare Domain und waehlt den haeufigsten Namen. Aus 20 Statistikzeilen der EU-Fassung werden so 11 echte Medien. Sie kennt ausserdem Weiterleitungsportale und liest die Sprache aus der Adresse, sodass english.elpais.com nicht mehr als DE gefuehrt wird. Neu sind 22 Pruefungen, insgesamt laufen 118 ohne Netzzugriff und ohne Kosten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 Zeilen
8.6 KiB
Python
185 Zeilen
8.6 KiB
Python
"""Testet die Ausgabetreue des Quellenverzeichnisses.
|
|
|
|
Hintergrund ist der Vergleich zweier Berichte zur selben Lage am 01.08.2026
|
|
(Ceuta). Beide Fassungen verwiesen im Text auf Quellennummern, die im
|
|
gedruckten Verzeichnis nicht auftauchten. Ursache war nicht die Analyse, in der
|
|
Datenbank passten Text und Liste zusammen, sondern die Ausgabe: Das Template
|
|
druckte die laufende Zeilennummer statt der Quellennummer aus dem Lagebild.
|
|
Sobald die Nummern Luecken haben, verschiebt sich damit jeder Beleg.
|
|
|
|
Laeuft ohne Netzzugriff, ohne Kosten und ohne Datenbank.
|
|
|
|
Aufruf aus dem Projektstamm:
|
|
venv/bin/python tests/test_quellenausgabe.py
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
|
|
|
|
from jinja2 import Environment, FileSystemLoader # noqa: E402
|
|
|
|
import report_generator as rg # noqa: E402
|
|
from services import media_registry as mr # noqa: E402
|
|
|
|
ok = 0
|
|
fail = 0
|
|
|
|
|
|
def pruefe(name, bedingung, extra=""):
|
|
global ok, fail
|
|
if bedingung:
|
|
ok += 1
|
|
print(" OK " + name)
|
|
else:
|
|
fail += 1
|
|
print(" FEHL " + name + " " + str(extra))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# A. Domain-Erkennung
|
|
# ---------------------------------------------------------------------------
|
|
print("\nA. Registrierbare Domain")
|
|
pruefe("A1 www wird abgeschnitten",
|
|
mr.registrable_domain("https://www.theguardian.com/world/x") == "theguardian.com",
|
|
mr.registrable_domain("https://www.theguardian.com/world/x"))
|
|
pruefe("A2 mehrteilige Endung bleibt erhalten",
|
|
mr.registrable_domain("https://www.bbc.co.uk/news/articles/y") == "bbc.co.uk",
|
|
mr.registrable_domain("https://www.bbc.co.uk/news/articles/y"))
|
|
pruefe("A3 Sprachsubdomain zaehlt zur selben Domain",
|
|
mr.registrable_domain("https://english.elpais.com/spain/a.html") == "elpais.com",
|
|
mr.registrable_domain("https://english.elpais.com/spain/a.html"))
|
|
pruefe("A4 leere Eingabe bleibt leer", mr.registrable_domain("") == "")
|
|
pruefe("A5 Weiterleitungsportal wird erkannt",
|
|
mr.ist_aggregator("https://news.google.com/rss/articles/CBMi123?oc=5"))
|
|
pruefe("A6 echtes Medium ist kein Aggregator",
|
|
not mr.ist_aggregator("https://www.tagesschau.de/ausland/europa/x-100.html"))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# B. Ein Verlag, ein Name
|
|
# ---------------------------------------------------------------------------
|
|
print("\nB. Namensvereinheitlichung")
|
|
eintraege = [
|
|
("https://www.theguardian.com/world/a", "Guardian World"),
|
|
("https://www.theguardian.com/world/b", "Guardian UK"),
|
|
("https://www.theguardian.com/live/c", "The Guardian"),
|
|
("https://www.theguardian.com/live/d", "The Guardian"),
|
|
("https://www.france24.com/en/x", "www.france24.com"),
|
|
]
|
|
namen = mr.namen_je_domain(eintraege)
|
|
pruefe("B1 drei Guardian-Varianten ergeben einen Namen",
|
|
namen.get("theguardian.com") == "The Guardian", namen.get("theguardian.com"))
|
|
pruefe("B2 Hostname als Name faellt auf die Tabelle zurueck",
|
|
namen.get("france24.com") == "France24", namen.get("france24.com"))
|
|
pruefe("B3 unbekannte Domain bekommt einen lesbaren Namen",
|
|
mr.name_aus_domain("beispielzeitung.de") == "Beispielzeitung",
|
|
mr.name_aus_domain("beispielzeitung.de"))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# C. Quellenverzeichnis behaelt die Nummern aus dem Lagebild
|
|
# ---------------------------------------------------------------------------
|
|
print("\nC. Quellenaufbereitung")
|
|
# Nachgebaut nach Lage 53 (EU-Fassung): 25 Eintraege, aber Nummern bis 28.
|
|
quellen_roh = [{"nr": n, "name": f"Medium {n}", "url": f"https://medium{n}.de/artikel"}
|
|
for n in [1, 2, 3, 20, 22, 24, 26, 27, 28]]
|
|
incident = {"sources_json": json.dumps(quellen_roh, ensure_ascii=False)}
|
|
sources = rg._prepare_sources(incident)
|
|
nummern = [s["nr"] for s in sources]
|
|
pruefe("C1 Luecken in der Nummerierung bleiben erhalten",
|
|
nummern == [1, 2, 3, 20, 22, 24, 26, 27, 28], nummern)
|
|
pruefe("C2 hoechste Nummer ueberlebt die Aufbereitung", max(nummern) == 28, max(nummern))
|
|
|
|
# Buchstaben-Suffixe und kaputte Nummern duerfen nichts umwerfen
|
|
gemischt = {"sources_json": json.dumps([
|
|
{"nr": "7a", "name": "A", "url": "https://a.de/1"},
|
|
{"nr": None, "name": "B", "url": "https://b.de/1"},
|
|
{"nr": 3, "name": "C", "url": "https://c.de/1"},
|
|
], ensure_ascii=False)}
|
|
gemischte_nrn = [s["nr"] for s in rg._prepare_sources(gemischt)]
|
|
pruefe("C3 Suffix wird zur Zahl, fehlende Nummer bekommt die Position",
|
|
all(isinstance(n, int) for n in gemischte_nrn) and 7 in gemischte_nrn,
|
|
gemischte_nrn)
|
|
|
|
# Namen im Verzeichnis vereinheitlicht
|
|
mehrfach = {"sources_json": json.dumps([
|
|
{"nr": 1, "name": "Guardian World", "url": "https://www.theguardian.com/a"},
|
|
{"nr": 2, "name": "The Guardian", "url": "https://www.theguardian.com/b"},
|
|
{"nr": 3, "name": "The Guardian", "url": "https://www.theguardian.com/c"},
|
|
], ensure_ascii=False)}
|
|
namen_liste = {s["name"] for s in rg._prepare_sources(mehrfach)}
|
|
pruefe("C4 Verzeichnis fuehrt den Verlag unter einem Namen",
|
|
namen_liste == {"The Guardian"}, namen_liste)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# D. Statistik und Verzeichnis widersprechen sich nicht mehr
|
|
# ---------------------------------------------------------------------------
|
|
print("\nD. Quellenstatistik")
|
|
artikel = [
|
|
{"source": "France24", "source_url": "https://www.france24.com/en/1", "language": "en"},
|
|
{"source": "France24", "source_url": "https://www.france24.com/en/2", "language": "en"},
|
|
{"source": "El País", "source_url": "https://english.elpais.com/spain/1.html", "language": "de"},
|
|
{"source": "tagesschau", "source_url": "https://www.tagesschau.de/1.html", "language": "de"},
|
|
{"source": "n-tv", "source_url": "https://www.n-tv.de/1.html", "language": "de"},
|
|
]
|
|
zitiert = rg._prepare_sources({"sources_json": json.dumps([
|
|
{"nr": 1, "name": "France24", "url": "https://www.france24.com/en/1"},
|
|
{"nr": 2, "name": "France24", "url": "https://www.france24.com/en/2"},
|
|
{"nr": 3, "name": "El País", "url": "https://english.elpais.com/spain/1.html"},
|
|
{"nr": 4, "name": "tagesschau", "url": "https://www.tagesschau.de/1.html"},
|
|
], ensure_ascii=False)})
|
|
stats = rg._prepare_source_stats(zitiert, artikel)
|
|
summe = sum(s["count"] for s in stats)
|
|
pruefe("D1 Summe der Belege entspricht dem Verzeichnis", summe == len(zitiert), (summe, len(zitiert)))
|
|
pruefe("D2 zwei France24-Belege ergeben eine Zeile",
|
|
[s for s in stats if s["name"] == "France24"][0]["count"] == 2, stats)
|
|
elpais = [s for s in stats if "Pa" in s["name"] or "País" in s["name"]]
|
|
pruefe("D3 englische Ausgabe wird nicht als DE gefuehrt",
|
|
elpais and elpais[0]["languages"] == "EN", elpais)
|
|
notiz = rg._source_stats_note(zitiert, artikel)
|
|
pruefe("D4 Hinweis nennt zitierte Belege und ausgewertete Meldungen",
|
|
"4 im Lagebild zitierten Belege" in notiz and "5 Meldungen" in notiz, notiz)
|
|
pruefe("D5 Hinweis nutzt echte Umlaute", "zaehlt" not in notiz and "zählt" in notiz, notiz)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# E. Das Template druckt die Quellennummer, nicht die Zeilennummer
|
|
# ---------------------------------------------------------------------------
|
|
print("\nE. Ausgabe im Bericht")
|
|
env = Environment(loader=FileSystemLoader(str(rg.TEMPLATE_DIR)))
|
|
template = env.get_template("report.html")
|
|
html = template.render(
|
|
incident={"title": "Testlage", "type": "adhoc"},
|
|
incident_type_label="Live-Monitoring",
|
|
report_date="01.08.2026, 20:39 Uhr",
|
|
creator="test@aegis-sight.de",
|
|
logo_base64="",
|
|
executive_summary="",
|
|
zusammenfassung_title="Neueste Entwicklungen",
|
|
sections={"quellen"},
|
|
scope="report",
|
|
lagebild_html="",
|
|
lagebild_timestamp="",
|
|
sources=sources,
|
|
fact_checks=[],
|
|
source_stats=[{"name": "Medium 28", "count": 1, "languages": "DE"}],
|
|
source_stats_note="Hinweistext zur Prüfung",
|
|
timeline=[],
|
|
articles=[],
|
|
meta={},
|
|
include_branding=True,
|
|
)
|
|
zeilen = [z for z in html.splitlines() if "medium28.de" in z]
|
|
pruefe("E1 letzter Eintrag wird als 28 gedruckt, nicht als 9",
|
|
zeilen and ">28<" in zeilen[0] and ">9<" not in zeilen[0], zeilen[:1])
|
|
pruefe("E2 Statistikspalte heisst Belege", "<th>Belege</th>" in html)
|
|
pruefe("E3 Hinweis unter der Statistik erscheint", "Hinweistext zur Prüfung" in html)
|
|
pruefe("E4 alle Eintraege erscheinen, nichts wird gekappt",
|
|
all(f"medium{n}.de" in html for n in [1, 2, 3, 20, 22, 24, 26, 27, 28]))
|
|
|
|
print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen")
|
|
sys.exit(1 if fail else 0)
|