Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
|
|
|
|
"""Tests für app.wahlperioden und app.auswertungen.
|
|
|
|
|
|
|
|
|
|
|
|
Issue #58 + Roadmap #59 Phase C. Verifiziert die Aggregations-Logik
|
|
|
|
|
|
gegen eine in-memory SQLite-DB mit kontrollierten Sample-Assessments.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
from app.auswertungen import (
|
|
|
|
|
|
aggregate_matrix,
|
|
|
|
|
|
aggregate_zeitreihe,
|
|
|
|
|
|
export_long_format,
|
|
|
|
|
|
)
|
|
|
|
|
|
from app.wahlperioden import all_wahlperioden, wahlperiode_for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# wahlperioden helper
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWahlperiodeFor:
|
|
|
|
|
|
def test_current_wp_for_recent_date(self):
|
|
|
|
|
|
assert wahlperiode_for("2026-03-18", "MV") == "MV-WP8"
|
|
|
|
|
|
|
|
|
|
|
|
def test_previous_wp_for_old_date(self):
|
|
|
|
|
|
# MV WP8 startete am 26.10.2021 — alles davor ist WP7
|
|
|
|
|
|
assert wahlperiode_for("2020-01-01", "MV") == "MV-WP7"
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_bl_returns_none(self):
|
|
|
|
|
|
assert wahlperiode_for("2026-01-01", "XX") is None
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_datum_returns_current_wp(self):
|
|
|
|
|
|
# Wenn kein Datum bekannt → wir nehmen die aktuelle WP an,
|
|
|
|
|
|
# weil das die einzig sinnvolle Default-Annahme ist
|
|
|
|
|
|
assert wahlperiode_for("", "NRW") == "NRW-WP18"
|
|
|
|
|
|
|
|
|
|
|
|
def test_all_wahlperioden_lists_each_bl_twice(self):
|
|
|
|
|
|
out = all_wahlperioden()
|
Phase G: BundestagAdapter via DIP-API (#56)
Schließt #56 (Bundespolitik überprüfbar machen). Neuer
``BundestagAdapter`` in ``app/parlamente.py``, neuer ``BUND``-Eintrag in
``app/bundeslaender.py`` als 17. Parlament-Slot.
API:
- DIP-Search-API auf ``search.dip.bundestag.de/api/v1/drucksache``
- API-Key aus ``dip-config.js`` gescraped (öffentlich, klartext)
- Auth via URL-Param ``?apikey=...`` plus ``Origin: https://dip.bundestag.de``-
Header (Origin-Locking, server-to-server-tauglich)
- Pagination via ``cursor``-Parameter, 100 Hits pro Page
- ``f.drucksachetyp=Antrag`` und ``f.wahlperiode=21`` als Server-Filter
Mapping:
- ``dokumentnummer`` → ``Drucksache.drucksache``
- ``titel`` → ``title``
- ``urheber[*].titel`` → durch ``parteien.extract_fraktionen`` zu
``["AfD"]``/``["GRÜNE"]``/etc. — die ``"Fraktion der AfD"``-
Schreibweise wird vom zentralen Mapper aus #55 bereits korrekt
geparst, kein Adapter-spezifisches Pattern nötig
- ``fundstelle.pdf_url`` → ``link``
- ``datum`` → bereits ISO ``YYYY-MM-DD``
``get_document(drucksache)`` nutzt ``f.dokumentnummer`` als direkter
Server-Filter, kein linearer Pagination-Scan.
BUND-Eintrag in ``bundeslaender.py``:
- ``code="BUND"``, ``parlament_name="Deutscher Bundestag"``,
``wahlperiode=21``, ``wahlperiode_start="2025-03-25"`` (Konstituierung
21. WP nach BTW 2025), ``regierungsfraktionen=["CDU", "CSU", "SPD"]``
(Kabinett Merz)
- ``aktiv=True`` — taucht automatisch in ``alle_bundeslaender()`` und
``aktive_bundeslaender()`` auf, damit die UI- und
Auswertungs-Pipelines BUND ohne zusätzliche Sonderpfade kennen
- 17 Einträge in ``BUNDESLAENDER`` statt 16 — Tests entsprechend
aktualisiert (``test_sixteen_bundeslaender_plus_bund``,
``test_alle_bundeslaender_returns_all``,
``test_all_wahlperioden_lists_each_bl_twice``)
Live-Probe direkt im Repo:
```
adapter: Deutscher Bundestag (DIP), wahlperiode=21
search returned 5 docs
21/5136 2026-03-31 | ['AfD'] | Transparenz, Wirtschaftlichkeit ...
21/5064 2026-03-27 | ['GRÜNE'] | Ausverkauf der Energieinfrastruktur ...
21/5059 2026-03-27 | ['AfD'] | Berufsfreiheit für Selbstständige ...
get_document('21/5136') -> drucksache=21/5136
```
176 Unit-Tests grün, Live-Verifikation Sub-A im Container nach Deploy.
Refs: #56, #59 (Phase G)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:04:11 +02:00
|
|
|
|
# 16 Bundesländer + BUND × 2 WPs = 34 Einträge (#56 fügt BUND hinzu)
|
|
|
|
|
|
assert len(out) == 34
|
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
|
|
|
|
# Aktuelle und vorherige WP für NRW
|
|
|
|
|
|
assert "NRW-WP18" in out
|
|
|
|
|
|
assert "NRW-WP17" in out
|
Phase G: BundestagAdapter via DIP-API (#56)
Schließt #56 (Bundespolitik überprüfbar machen). Neuer
``BundestagAdapter`` in ``app/parlamente.py``, neuer ``BUND``-Eintrag in
``app/bundeslaender.py`` als 17. Parlament-Slot.
API:
- DIP-Search-API auf ``search.dip.bundestag.de/api/v1/drucksache``
- API-Key aus ``dip-config.js`` gescraped (öffentlich, klartext)
- Auth via URL-Param ``?apikey=...`` plus ``Origin: https://dip.bundestag.de``-
Header (Origin-Locking, server-to-server-tauglich)
- Pagination via ``cursor``-Parameter, 100 Hits pro Page
- ``f.drucksachetyp=Antrag`` und ``f.wahlperiode=21`` als Server-Filter
Mapping:
- ``dokumentnummer`` → ``Drucksache.drucksache``
- ``titel`` → ``title``
- ``urheber[*].titel`` → durch ``parteien.extract_fraktionen`` zu
``["AfD"]``/``["GRÜNE"]``/etc. — die ``"Fraktion der AfD"``-
Schreibweise wird vom zentralen Mapper aus #55 bereits korrekt
geparst, kein Adapter-spezifisches Pattern nötig
- ``fundstelle.pdf_url`` → ``link``
- ``datum`` → bereits ISO ``YYYY-MM-DD``
``get_document(drucksache)`` nutzt ``f.dokumentnummer`` als direkter
Server-Filter, kein linearer Pagination-Scan.
BUND-Eintrag in ``bundeslaender.py``:
- ``code="BUND"``, ``parlament_name="Deutscher Bundestag"``,
``wahlperiode=21``, ``wahlperiode_start="2025-03-25"`` (Konstituierung
21. WP nach BTW 2025), ``regierungsfraktionen=["CDU", "CSU", "SPD"]``
(Kabinett Merz)
- ``aktiv=True`` — taucht automatisch in ``alle_bundeslaender()`` und
``aktive_bundeslaender()`` auf, damit die UI- und
Auswertungs-Pipelines BUND ohne zusätzliche Sonderpfade kennen
- 17 Einträge in ``BUNDESLAENDER`` statt 16 — Tests entsprechend
aktualisiert (``test_sixteen_bundeslaender_plus_bund``,
``test_alle_bundeslaender_returns_all``,
``test_all_wahlperioden_lists_each_bl_twice``)
Live-Probe direkt im Repo:
```
adapter: Deutscher Bundestag (DIP), wahlperiode=21
search returned 5 docs
21/5136 2026-03-31 | ['AfD'] | Transparenz, Wirtschaftlichkeit ...
21/5064 2026-03-27 | ['GRÜNE'] | Ausverkauf der Energieinfrastruktur ...
21/5059 2026-03-27 | ['AfD'] | Berufsfreiheit für Selbstständige ...
get_document('21/5136') -> drucksache=21/5136
```
176 Unit-Tests grün, Live-Verifikation Sub-A im Container nach Deploy.
Refs: #56, #59 (Phase G)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:04:11 +02:00
|
|
|
|
# BUND ist auch dabei
|
|
|
|
|
|
assert "BUND-WP21" in out
|
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.
Backend (`app/auswertungen.py`):
- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
(n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
(BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
(deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
NICHT mit dem RP-FW zusammengezählt
Wahlperioden-Helper (`app/wahlperioden.py`):
- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns
Endpoints in `app/main.py`:
- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download
Frontend (`app/templates/auswertungen.html`):
- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling
Tests (`tests/test_auswertungen.py`):
- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)
Refs: #58, #45, #59 (Phase C)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Test-DB-Fixture
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def sample_db(tmp_path: Path) -> Path:
|
|
|
|
|
|
"""Lege eine Mini-Assessments-DB an, die typische Fälle abdeckt."""
|
|
|
|
|
|
db = tmp_path / "test_assessments.db"
|
|
|
|
|
|
conn = sqlite3.connect(str(db))
|
|
|
|
|
|
conn.execute("""
|
|
|
|
|
|
CREATE TABLE assessments (
|
|
|
|
|
|
drucksache TEXT PRIMARY KEY,
|
|
|
|
|
|
title TEXT,
|
|
|
|
|
|
fraktionen TEXT,
|
|
|
|
|
|
datum TEXT,
|
|
|
|
|
|
bundesland TEXT,
|
|
|
|
|
|
gwoe_score REAL,
|
|
|
|
|
|
link TEXT,
|
|
|
|
|
|
gwoe_begruendung TEXT,
|
|
|
|
|
|
gwoe_matrix TEXT,
|
|
|
|
|
|
gwoe_schwerpunkt TEXT,
|
|
|
|
|
|
wahlprogramm_scores TEXT,
|
|
|
|
|
|
verbesserungen TEXT,
|
|
|
|
|
|
staerken TEXT,
|
|
|
|
|
|
schwaechen TEXT,
|
|
|
|
|
|
empfehlung TEXT,
|
|
|
|
|
|
empfehlung_symbol TEXT,
|
|
|
|
|
|
verbesserungspotenzial TEXT,
|
|
|
|
|
|
themen TEXT,
|
|
|
|
|
|
antrag_zusammenfassung TEXT,
|
|
|
|
|
|
antrag_kernpunkte TEXT,
|
|
|
|
|
|
source TEXT,
|
|
|
|
|
|
model TEXT,
|
|
|
|
|
|
created_at TEXT,
|
|
|
|
|
|
updated_at TEXT
|
|
|
|
|
|
)
|
|
|
|
|
|
""")
|
|
|
|
|
|
samples = [
|
|
|
|
|
|
# NRW WP18 — drei Anträge, zwei Parteien
|
|
|
|
|
|
("18/100", "NRW", "2024-01-15", '["CDU"]', 7.0),
|
|
|
|
|
|
("18/101", "NRW", "2024-02-15", '["SPD"]', 8.0),
|
|
|
|
|
|
("18/102", "NRW", "2024-03-15", '["CDU"]', 5.0),
|
|
|
|
|
|
# MV WP8 — Koalitionsantrag (zwei Parteien zählen beide)
|
|
|
|
|
|
("8/200", "MV", "2024-04-01", '["SPD","LINKE"]', 6.0),
|
|
|
|
|
|
("8/201", "MV", "2025-01-10", '["AfD"]', 2.0),
|
|
|
|
|
|
# MV WP7 — historischer Antrag vor wahlperiode_start (2021-10-26)
|
|
|
|
|
|
("7/100", "MV", "2020-05-01", '["CDU"]', 4.0),
|
|
|
|
|
|
# BB — FREIE WÄHLER soll als BVB-FW kanonisiert werden
|
|
|
|
|
|
("8/2", "BB", "2024-10-17", '["FREIE WÄHLER"]', 6.5),
|
|
|
|
|
|
]
|
|
|
|
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
|
|
for ds, bl, dat, fr, sc in samples:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
"INSERT INTO assessments (drucksache, title, fraktionen, datum, bundesland, "
|
|
|
|
|
|
"gwoe_score, source, model, created_at, updated_at) VALUES "
|
|
|
|
|
|
"(?, ?, ?, ?, ?, ?, 'test', 'test', ?, ?)",
|
|
|
|
|
|
(ds, f"Test {ds}", fr, dat, bl, sc, now, now),
|
|
|
|
|
|
)
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# aggregate_matrix
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAggregateMatrix:
|
|
|
|
|
|
def test_total_count(self, sample_db):
|
|
|
|
|
|
m = aggregate_matrix(db_path=sample_db)
|
|
|
|
|
|
assert m["total"] == 7
|
|
|
|
|
|
|
|
|
|
|
|
def test_bundeslaender_listed(self, sample_db):
|
|
|
|
|
|
m = aggregate_matrix(db_path=sample_db)
|
|
|
|
|
|
assert set(m["bundeslaender"]) == {"NRW", "MV", "BB"}
|
|
|
|
|
|
|
|
|
|
|
|
def test_nrw_cdu_average(self, sample_db):
|
|
|
|
|
|
# NRW-CDU: 7.0 + 5.0 → Avg 6.0, n=2
|
|
|
|
|
|
m = aggregate_matrix(db_path=sample_db)
|
|
|
|
|
|
cell = m["cells"]["NRW"]["CDU"]
|
|
|
|
|
|
assert cell["n"] == 2
|
|
|
|
|
|
assert cell["avg"] == 6.0
|
|
|
|
|
|
|
|
|
|
|
|
def test_koalition_counts_both_parties(self, sample_db):
|
|
|
|
|
|
# MV-SPD und MV-LINKE bekommen beide den Score 6.0 (n=1)
|
|
|
|
|
|
m = aggregate_matrix(db_path=sample_db)
|
|
|
|
|
|
assert m["cells"]["MV"]["SPD"]["n"] == 1
|
|
|
|
|
|
assert m["cells"]["MV"]["LINKE"]["n"] == 1
|
|
|
|
|
|
assert m["cells"]["MV"]["SPD"]["avg"] == 6.0
|
|
|
|
|
|
|
|
|
|
|
|
def test_filter_by_wahlperiode(self, sample_db):
|
|
|
|
|
|
# NRW-WP18-Filter → nur die 3 NRW-Anträge
|
|
|
|
|
|
m = aggregate_matrix(filter_wp="NRW-WP18", db_path=sample_db)
|
|
|
|
|
|
assert m["total"] == 3
|
|
|
|
|
|
assert set(m["bundeslaender"]) == {"NRW"}
|
|
|
|
|
|
|
|
|
|
|
|
def test_filter_excludes_old_wp(self, sample_db):
|
|
|
|
|
|
# MV-WP8 darf den 7/100-Antrag (datum=2020) NICHT enthalten
|
|
|
|
|
|
m = aggregate_matrix(filter_wp="MV-WP8", db_path=sample_db)
|
|
|
|
|
|
assert m["total"] == 2 # nur 8/200 und 8/201
|
|
|
|
|
|
# CDU darf NICHT vorkommen, weil der CDU-Antrag in WP7 war
|
|
|
|
|
|
assert "CDU" not in m["cells"].get("MV", {})
|
|
|
|
|
|
|
|
|
|
|
|
def test_bb_freie_waehler_normalized_to_bvb(self, sample_db):
|
|
|
|
|
|
# Die BB-FW-Drucksache muss als BVB-FW gezählt werden, NICHT als
|
|
|
|
|
|
# generisches FREIE WÄHLER — das ist der eigentliche Mehrwert
|
|
|
|
|
|
# des Parteinamen-Mappers (#55)
|
|
|
|
|
|
m = aggregate_matrix(db_path=sample_db)
|
|
|
|
|
|
bb_cells = m["cells"]["BB"]
|
|
|
|
|
|
assert "BVB-FW" in bb_cells
|
|
|
|
|
|
assert bb_cells["BVB-FW"]["n"] == 1
|
|
|
|
|
|
assert "FREIE WÄHLER" not in bb_cells
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_db_returns_empty_matrix(self, tmp_path):
|
|
|
|
|
|
m = aggregate_matrix(db_path=tmp_path / "missing.db")
|
|
|
|
|
|
assert m["total"] == 0
|
|
|
|
|
|
assert m["bundeslaender"] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# aggregate_zeitreihe
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAggregateZeitreihe:
|
|
|
|
|
|
def test_mv_cdu_two_wps(self, sample_db):
|
|
|
|
|
|
# MV-CDU hat einen Eintrag in WP7 (4.0) und keinen in WP8
|
|
|
|
|
|
z = aggregate_zeitreihe("MV", "CDU", db_path=sample_db)
|
|
|
|
|
|
wps = {entry["wp"]: entry for entry in z["wahlperioden"]}
|
|
|
|
|
|
assert "MV-WP7" in wps
|
|
|
|
|
|
assert wps["MV-WP7"]["avg"] == 4.0
|
|
|
|
|
|
assert wps["MV-WP7"]["n"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
def test_nrw_cdu_one_wp(self, sample_db):
|
|
|
|
|
|
z = aggregate_zeitreihe("NRW", "CDU", db_path=sample_db)
|
|
|
|
|
|
assert len(z["wahlperioden"]) == 1
|
|
|
|
|
|
assert z["wahlperioden"][0]["avg"] == 6.0
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_combination_empty(self, sample_db):
|
|
|
|
|
|
z = aggregate_zeitreihe("NRW", "AfD", db_path=sample_db)
|
|
|
|
|
|
assert z["wahlperioden"] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# export_long_format
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExportLongFormat:
|
|
|
|
|
|
def test_csv_has_header(self, sample_db):
|
|
|
|
|
|
csv_text = export_long_format(db_path=sample_db)
|
|
|
|
|
|
first_line = csv_text.splitlines()[0]
|
|
|
|
|
|
assert "drucksache" in first_line
|
|
|
|
|
|
assert "bundesland" in first_line
|
|
|
|
|
|
assert "wahlperiode" in first_line
|
|
|
|
|
|
assert "partei" in first_line
|
|
|
|
|
|
assert "gwoe_score" in first_line
|
|
|
|
|
|
|
|
|
|
|
|
def test_koalition_yields_two_rows(self, sample_db):
|
|
|
|
|
|
csv_text = export_long_format(db_path=sample_db)
|
|
|
|
|
|
lines = csv_text.splitlines()[1:] # ohne Header
|
|
|
|
|
|
# 8/200 ist Koalitionsantrag (SPD+LINKE) → 2 Zeilen
|
|
|
|
|
|
mv_8_200_lines = [l for l in lines if l.startswith("8/200,")]
|
|
|
|
|
|
assert len(mv_8_200_lines) == 2
|
|
|
|
|
|
|
|
|
|
|
|
def test_bb_fw_normalized_in_csv(self, sample_db):
|
|
|
|
|
|
csv_text = export_long_format(db_path=sample_db)
|
|
|
|
|
|
assert "BVB-FW" in csv_text
|
|
|
|
|
|
# Generic FREIE WÄHLER darf in der Zeile NICHT auftauchen
|
|
|
|
|
|
bb_lines = [l for l in csv_text.splitlines() if "BB" in l and "8/2," in l]
|
|
|
|
|
|
assert any("BVB-FW" in l for l in bb_lines)
|