feat: allow deleting housekeeper tasks

This commit is contained in:
Marcel Peterkau
2026-06-21 18:09:04 +02:00
parent 3e9f347435
commit c717d6806b
6 changed files with 110 additions and 2 deletions
+1
View File
@@ -22,6 +22,7 @@
"Beschädigte Beitragsdateien blockieren die Mitgliederansicht nicht mehr und werden vom Hausmeister ohne automatisches Überschreiben gemeldet.",
"Ein Akten-Preflight sperrt bei beschädigten Mitglieder-, Beitrags- oder Eventdateien alle Regeln für die betroffene Akte.",
"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 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.",
+1
View File
@@ -166,6 +166,7 @@ class HousekeeperFinding:
title: str
detail: str
due_date: date | None = None
key: str = ""
def money(value: str | int | float | Decimal) -> Decimal:
+31 -1
View File
@@ -64,10 +64,12 @@ class Housekeeper:
now = datetime.now().astimezone().isoformat(timespec="seconds")
items = _items_by_key(working)
successful_scopes: set[tuple[str, str]] = set()
member_ids = set(self.repository.list_member_ids())
_remove_orphaned_member_items(items, member_ids)
rules = load_rules(self.repository.root)
repository_config = self.repository.get_configuration()
for member_id in self.repository.list_member_ids():
for member_id in sorted(member_ids):
try:
member, contributions = self.repository.preflight_member_record(member_id)
except RepositoryError as exc:
@@ -126,6 +128,23 @@ class Housekeeper:
write_json_atomic(self.state_path, working)
return _open_findings(working["items"])
def delete_task(self, key: str) -> list[HousekeeperFinding]:
selected_key = key.strip()
if not selected_key:
raise RepositoryError("Der Task hat keinen gültigen Key.")
with _exclusive_lock(self.lock_path):
state = self._load_state()
items = _items_by_key(state)
item = items.get(selected_key)
if item is None:
return _open_findings(list(items.values()))
if item.get("action") != "task":
raise RepositoryError("Nur Hausmeister-Tasks können manuell gelöscht werden.")
del items[selected_key]
state["items"] = sorted(items.values(), key=lambda value: str(value.get("key", "")))
write_json_atomic(self.state_path, state)
return _open_findings(state["items"])
@staticmethod
def _refresh_record_integrity_task(
items: dict[str, dict[str, Any]],
@@ -351,6 +370,7 @@ def _open_findings(items: list[dict[str, Any]]) -> list[HousekeeperFinding]:
title=str(item.get("title", item.get("key", "Hausmeister"))),
detail=str(item.get("detail", "")),
due_date=due_date,
key=str(item.get("key", "")),
)
)
severity_order = {"error": 0, "warning": 1, "info": 2}
@@ -360,6 +380,16 @@ def _open_findings(items: list[dict[str, Any]]) -> list[HousekeeperFinding]:
)
def _remove_orphaned_member_items(items: dict[str, dict[str, Any]], member_ids: set[str]) -> None:
orphaned_keys = [
key
for key, item in items.items()
if item.get("member_id") and str(item["member_id"]) not in member_ids
]
for key in orphaned_keys:
del items[key]
@contextmanager
def _exclusive_lock(path: Path):
path.parent.mkdir(parents=True, exist_ok=True)
+7
View File
@@ -320,6 +320,7 @@ class MainWindow(ttk.Frame):
self.findings,
self.open_member,
self.run_housekeeper,
self.delete_housekeeper_task,
lambda: self.tabs.close(key),
)
self.tabs.add(
@@ -336,6 +337,12 @@ class MainWindow(ttk.Frame):
self.status_var.set(f"Hausmeisterlauf beendet: {len(self.findings)} Vorgänge.")
return self.findings
def delete_housekeeper_task(self, key: str) -> list[HousekeeperFinding]:
self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).delete_task(key)
self.refresh_overview(run_housekeeper=False)
self.status_var.set("Hausmeister-Task gelöscht.")
return self.findings
def refresh_overview(self, *, run_housekeeper: bool = True) -> None:
if run_housekeeper:
self.findings = Housekeeper(self.repository, self.config.housekeeper_settings()).run()
+30 -1
View File
@@ -1,7 +1,7 @@
import tkinter as tk
from collections import Counter
from collections.abc import Callable
from tkinter import ttk
from tkinter import messagebox, ttk
from ccma.domain.dates import format_date_for_display
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS, HousekeeperFinding, Member
@@ -188,12 +188,14 @@ class HousekeeperTab(ttk.Frame):
findings: list[HousekeeperFinding],
on_open_member: Callable[[str], None],
on_refresh: Callable[[], list[HousekeeperFinding]],
on_delete: Callable[[str], list[HousekeeperFinding]],
on_close: Callable[[], None],
):
super().__init__(master, padding=12)
self.findings = findings
self.on_open_member = on_open_member
self.on_refresh = on_refresh
self.on_delete = on_delete
self.on_close = on_close
self._build_ui()
@@ -241,6 +243,13 @@ class HousekeeperTab(ttk.Frame):
style="Status.TLabel",
)
self.detail_label.grid(row=0, column=0, sticky="ew")
self.delete_button = ttk.Button(
details,
text="Task löschen",
command=self._delete_selected,
state="disabled",
)
self.delete_button.grid(row=0, column=1, sticky="ne", padx=(12, 0))
details.bind(
"<Configure>",
lambda event: self.detail_label.configure(wraplength=max(300, event.width - 32)),
@@ -254,6 +263,7 @@ class HousekeeperTab(ttk.Frame):
def _render(self) -> None:
self.tree.delete(*self.tree.get_children())
self.detail_var.set("Eintrag auswählen, um Details anzuzeigen.")
self.delete_button.configure(state="disabled")
self.title_var.set(f"HAUSMEISTER · {len(self.findings)} Vorgänge")
for index, finding in enumerate(self.findings):
self.tree.insert(
@@ -267,11 +277,28 @@ class HousekeeperTab(ttk.Frame):
selected = self.tree.selection()
if not selected:
self.detail_var.set("Eintrag auswählen, um Details anzuzeigen.")
self.delete_button.configure(state="disabled")
return
index = int(selected[0])
if index >= len(self.findings):
return
self.detail_var.set(_finding_details(self.findings[index]))
self.delete_button.configure(state="normal" if self.findings[index].key else "disabled")
def _delete_selected(self) -> None:
selected = self.tree.selection()
if not selected:
return
finding = self.findings[int(selected[0])]
if not finding.key or not messagebox.askyesno(
"Hausmeister-Task löschen",
f"Task wirklich löschen?\n\n{finding.title}\n\n"
"Falls die Ursache weiter besteht, wird er beim nächsten Lauf neu angelegt.",
parent=self,
):
return
self.findings = self.on_delete(finding.key)
self._render()
def _open_selected(self) -> None:
selected = self.tree.selection()
@@ -281,6 +308,8 @@ class HousekeeperTab(ttk.Frame):
def _finding_details(finding: HousekeeperFinding) -> str:
lines = [f"{finding.severity.upper()} · {finding.code}", finding.title]
if finding.key:
lines.append(f"Key: {finding.key}")
if finding.due_date:
lines.append(f"Fällig: {format_date_for_display(finding.due_date.isoformat())}")
if finding.detail: