"""Workspace models — UI/navigation context only. Workspaces control which modules, menu items, calendar views, contact folders, saved views, and dashboard widgets are visible to a user. They NEVER affect RBAC, ABAC, entity permissions, owner/sharing rights, tenant memberships, RLS policies, or actual data access rights. See: docs/security_kernel.md for the permission intersection rule. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, func, 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 from app.models.owned_mixin import OwnedMixin class Workspace(Base, TenantMixin, OwnedMixin): """A workspace is a UI/navigation context for a user. It defines which modules are visible, which dashboard widgets appear, and how the sidebar is configured. It does NOT affect data access rights. """ __tablename__ = "workspaces" __table_args__ = ( UniqueConstraint("tenant_id", "name", name="uq_workspaces_tenant_name"), # Only one default workspace per tenant Index( "uq_workspace_default_per_tenant", "tenant_id", unique=True, postgresql_where=text("is_default = true"), ), Index("ix_workspaces_tenant", "tenant_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) icon: Mapped[str] = mapped_column(String(50), nullable=False, default="LayoutGrid") description: Mapped[str | None] = mapped_column(String(500), nullable=True) is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) created_by: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) class WorkspaceModule(Base, TenantMixin): """Which modules are visible in a workspace and their configuration.""" __tablename__ = "workspace_modules" __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "module_key", name="uq_wm_tenant_workspace_module" ), Index("ix_wm_workspace", "tenant_id", "workspace_id", "menu_order"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) workspace_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) module_key: Mapped[str] = mapped_column(String(100), nullable=False) is_visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) menu_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) class WorkspaceUser(Base, TenantMixin): """User assignment to a workspace with role (member or manager).""" __tablename__ = "workspace_users" __table_args__ = ( UniqueConstraint( "tenant_id", "workspace_id", "user_id", name="uq_wu_tenant_workspace_user" ), CheckConstraint("role IN ('member', 'manager')", name="ck_wu_role"), Index("ix_wu_workspace", "tenant_id", "workspace_id"), Index("ix_wu_user", "tenant_id", "user_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) workspace_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) role: Mapped[str] = mapped_column(String(20), nullable=False, default="member") is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) assigned_by: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) assigned_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class WorkspaceWidget(Base, TenantMixin): """Dashboard widget configuration per workspace. Multiple instances of the same widget type can exist in the same workspace. No UNIQUE constraint on (workspace_id, widget_key) — allows duplicates. """ __tablename__ = "workspace_widgets" __table_args__ = ( Index("ix_ww_workspace", "tenant_id", "workspace_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) workspace_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) widget_key: Mapped[str] = mapped_column(String(100), nullable=False) position_x: Mapped[int] = mapped_column(Integer, nullable=False, default=0) position_y: Mapped[int] = mapped_column(Integer, nullable=False, default=0) width: Mapped[int] = mapped_column(Integer, nullable=False, default=1) height: Mapped[int] = mapped_column(Integer, nullable=False, default=1) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), )