fix: tolerate invalid member dates in views

This commit is contained in:
Marcel Peterkau
2026-06-21 16:49:51 +02:00
parent dfd5b1192b
commit e63abbae81
5 changed files with 52 additions and 3 deletions
+1
View File
@@ -16,6 +16,7 @@
"Datumseingabe und -anzeige an das Systemformat angepasst; gespeichert wird weiterhin portabel im ISO-Format.", "Datumseingabe und -anzeige an das Systemformat angepasst; gespeichert wird weiterhin portabel im ISO-Format.",
"Eine ribbonweite Mitgliederliste mit direktem Zugriff auf alle Akten ergänzt.", "Eine ribbonweite Mitgliederliste mit direktem Zugriff auf alle Akten ergänzt.",
"Texthintergründe der Dashboard-Karten an die Kartenflächen angeglichen.", "Texthintergründe der Dashboard-Karten an die Kartenflächen angeglichen.",
"Mitgliederlisten bleiben bei fehlerhaften Datumswerten bedienbar; der Hausmeister meldet die betroffene Akte zur Korrektur.",
"Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.", "Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.",
"Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.", "Statusänderungen werden mit altem und neuem Klartextwert in der Mitgliederchronik protokolliert.",
"Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.", "Fensterposition, normaler Fensterzustand und Maximierung werden gespeichert; der Splash startet auf dem zuletzt verwendeten Monitor.",
+6 -2
View File
@@ -79,9 +79,13 @@ def normalize_date_input(value: str, field_name: str) -> str:
def format_date_for_display(value: str) -> str: def format_date_for_display(value: str) -> str:
if not value.strip(): text = value.strip()
if not text:
return "" return ""
parsed = parse_iso_date(value, "Datum") try:
parsed = parse_iso_date(text, "Datum")
except DateValidationError:
return text
return parsed.strftime(system_date_pattern()) if parsed else "" return parsed.strftime(system_date_pattern()) if parsed else ""
+23 -1
View File
@@ -4,7 +4,12 @@ import calendar
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date, timedelta from datetime import date, timedelta
from ccma.domain.dates import DateValidationError, parse_iso_date, validate_birth_date from ccma.domain.dates import (
DateValidationError,
parse_iso_date,
validate_birth_date,
validate_member_dates,
)
from ccma.domain.models import HousekeeperFinding from ccma.domain.models import HousekeeperFinding
from ccma.services.intervals import AnniversaryInterval, parse_anniversary_intervals from ccma.services.intervals import AnniversaryInterval, parse_anniversary_intervals
from ccma.storage.repository import MemberRepository from ccma.storage.repository import MemberRepository
@@ -48,6 +53,23 @@ class Housekeeper:
current_date = today or date.today() current_date = today or date.today()
findings: list[HousekeeperFinding] = [] findings: list[HousekeeperFinding] = []
for member in self.repository.list_members(): for member in self.repository.list_members():
try:
validate_member_dates(
birth_date=member.birth_date,
accepted_at=member.accepted_at,
membership_started_at=member.membership_started_at,
today=current_date,
)
except DateValidationError as exc:
findings.append(
HousekeeperFinding(
severity="error",
member_id=member.member_id,
code="invalid_member_dates",
title=f"{member.display_name}: Ungültige Datumsangabe",
detail=str(exc),
)
)
if member.status in { if member.status in {
"active", "active",
"suspended_contribution", "suspended_contribution",
+4
View File
@@ -36,6 +36,10 @@ def test_date_display_uses_system_pattern(monkeypatch) -> None:
assert format_date_for_display("2024-02-29") == "2024-02-29" assert format_date_for_display("2024-02-29") == "2024-02-29"
def test_invalid_date_remains_visible_for_correction() -> None:
assert format_date_for_display("31/12/2000") == "31/12/2000"
def test_birth_date_checks_future_and_plausibility() -> None: def test_birth_date_checks_future_and_plausibility() -> None:
today = date(2026, 6, 21) today = date(2026, 6, 21)
assert validate_birth_date("2000-06-22", today=today) == date(2000, 6, 22) assert validate_birth_date("2000-06-22", today=today) == date(2000, 6, 22)
+18
View File
@@ -1,3 +1,4 @@
import json
from datetime import date from datetime import date
from ccma.domain.models import ContributionData from ccma.domain.models import ContributionData
@@ -91,3 +92,20 @@ def test_housekeeper_reports_day_month_and_year_anniversaries(tmp_path) -> None:
"Anniversary2 Member hat heute 1-jähriges Mitgliedsjubiläum", "Anniversary2 Member hat heute 1-jähriges Mitgliedsjubiläum",
"Anniversary3 Member hat in 1 Tag 10-jähriges Mitgliedsjubiläum", "Anniversary3 Member hat in 1 Tag 10-jähriges Mitgliedsjubiläum",
} }
def test_housekeeper_reports_invalid_member_dates(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Broken", last_name="Date")
member_path = repository.members_root / member.member_id / "member.json"
raw = json.loads(member_path.read_text(encoding="utf-8"))
raw["person"]["birth_date"] = "31/12/2000"
member_path.write_text(json.dumps(raw), encoding="utf-8")
findings = Housekeeper(repository).run(today=date(2026, 6, 21))
invalid = [finding for finding in findings if finding.code == "invalid_member_dates"]
assert len(invalid) == 1
assert invalid[0].member_id == member.member_id
assert "Geburtsdatum" in invalid[0].detail