Seitenleiste: Eintraege ausblendbar (Backend)

Zwei Ebenen in einer Tabelle, nach dem Vorbild von DashboardLayout:
user_id NULL ist die Vorgabe des Administrators, eine Zeile mit user_id
die zusaetzliche Auswahl eines Kontos. Beide Listen gelten zusammen - wer
etwas versteckt, versteckt es zusaetzlich; einblenden, was der
Administrator ausgeblendet hat, kann niemand. Das erspart einen dritten
Zustand je Eintrag.

Als Schluessel dient der Routenpfad; der ist ohnehin eindeutig, eine
zweite Benennung waere nur eine weitere Stelle zum Auseinanderlaufen.

Drei Routen: lesen (Vorgabe, eigene Auswahl und die fertige Vereinigung
in einem Zug), eigene Auswahl setzen, Vorgabe setzen (nur Admin).

Die Eindeutigkeit laeuft ueber einen Index auf coalesce(user_id, -1).
Ein schlichtes UNIQUE(user_id) haette zwei Vorgabezeilen zugelassen, weil
NULL in SQL nie gleich NULL ist - das war im Entwurf noch falsch.

Versteckt wird nur der Eintrag, nicht die Seite: Lesezeichen,
Dashboard-Karten und Lagerort-QR sollen weiter funktionieren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-28 20:27:14 +01:00
co-authored by Claude Opus 5
parent 8f5d679d9d
commit be267e70a5
5 changed files with 305 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
"""Welche Einträge der Seitenleiste jemand ausgeblendet hat.
Zwei Ebenen: die **Vorgabe des Administrators** gilt für alle (``user_id``
NULL), die **eigene Auswahl** eines Kontos kommt obendrauf. Beide Listen gelten
zusammen — wer etwas versteckt, versteckt es zusätzlich; einblenden, was der
Administrator ausgeblendet hat, kann niemand.
Die eigene Auswahl darf jeder für sich ändern (es betrifft nur seine Ansicht),
die Vorgabe nur ein Administrator.
Versteckt wird ausschliesslich der **Eintrag**, nicht die Seite: Lesezeichen,
Dashboard-Karten und Lagerort-QR verweisen weiter auf Routen, die hier
abgewählt sein können, und sollen weiter funktionieren.
"""
import json
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import NavigationSichtbarkeit, User
from ..schemas import NavigationOut, NavigationSetzen
router = APIRouter(prefix="/navigation", tags=["navigation"])
def _liste(zeile: NavigationSichtbarkeit | None) -> list[str]:
"""Die gespeicherte Liste – kaputter Inhalt gilt als leer.
Lieber eine vollständige Leiste als eine Fehlermeldung: Ein unlesbarer
Eintrag darf niemanden aussperren.
"""
if zeile is None:
return []
try:
werte = json.loads(zeile.versteckt)
except ValueError:
return []
return [w for w in werte if isinstance(w, str)] if isinstance(werte, list) else []
def _zeile(db: Session, user_id: int | None) -> NavigationSichtbarkeit | None:
return (
db.query(NavigationSichtbarkeit)
.filter(NavigationSichtbarkeit.user_id.is_(None) if user_id is None
else NavigationSichtbarkeit.user_id == user_id)
.first()
)
def _schreiben(db: Session, user_id: int | None, versteckt: list[str]) -> None:
"""Die Zeile setzen oder anlegen.
Doppelte und leere Einträge fallen weg; die Reihenfolge ist bedeutungslos
und wird sortiert, damit zwei gleiche Auswahlen auch gleich aussehen.
"""
sauber = sorted({w.strip() for w in versteckt if w and w.strip()})
zeile = _zeile(db, user_id)
if zeile is None:
zeile = NavigationSichtbarkeit(user_id=user_id, versteckt=json.dumps(sauber))
db.add(zeile)
else:
zeile.versteckt = json.dumps(sauber)
db.commit()
def _antwort(db: Session, user: User) -> NavigationOut:
vorgabe = _liste(_zeile(db, None))
eigen = _liste(_zeile(db, user.id))
return NavigationOut(
vorgabe=vorgabe, eigen=eigen, versteckt=sorted(set(vorgabe) | set(eigen))
)
@router.get("", response_model=NavigationOut)
def navigation_lesen(
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> NavigationOut:
"""Vorgabe, eigene Auswahl und die fertige Vereinigung."""
return _antwort(db, user)
@router.put("", response_model=NavigationOut)
def eigene_auswahl_setzen(
payload: NavigationSetzen,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> NavigationOut:
"""Die eigene Auswahl – betrifft nur die eigene Ansicht."""
_schreiben(db, user.id, payload.versteckt)
return _antwort(db, user)
@router.put("/vorgabe", response_model=NavigationOut)
def vorgabe_setzen(
payload: NavigationSetzen,
db: Session = Depends(get_db),
user: User = Depends(require_admin),
) -> NavigationOut:
"""Die Vorgabe für alle."""
_schreiben(db, None, payload.versteckt)
return _antwort(db, user)