gwoe-antragspruefer/tests/test_auswertungen_stimmverhalten.py
Dotty Dotter d81753c4fb feat(#167): Empfehlungs-Konsistenz + CSV-Export Stimmverhalten
Phase-2-Erweiterungen des Stimmverhalten-Tabs:

**1. Empfehlungs-Konsistenz (#167):**
Pro Fraktion: Anteil der Anträge mit GWÖ-Empfehlung
"Uneingeschränkt unterstützen" oder "Unterstützen mit Änderungen",
bei denen die Fraktion trotzdem NEIN gestimmt hat. Orthogonal zur
Heuchelei-Quote — prüft NICHT gegen Wahlprogramm-Treue, sondern gegen
die GWÖ-Empfehlung des Systems.

- `aggregate_empfehlungs_konsistenz()` in app/auswertungen.py
- `GET /api/auswertungen/empfehlungs-konsistenz`
- 5. Chart-Sub-Section im Stimmverhalten-Tab (rote Bar Chart, 0..100%)

**2. CSV-Export (Phase-1-Querschnitts-TODO):**
Long-Format-CSV mit Spalten: drucksache, bundesland, wahlperiode, datum,
gwoe_score, empfehlung, partei, vote, ist_antragsteller. Macht alle
Stimmverhalten-Aussagen wissenschaftlich auswertbar (R/pandas/Excel).

- `export_stimmverhalten_csv()` in app/auswertungen.py
- `GET /api/auswertungen/stimmverhalten.csv` mit
  Filter-Parametern bundesland/wahlperiode/exclude_antragsteller
- "CSV-Export"-Button im Stimmverhalten-Tab neben dem Toggle

**Tests:** 27 Stimmverhalten-Tests (war 18, +4 Empfehlungs-Konsistenz,
+5 CSV-Export). Fixture um `empfehlung`-Spalte erweitert.

Suite: 989 Tests grün (war 980).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:56:35 +02:00

495 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests fuer app.auswertungen.aggregate_stimm_index/heuchelei/pro_wert/cross_bl.
Verifiziert die JOIN-Aggregations-Logik gegen eine in-memory SQLite-DB
mit kontrollierten Sample-Assessments und Sample-Vote-Results.
"""
from __future__ import annotations
import json
import sqlite3
from datetime import datetime
from pathlib import Path
import pytest
from app.auswertungen import (
aggregate_empfehlungs_konsistenz,
aggregate_heuchelei,
aggregate_stimm_index,
aggregate_stimm_index_cross_bl,
aggregate_stimm_index_pro_wert,
export_stimmverhalten_csv,
_wert_score_for_assessment,
)
# ─────────────────────────────────────────────────────────────────────────────
# Fixture: assessments + plenum_vote_results
# ─────────────────────────────────────────────────────────────────────────────
def _matrix(*ratings: tuple[str, int]) -> str:
"""Build a gwoe_matrix JSON-Array. Args: (field, rating) tuples."""
return json.dumps([
{"field": f, "label": f, "aspect": f[-1], "rating": r,
"symbol": "++" if r >= 4 else ("+" if r >= 1 else "")}
for f, r in ratings
])
def _wp_scores(*items: tuple[str, int, bool]) -> str:
"""Build a wahlprogramm_scores JSON-Array. Args: (fraktion, score, is_antrag)."""
return json.dumps([
{
"fraktion": fr,
"ist_antragsteller": ist,
"wahlprogramm": {"score": sc, "begründung": "stub"},
}
for fr, sc, ist in items
])
@pytest.fixture
def sample_db(tmp_path: Path) -> Path:
"""Lege eine Mini-DB mit assessments + plenum_vote_results an, die typische
Faelle abdeckt:
- NRW Antrag mit hohem GWOe-Score, GRÜNE/SPD ja, CDU/AfD nein
- NRW Antrag mit niedrigem GWOe-Score, AfD ja, GRÜNE nein (Anti-Pattern)
- NRW Antrag mit hohem Score, CDU Antragsteller (sollte bei Default
excluded werden)
- MV Anträge fuer Cross-BL-Test
- Heuchelei-Sample: GRÜNE-Antrag mit GRÜNE-wahlprogramm_score=9 aber
GRÜNE stimmt NEIN (synthetisch konstruiert)
"""
db = tmp_path / "test_stimmverhalten.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
)
""")
conn.execute("""
CREATE TABLE plenum_vote_results (
bundesland TEXT NOT NULL,
drucksache TEXT NOT NULL,
ergebnis TEXT NOT NULL,
einstimmig INTEGER NOT NULL DEFAULT 0,
fraktionen_ja TEXT NOT NULL DEFAULT '[]',
fraktionen_nein TEXT NOT NULL DEFAULT '[]',
fraktionen_enthaltung TEXT NOT NULL DEFAULT '[]',
quelle_protokoll TEXT NOT NULL,
quelle_url TEXT,
parsed_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (bundesland, drucksache, quelle_protokoll)
)
""")
# ─── Assessments ───
# Format: (ds, bl, datum, fraktionen, score, matrix, wp_scores, empfehlung)
now = datetime.utcnow().isoformat()
UNTER = "Uneingeschränkt unterstützen"
AENDR = "Unterstützen mit Änderungen"
UEBR = "Überarbeiten"
ABLEH = "Ablehnen"
assessments = [
# NRW WP18 — High-Score-Anträge mit GWÖ-Empfehlung positiv
("18/A", "NRW", "2024-01-15", '["GRÜNE"]', 8.5,
_matrix(("A1", 4), ("B2", 3), ("C3", 5), ("D4", 4), ("E5", 5)),
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
("SPD", 7, False), ("AfD", 1, False)), UNTER),
("18/B", "NRW", "2024-02-15", '["GRÜNE"]', 7.5,
_matrix(("A2", 3), ("C3", 4), ("D4", 3), ("E5", 4)),
_wp_scores(("GRÜNE", 8, True), ("CDU", 2, False),
("SPD", 6, False), ("AfD", 1, False)), AENDR),
("18/C", "NRW", "2024-03-15", '["GRÜNE"]', 9.0,
_matrix(("A3", 5), ("D4", 4), ("E5", 5)),
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
("SPD", 7, False), ("AfD", 1, False)), UNTER),
("18/D", "NRW", "2024-04-15", '["GRÜNE"]', 7.0,
_matrix(("B2", 3), ("C2", 4), ("D2", 3)),
_wp_scores(("GRÜNE", 8, True), ("CDU", 4, False),
("SPD", 7, False), ("AfD", 2, False)), AENDR),
("18/E", "NRW", "2024-05-15", '["GRÜNE"]', 8.0,
_matrix(("A1", 4), ("B1", 3), ("E5", 4)),
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
("SPD", 7, False), ("AfD", 1, False)), UNTER),
# AfD-Antrag mit niedrigem Score → empfehlung=ablehnen
("18/F", "NRW", "2024-06-15", '["AfD"]', 2.0,
_matrix(("A1", -3), ("B2", -2), ("E5", -4)),
_wp_scores(("AfD", 8, True), ("GRÜNE", 1, False),
("CDU", 4, False), ("SPD", 2, False)), ABLEH),
("18/G", "NRW", "2024-07-15", '["AfD"]', 1.5,
_matrix(("A1", -4), ("E5", -5)),
_wp_scores(("AfD", 9, True), ("GRÜNE", 1, False),
("CDU", 3, False), ("SPD", 1, False)), ABLEH),
("18/H", "NRW", "2024-08-15", '["CDU"]', 5.0,
_matrix(("D4", 1), ("D3", 0)),
_wp_scores(("CDU", 7, True), ("GRÜNE", 4, False),
("SPD", 5, False), ("AfD", 3, False)), UEBR),
("18/I", "NRW", "2024-09-15", '["SPD"]', 6.5,
_matrix(("B2", 3), ("D2", 2)),
_wp_scores(("SPD", 8, True), ("GRÜNE", 6, False),
("CDU", 4, False), ("AfD", 1, False)), AENDR),
("18/J", "NRW", "2024-10-15", '["SPD"]', 4.0,
_matrix(("D4", 1), ("E5", 0)),
_wp_scores(("SPD", 5, True), ("GRÜNE", 3, False),
("CDU", 5, False), ("AfD", 2, False)), UEBR),
# MV WP8 fuer Cross-BL
("8/A", "MV", "2024-04-01", '["GRÜNE"]', 7.0,
_matrix(("A1", 3), ("D4", 4)),
_wp_scores(("GRÜNE", 8, True), ("CDU", 4, False),
("SPD", 6, False), ("AfD", 2, False)), AENDR),
("8/B", "MV", "2024-05-01", '["GRÜNE"]', 8.0,
_matrix(("B2", 4), ("D4", 5)),
_wp_scores(("GRÜNE", 9, True), ("CDU", 3, False),
("SPD", 7, False), ("AfD", 1, False)), UNTER),
("8/C", "MV", "2024-06-01", '["AfD"]', 2.0,
_matrix(("A1", -3), ("E5", -4)),
_wp_scores(("AfD", 9, True), ("GRÜNE", 1, False),
("CDU", 3, False), ("SPD", 2, False)), ABLEH),
]
for ds, bl, dat, fr, sc, mat, wps, emp in assessments:
conn.execute(
"INSERT INTO assessments (drucksache, title, fraktionen, datum, "
"bundesland, gwoe_score, gwoe_matrix, wahlprogramm_scores, "
"empfehlung, source, model, created_at, updated_at) VALUES "
"(?, ?, ?, ?, ?, ?, ?, ?, ?, 'test', 'test', ?, ?)",
(ds, f"Test {ds}", fr, dat, bl, sc, mat, wps, emp, now, now),
)
# ─── Vote-Results ───
# Jeder NRW-Hoch-Antrag (A-E,I): GRÜNE+SPD ja, CDU+AfD nein
# AfD-Antrag F,G: AfD ja, GRÜNE+SPD+CDU nein
# CDU-Antrag H: CDU ja, GRÜNE+SPD+AfD nein
# SPD-Antrag J: SPD ja, GRÜNE+CDU+AfD nein
# MV-Hoch-Anträge: GRÜNE+SPD ja, CDU+AfD nein
# MV-AfD-Antrag: AfD ja, andere nein
votes = [
("NRW", "18/A", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-A"),
("NRW", "18/B", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-B"),
("NRW", "18/C", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-C"),
("NRW", "18/D", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-D"),
("NRW", "18/E", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-E"),
("NRW", "18/F", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "MMP18-F"),
("NRW", "18/G", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "MMP18-G"),
("NRW", "18/H", "angenommen", '["CDU","AfD"]', '["GRÜNE","SPD"]', "MMP18-H"),
("NRW", "18/I", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "MMP18-I"),
("NRW", "18/J", "abgelehnt", '["SPD"]', '["GRÜNE","CDU","AfD"]', "MMP18-J"),
("MV", "8/A", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "PlPr8-A"),
("MV", "8/B", "angenommen", '["GRÜNE","SPD"]', '["CDU","AfD"]', "PlPr8-B"),
("MV", "8/C", "abgelehnt", '["AfD"]', '["GRÜNE","SPD","CDU"]', "PlPr8-C"),
]
for bl, ds, erg, ja, nein, prot in votes:
conn.execute(
"INSERT INTO plenum_vote_results "
"(bundesland, drucksache, ergebnis, fraktionen_ja, fraktionen_nein, "
" quelle_protokoll) VALUES (?, ?, ?, ?, ?, ?)",
(bl, ds, erg, ja, nein, prot),
)
conn.commit()
conn.close()
return db
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_stimm_index
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateStimmIndex:
def test_grüne_high_index(self, sample_db):
"""GRÜNE: stimmt JA bei hohen GWÖ-Anträgen, NEIN bei niedrigen → hoher Index."""
out = aggregate_stimm_index(db_path=sample_db, min_n=2)
gruene = next(f for f in out["fraktionen"] if f["partei"] == "GRÜNE")
# JA: 18/A,B,D,E,I + 8/A,B (NRW+MV high score). Antragsteller-exclude
# entfernt 18/A-E + 8/A,B fuer GRÜNE → bleibt 18/I als JA. NEIN: 18/H + 18/J + 18/F + 18/G + 8/C.
assert gruene["n_ja"] >= 1
assert gruene["n_nein"] >= 4
# Wenn beide N>0: stimm_index sollte positiv sein
if gruene["stimm_index"] is not None:
assert gruene["stimm_index"] > 0
def test_afd_low_index(self, sample_db):
"""AfD: stimmt NEIN bei hohen GWÖ-Anträgen, JA bei niedrigen → negativer Index."""
out = aggregate_stimm_index(db_path=sample_db, min_n=2)
afd = next(f for f in out["fraktionen"] if f["partei"] == "AfD")
# AfD ist Antragsteller bei 18/F,G,8/C → diese werden ausgeschlossen.
# Bleibt: NEIN bei 18/A-E, 18/I, 18/J, 8/A, 8/B. AfD JA bei 18/H (CDU-Antrag).
assert afd["n_nein"] >= 4
# Wenn JA und NEIN beide gefuellt: Index negativ
if afd["stimm_index"] is not None:
assert afd["stimm_index"] < 0
def test_exclude_antragsteller_default(self, sample_db):
"""Default schliesst eigene Antraege aus."""
with_excl = aggregate_stimm_index(
db_path=sample_db, exclude_antragsteller=True, min_n=2,
)
without_excl = aggregate_stimm_index(
db_path=sample_db, exclude_antragsteller=False, min_n=2,
)
# GRÜNE hat 7 eigene Anträge → ohne Exclude mehr n_ja
gr_with = next(f for f in with_excl["fraktionen"] if f["partei"] == "GRÜNE")
gr_without = next(f for f in without_excl["fraktionen"] if f["partei"] == "GRÜNE")
assert gr_without["n_ja"] > gr_with["n_ja"]
def test_filter_by_bundesland(self, sample_db):
nrw = aggregate_stimm_index(db_path=sample_db, filter_bl="NRW", min_n=2)
assert nrw["n_assessments_matched"] == 10
mv = aggregate_stimm_index(db_path=sample_db, filter_bl="MV", min_n=2)
assert mv["n_assessments_matched"] == 3
def test_min_n_threshold(self, sample_db):
"""Fraktion mit n_ja=2, min_n=5 → ausreichend=False."""
out = aggregate_stimm_index(db_path=sample_db, min_n=5)
for f in out["fraktionen"]:
if f["n_ja"] < 5 or f["n_nein"] < 5:
assert f["ausreichend"] is False
def test_empty_db(self, tmp_path):
out = aggregate_stimm_index(db_path=tmp_path / "missing.db")
assert out["n_assessments_matched"] == 0
assert out["fraktionen"] == []
def test_sorted_by_index_desc(self, sample_db):
"""Output sorted by stimm_index descending — None at end."""
out = aggregate_stimm_index(db_path=sample_db, min_n=1)
indices = [f["stimm_index"] for f in out["fraktionen"]]
# All not-None values should be in descending order
not_none = [v for v in indices if v is not None]
assert not_none == sorted(not_none, reverse=True)
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_heuchelei
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateHeuchelei:
def test_returns_structure(self, sample_db):
out = aggregate_heuchelei(db_path=sample_db, min_n=1)
assert "fraktionen" in out
assert "n_assessments_matched" in out
def test_heuchelei_quote_calculation(self, sample_db):
out = aggregate_heuchelei(
db_path=sample_db, score_threshold=7.0, min_n=1,
)
# SPD: wahlprogramm_score>=7 in 18/A-E (5x). SPD stimmt JA in allen
# → heuchelei_quote = 0/5 = 0.
spd = next(f for f in out["fraktionen"] if f["partei"] == "SPD")
assert spd["n_im_programm"] >= 5
assert spd["heuchelei_quote"] == 0 or spd["heuchelei_quote"] is None
def test_threshold_filter(self, sample_db):
"""Hoher Threshold reduziert n_im_programm."""
low = aggregate_heuchelei(db_path=sample_db, score_threshold=1.0, min_n=1)
high = aggregate_heuchelei(db_path=sample_db, score_threshold=9.5, min_n=1)
# Bei threshold=1 sind quasi alle Fraktionen drin, bei 9.5 sehr wenige
low_total = sum(f["n_im_programm"] for f in low["fraktionen"])
high_total = sum(f["n_im_programm"] for f in high["fraktionen"])
assert low_total > high_total
# ─────────────────────────────────────────────────────────────────────────────
# _wert_score_for_assessment helper
# ─────────────────────────────────────────────────────────────────────────────
class TestWertScoreHelper:
def test_extracts_per_column(self):
matrix = [
{"field": "A1", "rating": 3},
{"field": "B1", "rating": 5},
{"field": "C2", "rating": 4},
]
result = _wert_score_for_assessment(matrix)
# Spalte 1 (Würde): A1=3 + B1=5 → Ø=4
assert result["1"] == 4.0
# Spalte 2 (Solidarität): C2=4 → Ø=4
assert result["2"] == 4.0
def test_empty_matrix(self):
assert _wert_score_for_assessment([]) == {}
def test_invalid_entries_skipped(self):
matrix = [
{"field": "A1", "rating": 3},
{"field": "", "rating": 5}, # empty field skipped
{"field": "X9", "rating": 2}, # invalid column suffix
"not a dict", # type error
]
result = _wert_score_for_assessment(matrix)
assert result == {"1": 3.0}
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_stimm_index_pro_wert
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateProWert:
def test_structure(self, sample_db):
out = aggregate_stimm_index_pro_wert(db_path=sample_db, min_n=1)
assert out["werte"] == [
"Menschenwürde", "Solidarität", "Ökologische Nachhaltigkeit",
"Soziale Gerechtigkeit", "Transparenz & Demokratie",
]
assert "cells" in out
for partei in out["fraktionen"]:
assert set(out["cells"][partei].keys()) == set(out["werte"])
def test_cell_format(self, sample_db):
out = aggregate_stimm_index_pro_wert(db_path=sample_db, min_n=1)
if not out["fraktionen"]:
pytest.skip("no parteien — DB empty")
first_partei = out["fraktionen"][0]
cell = out["cells"][first_partei]["Menschenwürde"]
assert "stimm_index" in cell
assert "n_ja" in cell
assert "n_nein" in cell
assert "ausreichend" in cell
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_stimm_index_cross_bl
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateCrossBl:
def test_structure(self, sample_db):
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=1)
assert "NRW" in out["bundeslaender"]
assert "MV" in out["bundeslaender"]
assert "fraktionen" in out
assert "fraktionen_alle" in out
def test_only_multi_bl_in_fraktionen(self, sample_db):
"""Fraktionen-Liste enthält nur Parteien mit ≥2 BL ausreichend-erfüllt."""
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=2)
# Bei min_n=2 sind die meisten Kombinationen nicht ausreichend.
# Test prueft nur Struktur: fraktionen ⊆ fraktionen_alle.
assert set(out["fraktionen"]).issubset(set(out["fraktionen_alle"]))
def test_cell_format(self, sample_db):
out = aggregate_stimm_index_cross_bl(db_path=sample_db, min_n=1)
if out["fraktionen_alle"]:
partei = out["fraktionen_alle"][0]
for bl in out["bundeslaender"]:
cell = out["cells"][partei][bl]
assert "stimm_index" in cell
assert "n_ja" in cell
assert "n_nein" in cell
# ─────────────────────────────────────────────────────────────────────────────
# aggregate_empfehlungs_konsistenz (#167)
# ─────────────────────────────────────────────────────────────────────────────
class TestAggregateEmpfehlungsKonsistenz:
def test_structure(self, sample_db):
out = aggregate_empfehlungs_konsistenz(db_path=sample_db, min_n=1)
assert "fraktionen" in out
assert "n_assessments_matched" in out
assert out["n_assessments_matched"] >= 1
def test_afd_high_konsistenz_quote(self, sample_db):
"""AfD stimmt NEIN bei allen GWÖ-positiv-empfohlenen Anträgen → Quote ~ 1.0."""
out = aggregate_empfehlungs_konsistenz(db_path=sample_db, min_n=1)
afd = next((f for f in out["fraktionen"] if f["partei"] == "AfD"), None)
assert afd is not None
# AfD wurde bei 18/A,B,C,D,E,8/A,8/B als NEIN eingetragen (alle UNTER/AENDR)
# und 18/I (AENDR) auch NEIN. → quote sollte hoch sein
assert afd["n_empfohlen"] >= 5
if afd["konsistenz_quote"] is not None:
assert afd["konsistenz_quote"] > 0.5
def test_grüne_low_konsistenz_quote(self, sample_db):
"""GRÜNE stimmt JA bei eigenen GWÖ-positiv Anträgen → Quote sehr niedrig."""
out = aggregate_empfehlungs_konsistenz(db_path=sample_db, min_n=1)
gr = next((f for f in out["fraktionen"] if f["partei"] == "GRÜNE"), None)
assert gr is not None
if gr["konsistenz_quote"] is not None:
assert gr["konsistenz_quote"] < 0.3
def test_only_positive_empfehlungen_count(self, sample_db):
"""Anträge mit empfehlung=Ablehnen/Überarbeiten dürfen NICHT zählen."""
out = aggregate_empfehlungs_konsistenz(db_path=sample_db, min_n=1)
# 7 Anträge mit empfehlung positiv (UNTER+AENDR): 18/A,B,C,D,E,I + 8/A,8/B
# = 8 positive. NICHT mitgezählt: 18/F,G,H,J + 8/C
assert out["n_assessments_matched"] == 8
# ─────────────────────────────────────────────────────────────────────────────
# export_stimmverhalten_csv
# ─────────────────────────────────────────────────────────────────────────────
class TestExportStimmverhaltenCsv:
def test_csv_header(self, sample_db):
csv_text = export_stimmverhalten_csv(db_path=sample_db)
first_line = csv_text.splitlines()[0]
assert "drucksache" in first_line
assert "partei" in first_line
assert "vote" in first_line
assert "empfehlung" in first_line
assert "ist_antragsteller" in first_line
def test_csv_has_rows(self, sample_db):
csv_text = export_stimmverhalten_csv(db_path=sample_db)
lines = csv_text.splitlines()
# Header + 13 Anträge × ~3 Voter pro Antrag (excl Antragsteller)
assert len(lines) > 20
def test_csv_excludes_antragsteller_by_default(self, sample_db):
csv_text = export_stimmverhalten_csv(db_path=sample_db)
# GRÜNE ist Antragsteller bei 18/A-E + 8/A,B → keine Zeilen
# mit ist_antragsteller=1 erlaubt im Default
for line in csv_text.splitlines()[1:]:
cols = line.split(",")
ist_antrag = cols[-1].strip()
assert ist_antrag == "0"
def test_csv_includes_antragsteller_when_disabled(self, sample_db):
csv_text = export_stimmverhalten_csv(
db_path=sample_db, exclude_antragsteller=False,
)
antragsteller_rows = [
line for line in csv_text.splitlines()[1:]
if line.strip().endswith(",1")
]
assert len(antragsteller_rows) > 0
def test_csv_filter_by_bundesland(self, sample_db):
csv_text = export_stimmverhalten_csv(db_path=sample_db, filter_bl="MV")
# nur MV-Drucksachen 8/A, 8/B, 8/C
for line in csv_text.splitlines()[1:]:
assert ",MV," in line