diff --git a/backend/migrations/0004_election_results.sql b/backend/migrations/0004_election_results.sql new file mode 100644 index 0000000..bfeb6bf --- /dev/null +++ b/backend/migrations/0004_election_results.sql @@ -0,0 +1,7 @@ +ALTER TABLE claim DROP CONSTRAINT claim_claim_type_check; +ALTER TABLE claim ADD CONSTRAINT claim_claim_type_check CHECK (claim_type IN ( + 'role', 'vote', 'donation', 'money_flow', 'statement', 'appearance', 'meeting', 'travel', + 'side_job', 'legal_proceeding', 'lobbying', 'media_mention', 'career', + 'parliamentary_question', 'government_decision', 'wiki_edit', 'declaration', 'ownership', + 'affiliation', 'party_finance', 'election_result', 'other')); +COMMENT ON COLUMN claim.claim_type IS 'Kind of fact. election_result: a party''s result in a region (counterpart = the elected body) or a candidate''s preference votes (person, counterpart = party).'; diff --git a/backend/politrack/api/schemas.py b/backend/politrack/api/schemas.py index a985cf8..81cc19c 100644 --- a/backend/politrack/api/schemas.py +++ b/backend/politrack/api/schemas.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, ConfigDict ClaimType = Literal["role", "vote", "donation", "money_flow", "statement", "appearance", "meeting", "travel", "side_job", "legal_proceeding", "lobbying", "media_mention", "career", "parliamentary_question", "government_decision", "wiki_edit", "declaration", "ownership", "affiliation", "party_finance", - "other"] + "election_result", "other"] PersonStatus = Literal["active", "former", "party_member", "deceased"] diff --git a/backend/politrack/core/resolve.py b/backend/politrack/core/resolve.py index 8b065eb..3712f13 100644 --- a/backend/politrack/core/resolve.py +++ b/backend/politrack/core/resolve.py @@ -7,6 +7,8 @@ Order (first hit wins, ambiguity never guesses): Hint strategies for persons (documented in docs/data-model.md): surname_in_body + on members of a chamber on a date with that surname (roll-call lists) office + on holder of an office title on a date (Council of Ministers minutes) + birth_year exact name and year of birth (candidate lists); add no_name_match so that a name + alone never matches when the year doesn't """ from __future__ import annotations @@ -133,6 +135,14 @@ class Resolver: return suffix[0]["id"] if len(suffix) == 1 else None if "office" in hints: return self.office_holder(hints["office"], on) + if "birth_year" in hints and ref.name: + rows = self.conn.execute( + """SELECT DISTINCT a.entity_id AS id FROM alias a JOIN person p ON p.id = a.entity_id + WHERE a.entity_type = 'person' AND a.alias_norm = %s + AND EXTRACT(YEAR FROM p.birth_date) = %s""", + (normalize(strip_titles(ref.name)), int(hints["birth_year"])), + ).fetchall() + return rows[0]["id"] if len(rows) == 1 else None return None def office_holder(self, office: str, on: date | None) -> int | None: diff --git a/backend/politrack/mcp_server.py b/backend/politrack/mcp_server.py index 9ae9d2a..ded0493 100644 --- a/backend/politrack/mcp_server.py +++ b/backend/politrack/mcp_server.py @@ -115,7 +115,8 @@ def list_claims( ) -> str: """Claims about a person/party/organisation, newest first. claim_types: role, vote, donation, money_flow, statement, appearance, meeting, travel, side_job, legal_proceeding, lobbying, media_mention, career, - parliamentary_question, government_decision, wiki_edit, declaration, ownership, affiliation, party_finance. + parliamentary_question, government_decision, wiki_edit, declaration, ownership, affiliation, party_finance, + election_result. Dates as YYYY-MM-DD.""" with connection() as conn: return _dump(queries.list_claims( diff --git a/backend/politrack/sources/elections.py b/backend/politrack/sources/elections.py new file mode 100644 index 0000000..92e2432 --- /dev/null +++ b/backend/politrack/sources/elections.py @@ -0,0 +1,224 @@ +"""Official election results from the Federal Ministry of the Interior (BMI, Bundeswahlbehörde). + +Per election: the final result (XLSX, one row per region with votes per party) and the preference votes of the +candidates (XLSX, per list). Only the federal and state totals of the result are kept; municipal rows stay in the +raw archive. Candidates become claims only if they resolve to a known politician: by exact name plus birth year +where the file has one, otherwise by exact, unambiguous name. Unknown candidates are counted, never stored, and +their occupation is never kept. + +Not covered yet: the 2019 state and regional lists (a print layout with side-by-side columns), elections before +2019, Landtag and municipal elections (published by the states). +""" + +from __future__ import annotations + +import io +import re +from collections.abc import Iterator +from dataclasses import dataclass +from datetime import date +from typing import Any + +from politrack.core import Adapter, Request, Response, RunContext, SourceSpec, register +from politrack.core.records import Claim, Evidence, OrgRef, PersonRef +from politrack.text import strip_titles + +BMI = "https://www.bmi.gv.at/412" +SPEC = SourceSpec( + slug="bmi-wahlen", name="Bundesministerium für Inneres – Wahlergebnisse", kind="official", + homepage=f"{BMI}/start.aspx", license="Amtliches Werk (§7 UrhG); Ergebnisse auch als CC BY 4.0 auf data.gv.at", + access="bulk", trust=95, min_interval=3.0, + tos_notes="Final results and preference votes as published by the Bundeswahlbehörde (XLSX). robots.txt sets " + "no rules. Only candidates who resolve to a known politician are stored; occupations are never kept.", +) + + +@dataclass(frozen=True) +class Election: + key: str + name_de: str + name_en: str + held_on: date + body: str # slug of the elected body + path: str # folder on bmi.gv.at + result: str + preferences: tuple[tuple[str, str], ...] # (layout, file): layouts "bund", "land", "eu" + + +ELECTIONS = ( + Election("nrw2024", "Nationalratswahl 2024", "2024 National Council election", date(2024, 9, 29), "nationalrat", + "nationalratswahlen/nationalratswahl_2024/files", "endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx", + (("bund", "nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx"), + ("land", "nrw2024_aufstellung_der_vorzugsstimmen_der_landes_undregionalbewerber_16102024.xlsx"))), + Election("nrw2019", "Nationalratswahl 2019", "2019 National Council election", date(2019, 9, 29), "nationalrat", + "nationalratswahlen/nationalratswahl_2019/files", "endgultiges_gesamtergebnis_nrw19_16102019.xlsx", + (("bund", "nrw19_vorzugsstimmen_bundesweit_16102019.xlsx"),)), + Election("euw2024", "Europawahl 2024", "2024 European Parliament election", date(2024, 6, 9), + "europaeisches-parlament", "europawahlen/europawahl_2024/files", "endgueltiges_ergebnis_europawahl_26062024.xlsx", + (("eu", "gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx"),)), +) +BY_KEY = {e.key: e for e in ELECTIONS} + +REGION_CODE = re.compile(r"^G\d0000$") # G00000 Österreich, G10000 Burgenland ... G90000 Wien +PARTY_HEAD = re.compile(r"\(([^()]+)\)\s*$") +LIST_HEAD = re.compile(r"^(Landes|Regional)parteilisten im (?:Landes|Regional)wahlkreis\s+(.+?)\s*$") + + +def de_int(n: int) -> str: + return f"{n:,}".replace(",", ".") + + +def de_pct(p: float) -> str: + return f"{p:.2f}".replace(".", ",") + + +def sheets(body: bytes) -> list[tuple[str, list[tuple]]]: + import openpyxl + + wb = openpyxl.load_workbook(io.BytesIO(body), read_only=True, data_only=True) + return [(ws.title, list(ws.iter_rows(values_only=True))) for ws in wb.worksheets] + + +def label(*cells: Any) -> str: + return " ".join(str(c) for c in cells if c not in (None, "")).replace("\n", "").strip().lower() + + +def parse_results(rows: list[tuple]) -> list[dict[str, Any]]: + """Votes per party for Austria and the nine states. The header has party names in the first row, either + with the totals in the same row (2024) or split over two rows ("Stimmen" / "gültige", 2019 and EU 2024).""" + head, sub = rows[0], rows[1] if rows[1] and rows[1][0] is None else () + labels = [label(head[i], sub[i] if i < len(sub) else None) for i in range(len(head))] + valid_col = next(i for i, text in enumerate(labels) if text.endswith("gültige") and "ungültige" not in text) + parties = [(i, str(name).strip()) for i, name in enumerate(head) + if i > valid_col and isinstance(name, str) and name.strip() not in ("", "%")] + out = [] + for row in rows: + code = row[0] + if not (isinstance(code, str) and REGION_CODE.match(code)): + continue + valid = int(row[valid_col] or 0) + for col, party in parties: + votes = int(row[col] or 0) + if not votes: + continue + # the share as published (next column), so the figures match the official result exactly + published = row[col + 1] if col + 1 < len(row) and head[col + 1] == "%" else None + percent = (round(float(published), 2) if isinstance(published, (int, float)) + else round(votes * 100 / valid, 2) if valid else None) + out.append({"code": code, "region": str(row[1]).strip(), "electorate": int(row[2] or 0), + "valid": valid, "party": party, "votes": votes, "percent": percent}) + return out + + +def split_name(text: str) -> tuple[str, str]: + """'Brunner, Dr. Magnus' -> ('Brunner', 'Dr. Magnus'); 'Babler, MSc, Andreas' -> ('Babler', 'Andreas').""" + family, _, rest = str(text).partition(",") + parts = [p.strip() for p in rest.split(",")] + return family.strip(), next((p for p in parts if strip_titles(p)), "") + + +def parse_preferences(layout: str, workbook: list[tuple[str, list[tuple]]]) -> list[dict[str, Any]]: + """Candidates with their preference votes. Returns rank, name, birth year (if published), party, list.""" + out: list[dict[str, Any]] = [] + for sheet, rows in workbook: + party, party_name, list_label = None, None, "Bundesliste" + total_col = None + if layout == "eu": + party, party_name, list_label = sheet.strip(), sheet.strip(), "Bundesliste" + for row in rows: + cells = [c for c in row if c is not None and c != ""] + if not cells: + continue + first = row[0] + if layout == "eu" and first == "LP": + total_col = next((i for i, c in enumerate(row) if c == "Gesamt"), None) + continue + if isinstance(first, str) and len(cells) == 1: + if (m := LIST_HEAD.match(first)): + region = re.sub(r"^\w+\s*-\s*", "", m.group(2)) # "1A - Burgenland Nord" -> "Burgenland Nord" + list_label = f"{m.group(1)}parteiliste {region}" + elif (m := PARTY_HEAD.search(first)) and layout != "eu": + party, party_name, list_label = m.group(1).strip(), first.strip(), "Bundesliste" + continue + rank = str(first).rstrip(".") if first is not None else "" + if not rank.isdigit() or party is None: + continue + if layout == "land": + family, given, year, votes = row[1], row[2], row[4], row[6] + elif layout == "eu": + family, given = split_name(row[1]) + year, votes = None, row[total_col] if total_col is not None else None + else: + family, given = split_name(row[1]) + year, votes = row[2], row[4] + if not family or not isinstance(votes, (int, float)): + continue + out.append({"rank": int(rank), "name": f"{given} {family}".strip(), "birth_year": int(year) + if isinstance(year, (int, float)) else None, "votes": int(votes), "party": party, + "party_name": party_name, "list": list_label}) + return out + + +@register +class ElectionResults(Adapter): + spec = SPEC + name = "bmi.elections" + description = "Final results of federal and EU elections and candidates' preference votes (BMI)" + schedule = "0 6 1 * *" # the files are final; monthly re-checks cost a conditional request each + queue = "bulk" + + def start(self, ctx: RunContext) -> Iterator[Request]: + wanted = set(ctx.params.get("elections") or BY_KEY) + for election in ELECTIONS: + if election.key not in wanted: + continue + base = f"{BMI}/{election.path}" + yield Request(url=f"{base}/{election.result}", kind="result", meta={"election": election.key}) + for layout, file in election.preferences: + yield Request(url=f"{base}/{file}", kind="preferences", meta={"election": election.key, "layout": layout}) + + def parse(self, ctx: RunContext, response: Response) -> Iterator[Claim]: + election = BY_KEY[response.meta["election"]] + workbook = sheets(response.body) + if response.request.kind == "result": + yield from self.result_claims(ctx, election, response.request.url, workbook[0][1]) + else: + yield from self.preference_claims(ctx, election, response.request.url, + parse_preferences(response.meta["layout"], workbook)) + + def result_claims(self, ctx: RunContext, election: Election, url: str, rows: list[tuple]) -> Iterator[Claim]: + for r in parse_results(rows): + pct = f"{de_pct(r['percent'])} %" if r["percent"] is not None else "?" + yield Claim( + claim_type="election_result", org=OrgRef(short_name=r["party"], kind="party", create=False), + counterpart_org=OrgRef(slug=election.body), occurred_at=election.held_on, + key=("election", election.key, r["code"], r["party"]), + summary_de=f"{r['party']}: {pct} ({de_int(r['votes'])} Stimmen) bei der {election.name_de} – {r['region']}", + summary_en=f"{r['party']}: {pct.replace(',', '.')} ({r['votes']:,} votes) in the {election.name_en} " + f"– {r['region']}", + data={"election": election.key, "party": r["party"], "region": r["region"], "region_code": r["code"], + "votes": r["votes"], "percent": r["percent"], "valid_votes": r["valid"], + "electorate": r["electorate"]}, + evidence=[Evidence(url=url, doc_type="dataset_row", title=f"BMI: {election.name_de}, endgültiges Ergebnis", + published_at=election.held_on, locator=f"{r['code']}:{r['party']}", + quote=f"{r['region']} | {r['party']} | {r['votes']} | {r['valid']} gültige")], + ) + + def preference_claims(self, ctx: RunContext, election: Election, url: str, + candidates: list[dict[str, Any]]) -> Iterator[Claim]: + for c in candidates: + hints = {"birth_year": c["birth_year"], "no_name_match": True} if c["birth_year"] else {} + yield Claim( + claim_type="election_result", person=PersonRef(name=c["name"], hints=hints), + counterpart_org=OrgRef(short_name=c["party"], kind="party", create=False), occurred_at=election.held_on, + key=("preference-votes", election.key, c["list"], c["party"], c["rank"], c["name"]), + summary_de=f"{de_int(c['votes'])} Vorzugsstimmen bei der {election.name_de} " + f"({c['list']} {c['party']}, Platz {c['rank']})", + summary_en=f"{c['votes']:,} preference votes in the {election.name_en} " + f"({c['party']}, {c['list']}, position {c['rank']})", + data={"election": election.key, "preference_votes": c["votes"], "rank": c["rank"], "list": c["list"], + "party": c["party"]}, + evidence=[Evidence(url=url, doc_type="dataset_row", title=f"BMI: {election.name_de}, Vorzugsstimmen", + published_at=election.held_on, locator=f"{c['list']}:{c['party']}:{c['rank']}", + quote=f"{c['list']} {c['party']}, Platz {c['rank']}: {c['votes']} Vorzugsstimmen")], + ) diff --git a/backend/tests/fixtures/bmi.elections/preferences_bund.expected.json b/backend/tests/fixtures/bmi.elections/preferences_bund.expected.json new file mode 100644 index 0000000..2438a51 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_bund.expected.json @@ -0,0 +1,262 @@ +[ + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Bundesliste", + "party": "ÖVP", + "preference_votes": 60402, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:ÖVP:1", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Bundesliste ÖVP, Platz 1: 60402 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Bundesliste", + "ÖVP", + 1, + "Karl Nehammer" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1972, + "no_name_match": true + }, + "ids": {}, + "name": "Karl Nehammer" + }, + "review": false, + "sensitive": false, + "summary_de": "60.402 Vorzugsstimmen bei der Nationalratswahl 2024 (Bundesliste ÖVP, Platz 1)", + "summary_en": "60,402 preference votes in the 2024 National Council election (ÖVP, Bundesliste, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Bundesliste", + "party": "ÖVP", + "preference_votes": 7820, + "rank": 2 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:ÖVP:2", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Bundesliste ÖVP, Platz 2: 7820 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Bundesliste", + "ÖVP", + 2, + "Claudia Plakolm" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1994, + "no_name_match": true + }, + "ids": {}, + "name": "Claudia Plakolm" + }, + "review": false, + "sensitive": false, + "summary_de": "7.820 Vorzugsstimmen bei der Nationalratswahl 2024 (Bundesliste ÖVP, Platz 2)", + "summary_en": "7,820 preference votes in the 2024 National Council election (ÖVP, Bundesliste, position 2)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Bundesliste", + "party": "ÖVP", + "preference_votes": 2069, + "rank": 3 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:ÖVP:3", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Bundesliste ÖVP, Platz 3: 2069 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Bundesliste", + "ÖVP", + 3, + "Dr. Magnus Brunner" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1972, + "no_name_match": true + }, + "ids": {}, + "name": "Dr. Magnus Brunner" + }, + "review": false, + "sensitive": false, + "summary_de": "2.069 Vorzugsstimmen bei der Nationalratswahl 2024 (Bundesliste ÖVP, Platz 3)", + "summary_en": "2,069 preference votes in the 2024 National Council election (ÖVP, Bundesliste, position 3)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Bundesliste", + "party": "SPÖ", + "preference_votes": 46440, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:SPÖ:1", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Bundesliste SPÖ, Platz 1: 46440 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Bundesliste", + "SPÖ", + 1, + "Andreas Babler" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1973, + "no_name_match": true + }, + "ids": {}, + "name": "Andreas Babler" + }, + "review": false, + "sensitive": false, + "summary_de": "46.440 Vorzugsstimmen bei der Nationalratswahl 2024 (Bundesliste SPÖ, Platz 1)", + "summary_en": "46,440 preference votes in the 2024 National Council election (SPÖ, Bundesliste, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + } +] diff --git a/backend/tests/fixtures/bmi.elections/preferences_bund.request.json b/backend/tests/fixtures/bmi.elections/preferences_bund.request.json new file mode 100644 index 0000000..bbca639 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_bund.request.json @@ -0,0 +1,13 @@ +{ + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_bundesbewerber_16102024.xlsx", + "kind": "preferences", + "method": "GET", + "params": null, + "json": null, + "meta": { + "election": "nrw2024", + "layout": "bund" + }, + "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "body_file": "preferences_bund.xlsx" +} \ No newline at end of file diff --git a/backend/tests/fixtures/bmi.elections/preferences_bund.xlsx b/backend/tests/fixtures/bmi.elections/preferences_bund.xlsx new file mode 100644 index 0000000..ca8a4a5 Binary files /dev/null and b/backend/tests/fixtures/bmi.elections/preferences_bund.xlsx differ diff --git a/backend/tests/fixtures/bmi.elections/preferences_eu.expected.json b/backend/tests/fixtures/bmi.elections/preferences_eu.expected.json new file mode 100644 index 0000000..e69a8e1 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_eu.expected.json @@ -0,0 +1,250 @@ +[ + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "list": "Bundesliste", + "party": "ÖVP", + "preference_votes": 58013, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:ÖVP:1", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Bundesliste ÖVP, Platz 1: 58013 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "euw2024", + "Bundesliste", + "ÖVP", + 1, + "Dr. Reinhold Lopatka" + ], + "occurred_at": "2024-06-09", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": {}, + "ids": {}, + "name": "Dr. Reinhold Lopatka" + }, + "review": false, + "sensitive": false, + "summary_de": "58.013 Vorzugsstimmen bei der Europawahl 2024 (Bundesliste ÖVP, Platz 1)", + "summary_en": "58,013 preference votes in the 2024 European Parliament election (ÖVP, Bundesliste, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "list": "Bundesliste", + "party": "ÖVP", + "preference_votes": 38426, + "rank": 2 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:ÖVP:2", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Bundesliste ÖVP, Platz 2: 38426 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "euw2024", + "Bundesliste", + "ÖVP", + 2, + "Mag. Dr. Angelika Winzig" + ], + "occurred_at": "2024-06-09", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": {}, + "ids": {}, + "name": "Mag. Dr. Angelika Winzig" + }, + "review": false, + "sensitive": false, + "summary_de": "38.426 Vorzugsstimmen bei der Europawahl 2024 (Bundesliste ÖVP, Platz 2)", + "summary_en": "38,426 preference votes in the 2024 European Parliament election (ÖVP, Bundesliste, position 2)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "list": "Bundesliste", + "party": "SPÖ", + "preference_votes": 61982, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:SPÖ:1", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Bundesliste SPÖ, Platz 1: 61982 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "euw2024", + "Bundesliste", + "SPÖ", + 1, + "Mag. Andreas Schieder" + ], + "occurred_at": "2024-06-09", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": {}, + "ids": {}, + "name": "Mag. Andreas Schieder" + }, + "review": false, + "sensitive": false, + "summary_de": "61.982 Vorzugsstimmen bei der Europawahl 2024 (Bundesliste SPÖ, Platz 1)", + "summary_en": "61,982 preference votes in the 2024 European Parliament election (SPÖ, Bundesliste, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "list": "Bundesliste", + "party": "SPÖ", + "preference_votes": 17448, + "rank": 2 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Bundesliste:SPÖ:2", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Bundesliste SPÖ, Platz 2: 17448 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "euw2024", + "Bundesliste", + "SPÖ", + 2, + "Mag. Evelyn Regner" + ], + "occurred_at": "2024-06-09", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": {}, + "ids": {}, + "name": "Mag. Evelyn Regner" + }, + "review": false, + "sensitive": false, + "summary_de": "17.448 Vorzugsstimmen bei der Europawahl 2024 (Bundesliste SPÖ, Platz 2)", + "summary_en": "17,448 preference votes in the 2024 European Parliament election (SPÖ, Bundesliste, position 2)", + "topics": [], + "valid_from": null, + "valid_to": null + } +] diff --git a/backend/tests/fixtures/bmi.elections/preferences_eu.request.json b/backend/tests/fixtures/bmi.elections/preferences_eu.request.json new file mode 100644 index 0000000..e956c6c --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_eu.request.json @@ -0,0 +1,13 @@ +{ + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/gesamtaufstellung_erzielte_vorzugsstimmen_eu_wahl_2024_parteien.xlsx", + "kind": "preferences", + "method": "GET", + "params": null, + "json": null, + "meta": { + "election": "euw2024", + "layout": "eu" + }, + "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "body_file": "preferences_eu.xlsx" +} \ No newline at end of file diff --git a/backend/tests/fixtures/bmi.elections/preferences_eu.xlsx b/backend/tests/fixtures/bmi.elections/preferences_eu.xlsx new file mode 100644 index 0000000..e4e584a Binary files /dev/null and b/backend/tests/fixtures/bmi.elections/preferences_eu.xlsx differ diff --git a/backend/tests/fixtures/bmi.elections/preferences_land.expected.json b/backend/tests/fixtures/bmi.elections/preferences_land.expected.json new file mode 100644 index 0000000..13744bb --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_land.expected.json @@ -0,0 +1,197 @@ +[ + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Landesparteiliste Burgenland", + "party": "ÖVP", + "preference_votes": 3937, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Landesparteiliste Burgenland:ÖVP:1", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Landesparteiliste Burgenland ÖVP, Platz 1: 3937 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_landes_undregionalbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Landesparteiliste Burgenland", + "ÖVP", + 1, + "Christoph Zarits" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1980, + "no_name_match": true + }, + "ids": {}, + "name": "Christoph Zarits" + }, + "review": false, + "sensitive": false, + "summary_de": "3.937 Vorzugsstimmen bei der Nationalratswahl 2024 (Landesparteiliste Burgenland ÖVP, Platz 1)", + "summary_en": "3,937 preference votes in the 2024 National Council election (ÖVP, Landesparteiliste Burgenland, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Landesparteiliste Burgenland", + "party": "ÖVP", + "preference_votes": 2734, + "rank": 2 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Landesparteiliste Burgenland:ÖVP:2", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Landesparteiliste Burgenland ÖVP, Platz 2: 2734 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_landes_undregionalbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Landesparteiliste Burgenland", + "ÖVP", + 2, + "Nikolaus Berlakovich" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1961, + "no_name_match": true + }, + "ids": {}, + "name": "Nikolaus Berlakovich" + }, + "review": false, + "sensitive": false, + "summary_de": "2.734 Vorzugsstimmen bei der Nationalratswahl 2024 (Landesparteiliste Burgenland ÖVP, Platz 2)", + "summary_en": "2,734 preference votes in the 2024 National Council election (ÖVP, Landesparteiliste Burgenland, position 2)", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "list": "Regionalparteiliste Burgenland Nord", + "party": "ÖVP", + "preference_votes": 7361, + "rank": 1 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "Regionalparteiliste Burgenland Nord:ÖVP:1", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Regionalparteiliste Burgenland Nord ÖVP, Platz 1: 7361 Vorzugsstimmen", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, Vorzugsstimmen", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_landes_undregionalbewerber_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "preference-votes", + "nrw2024", + "Regionalparteiliste Burgenland Nord", + "ÖVP", + 1, + "Christoph Zarits" + ], + "occurred_at": "2024-09-29", + "org": null, + "person": { + "__type__": "PersonRef", + "hints": { + "birth_year": 1980, + "no_name_match": true + }, + "ids": {}, + "name": "Christoph Zarits" + }, + "review": false, + "sensitive": false, + "summary_de": "7.361 Vorzugsstimmen bei der Nationalratswahl 2024 (Regionalparteiliste Burgenland Nord ÖVP, Platz 1)", + "summary_en": "7,361 preference votes in the 2024 National Council election (ÖVP, Regionalparteiliste Burgenland Nord, position 1)", + "topics": [], + "valid_from": null, + "valid_to": null + } +] diff --git a/backend/tests/fixtures/bmi.elections/preferences_land.request.json b/backend/tests/fixtures/bmi.elections/preferences_land.request.json new file mode 100644 index 0000000..28bf794 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/preferences_land.request.json @@ -0,0 +1,13 @@ +{ + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/nrw2024_aufstellung_der_vorzugsstimmen_der_landes_undregionalbewerber_16102024.xlsx", + "kind": "preferences", + "method": "GET", + "params": null, + "json": null, + "meta": { + "election": "nrw2024", + "layout": "land" + }, + "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "body_file": "preferences_land.xlsx" +} \ No newline at end of file diff --git a/backend/tests/fixtures/bmi.elections/preferences_land.xlsx b/backend/tests/fixtures/bmi.elections/preferences_land.xlsx new file mode 100644 index 0000000..752ccbc Binary files /dev/null and b/backend/tests/fixtures/bmi.elections/preferences_land.xlsx differ diff --git a/backend/tests/fixtures/bmi.elections/result_euw2024.expected.json b/backend/tests/fixtures/bmi.elections/result_euw2024.expected.json new file mode 100644 index 0000000..c7b44b3 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/result_euw2024.expected.json @@ -0,0 +1,266 @@ +[ + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "europaeisches-parlament" + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "electorate": 6372205, + "party": "ÖVP", + "percent": 24.52, + "region": "Österreich", + "region_code": "G00000", + "valid_votes": 3523935, + "votes": 864072 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G00000:ÖVP", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Österreich | ÖVP | 864072 | 3523935 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/endgueltiges_ergebnis_europawahl_26062024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "euw2024", + "G00000", + "ÖVP" + ], + "occurred_at": "2024-06-09", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "ÖVP: 24,52 % (864.072 Stimmen) bei der Europawahl 2024 – Österreich", + "summary_en": "ÖVP: 24.52 % (864,072 votes) in the 2024 European Parliament election – Österreich", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "europaeisches-parlament" + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "electorate": 6372205, + "party": "SPÖ", + "percent": 23.22, + "region": "Österreich", + "region_code": "G00000", + "valid_votes": 3523935, + "votes": 818287 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G00000:SPÖ", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Österreich | SPÖ | 818287 | 3523935 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/endgueltiges_ergebnis_europawahl_26062024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "euw2024", + "G00000", + "SPÖ" + ], + "occurred_at": "2024-06-09", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "SPÖ: 23,22 % (818.287 Stimmen) bei der Europawahl 2024 – Österreich", + "summary_en": "SPÖ: 23.22 % (818,287 votes) in the 2024 European Parliament election – Österreich", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "europaeisches-parlament" + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "electorate": 235492, + "party": "ÖVP", + "percent": 27.91, + "region": "Burgenland", + "region_code": "G10000", + "valid_votes": 134907, + "votes": 37654 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G10000:ÖVP", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Burgenland | ÖVP | 37654 | 134907 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/endgueltiges_ergebnis_europawahl_26062024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "euw2024", + "G10000", + "ÖVP" + ], + "occurred_at": "2024-06-09", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "ÖVP: 27,91 % (37.654 Stimmen) bei der Europawahl 2024 – Burgenland", + "summary_en": "ÖVP: 27.91 % (37,654 votes) in the 2024 European Parliament election – Burgenland", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "europaeisches-parlament" + }, + "counterpart_person": null, + "data": { + "election": "euw2024", + "electorate": 235492, + "party": "SPÖ", + "percent": 29.84, + "region": "Burgenland", + "region_code": "G10000", + "valid_votes": 134907, + "votes": 40253 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G10000:SPÖ", + "meta": {}, + "published_at": "2024-06-09", + "quote": "Burgenland | SPÖ | 40253 | 134907 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Europawahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/endgueltiges_ergebnis_europawahl_26062024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "euw2024", + "G10000", + "SPÖ" + ], + "occurred_at": "2024-06-09", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "SPÖ: 29,84 % (40.253 Stimmen) bei der Europawahl 2024 – Burgenland", + "summary_en": "SPÖ: 29.84 % (40,253 votes) in the 2024 European Parliament election – Burgenland", + "topics": [], + "valid_from": null, + "valid_to": null + } +] diff --git a/backend/tests/fixtures/bmi.elections/result_euw2024.request.json b/backend/tests/fixtures/bmi.elections/result_euw2024.request.json new file mode 100644 index 0000000..d367e04 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/result_euw2024.request.json @@ -0,0 +1,12 @@ +{ + "url": "https://www.bmi.gv.at/412/europawahlen/europawahl_2024/files/endgueltiges_ergebnis_europawahl_26062024.xlsx", + "kind": "result", + "method": "GET", + "params": null, + "json": null, + "meta": { + "election": "euw2024" + }, + "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "body_file": "result_euw2024.xlsx" +} \ No newline at end of file diff --git a/backend/tests/fixtures/bmi.elections/result_euw2024.xlsx b/backend/tests/fixtures/bmi.elections/result_euw2024.xlsx new file mode 100644 index 0000000..6ee8f43 Binary files /dev/null and b/backend/tests/fixtures/bmi.elections/result_euw2024.xlsx differ diff --git a/backend/tests/fixtures/bmi.elections/result_nrw2024.expected.json b/backend/tests/fixtures/bmi.elections/result_nrw2024.expected.json new file mode 100644 index 0000000..01c7698 --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/result_nrw2024.expected.json @@ -0,0 +1,398 @@ +[ + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 6346059, + "party": "ÖVP", + "percent": 26.26, + "region": "Österreich", + "region_code": "G00000", + "valid_votes": 4882888, + "votes": 1282734 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G00000:ÖVP", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Österreich | ÖVP | 1282734 | 4882888 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G00000", + "ÖVP" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "ÖVP: 26,26 % (1.282.734 Stimmen) bei der Nationalratswahl 2024 – Österreich", + "summary_en": "ÖVP: 26.26 % (1,282,734 votes) in the 2024 National Council election – Österreich", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 6346059, + "party": "SPÖ", + "percent": 21.13, + "region": "Österreich", + "region_code": "G00000", + "valid_votes": 4882888, + "votes": 1032233 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G00000:SPÖ", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Österreich | SPÖ | 1032233 | 4882888 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G00000", + "SPÖ" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "SPÖ: 21,13 % (1.032.233 Stimmen) bei der Nationalratswahl 2024 – Österreich", + "summary_en": "SPÖ: 21.13 % (1,032,233 votes) in the 2024 National Council election – Österreich", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 6346059, + "party": "FPÖ", + "percent": 28.84, + "region": "Österreich", + "region_code": "G00000", + "valid_votes": 4882888, + "votes": 1408512 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G00000:FPÖ", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Österreich | FPÖ | 1408512 | 4882888 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G00000", + "FPÖ" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "FPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "FPÖ: 28,84 % (1.408.512 Stimmen) bei der Nationalratswahl 2024 – Österreich", + "summary_en": "FPÖ: 28.84 % (1,408,512 votes) in the 2024 National Council election – Österreich", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 233738, + "party": "ÖVP", + "percent": 28.55, + "region": "Burgenland", + "region_code": "G10000", + "valid_votes": 190099, + "votes": 54287 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G10000:ÖVP", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Burgenland | ÖVP | 54287 | 190099 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G10000", + "ÖVP" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "ÖVP", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "ÖVP: 28,55 % (54.287 Stimmen) bei der Nationalratswahl 2024 – Burgenland", + "summary_en": "ÖVP: 28.55 % (54,287 votes) in the 2024 National Council election – Burgenland", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 233738, + "party": "SPÖ", + "percent": 27.02, + "region": "Burgenland", + "region_code": "G10000", + "valid_votes": 190099, + "votes": 51374 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G10000:SPÖ", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Burgenland | SPÖ | 51374 | 190099 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G10000", + "SPÖ" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "SPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "SPÖ: 27,02 % (51.374 Stimmen) bei der Nationalratswahl 2024 – Burgenland", + "summary_en": "SPÖ: 27.02 % (51,374 votes) in the 2024 National Council election – Burgenland", + "topics": [], + "valid_from": null, + "valid_to": null + }, + { + "__type__": "Claim", + "claim_type": "election_result", + "counterpart_org": { + "__type__": "OrgRef", + "create": true, + "ids": {}, + "kind": "other", + "name": null, + "short_name": null, + "slug": "nationalrat" + }, + "counterpart_person": null, + "data": { + "election": "nrw2024", + "electorate": 233738, + "party": "FPÖ", + "percent": 28.78, + "region": "Burgenland", + "region_code": "G10000", + "valid_votes": 190099, + "votes": 54718 + }, + "evidence": [ + { + "__type__": "Evidence", + "confidence": 1.0, + "doc_type": "dataset_row", + "excerpt": null, + "full_text": null, + "locator": "G10000:FPÖ", + "meta": {}, + "published_at": "2024-09-29", + "quote": "Burgenland | FPÖ | 54718 | 190099 gültige", + "source": null, + "stance": "supports", + "title": "BMI: Nationalratswahl 2024, endgültiges Ergebnis", + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx" + } + ], + "facet": null, + "key": [ + "election", + "nrw2024", + "G10000", + "FPÖ" + ], + "occurred_at": "2024-09-29", + "org": { + "__type__": "OrgRef", + "create": false, + "ids": {}, + "kind": "party", + "name": null, + "short_name": "FPÖ", + "slug": null + }, + "person": null, + "review": false, + "sensitive": false, + "summary_de": "FPÖ: 28,78 % (54.718 Stimmen) bei der Nationalratswahl 2024 – Burgenland", + "summary_en": "FPÖ: 28.78 % (54,718 votes) in the 2024 National Council election – Burgenland", + "topics": [], + "valid_from": null, + "valid_to": null + } +] diff --git a/backend/tests/fixtures/bmi.elections/result_nrw2024.request.json b/backend/tests/fixtures/bmi.elections/result_nrw2024.request.json new file mode 100644 index 0000000..20a5ccd --- /dev/null +++ b/backend/tests/fixtures/bmi.elections/result_nrw2024.request.json @@ -0,0 +1,12 @@ +{ + "url": "https://www.bmi.gv.at/412/nationalratswahlen/nationalratswahl_2024/files/endgueltiges_ergebnis_beschluss_bundeswahlbehoerde_16102024.xlsx", + "kind": "result", + "method": "GET", + "params": null, + "json": null, + "meta": { + "election": "nrw2024" + }, + "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "body_file": "result_nrw2024.xlsx" +} \ No newline at end of file diff --git a/backend/tests/fixtures/bmi.elections/result_nrw2024.xlsx b/backend/tests/fixtures/bmi.elections/result_nrw2024.xlsx new file mode 100644 index 0000000..aaea51a Binary files /dev/null and b/backend/tests/fixtures/bmi.elections/result_nrw2024.xlsx differ diff --git a/docs/data-model.md b/docs/data-model.md index c2da534..25b38e8 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -69,8 +69,12 @@ counterpart (`counterpart_person_id`, `counterpart_org_id`), time (`occurred_at` | `government_decision` | – | Council of Ministers minutes | | `wiki_edit` | – | Wikipedia edits from institutional IP ranges (subject = network owner) | | `declaration` | – | EP declarations of interests | -| `ownership`, `affiliation` | – | party reports (holdings, affiliated organisations) | +| `ownership`, `affiliation` | – | party reports (holdings, affiliated organisations), committee memberships | | `party_finance` | – | party annual reports (totals in `data`) | +| `election_result` | – | BMI: a party's votes in Austria or a state (counterpart = elected body), a candidate's preference votes (counterpart = party) | + +EP votes on amendments and parts of a text: `vote_event.part_vote` and `claim.data.part_vote` are true; general +lists, counts and search chunks leave them out (`NOT_PART_VOTE` in `queries/common.py`). **Status:** `reported` (one source) → `corroborated` (≥ 2 distinct sources support/mention it) or `disputed` (a source contradicts). Computed by the Loader on every new piece of evidence and by diff --git a/docs/roadmap.md b/docs/roadmap.md index 587aa1e..dda0dc7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -34,6 +34,7 @@ Next.js frontend (de/en), Docker Compose for Unraid. - [x] X deletion sync (`social.x_deletions`): posts gone from X keep a marked record without their text - [x] EP roll-call votes of Austrian MEPs, including amendments and part votes (`europarl.votes`) - [x] Committee memberships of the configured legislative periods (list 250, `parlament.persons`) +- [x] Election results and preference votes: NR 2024, NR 2019, EU 2024 (`bmi.elections`) - [x] Draft legal pages (`/impressum`, `/datenschutz`), corrections contact, portrait proxy (`/api/img`) - [x] Pages: organisations, money explorer, social feed, Wikipedia edits, person network ("follow the money"), say-vs-vote on roll calls @@ -49,7 +50,8 @@ Next.js frontend (de/en), Docker Compose for Unraid. **Data** - [ ] Full speech texts and interjections from the protocols - [ ] EU Council participant lists, ministries' press releases, Federal President's schedule -- [ ] Election results and candidate lists; AMA/TAM subsidy recipients (after checking bulk access and terms) +- [ ] Election results before 2019, NR 2019 state lists, Landtag and municipal elections +- [ ] AMA/TAM subsidy recipients (after checking bulk access and terms) - [ ] Cabinet staff and appointments to state-owned companies (from answers to parliamentary questions) - [ ] One adapter per Landtag (OGD where available); municipal councils and mayors - [ ] Lobbying register – if the BMJ ever offers stable URLs or an export diff --git a/docs/sources.md b/docs/sources.md index f2b8c27..99381ea 100644 --- a/docs/sources.md +++ b/docs/sources.md @@ -57,7 +57,7 @@ Status: ✅ implemented (adapter name in `code`) · 🟡 planned · ⛔ delibera | Municipalities | mayors, councils | fragmented | – | 🟡 via Wikidata | | EU Council participant lists | which minister attended which Council | PDF per meeting | EU re-use policy | 🟡 not implemented yet | | AMA farm subsidies, EU state-aid register (TAM) | subsidy recipients | search interfaces | – | 🟡 not implemented yet (bulk access and terms still to be checked) | -| Election results, candidate lists | results per municipality, candidates | BMI / data.gv.at, official gazettes | open data / official | 🟡 not implemented yet | +| Election results, preference votes | party results for Austria and the states; candidates' preference votes (only candidates who resolve to a known politician) | BMI XLSX (Bundeswahlbehörde) | official works; CC BY 4.0 on data.gv.at | ✅ `bmi.elections` (NR 2024, NR 2019, EU 2024); 🟡 earlier elections, NR 2019 state lists, Landtag and municipal elections | | Insolvency notices (Ediktsdatei) | companies tied to politicians | Justiz web application | terms to be checked before automating | 🟡 | | Firmenbuch, Grundbuch, beneficial owners (WiEReG) | company roles, property | paid clearing houses; WiEReG public access restricted since the 2022 CJEU ruling | – | ⛔ cost and legal limits | | Criminal record (*Strafregister*) | – | not public | – | ⛔ does not exist publicly | diff --git a/web/messages/de.json b/web/messages/de.json index 2f15181..1b93b0f 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -266,6 +266,7 @@ "contradicts": "widerspricht", "none": "Keine Angaben mit deinen Quellen- und Themenfiltern.", "type": { + "election_result": "Wahlergebnis", "role": "Funktion", "vote": "Abstimmung", "donation": "Spende", diff --git a/web/messages/en.json b/web/messages/en.json index 2967826..33ed5dd 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -231,6 +231,7 @@ "contradicts": "contradicts", "none": "No claims match your source and topic filters.", "type": { + "election_result": "Election result", "role": "Office", "vote": "Vote", "donation": "Donation", diff --git a/web/src/app/[locale]/parteien/[slug]/page.tsx b/web/src/app/[locale]/parteien/[slug]/page.tsx index 8dd4a8c..96b17ff 100644 --- a/web/src/app/[locale]/parteien/[slug]/page.tsx +++ b/web/src/app/[locale]/parteien/[slug]/page.tsx @@ -42,7 +42,7 @@ export default async function PartyPage({params}: Props) { const party = await loadParty(slug); const [votes, claims, posts] = await Promise.all([ api.votes({party: slug, limit: 10}), - api.claims({party: slug, types: ['donation', 'media_mention', 'legal_proceeding', 'money_flow', 'lobbying'], limit: 20}), + api.claims({party: slug, types: ['donation', 'media_mention', 'legal_proceeding', 'money_flow', 'lobbying', 'election_result'], limit: 20}), api.social({party: slug, limit: 6}) ]); diff --git a/web/src/app/[locale]/personen/[slug]/page.tsx b/web/src/app/[locale]/personen/[slug]/page.tsx index fb5211e..de5ddae 100644 --- a/web/src/app/[locale]/personen/[slug]/page.tsx +++ b/web/src/app/[locale]/personen/[slug]/page.tsx @@ -23,7 +23,7 @@ import {isCurrent, roleLabel} from '@/lib/roles'; // Claim types per tab (also used for the tab counts); tabs with their own component are listed in CUSTOM_TABS. const TABS = { overview: ['statement', 'vote', 'donation', 'side_job', 'media_mention', 'meeting', 'travel', 'legal_proceeding', - 'career', 'government_decision', 'declaration'], + 'career', 'government_decision', 'declaration', 'election_result'], timeline: [], votes: ['vote'], travel: ['travel', 'meeting', 'appearance', 'government_decision'], diff --git a/web/src/components/badges.tsx b/web/src/components/badges.tsx index c5da783..fb2ed0d 100644 --- a/web/src/components/badges.tsx +++ b/web/src/components/badges.tsx @@ -26,7 +26,8 @@ const TYPE_ICONS: Partial> = { wiki_edit: 'pencil', declaration: 'document', ownership: 'link', - affiliation: 'link' + affiliation: 'link', + election_result: 'vote' }; export async function TypeLabel({type}: {type: ClaimType}) { diff --git a/web/src/lib/api-types.ts b/web/src/lib/api-types.ts index bf60ca8..de91c70 100644 --- a/web/src/lib/api-types.ts +++ b/web/src/lib/api-types.ts @@ -22,7 +22,7 @@ export type CalendarEvent = { export type Claim = { id: number; - claim_type: 'role' | 'vote' | 'donation' | 'money_flow' | 'statement' | 'appearance' | 'meeting' | 'travel' | 'side_job' | 'legal_proceeding' | 'lobbying' | 'media_mention' | 'career' | 'parliamentary_question' | 'government_decision' | 'wiki_edit' | 'declaration' | 'ownership' | 'affiliation' | 'party_finance' | 'other'; + claim_type: 'role' | 'vote' | 'donation' | 'money_flow' | 'statement' | 'appearance' | 'meeting' | 'travel' | 'side_job' | 'legal_proceeding' | 'lobbying' | 'media_mention' | 'career' | 'parliamentary_question' | 'government_decision' | 'wiki_edit' | 'declaration' | 'ownership' | 'affiliation' | 'party_finance' | 'election_result' | 'other'; summary_de: string; summary_en: string | null; occurred_at: string | null;