b3e259fc25
Check Cross-Plugin Imports / check (push) Has been cancelled
- dashboards-Tabelle (Layout JSONB, Tabs, is_default, partial unique name index) - 6 CRUD-Endpoints /api/v1/dashboards, Owner-only (saved_views-Präzedenz), Audit - Lazy Default-Seed aus MiniApp-Registry (permission-gefiltert, 12-Spalten-Flow) - CORE_PERMISSIONS dashboard:read/write (fixt Phantom-Permission in dashboard.py) - Migration 0144: RLS crm_api+crm_worker + konvergenter Fix der 3 Phase-L-Policies - Tests: test_dashboards_backend.py 23/23 (TDD rot->grün); Regression 162/163
53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
"""Dashboard model — personal per-user dashboards (Phase M2).
|
|
|
|
Dashboards are personal (user-owned) layouts of MiniApp instances: tabs,
|
|
widget placements and per-instance settings, stored as JSONB. Access is
|
|
owner-only (saved_views precedent) — the active workspace limits only the
|
|
available widget types (Phase N), never this personal layout.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from sqlalchemy import Boolean, ForeignKey, Index, String, text
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.core.db import Base, TenantMixin
|
|
|
|
|
|
class Dashboard(Base, TenantMixin):
|
|
"""Personal dashboard — per-user layout of MiniApp instances."""
|
|
|
|
__tablename__ = "dashboards"
|
|
__table_args__ = (
|
|
# Partial unique: soft-deleted dashboards free their name (unlike the
|
|
# saved_views plain constraint, which keeps names occupied forever).
|
|
Index(
|
|
"uq_dashboards_tenant_user_name",
|
|
"tenant_id",
|
|
"user_id",
|
|
"name",
|
|
unique=True,
|
|
postgresql_where=text("deleted_at IS NULL"),
|
|
),
|
|
Index("ix_dashboards_tenant_user", "tenant_id", "user_id"),
|
|
)
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
)
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
# Layout JSONB (validated by app.schemas.dashboard.DashboardLayout):
|
|
# {version, tabs: [{id, name, widgets: [{app_id, settings, col, row, spans}]}]}
|
|
layout: Mapped[dict[str, Any]] = mapped_column(
|
|
JSONB, nullable=False, default=dict
|
|
)
|
|
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
|
)
|