Kunden-Admins pflegen Mail, Nextcloud, ownCloud, WebDAV, SMB und Web-Proxy ihrer Organisation (Formulare aus den Typ-Definitionen des Servers, keine Zugangsdaten), Wirkung fuer die ganze Org, OEs oder Gruppen. Unterseite erscheint nur fuer freigeschaltete Organisationen; root pflegt immer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
import hashlib
|
||
from pathlib import Path
|
||
|
||
from fastapi.templating import Jinja2Templates
|
||
|
||
templates = Jinja2Templates(directory="templates")
|
||
|
||
|
||
def _static_version() -> str:
|
||
"""
|
||
Cache-Busting für /static/*-Dateien (11.09.2026, live gefunden: eine
|
||
Änderung an table-tools.js kam beim Nutzer nicht an, weil der Browser
|
||
die alte Version unter derselben URL gecacht hatte - keine
|
||
Anfrage-URL änderte sich, also gab es für den Browser keinen Grund,
|
||
neu zu laden). Hängt vom tatsächlichen Dateiinhalt ab (nicht der
|
||
Änderungszeit - robuster gegenüber Deploy-Eigenheiten, die mtimes
|
||
verändern können, ohne dass sich der Inhalt ändert), einmalig beim
|
||
Prozessstart berechnet.
|
||
"""
|
||
|
||
static_dir = Path(__file__).parent / "static"
|
||
hasher = hashlib.sha256()
|
||
for datei in sorted(static_dir.glob("*")):
|
||
if datei.is_file():
|
||
hasher.update(datei.read_bytes())
|
||
return hasher.hexdigest()[:12]
|
||
|
||
|
||
templates.env.globals["static_version"] = _static_version()
|
||
|
||
|
||
def datum(wert, mit_zeit: bool = True) -> str:
|
||
"""
|
||
GUI-Runde 09.10.2026: ISO-Zeitstempel ("2026-10-09T11:20:53.460997+02:00")
|
||
als deutsches Datum in Ortszeit anzeigen ("09.10.2026, 11:20"). Leere
|
||
Werte werden zu "–", nicht parsebare unverändert durchgereicht.
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from zoneinfo import ZoneInfo
|
||
|
||
if not wert:
|
||
return "–"
|
||
try:
|
||
zeitpunkt = wert if isinstance(wert, datetime) else datetime.fromisoformat(str(wert))
|
||
except ValueError:
|
||
return str(wert)
|
||
if zeitpunkt.tzinfo is not None:
|
||
zeitpunkt = zeitpunkt.astimezone(ZoneInfo("Europe/Berlin"))
|
||
return zeitpunkt.strftime("%d.%m.%Y, %H:%M" if mit_zeit else "%d.%m.%Y")
|
||
|
||
|
||
templates.env.filters["datum"] = datum
|
||
|
||
|
||
def ring_segmente(teile: list[dict], radius: float = 54, staerke: float = 16) -> list[dict]:
|
||
"""
|
||
GUI-Runde 09.10.2026 (Punkt 6): Ringdiagramm-Segmente als SVG-Pfade für
|
||
das Makro in _diagramme.html - Jinja hat keine Winkelfunktionen. teile:
|
||
[{name, anzahl}], Einträge mit 0 entfallen. Mittelpunkt (0, 0), Start
|
||
oben, im Uhrzeigersinn. Liefert je Segment name, anzahl, prozent, pfad.
|
||
"""
|
||
|
||
import math
|
||
|
||
teile = [t for t in teile if t.get("anzahl")]
|
||
gesamt = sum(t["anzahl"] for t in teile)
|
||
if not gesamt:
|
||
return []
|
||
|
||
aussen, innen = radius, radius - staerke
|
||
winkel = -math.pi / 2
|
||
ergebnis = []
|
||
for t in teile:
|
||
anteil = t["anzahl"] / gesamt
|
||
ende = winkel + anteil * 2 * math.pi
|
||
if anteil >= 0.9999:
|
||
# Voller Ring: zwei Halbkreise je Kante, sonst fallen Start und Ende zusammen.
|
||
pfad = (
|
||
f"M 0 {-aussen} A {aussen} {aussen} 0 1 1 0 {aussen} A {aussen} {aussen} 0 1 1 0 {-aussen} Z "
|
||
f"M 0 {-innen} A {innen} {innen} 0 1 0 0 {innen} A {innen} {innen} 0 1 0 0 {-innen} Z"
|
||
)
|
||
else:
|
||
gross = 1 if ende - winkel > math.pi else 0
|
||
p = lambda r, w: f"{r * math.cos(w):.2f} {r * math.sin(w):.2f}"
|
||
pfad = (
|
||
f"M {p(aussen, winkel)} A {aussen} {aussen} 0 {gross} 1 {p(aussen, ende)} "
|
||
f"L {p(innen, ende)} A {innen} {innen} 0 {gross} 0 {p(innen, winkel)} Z"
|
||
)
|
||
ergebnis.append({
|
||
"name": t["name"],
|
||
"anzahl": t["anzahl"],
|
||
"prozent": round(anteil * 100),
|
||
"pfad": pfad,
|
||
})
|
||
winkel = ende
|
||
return ergebnis
|
||
|
||
|
||
templates.env.globals["ring_segmente"] = ring_segmente
|
||
|
||
|
||
# Org-Ressourcen (15.10.2026): die Kunden-Unterseite "Ressourcen" erscheint
|
||
# nur, wenn der Server sie für die Organisation freigeschaltet hat
|
||
# (ORG_RESSOURCEN_NUR_VORAB). Kurz zwischengespeichert, damit nicht jede
|
||
# Seite der Organisation-Navigation eine zusätzliche Anfrage auslöst.
|
||
_ressourcen_freigabe_cache: dict = {}
|
||
|
||
|
||
def org_ressourcen_freigeschaltet(organization_id) -> bool:
|
||
import time
|
||
|
||
from ressourcen_formular import ressourcen_freigeschaltet
|
||
|
||
schluessel = str(organization_id)
|
||
eintrag = _ressourcen_freigabe_cache.get(schluessel)
|
||
if eintrag and time.monotonic() - eintrag[1] < 60:
|
||
return eintrag[0]
|
||
try:
|
||
wert = ressourcen_freigeschaltet(schluessel)
|
||
except Exception:
|
||
wert = False
|
||
_ressourcen_freigabe_cache[schluessel] = (wert, time.monotonic())
|
||
return wert
|
||
|
||
|
||
templates.env.globals["org_ressourcen_freigeschaltet"] = org_ressourcen_freigeschaltet
|