mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04:52 +02:00
refactor: localize UI labels and store filenames
This commit is contained in:
@@ -36,7 +36,7 @@ On first start, select or create the central member-store directory. The
|
|||||||
```text
|
```text
|
||||||
member-store/
|
member-store/
|
||||||
├── repository.json
|
├── repository.json
|
||||||
├── hausmeister.json
|
├── housekeeper.json
|
||||||
├── rules/
|
├── rules/
|
||||||
└── members/
|
└── members/
|
||||||
└── <uuid>/
|
└── <uuid>/
|
||||||
@@ -57,7 +57,7 @@ Store rules are trusted executable Python code. Only place reviewed rules from
|
|||||||
trusted sources in this directory. Rules return structured `RuleAction` objects;
|
trusted sources in this directory. Rules return structured `RuleAction` objects;
|
||||||
CCMA performs all file writes, duplicate checks, audit events, and atomic updates.
|
CCMA performs all file writes, duplicate checks, audit events, and atomic updates.
|
||||||
|
|
||||||
`hausmeister.json` is written only after a complete run. Each refreshed task gets
|
`housekeeper.json` is written only after a complete run. Each refreshed task gets
|
||||||
the pending run ID. A failed run therefore cannot advance the stored counter or
|
the pending run ID. A failed run therefore cannot advance the stored counter or
|
||||||
silently resolve existing tasks.
|
silently resolve existing tasks.
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"Der Hausmeister-Tab zeigt den vollständigen Inhalt eines markierten Vorgangs in einem mehrzeiligen Detailbereich.",
|
"Der Hausmeister-Tab zeigt den vollständigen Inhalt eines markierten Vorgangs in einem mehrzeiligen Detailbereich.",
|
||||||
"Hausmeister-Tasks lassen sich manuell löschen; Einträge entfernter Mitgliederakten werden beim nächsten Lauf bereinigt.",
|
"Hausmeister-Tasks lassen sich manuell löschen; Einträge entfernter Mitgliederakten werden beim nächsten Lauf bereinigt.",
|
||||||
"Forderungen besitzen eigene Tabs mit Positionen, Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
"Forderungen besitzen eigene Tabs mit Positionen, Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
||||||
|
"Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ class Housekeeper:
|
|||||||
def __init__(self, repository: MemberRepository, settings: HousekeeperSettings | None = None):
|
def __init__(self, repository: MemberRepository, settings: HousekeeperSettings | None = None):
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.settings = settings or HousekeeperSettings()
|
self.settings = settings or HousekeeperSettings()
|
||||||
self.state_path = repository.root / "hausmeister.json"
|
self.state_path = repository.root / "housekeeper.json"
|
||||||
self.lock_path = repository.root / ".hausmeister.lock"
|
self.lock_path = repository.root / ".housekeeper.lock"
|
||||||
|
|
||||||
def run(self, today: date | None = None) -> list[HousekeeperFinding]:
|
def run(self, today: date | None = None) -> list[HousekeeperFinding]:
|
||||||
current_date = today or date.today()
|
current_date = today or date.today()
|
||||||
@@ -125,7 +125,7 @@ class Housekeeper:
|
|||||||
"items": sorted(items.values(), key=lambda item: str(item.get("key", ""))),
|
"items": sorted(items.values(), key=lambda item: str(item.get("key", ""))),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
write_json_atomic(self.state_path, working)
|
self._write_state(working)
|
||||||
return _open_findings(working["items"])
|
return _open_findings(working["items"])
|
||||||
|
|
||||||
def delete_task(self, key: str) -> list[HousekeeperFinding]:
|
def delete_task(self, key: str) -> list[HousekeeperFinding]:
|
||||||
@@ -142,7 +142,7 @@ class Housekeeper:
|
|||||||
raise RepositoryError("Nur Hausmeister-Tasks können manuell gelöscht werden.")
|
raise RepositoryError("Nur Hausmeister-Tasks können manuell gelöscht werden.")
|
||||||
del items[selected_key]
|
del items[selected_key]
|
||||||
state["items"] = sorted(items.values(), key=lambda value: str(value.get("key", "")))
|
state["items"] = sorted(items.values(), key=lambda value: str(value.get("key", "")))
|
||||||
write_json_atomic(self.state_path, state)
|
self._write_state(state)
|
||||||
return _open_findings(state["items"])
|
return _open_findings(state["items"])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -343,7 +343,10 @@ class Housekeeper:
|
|||||||
raise ValueError("ungültige Struktur")
|
raise ValueError("ungültige Struktur")
|
||||||
return state
|
return state
|
||||||
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
||||||
raise RepositoryError(f"hausmeister.json konnte nicht gelesen werden: {exc}") from exc
|
raise RepositoryError(f"housekeeper.json konnte nicht gelesen werden: {exc}") from exc
|
||||||
|
|
||||||
|
def _write_state(self, state: dict[str, Any]) -> None:
|
||||||
|
write_json_atomic(self.state_path, state)
|
||||||
|
|
||||||
|
|
||||||
def _items_by_key(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
def _items_by_key(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from ccma.domain.contributions import (
|
|||||||
)
|
)
|
||||||
from ccma.domain.dates import date_input_hint, format_date_for_display
|
from ccma.domain.dates import date_input_hint, format_date_for_display
|
||||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||||
|
from ccma.ui.labels import CLAIM_ITEM_TYPE_LABELS, display_label, storage_key
|
||||||
|
|
||||||
|
|
||||||
class ClaimTab(ttk.Frame):
|
class ClaimTab(ttk.Frame):
|
||||||
@@ -181,7 +182,7 @@ class ClaimTab(ttk.Frame):
|
|||||||
"end",
|
"end",
|
||||||
iid=str(item.get("item_id", "")),
|
iid=str(item.get("item_id", "")),
|
||||||
values=(
|
values=(
|
||||||
item.get("type", ""),
|
display_label(CLAIM_ITEM_TYPE_LABELS, str(item.get("type", ""))),
|
||||||
item.get("description", ""),
|
item.get("description", ""),
|
||||||
item.get("quantity", "1"),
|
item.get("quantity", "1"),
|
||||||
item.get("unit_price", item.get("amount", "")),
|
item.get("unit_price", item.get("amount", "")),
|
||||||
@@ -294,7 +295,7 @@ class ItemDialog(_Dialog):
|
|||||||
def __init__(self, master, repository, member_id, claim_id, on_saved):
|
def __init__(self, master, repository, member_id, claim_id, on_saved):
|
||||||
super().__init__(master, "Forderungsposition hinzufügen", on_saved)
|
super().__init__(master, "Forderungsposition hinzufügen", on_saved)
|
||||||
self.repository, self.member_id, self.claim_id = repository, member_id, claim_id
|
self.repository, self.member_id, self.claim_id = repository, member_id, claim_id
|
||||||
self.type_var = tk.StringVar(value="correction")
|
self.type_var = tk.StringVar(value=CLAIM_ITEM_TYPE_LABELS["correction"])
|
||||||
self.description_var = tk.StringVar()
|
self.description_var = tk.StringVar()
|
||||||
self.quantity_var = tk.StringVar(value="1")
|
self.quantity_var = tk.StringVar(value="1")
|
||||||
self.unit_var = tk.StringVar()
|
self.unit_var = tk.StringVar()
|
||||||
@@ -310,7 +311,7 @@ class ItemDialog(_Dialog):
|
|||||||
ttk.Combobox(
|
ttk.Combobox(
|
||||||
self.frame,
|
self.frame,
|
||||||
textvariable=variable,
|
textvariable=variable,
|
||||||
values=("base", "product", "service", "fee", "discount", "credit", "correction"),
|
values=list(CLAIM_ITEM_TYPE_LABELS.values()),
|
||||||
state="readonly",
|
state="readonly",
|
||||||
width=32,
|
width=32,
|
||||||
).grid(row=row, column=1, sticky="ew", pady=5)
|
).grid(row=row, column=1, sticky="ew", pady=5)
|
||||||
@@ -326,7 +327,7 @@ class ItemDialog(_Dialog):
|
|||||||
description=self.description_var.get(),
|
description=self.description_var.get(),
|
||||||
quantity=self.quantity_var.get(),
|
quantity=self.quantity_var.get(),
|
||||||
unit_price=self.unit_var.get(),
|
unit_price=self.unit_var.get(),
|
||||||
item_type=self.type_var.get(),
|
item_type=storage_key(CLAIM_ITEM_TYPE_LABELS, self.type_var.get()),
|
||||||
)
|
)
|
||||||
except RepositoryError as exc:
|
except RepositoryError as exc:
|
||||||
messagebox.showerror("Position konnte nicht gespeichert werden", str(exc), parent=self)
|
messagebox.showerror("Position konnte nicht gespeichert werden", str(exc), parent=self)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
THEME_LABELS = {
|
||||||
|
"dark": "Dunkel",
|
||||||
|
"light": "Hell",
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAIM_ITEM_TYPE_LABELS = {
|
||||||
|
"base": "Grundposition",
|
||||||
|
"product": "Produkt",
|
||||||
|
"service": "Dienstleistung",
|
||||||
|
"fee": "Gebühr",
|
||||||
|
"discount": "Rabatt",
|
||||||
|
"credit": "Gutschrift",
|
||||||
|
"correction": "Korrektur",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def display_label(labels: Mapping[str, str], key: str) -> str:
|
||||||
|
return labels.get(key, key)
|
||||||
|
|
||||||
|
|
||||||
|
def storage_key(labels: Mapping[str, str], label: str) -> str:
|
||||||
|
return next((key for key, value in labels.items() if value == label), label)
|
||||||
@@ -12,6 +12,7 @@ from ccma.domain.dates import age_label, date_input_hint, format_date_for_displa
|
|||||||
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
|
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
|
||||||
from ccma.domain.models import Event
|
from ccma.domain.models import Event
|
||||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||||
|
from ccma.ui.labels import display_label, storage_key
|
||||||
|
|
||||||
|
|
||||||
class MemberTab(ttk.Frame):
|
class MemberTab(ttk.Frame):
|
||||||
@@ -121,7 +122,7 @@ class MemberTab(ttk.Frame):
|
|||||||
ttk.Combobox(
|
ttk.Combobox(
|
||||||
data_tab,
|
data_tab,
|
||||||
textvariable=self.variables["status"],
|
textvariable=self.variables["status"],
|
||||||
values=list(STATUS_LABELS),
|
values=list(STATUS_LABELS.values()),
|
||||||
state="readonly",
|
state="readonly",
|
||||||
width=39,
|
width=39,
|
||||||
).grid(row=len(fields), column=1, sticky="ew", pady=5)
|
).grid(row=len(fields), column=1, sticky="ew", pady=5)
|
||||||
@@ -196,7 +197,10 @@ class MemberTab(ttk.Frame):
|
|||||||
date_fields = {"birth_date", "accepted_at", "membership_started_at"}
|
date_fields = {"birth_date", "accepted_at", "membership_started_at"}
|
||||||
for key, variable in self.variables.items():
|
for key, variable in self.variables.items():
|
||||||
value = getattr(self.member, key)
|
value = getattr(self.member, key)
|
||||||
variable.set(format_date_for_display(value) if key in date_fields else value)
|
if key == "status":
|
||||||
|
variable.set(display_label(STATUS_LABELS, str(value)))
|
||||||
|
else:
|
||||||
|
variable.set(format_date_for_display(value) if key in date_fields else value)
|
||||||
self._refresh_events()
|
self._refresh_events()
|
||||||
self._refresh_contributions()
|
self._refresh_contributions()
|
||||||
self._refresh_documents()
|
self._refresh_documents()
|
||||||
@@ -248,7 +252,10 @@ class MemberTab(ttk.Frame):
|
|||||||
|
|
||||||
def _save(self) -> None:
|
def _save(self) -> None:
|
||||||
for key, variable in self.variables.items():
|
for key, variable in self.variables.items():
|
||||||
setattr(self.member, key, variable.get().strip())
|
value = variable.get().strip()
|
||||||
|
if key == "status":
|
||||||
|
value = storage_key(STATUS_LABELS, value)
|
||||||
|
setattr(self.member, key, value)
|
||||||
try:
|
try:
|
||||||
self.repository.save_member(self.member)
|
self.repository.save_member(self.member)
|
||||||
except RepositoryError as exc:
|
except RepositoryError as exc:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from ccma.services.intervals import (
|
|||||||
from ccma.storage.repository import MemberRepository, RepositoryError, validate_member_number_pattern
|
from ccma.storage.repository import MemberRepository, RepositoryError, validate_member_number_pattern
|
||||||
from ccma.ui.changelog_view import ChangelogView
|
from ccma.ui.changelog_view import ChangelogView
|
||||||
from ccma.ui.icons import IconStore
|
from ccma.ui.icons import IconStore
|
||||||
|
from ccma.ui.labels import THEME_LABELS, display_label, storage_key
|
||||||
|
|
||||||
|
|
||||||
class OptionsDialog(tk.Toplevel):
|
class OptionsDialog(tk.Toplevel):
|
||||||
@@ -30,7 +31,7 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
self.icons = IconStore(self)
|
self.icons = IconStore(self)
|
||||||
self.store_var = tk.StringVar(value=config.store_path)
|
self.store_var = tk.StringVar(value=config.store_path)
|
||||||
self.gnucash_var = tk.StringVar(value=config.gnucash_path)
|
self.gnucash_var = tk.StringVar(value=config.gnucash_path)
|
||||||
self.theme_var = tk.StringVar(value=config.theme_mode)
|
self.theme_var = tk.StringVar(value=display_label(THEME_LABELS, config.theme_mode))
|
||||||
self.housekeeper_var = tk.BooleanVar(value=config.run_housekeeper_on_startup)
|
self.housekeeper_var = tk.BooleanVar(value=config.run_housekeeper_on_startup)
|
||||||
self.birthday_before_var = tk.StringVar(value=str(config.birthday_days_before))
|
self.birthday_before_var = tk.StringVar(value=str(config.birthday_days_before))
|
||||||
self.birthday_after_var = tk.StringVar(value=str(config.birthday_days_after))
|
self.birthday_after_var = tk.StringVar(value=str(config.birthday_days_after))
|
||||||
@@ -137,7 +138,7 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
ttk.Combobox(
|
ttk.Combobox(
|
||||||
parent,
|
parent,
|
||||||
textvariable=self.theme_var,
|
textvariable=self.theme_var,
|
||||||
values=("dark", "light"),
|
values=list(THEME_LABELS.values()),
|
||||||
state="readonly",
|
state="readonly",
|
||||||
width=18,
|
width=18,
|
||||||
).grid(row=0, column=1, sticky="w", pady=6)
|
).grid(row=0, column=1, sticky="w", pady=6)
|
||||||
@@ -339,7 +340,7 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
store_changed = old_store != store
|
store_changed = old_store != store
|
||||||
self.config_obj.store_path = str(store)
|
self.config_obj.store_path = str(store)
|
||||||
self.config_obj.gnucash_path = str(gnucash) if gnucash else ""
|
self.config_obj.gnucash_path = str(gnucash) if gnucash else ""
|
||||||
self.config_obj.theme_mode = self.theme_var.get()
|
self.config_obj.theme_mode = storage_key(THEME_LABELS, self.theme_var.get())
|
||||||
self.config_obj.run_housekeeper_on_startup = self.housekeeper_var.get()
|
self.config_obj.run_housekeeper_on_startup = self.housekeeper_var.get()
|
||||||
self.config_obj.birthday_days_before = birthday_before
|
self.config_obj.birthday_days_before = birthday_before
|
||||||
self.config_obj.birthday_days_after = birthday_after
|
self.config_obj.birthday_days_after = birthday_after
|
||||||
|
|||||||
+14
-12
@@ -36,8 +36,10 @@ def evaluate(context):
|
|||||||
)
|
)
|
||||||
|
|
||||||
findings = Housekeeper(repository).run(today=date(2026, 6, 21))
|
findings = Housekeeper(repository).run(today=date(2026, 6, 21))
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
assert not (repository.root / "hausmeister.json").exists()
|
||||||
|
assert not (repository.root / ".hausmeister.lock").exists()
|
||||||
assert any(finding.code == "override_active" for finding in findings)
|
assert any(finding.code == "override_active" for finding in findings)
|
||||||
rule = next(item for item in state["rules"] if item["filename"] == "birthdate_check.py")
|
rule = next(item for item in state["rules"] if item["filename"] == "birthdate_check.py")
|
||||||
assert rule["source"] == "store-override"
|
assert rule["source"] == "store-override"
|
||||||
@@ -60,7 +62,7 @@ def test_housekeeper_claim_actions_are_idempotent(tmp_path) -> None:
|
|||||||
first_claims = repository.get_contributions(member.member_id).claims
|
first_claims = repository.get_contributions(member.member_id).claims
|
||||||
housekeeper.run(today=date(2026, 4, 15))
|
housekeeper.run(today=date(2026, 4, 15))
|
||||||
second_claims = repository.get_contributions(member.member_id).claims
|
second_claims = repository.get_contributions(member.member_id).claims
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert {claim["claim_key"] for claim in first_claims} == {
|
assert {claim["claim_key"] for claim in first_claims} == {
|
||||||
"admission-fee",
|
"admission-fee",
|
||||||
@@ -85,7 +87,7 @@ def test_housekeeper_resolves_tasks_not_seen_in_current_run(tmp_path) -> None:
|
|||||||
member.birth_date = "1990-01-01"
|
member.birth_date = "1990-01-01"
|
||||||
repository.save_member(member)
|
repository.save_member(member)
|
||||||
housekeeper.run(today=date(2026, 6, 21))
|
housekeeper.run(today=date(2026, 6, 21))
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
task = next(item for item in state["items"] if item["key"].endswith(":missing"))
|
task = next(item for item in state["items"] if item["key"].endswith(":missing"))
|
||||||
|
|
||||||
assert task["status"] == "resolved"
|
assert task["status"] == "resolved"
|
||||||
@@ -99,14 +101,14 @@ def test_failed_run_does_not_advance_persisted_run_id(tmp_path) -> None:
|
|||||||
repository.create_member(first_name="Failed", last_name="Rule")
|
repository.create_member(first_name="Failed", last_name="Rule")
|
||||||
housekeeper = Housekeeper(repository)
|
housekeeper = Housekeeper(repository)
|
||||||
housekeeper.run(today=date(2026, 6, 21))
|
housekeeper.run(today=date(2026, 6, 21))
|
||||||
state_before = (repository.root / "hausmeister.json").read_bytes()
|
state_before = (repository.root / "housekeeper.json").read_bytes()
|
||||||
(repository.root / "rules" / "broken.py").write_text("this is not python !!!", encoding="utf-8")
|
(repository.root / "rules" / "broken.py").write_text("this is not python !!!", encoding="utf-8")
|
||||||
|
|
||||||
with pytest.raises(RuleLoadError):
|
with pytest.raises(RuleLoadError):
|
||||||
housekeeper.run(today=date(2026, 6, 21))
|
housekeeper.run(today=date(2026, 6, 21))
|
||||||
|
|
||||||
assert (repository.root / "hausmeister.json").read_bytes() == state_before
|
assert (repository.root / "housekeeper.json").read_bytes() == state_before
|
||||||
assert not (repository.root / ".hausmeister.lock").exists()
|
assert not (repository.root / ".housekeeper.lock").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_broken_contributions_file_creates_task_without_overwriting_file(tmp_path) -> None:
|
def test_broken_contributions_file_creates_task_without_overwriting_file(tmp_path) -> None:
|
||||||
@@ -124,7 +126,7 @@ def test_broken_contributions_file_creates_task_without_overwriting_file(tmp_pat
|
|||||||
|
|
||||||
assert path.read_bytes() == b""
|
assert path.read_bytes() == b""
|
||||||
assert [finding.code for finding in findings] == ["invalid_member_record"]
|
assert [finding.code for finding in findings] == ["invalid_member_record"]
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
task = next(item for item in state["items"] if item["code"] == "invalid_member_record")
|
task = next(item for item in state["items"] if item["code"] == "invalid_member_record")
|
||||||
assert task["status"] == "open"
|
assert task["status"] == "open"
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ def test_preflight_skips_all_rules_for_broken_member_file(tmp_path) -> None:
|
|||||||
member_path.write_text("{", encoding="utf-8")
|
member_path.write_text("{", encoding="utf-8")
|
||||||
|
|
||||||
findings = Housekeeper(repository).run(today=date(2026, 6, 21))
|
findings = Housekeeper(repository).run(today=date(2026, 6, 21))
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert [finding.code for finding in findings] == ["invalid_member_record"]
|
assert [finding.code for finding in findings] == ["invalid_member_record"]
|
||||||
assert len(state["items"]) == 1
|
assert len(state["items"]) == 1
|
||||||
@@ -176,7 +178,7 @@ def test_preflight_task_resolves_after_record_is_repaired(tmp_path) -> None:
|
|||||||
contributions_path.write_bytes(original)
|
contributions_path.write_bytes(original)
|
||||||
|
|
||||||
housekeeper.run(today=date(2026, 6, 21))
|
housekeeper.run(today=date(2026, 6, 21))
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
task = next(item for item in state["items"] if item["code"] == "invalid_member_record")
|
task = next(item for item in state["items"] if item["code"] == "invalid_member_record")
|
||||||
|
|
||||||
assert task["status"] == "resolved"
|
assert task["status"] == "resolved"
|
||||||
@@ -192,7 +194,7 @@ def test_housekeeper_task_can_be_deleted_and_returns_on_next_run(tmp_path) -> No
|
|||||||
finding = next(item for item in findings if item.code == "missing_birth_date")
|
finding = next(item for item in findings if item.code == "missing_birth_date")
|
||||||
|
|
||||||
remaining = housekeeper.delete_task(finding.key)
|
remaining = housekeeper.delete_task(finding.key)
|
||||||
state_after_delete = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state_after_delete = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert remaining == []
|
assert remaining == []
|
||||||
assert state_after_delete["run_counter"] == 1
|
assert state_after_delete["run_counter"] == 1
|
||||||
@@ -200,7 +202,7 @@ def test_housekeeper_task_can_be_deleted_and_returns_on_next_run(tmp_path) -> No
|
|||||||
|
|
||||||
recreated = housekeeper.run(today=date(2026, 6, 21))
|
recreated = housekeeper.run(today=date(2026, 6, 21))
|
||||||
recreated_finding = next(item for item in recreated if item.code == "missing_birth_date")
|
recreated_finding = next(item for item in recreated if item.code == "missing_birth_date")
|
||||||
state_after_run = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state_after_run = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
recreated_task = next(item for item in state_after_run["items"] if item["key"] == recreated_finding.key)
|
recreated_task = next(item for item in state_after_run["items"] if item["key"] == recreated_finding.key)
|
||||||
|
|
||||||
assert recreated_finding.key == finding.key
|
assert recreated_finding.key == finding.key
|
||||||
@@ -216,7 +218,7 @@ def test_housekeeper_removes_items_for_deleted_member_directory(tmp_path) -> Non
|
|||||||
shutil.rmtree(repository.members_root / member.member_id)
|
shutil.rmtree(repository.members_root / member.member_id)
|
||||||
|
|
||||||
findings = housekeeper.run(today=date(2026, 6, 21))
|
findings = housekeeper.run(today=date(2026, 6, 21))
|
||||||
state = json.loads((repository.root / "hausmeister.json").read_text(encoding="utf-8"))
|
state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert findings == []
|
assert findings == []
|
||||||
assert not any(item.get("member_id") == member.member_id for item in state["items"])
|
assert not any(item.get("member_id") == member.member_id for item in state["items"])
|
||||||
|
|||||||
@@ -60,3 +60,20 @@ def test_housekeeper_details_are_multiline() -> None:
|
|||||||
assert rendered.splitlines()[0] == "ERROR · invalid_member_record"
|
assert rendered.splitlines()[0] == "ERROR · invalid_member_record"
|
||||||
assert "Mitgliederakte beschädigt\nFällig:" in rendered
|
assert "Mitgliederakte beschädigt\nFällig:" in rendered
|
||||||
assert rendered.endswith("nicht automatisch überschrieben.")
|
assert rendered.endswith("nicht automatisch überschrieben.")
|
||||||
|
|
||||||
|
|
||||||
|
def test_german_ui_labels_round_trip_to_english_storage_keys() -> None:
|
||||||
|
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS
|
||||||
|
from ccma.ui.labels import (
|
||||||
|
CLAIM_ITEM_TYPE_LABELS,
|
||||||
|
THEME_LABELS,
|
||||||
|
display_label,
|
||||||
|
storage_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert display_label(THEME_LABELS, "dark") == "Dunkel"
|
||||||
|
assert storage_key(THEME_LABELS, "Hell") == "light"
|
||||||
|
assert display_label(CLAIM_ITEM_TYPE_LABELS, "credit") == "Gutschrift"
|
||||||
|
assert storage_key(CLAIM_ITEM_TYPE_LABELS, "Dienstleistung") == "service"
|
||||||
|
assert display_label(MEMBERSHIP_STATUS_LABELS, "active") == "AKTIV"
|
||||||
|
assert storage_key(MEMBERSHIP_STATUS_LABELS, "EHRENMITGLIED") == "honorary"
|
||||||
|
|||||||
Reference in New Issue
Block a user