Mehrere Dashboards und dieselbe Karte mehrfach
Zwei Grenzen sassen tief im Datenmodell. Erstens war "i" im gespeicherten Layout zugleich die Kartenart; da react-grid-layout eindeutige Kennungen verlangt, ging jede Art genau einmal - zwei Artikel gleichzeitig beobachten war damit unmoeglich. Zweitens war dashboard_layouts.user_id unique, also genau ein Dashboard je Benutzer. Jetzt ist "i" die Kennung dieser einen Karte und "type" ihre Art, dazu kommt "props" fuer das, was nur diese Karte angeht (etwa welcher Artikel). Aeltere Anordnungen haben kein "type"; fuer sie gilt die alte Kennung als Art, sodass gespeicherte Startseiten unveraendert weiterlaufen. Die Tabelle bekommt Name und Reihenfolge, das unique faellt. Sein Name haengt davon ab, wie die Tabelle entstanden ist, deshalb wird er in pg_constraint nachgeschlagen statt geraten. Eine Feinheit beim Anlegen: Wer noch auf der Admin-Vorgabe sitzt und sein erstes eigenes Dashboard anlegt, wuerde die Vorgabe schlagartig verlieren - sobald eigene Zeilen da sind, zaehlen nur noch die. Deshalb wird sie vorher uebernommen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -61,10 +61,26 @@ def _ensure_schema() -> None:
|
||||
"NOT NULL DEFAULT 'day'",
|
||||
"ALTER TABLE products ADD COLUMN IF NOT EXISTS category_id INTEGER "
|
||||
"REFERENCES categories(id) ON DELETE SET NULL",
|
||||
# Mehrere Dashboards je Benutzer: Name und Reihenfolge kommen dazu.
|
||||
"ALTER TABLE dashboard_layouts ADD COLUMN IF NOT EXISTS name VARCHAR(80) "
|
||||
"NOT NULL DEFAULT 'Übersicht'",
|
||||
"ALTER TABLE dashboard_layouts ADD COLUMN IF NOT EXISTS position INTEGER "
|
||||
"NOT NULL DEFAULT 0",
|
||||
]
|
||||
with engine.begin() as conn:
|
||||
for stmt in stmts:
|
||||
conn.execute(text(stmt))
|
||||
# Solange user_id eindeutig war, ging genau ein Dashboard je Benutzer.
|
||||
# Der Name der Beschraenkung haengt davon ab, wie die Tabelle entstanden
|
||||
# ist, deshalb wird er nachgeschlagen statt geraten.
|
||||
namen = conn.execute(
|
||||
text(
|
||||
"SELECT conname FROM pg_constraint "
|
||||
"WHERE conrelid = 'dashboard_layouts'::regclass AND contype = 'u'"
|
||||
)
|
||||
).scalars().all()
|
||||
for name in namen:
|
||||
conn.execute(text(f'ALTER TABLE dashboard_layouts DROP CONSTRAINT "{name}"'))
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
@@ -336,20 +336,28 @@ class BrandingAsset(Base):
|
||||
|
||||
|
||||
class DashboardLayout(Base):
|
||||
"""Anordnung der Karten auf der Startseite.
|
||||
"""Ein Dashboard: benannte Anordnung von Karten.
|
||||
|
||||
``user_id = NULL`` ist die Vorgabe des Administrators: Sie dient neuen
|
||||
Benutzern als Ausgangspunkt und lässt sich – falls in den Einstellungen so
|
||||
gesetzt – auch verbindlich für alle machen.
|
||||
|
||||
Jeder Benutzer darf **mehrere** Dashboards haben (deshalb steht auf
|
||||
``user_id`` kein unique mehr); ``position`` bestimmt ihre Reihenfolge in der
|
||||
Seitenleiste.
|
||||
"""
|
||||
|
||||
__tablename__ = "dashboard_layouts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=True
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
# JSON-Text: [{"i": "expiring", "x": 0, "y": 0, "w": 4, "h": 3, "props": {}}]
|
||||
name: Mapped[str] = mapped_column(String(80), nullable=False, default="Übersicht")
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
# JSON-Text. "i" ist die Kennung *dieser Karte*, "type" ihre Art - erst
|
||||
# dadurch laesst sich dieselbe Art mehrfach auf ein Dashboard legen:
|
||||
# [{"i": "k3", "type": "expiring", "x": 0, "y": 0, "w": 4, "h": 3, "props": {}}]
|
||||
layout: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_now, onupdate=_now
|
||||
|
||||
@@ -34,9 +34,11 @@ from ..models import (
|
||||
from ..schemas import (
|
||||
ActivityPoint,
|
||||
CategoryShare,
|
||||
DashboardLayoutIn,
|
||||
DashboardLayoutOut,
|
||||
DashboardCreate,
|
||||
DashboardListOut,
|
||||
DashboardOut,
|
||||
DashboardStats,
|
||||
DashboardUpdate,
|
||||
ExpirySplit,
|
||||
FlowPoint,
|
||||
TimelinePoint,
|
||||
@@ -53,8 +55,8 @@ ENFORCED_KEY = "dashboard_enforced"
|
||||
# Höhen zählen Rasterzeilen à 40 px; die Oberfläche hebt zu flache Karten
|
||||
# zusätzlich auf ihre Mindesthöhe an.
|
||||
BUILTIN_LAYOUT: list[dict] = [
|
||||
{"i": "actions", "x": 0, "y": 0, "w": 4, "h": 3},
|
||||
{"i": "status", "x": 4, "y": 0, "w": 8, "h": 3},
|
||||
{"i": "actions", "type": "actions", "x": 0, "y": 0, "w": 4, "h": 3},
|
||||
{"i": "status", "type": "status", "x": 4, "y": 0, "w": 8, "h": 3},
|
||||
# Bewusst die kombinierte Karte: "expiring" zeigt nur noch Laufendes, in der
|
||||
# Vorgabe soll aber auch Ueberfaelliges ohne Zutun sichtbar sein.
|
||||
{"i": "expiry-all", "x": 0, "y": 3, "w": 6, "h": 6},
|
||||
@@ -70,119 +72,209 @@ def _enforced(db: Session) -> bool:
|
||||
return bool(row and row.value == "1")
|
||||
|
||||
|
||||
def _load(db: Session, user_id: int | None) -> list[dict] | None:
|
||||
row = (
|
||||
db.query(DashboardLayout)
|
||||
.filter(DashboardLayout.user_id.is_(None) if user_id is None else DashboardLayout.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
def _normalize(layout: list[dict]) -> list[dict]:
|
||||
"""Karten auf die Instanz-Form bringen.
|
||||
|
||||
Frueher war ``i`` zugleich die Kartenart – dadurch ging jede Art genau
|
||||
einmal. Jetzt ist ``i`` die Kennung dieser einen Karte und ``type`` ihre
|
||||
Art. Aeltere gespeicherte Anordnungen haben kein ``type``; fuer sie ist die
|
||||
alte Kennung die Art.
|
||||
"""
|
||||
result: list[dict] = []
|
||||
for eintrag in layout:
|
||||
if not isinstance(eintrag, dict) or "i" not in eintrag:
|
||||
continue
|
||||
karte = dict(eintrag)
|
||||
karte.setdefault("type", karte["i"])
|
||||
karte.setdefault("props", {})
|
||||
result.append(karte)
|
||||
return result
|
||||
|
||||
|
||||
def _parse(row: DashboardLayout) -> list[dict]:
|
||||
try:
|
||||
data = json.loads(row.layout)
|
||||
except ValueError:
|
||||
return None
|
||||
return data if isinstance(data, list) else None
|
||||
return []
|
||||
return _normalize(data) if isinstance(data, list) else []
|
||||
|
||||
|
||||
def _store(db: Session, user_id: int | None, layout: list[dict]) -> None:
|
||||
row = (
|
||||
db.query(DashboardLayout)
|
||||
.filter(DashboardLayout.user_id.is_(None) if user_id is None else DashboardLayout.user_id == user_id)
|
||||
.first()
|
||||
def _rows(db: Session, user_id: int | None) -> list[DashboardLayout]:
|
||||
query = db.query(DashboardLayout)
|
||||
query = query.filter(
|
||||
DashboardLayout.user_id.is_(None)
|
||||
if user_id is None
|
||||
else DashboardLayout.user_id == user_id
|
||||
)
|
||||
return query.order_by(DashboardLayout.position, DashboardLayout.id).all()
|
||||
|
||||
|
||||
def _as_out(rows: list[DashboardLayout]) -> list[DashboardOut]:
|
||||
return [
|
||||
DashboardOut(id=row.id, name=row.name, position=row.position, layout=_parse(row))
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _owned(db: Session, dashboard_id: int, user: User) -> DashboardLayout:
|
||||
row = db.get(DashboardLayout, dashboard_id)
|
||||
if row is None or row.user_id != user.id:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Dashboard nicht gefunden")
|
||||
return row
|
||||
|
||||
|
||||
def _guard_enforced(db: Session) -> None:
|
||||
if _enforced(db) and _rows(db, None):
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
"Die Startseite ist vom Administrator fest vorgegeben.",
|
||||
)
|
||||
text = json.dumps(layout, ensure_ascii=False)
|
||||
if row is None:
|
||||
db.add(DashboardLayout(user_id=user_id, layout=text))
|
||||
else:
|
||||
row.layout = text
|
||||
db.commit()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Anordnung
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/layout", response_model=DashboardLayoutOut)
|
||||
def get_layout(
|
||||
@router.get("/layouts", response_model=DashboardListOut)
|
||||
def list_dashboards(
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user)
|
||||
) -> DashboardLayoutOut:
|
||||
"""Eigene Anordnung, sonst die Vorgabe, sonst die eingebaute."""
|
||||
) -> DashboardListOut:
|
||||
"""Eigene Dashboards, sonst die Vorgabe, sonst das eingebaute."""
|
||||
erzwungen = _enforced(db)
|
||||
vorgabe = _load(db, None)
|
||||
vorgabe = _rows(db, None)
|
||||
|
||||
if erzwungen and vorgabe is not None:
|
||||
return DashboardLayoutOut(layout=vorgabe, source="default", enforced=True, has_default=True)
|
||||
if erzwungen and vorgabe:
|
||||
return DashboardListOut(
|
||||
dashboards=_as_out(vorgabe), source="default", enforced=True, has_default=True
|
||||
)
|
||||
|
||||
eigene = _load(db, user.id)
|
||||
if eigene is not None:
|
||||
return DashboardLayoutOut(
|
||||
layout=eigene, source="user", enforced=erzwungen, has_default=vorgabe is not None
|
||||
eigene = _rows(db, user.id)
|
||||
if eigene:
|
||||
return DashboardListOut(
|
||||
dashboards=_as_out(eigene), source="user",
|
||||
enforced=erzwungen, has_default=bool(vorgabe),
|
||||
)
|
||||
if vorgabe is not None:
|
||||
return DashboardLayoutOut(
|
||||
layout=vorgabe, source="default", enforced=erzwungen, has_default=True
|
||||
if vorgabe:
|
||||
return DashboardListOut(
|
||||
dashboards=_as_out(vorgabe), source="default",
|
||||
enforced=erzwungen, has_default=True,
|
||||
)
|
||||
return DashboardLayoutOut(
|
||||
layout=BUILTIN_LAYOUT, source="builtin", enforced=erzwungen, has_default=False
|
||||
# Ohne alles das eingebaute Dashboard - mit id 0, weil es keine Zeile hat.
|
||||
return DashboardListOut(
|
||||
dashboards=[DashboardOut(id=0, name="Übersicht", position=0,
|
||||
layout=_normalize(BUILTIN_LAYOUT))],
|
||||
source="builtin", enforced=erzwungen, has_default=False,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/layout", response_model=DashboardLayoutOut)
|
||||
def put_layout(
|
||||
payload: DashboardLayoutIn,
|
||||
@router.post("/layouts", response_model=DashboardOut, status_code=status.HTTP_201_CREATED)
|
||||
def create_dashboard(
|
||||
payload: DashboardCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> DashboardLayoutOut:
|
||||
if _enforced(db) and _load(db, None) is not None:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
"Die Startseite ist vom Administrator fest vorgegeben.",
|
||||
) -> DashboardOut:
|
||||
_guard_enforced(db)
|
||||
bestehende = _rows(db, user.id)
|
||||
# Legt der Benutzer sein erstes eigenes Dashboard an, waehrend er noch auf
|
||||
# der Vorgabe sitzt, wuerde diese sonst schlagartig verschwinden. Deshalb
|
||||
# wird sie vorher als eigene Dashboards uebernommen.
|
||||
if not bestehende:
|
||||
for row in _rows(db, None):
|
||||
db.add(DashboardLayout(user_id=user.id, name=row.name,
|
||||
position=row.position, layout=row.layout))
|
||||
db.flush()
|
||||
bestehende = _rows(db, user.id)
|
||||
|
||||
position = max((row.position for row in bestehende), default=-1) + 1
|
||||
neu = DashboardLayout(
|
||||
user_id=user.id, name=payload.name.strip(), position=position,
|
||||
layout=json.dumps(_normalize(payload.layout), ensure_ascii=False),
|
||||
)
|
||||
_store(db, user.id, payload.layout)
|
||||
return get_layout(db=db, user=user)
|
||||
db.add(neu)
|
||||
db.commit()
|
||||
db.refresh(neu)
|
||||
return DashboardOut(id=neu.id, name=neu.name, position=neu.position, layout=_parse(neu))
|
||||
|
||||
|
||||
@router.delete("/layout")
|
||||
def reset_layout(
|
||||
@router.patch("/layouts/{dashboard_id}", response_model=DashboardOut)
|
||||
def update_dashboard(
|
||||
dashboard_id: int,
|
||||
payload: DashboardUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> DashboardOut:
|
||||
_guard_enforced(db)
|
||||
row = _owned(db, dashboard_id, user)
|
||||
|
||||
daten = payload.model_dump(exclude_unset=True)
|
||||
if daten.get("name"):
|
||||
row.name = daten["name"].strip()
|
||||
if daten.get("layout") is not None:
|
||||
row.layout = json.dumps(_normalize(daten["layout"]), ensure_ascii=False)
|
||||
if daten.get("position") is not None:
|
||||
row.position = daten["position"]
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return DashboardOut(id=row.id, name=row.name, position=row.position, layout=_parse(row))
|
||||
|
||||
|
||||
@router.delete("/layouts/{dashboard_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_dashboard(
|
||||
dashboard_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
) -> Response:
|
||||
_guard_enforced(db)
|
||||
row = _owned(db, dashboard_id, user)
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.delete("/layouts")
|
||||
def reset_dashboards(
|
||||
db: Session = Depends(get_db), user: User = Depends(get_current_user)
|
||||
) -> Response:
|
||||
"""Eigene Anordnung verwerfen – danach gilt wieder die Vorgabe.
|
||||
"""Alle eigenen Dashboards verwerfen – danach gilt wieder die Vorgabe.
|
||||
|
||||
Der Statuscode steht bewusst am Response und nicht im Dekorator: Durch
|
||||
``from __future__ import annotations`` wird ``-> None`` zu einer Zeichenkette,
|
||||
die FastAPI zu ``NoneType`` auflöst und als Antwortmodell wertet – zusammen
|
||||
mit 204 (das keinen Rumpf haben darf) bricht der Start dann ab.
|
||||
"""
|
||||
row = db.query(DashboardLayout).filter(DashboardLayout.user_id == user.id).first()
|
||||
if row is not None:
|
||||
for row in _rows(db, user.id):
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.put("/layout/default", response_model=DashboardLayoutOut)
|
||||
def put_default_layout(
|
||||
payload: DashboardLayoutIn,
|
||||
@router.put("/layouts/default", response_model=DashboardListOut)
|
||||
def put_default_dashboards(
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
) -> DashboardLayoutOut:
|
||||
_store(db, None, payload.layout)
|
||||
return get_layout(db=db, user=admin)
|
||||
) -> DashboardListOut:
|
||||
"""Die eigenen Dashboards zur Vorgabe für alle machen."""
|
||||
for row in _rows(db, None):
|
||||
db.delete(row)
|
||||
for row in _rows(db, admin.id):
|
||||
db.add(DashboardLayout(user_id=None, name=row.name,
|
||||
position=row.position, layout=row.layout))
|
||||
db.commit()
|
||||
return list_dashboards(db=db, user=admin)
|
||||
|
||||
|
||||
@router.put("/layout/enforced", response_model=DashboardLayoutOut)
|
||||
@router.put("/layout/enforced", response_model=DashboardListOut)
|
||||
def set_enforced(
|
||||
value: bool,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(require_admin),
|
||||
) -> DashboardLayoutOut:
|
||||
) -> DashboardListOut:
|
||||
row = db.get(Setting, ENFORCED_KEY)
|
||||
if row is None:
|
||||
db.add(Setting(key=ENFORCED_KEY, value="1" if value else "0"))
|
||||
else:
|
||||
row.value = "1" if value else "0"
|
||||
db.commit()
|
||||
return get_layout(db=db, user=admin)
|
||||
return list_dashboards(db=db, user=admin)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -424,8 +424,27 @@ class DashboardLayoutIn(BaseModel):
|
||||
layout: list[dict]
|
||||
|
||||
|
||||
class DashboardLayoutOut(BaseModel):
|
||||
class DashboardCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=80)
|
||||
layout: list[dict] = []
|
||||
|
||||
|
||||
class DashboardUpdate(BaseModel):
|
||||
"""Umbenennen und/oder umbauen – nur mitgeschickte Felder zaehlen."""
|
||||
name: str | None = Field(default=None, min_length=1, max_length=80)
|
||||
layout: list[dict] | None = None
|
||||
position: int | None = None
|
||||
|
||||
|
||||
class DashboardOut(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
position: int
|
||||
layout: list[dict]
|
||||
|
||||
|
||||
class DashboardListOut(BaseModel):
|
||||
dashboards: list[DashboardOut]
|
||||
# Woher die Anordnung stammt: eigene, Admin-Vorgabe oder eingebaut.
|
||||
source: str
|
||||
# Ist die Vorgabe verbindlich, darf der Benutzer nicht umbauen.
|
||||
@@ -433,6 +452,13 @@ class DashboardLayoutOut(BaseModel):
|
||||
has_default: bool
|
||||
|
||||
|
||||
class DashboardLayoutOut(BaseModel):
|
||||
layout: list[dict]
|
||||
source: str
|
||||
enforced: bool
|
||||
has_default: bool
|
||||
|
||||
|
||||
class DashboardStats(BaseModel):
|
||||
products_in_stock: int # Artikel mit Bestand > 0
|
||||
article_units: float # Summe der Artikeleinheiten
|
||||
|
||||
@@ -188,54 +188,120 @@ def test_activity_zaehlt_ein_und_auslagerungen(db, rice, user):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Anordnung
|
||||
def _erstes(antwort):
|
||||
return antwort.dashboards[0]
|
||||
|
||||
|
||||
def test_layout_faellt_auf_die_eingebaute_zurueck(db, user):
|
||||
antwort = dashboard.get_layout(db=db, user=user)
|
||||
antwort = dashboard.list_dashboards(db=db, user=user)
|
||||
assert antwort.source == "builtin"
|
||||
assert antwort.enforced is False
|
||||
assert antwort.has_default is False
|
||||
assert any(k["i"] == "expiry-all" for k in antwort.layout)
|
||||
assert len(antwort.dashboards) == 1
|
||||
assert any(k["type"] == "expiry-all" for k in _erstes(antwort).layout)
|
||||
|
||||
|
||||
def test_eigene_anordnung_wird_gespeichert(db, user):
|
||||
from app.schemas import DashboardLayoutIn
|
||||
from app.schemas import DashboardCreate, DashboardUpdate
|
||||
|
||||
eigen = [{"i": "shopping", "x": 0, "y": 0, "w": 6, "h": 3}]
|
||||
dashboard.put_layout(DashboardLayoutIn(layout=eigen), db=db, user=user)
|
||||
neu = dashboard.create_dashboard(
|
||||
DashboardCreate(name="Vorräte", layout=[]), db=db, user=user
|
||||
)
|
||||
eigen = [{"i": "k1", "type": "shopping", "x": 0, "y": 0, "w": 6, "h": 3, "props": {}}]
|
||||
dashboard.update_dashboard(neu.id, DashboardUpdate(layout=eigen), db=db, user=user)
|
||||
|
||||
antwort = dashboard.get_layout(db=db, user=user)
|
||||
antwort = dashboard.list_dashboards(db=db, user=user)
|
||||
assert antwort.source == "user"
|
||||
assert antwort.layout == eigen
|
||||
assert _erstes(antwort).name == "Vorräte"
|
||||
assert _erstes(antwort).layout == eigen
|
||||
|
||||
|
||||
def test_dieselbe_kartenart_mehrfach(db, user):
|
||||
"""Der eigentliche Zweck der Instanz-Kennung: zwei Karten derselben Art,
|
||||
jede auf einen anderen Artikel eingestellt."""
|
||||
from app.schemas import DashboardCreate
|
||||
|
||||
layout = [
|
||||
{"i": "a", "type": "product-timeline", "x": 0, "y": 0, "w": 6, "h": 6,
|
||||
"props": {"product_id": 1}},
|
||||
{"i": "b", "type": "product-timeline", "x": 6, "y": 0, "w": 6, "h": 6,
|
||||
"props": {"product_id": 2}},
|
||||
]
|
||||
neu = dashboard.create_dashboard(
|
||||
DashboardCreate(name="Beobachtung", layout=layout), db=db, user=user
|
||||
)
|
||||
assert len(neu.layout) == 2
|
||||
assert {k["props"]["product_id"] for k in neu.layout} == {1, 2}
|
||||
|
||||
|
||||
def test_alte_anordnung_bekommt_die_art_aus_der_kennung(db):
|
||||
"""Gespeicherte Anordnungen von frueher haben kein "type" - dort war die
|
||||
Kennung die Art. Ohne das Nachziehen waere jede alte Karte unbekannt."""
|
||||
alt = [{"i": "shopping", "x": 0, "y": 0, "w": 6, "h": 3}]
|
||||
normalisiert = dashboard._normalize(alt)
|
||||
assert normalisiert[0]["type"] == "shopping"
|
||||
assert normalisiert[0]["props"] == {}
|
||||
|
||||
|
||||
def test_erzwungene_vorgabe_verhindert_eigene_anordnung(db, user):
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.schemas import DashboardLayoutIn
|
||||
from app.schemas import DashboardCreate
|
||||
|
||||
vorgabe = [{"i": "status", "x": 0, "y": 0, "w": 12, "h": 1}]
|
||||
dashboard.put_default_layout(DashboardLayoutIn(layout=vorgabe), db=db, admin=user)
|
||||
dashboard.create_dashboard(DashboardCreate(name="Vorgabe", layout=[]), db=db, user=user)
|
||||
dashboard.put_default_dashboards(db=db, admin=user)
|
||||
dashboard.set_enforced(True, db=db, admin=user)
|
||||
|
||||
antwort = dashboard.get_layout(db=db, user=user)
|
||||
antwort = dashboard.list_dashboards(db=db, user=user)
|
||||
assert antwort.source == "default"
|
||||
assert antwort.enforced is True
|
||||
assert antwort.layout == vorgabe
|
||||
|
||||
with pytest.raises(HTTPException) as fehler:
|
||||
dashboard.put_layout(DashboardLayoutIn(layout=[]), db=db, user=user)
|
||||
dashboard.create_dashboard(DashboardCreate(name="X", layout=[]), db=db, user=user)
|
||||
assert fehler.value.status_code == 403
|
||||
|
||||
|
||||
def test_zuruecksetzen_stellt_die_vorgabe_wieder_her(db, user):
|
||||
from app.schemas import DashboardLayoutIn
|
||||
from app.schemas import DashboardCreate
|
||||
|
||||
vorgabe = [{"i": "status", "x": 0, "y": 0, "w": 12, "h": 1}]
|
||||
dashboard.put_default_layout(DashboardLayoutIn(layout=vorgabe), db=db, admin=user)
|
||||
dashboard.put_layout(DashboardLayoutIn(layout=[{"i": "shopping", "x": 0, "y": 0, "w": 4, "h": 3}]),
|
||||
db=db, user=user)
|
||||
assert dashboard.get_layout(db=db, user=user).source == "user"
|
||||
dashboard.create_dashboard(DashboardCreate(name="Vorgabe", layout=[]), db=db, user=user)
|
||||
dashboard.put_default_dashboards(db=db, admin=user)
|
||||
dashboard.create_dashboard(DashboardCreate(name="Eigenes", layout=[]), db=db, user=user)
|
||||
assert dashboard.list_dashboards(db=db, user=user).source == "user"
|
||||
|
||||
dashboard.reset_layout(db=db, user=user)
|
||||
antwort = dashboard.get_layout(db=db, user=user)
|
||||
dashboard.reset_dashboards(db=db, user=user)
|
||||
antwort = dashboard.list_dashboards(db=db, user=user)
|
||||
assert antwort.source == "default"
|
||||
assert antwort.layout == vorgabe
|
||||
assert _erstes(antwort).name == "Vorgabe"
|
||||
|
||||
|
||||
def test_erstes_eigenes_uebernimmt_die_vorgabe(db, user):
|
||||
"""Sonst verschwaende das Anlegen eines zweiten Dashboards die Vorgabe:
|
||||
Sobald eigene Zeilen existieren, zaehlen nur noch die."""
|
||||
from app.schemas import DashboardCreate
|
||||
|
||||
dashboard.create_dashboard(DashboardCreate(name="Vorgabe", layout=[]), db=db, user=user)
|
||||
dashboard.put_default_dashboards(db=db, admin=user)
|
||||
dashboard.reset_dashboards(db=db, user=user)
|
||||
assert dashboard.list_dashboards(db=db, user=user).source == "default"
|
||||
|
||||
dashboard.create_dashboard(DashboardCreate(name="Getränke", layout=[]), db=db, user=user)
|
||||
antwort = dashboard.list_dashboards(db=db, user=user)
|
||||
assert [d.name for d in antwort.dashboards] == ["Vorgabe", "Getränke"]
|
||||
|
||||
|
||||
def test_fremdes_dashboard_ist_unsichtbar(db, user):
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.models import Role, User
|
||||
from app.schemas import DashboardCreate, DashboardUpdate
|
||||
|
||||
fremd = User(username="andere", password_hash="x", role=Role.user)
|
||||
db.add(fremd)
|
||||
db.commit()
|
||||
db.refresh(fremd)
|
||||
|
||||
meins = dashboard.create_dashboard(DashboardCreate(name="Meins", layout=[]), db=db, user=user)
|
||||
with pytest.raises(HTTPException) as fehler:
|
||||
dashboard.update_dashboard(meins.id, DashboardUpdate(name="Geklaut"), db=db, user=fremd)
|
||||
assert fehler.value.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user