Ergänzung Datenschutzerklärung und Impressum, Fristenformular in Einstellungen

This commit is contained in:
2026-08-10 06:44:11 +02:00
parent 8ffbda875e
commit 777e3e5447
25 changed files with 1310 additions and 139 deletions

147
tools/check-retention.py Normal file
View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Gleicht Fristen, Aufräumregeln und Datenschutzerklärung ab.
python3 tools/check-retention.py
Drei Fragen, die sich sonst nur von Hand beantworten lassen:
1. Wird jede Frist aus der Verwaltung auch tatsächlich irgendwo
angewandt? Eine Einstellung, die nichts bewirkt, ist schlimmer als
keine - sie behauptet eine Wirkung.
2. Wird jede Tabelle mit einem Ablauf- oder Löschzeitpunkt vom
Aufräumlauf erfasst? Sonst wächst sie unbegrenzt, und die
Datenschutzerklärung sagt die Unwahrheit.
3. Werden die Fristen im Aufräumlauf aus der Laufzeitkonfiguration
gelesen - oder stehen dort noch feste Zahlen?
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
APP = ROOT / "backend" / "app"
RUNTIME = APP / "runtime_settings.py"
MAINTENANCE = APP / "maintenance.py"
MODELS = APP / "models.py"
# Tabellen mit Zeitfeldern, die BEWUSST nicht aufgeräumt werden.
NO_CLEANUP = {
# Preise sind der Zweck der Sammlung und ohne Personenbezug.
"price_point": "unbefristet, kein Personenbezug",
# Wird beim Zustellversuch aufgeräumt, nicht zeitgesteuert.
"push_subscription": "Aufräumen bei ungültigem Endpunkt",
# Haengt an der Mitgliedschaft, verschwindet per CASCADE.
"notify_state": "CASCADE mit der Liste",
# Laufzeitkonfiguration, kein Ablauf.
"setting": "Konfiguration",
# Kernbestand.
"user": "Kontenbereinigung in _retire_users",
"shopping_list": "Archivbereinigung in _archive_lists",
"list_member": "CASCADE",
"market": "Soft Delete, siehe cleanup_deleted_days",
"category": "Soft Delete, siehe cleanup_deleted_days",
"article": "Soft Delete, siehe cleanup_deleted_days",
"article_attribute": "CASCADE",
"article_market": "CASCADE",
"list_item": "Soft Delete, siehe cleanup_deleted_days",
"user_session": "über expires_at aufgeräumt",
}
def duration_keys() -> list[str]:
source = RUNTIME.read_text(encoding="utf-8")
return re.findall(r'^\s*"(\w+)",\s*"', source, re.M)
def main() -> int:
problems: list[str] = []
notes: list[str] = []
keys = duration_keys()
if not keys:
print("Keine Fristen in runtime_settings.py gefunden.", file=sys.stderr)
return 2
# --- 1. Wird jede Frist irgendwo angewandt? ---
sources = {
path: path.read_text(encoding="utf-8")
for path in APP.rglob("*.py")
if path.name not in {"runtime_settings.py", "config.py"}
}
for key in keys:
users = [
path.relative_to(ROOT)
for path, text in sources.items()
if f'"{key}"' in text
]
if not users:
problems.append(
f"Frist '{key}' ist einstellbar, wird aber nirgends verwendet"
)
else:
notes.append(f" {key:26} -> {', '.join(str(u) for u in users)}")
# --- 2. Erfasst der Aufräumlauf alle Tabellen mit Ablauf? ---
models = MODELS.read_text(encoding="utf-8")
maintenance = MAINTENANCE.read_text(encoding="utf-8")
for match in re.finditer(r'__tablename__ = "(\w+)"', models):
table = match.group(1)
block = models[match.start():]
stop = block.find("\nclass ", 1)
if stop > 0:
block = block[:stop]
has_expiry = bool(
re.search(r"^\s+(expires_at|deleted_at|fetched_at|created_at|window_start)",
block, re.M)
)
if not has_expiry:
continue
# Klassenname aus der Tabelle ableiten
class_match = re.search(
r"class (\w+)\(Base\):(?:(?!\nclass ).)*?__tablename__ = \"" + table + '"',
models, re.S,
)
class_name = class_match.group(1) if class_match else None
covered = class_name and class_name in maintenance
if not covered and table not in NO_CLEANUP:
problems.append(
f"Tabelle '{table}' hat ein Zeitfeld, wird aber vom "
"Aufräumlauf nicht erfasst"
)
# --- 3. Feste Zahlen im Aufräumlauf? ---
for line_no, line in enumerate(maintenance.splitlines(), 1):
if "timedelta(days=" not in line:
continue
# Erlaubt: aus Variablen berechnet, oder die fest dokumentierten
# Sonderfälle (7 Tage Puffer für Mail-Token, 1 Tag Rate Limit).
literal = re.search(r"timedelta\(days=(\d+)\)", line)
if literal and literal.group(1) not in {"1", "7"}:
problems.append(
f"maintenance.py Zeile {line_no}: feste Frist "
f"{literal.group(1)} Tage - gehört in runtime_settings.py"
)
print("Fristen und ihre Verwendung:")
print("\n".join(notes))
if problems:
print(f"\n{len(problems)} Problem(e):\n")
for entry in problems:
print(f" - {entry}")
return 1
print(f"\n{len(keys)} Fristen, alle angewandt; Aufräumlauf vollständig.")
return 0
if __name__ == "__main__":
raise SystemExit(main())