- Eine ValidationError-Klasse statt zwei (links.py, projects.py), ein Fehler-Handler in main.py - clean_text() ersetzt vier fast gleiche Text-Prüfungen (Kanban, Haushalt, Reisen, Artikel); Fehlermeldungen nennen jetzt überall die Höchstlänge - Zeitstempel (now_iso, iso_ago, iso_after) und Kontenliste (user_list) an einer Stelle; ungenutzte Importe entfernt - Tests für die Helfer und für einheitliche 400-Fehler aus allen Bereichen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
1.7 KiB
Python
49 lines
1.7 KiB
Python
"""Gemeinsame Helfer fürs Backend: Eingabefehler, Texte prüfen, Zeitstempel, Kontenliste."""
|
|
import re
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
|
class ValidationError(ValueError):
|
|
"""Ungültige Eingabe; main.py macht daraus HTTP 400 mit {"error": Meldung}."""
|
|
|
|
|
|
def clean_text(value, field: str, max_len: int, *, required: bool = True, collapse: bool = True) -> str:
|
|
"""Text säubern und prüfen. collapse: Leerraum (auch Zeilenumbrüche) zu einem Leerzeichen zusammenfassen."""
|
|
if not isinstance(value, str):
|
|
value = ""
|
|
v = re.sub(r"\s+", " ", value).strip() if collapse else value.strip()
|
|
if required and not v:
|
|
raise ValidationError(f"{field} fehlt.")
|
|
if len(v) > max_len:
|
|
raise ValidationError(f"{field} ist zu lang (max. {max_len} Zeichen).")
|
|
return v
|
|
|
|
|
|
# ---------- Zeitstempel: ISO 8601 in UTC mit Millisekunden, z. B. 2026-10-15T08:57:47.533Z ----------
|
|
# Einheitliches Format, damit Vergleiche als Text in SQLite stimmen (done_at <= ? usw.).
|
|
|
|
|
|
def iso(moment: datetime) -> str:
|
|
return moment.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
|
|
|
|
def now_iso() -> str:
|
|
return iso(datetime.now(timezone.utc))
|
|
|
|
|
|
def iso_ago(delta: timedelta) -> str:
|
|
return iso(datetime.now(timezone.utc) - delta)
|
|
|
|
|
|
def iso_after(value: str, delta: timedelta) -> str:
|
|
return iso(datetime.fromisoformat(value.replace("Z", "+00:00")) + delta)
|
|
|
|
|
|
# ---------- Konten ----------
|
|
|
|
|
|
def user_list(conn) -> list:
|
|
"""Alle Konten für Auswahlfelder (Bearbeiter, Freigaben), alphabetisch."""
|
|
rows = conn.execute("SELECT id, display_name FROM users ORDER BY display_name COLLATE NOCASE").fetchall()
|
|
return [{"id": u["id"], "name": u["display_name"]} for u in rows]
|