mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 19:26:53 +02:00
feat: allow deleting housekeeper tasks
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user