mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 19:26:53 +02:00
fix: tolerate damaged contribution files
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"Mitgliederlisten bleiben bei fehlerhaften Datumswerten bedienbar; der Hausmeister meldet die betroffene Akte zur Korrektur.",
|
||||
"Universelle, dateibasierte Hausmeister-Regeln mit atomarem Run-Journal, Task-Lifecycle und idempotenten Forderungsaktionen eingeführt.",
|
||||
"Regelskripte im Mitglieder-Store können eingebaute Regeln anhand ihres Dateinamens gezielt ersetzen.",
|
||||
"Beschädigte Beitragsdateien blockieren die Mitgliederansicht nicht mehr und werden vom Hausmeister ohne automatisches Überschreiben gemeldet.",
|
||||
"Hausmeister um konfigurierbare Geburtstags- und Mitgliedsjubiläumsmeldungen erweitert.",
|
||||
"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.",
|
||||
|
||||
@@ -15,6 +15,7 @@ class RuleContext:
|
||||
today: date
|
||||
settings: Any
|
||||
repository_config: Mapping[str, Any]
|
||||
contributions_error: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
||||
@@ -7,6 +7,8 @@ ORDER = 50
|
||||
|
||||
|
||||
def evaluate(context: RuleContext):
|
||||
if context.contributions_error:
|
||||
return []
|
||||
actions = []
|
||||
for claim in context.contributions.claims:
|
||||
if str(claim.get("status", "open")) not in {"open", "partially_paid"}:
|
||||
|
||||
@@ -12,6 +12,8 @@ CENT = Decimal("0.01")
|
||||
|
||||
|
||||
def evaluate(context: RuleContext):
|
||||
if context.contributions_error:
|
||||
return []
|
||||
member = context.member
|
||||
if member.honorary or member.status not in CONTRIBUTION_STATUSES:
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from ccma.rules.api import RuleContext, task
|
||||
|
||||
RULE_ID = "contributions-check"
|
||||
ORDER = 5
|
||||
|
||||
|
||||
def evaluate(context: RuleContext):
|
||||
if not context.contributions_error:
|
||||
return []
|
||||
return [
|
||||
task(
|
||||
rule_id=RULE_ID,
|
||||
member=context.member,
|
||||
key_suffix="invalid-file",
|
||||
severity="error",
|
||||
code="invalid_contributions_file",
|
||||
title=f"{context.member.display_name}: Beitragsdatei beschädigt",
|
||||
detail=(f"{context.contributions_error}. Die Datei wird nicht automatisch überschrieben."),
|
||||
)
|
||||
]
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from ccma.domain.models import HousekeeperFinding
|
||||
from ccma.domain.models import ContributionData, HousekeeperFinding
|
||||
from ccma.rules.api import RuleAction, RuleContext
|
||||
from ccma.rules.loader import LoadedRule, load_rules
|
||||
from ccma.services.intervals import AnniversaryInterval, parse_anniversary_intervals
|
||||
@@ -68,21 +68,30 @@ class Housekeeper:
|
||||
rules = load_rules(self.repository.root)
|
||||
repository_config = self.repository.get_configuration()
|
||||
for member in self.repository.list_members():
|
||||
try:
|
||||
contributions = self.repository.get_contributions(member.member_id)
|
||||
contributions_error = None
|
||||
except RepositoryError as exc:
|
||||
contributions = ContributionData()
|
||||
contributions_error = str(exc)
|
||||
for rule in rules:
|
||||
scope = (rule.rule_id, member.member_id)
|
||||
try:
|
||||
context = RuleContext(
|
||||
member=member,
|
||||
contributions=self.repository.get_contributions(member.member_id),
|
||||
contributions=contributions,
|
||||
today=current_date,
|
||||
settings=self.settings,
|
||||
repository_config=repository_config,
|
||||
contributions_error=contributions_error,
|
||||
)
|
||||
actions = rule.evaluate(context)
|
||||
if not isinstance(actions, list):
|
||||
raise TypeError("evaluate(context) muss eine Liste zurückgeben")
|
||||
for action in actions:
|
||||
self._apply_action(items, action, rule, run_id, now)
|
||||
if action.action == "create_claim":
|
||||
contributions = self.repository.get_contributions(member.member_id)
|
||||
except Exception as exc:
|
||||
self._refresh_task(
|
||||
items,
|
||||
|
||||
@@ -83,6 +83,7 @@ class MemberRepository:
|
||||
accepted_at=member.accepted_at,
|
||||
membership_started_at=member.membership_started_at,
|
||||
)
|
||||
self.get_contributions(member.member_id)
|
||||
if member.member_id != member_dir.name:
|
||||
errors.append(f"{member_dir.name}/member.json: member_id stimmt nicht mit Ordner überein")
|
||||
normalized_number = member.member_number.casefold().strip()
|
||||
@@ -92,6 +93,8 @@ class MemberRepository:
|
||||
)
|
||||
elif normalized_number:
|
||||
seen_numbers[normalized_number] = member.member_id
|
||||
except RepositoryError as exc:
|
||||
errors.append(str(exc))
|
||||
except (
|
||||
OSError,
|
||||
ValueError,
|
||||
@@ -196,7 +199,15 @@ class MemberRepository:
|
||||
path = self._member_path(member_id) / "contributions.json"
|
||||
if not path.exists():
|
||||
return ContributionData()
|
||||
return ContributionData.from_dict(read_json(path))
|
||||
try:
|
||||
raw = read_json(path)
|
||||
if not isinstance(raw, dict):
|
||||
raise TypeError("Wurzelelement muss ein JSON-Objekt sein")
|
||||
return ContributionData.from_dict(raw)
|
||||
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise RepositoryError(
|
||||
f"{member_id}/contributions.json konnte nicht gelesen werden: {exc}"
|
||||
) from exc
|
||||
|
||||
def save_contributions(self, member_id: str, data: ContributionData) -> None:
|
||||
self.get_member(member_id)
|
||||
|
||||
@@ -207,8 +207,12 @@ class MemberTab(ttk.Frame):
|
||||
self.timeline.insert("", "end", values=(_format_timestamp(event), _event_label(event)))
|
||||
|
||||
def _refresh_contributions(self) -> None:
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
self.claims.delete(*self.claims.get_children())
|
||||
try:
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
except RepositoryError as exc:
|
||||
self.contribution_summary.set(f"FEHLER: {exc}")
|
||||
return
|
||||
for claim in data.claims:
|
||||
self.claims.insert(
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user