from __future__ import annotations from datetime import datetime from typing import List, Optional from sqlalchemy import ( Boolean, CheckConstraint, DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from sqlalchemy.ext.mutable import MutableList from sqlalchemy import JSON from config.database import Base class NavigationGroup(Base): __tablename__ = "navigation_groups" __table_args__ = ( UniqueConstraint("slug", name="uq_navigation_groups_slug"), Index("ix_navigation_groups_sort_order", "sort_order"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True) slug: Mapped[str] = mapped_column(String(64), nullable=False) label: Mapped[str] = mapped_column(String(128), nullable=False) sort_order: Mapped[int] = mapped_column( Integer, nullable=False, default=100) icon: Mapped[Optional[str]] = mapped_column(String(64)) tooltip: Mapped[Optional[str]] = mapped_column(String(255)) is_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, default=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() ) links: Mapped[List["NavigationLink"]] = relationship( "NavigationLink", back_populates="group", cascade="all, delete-orphan", order_by="NavigationLink.sort_order", ) def __repr__(self) -> str: # pragma: no cover return f"NavigationGroup(id={self.id!r}, slug={self.slug!r})" class NavigationLink(Base): __tablename__ = "navigation_links" __table_args__ = ( UniqueConstraint("group_id", "slug", name="uq_navigation_links_group_slug"), Index("ix_navigation_links_group_sort", "group_id", "sort_order"), Index("ix_navigation_links_parent_sort", "parent_link_id", "sort_order"), CheckConstraint( "(route_name IS NOT NULL) OR (href_override IS NOT NULL)", name="ck_navigation_links_route_or_href", ), ) id: Mapped[int] = mapped_column(Integer, primary_key=True) group_id: Mapped[int] = mapped_column( ForeignKey("navigation_groups.id", ondelete="CASCADE"), nullable=False ) parent_link_id: Mapped[Optional[int]] = mapped_column( ForeignKey("navigation_links.id", ondelete="CASCADE") ) slug: Mapped[str] = mapped_column(String(64), nullable=False) label: Mapped[str] = mapped_column(String(128), nullable=False) route_name: Mapped[Optional[str]] = mapped_column(String(128)) href_override: Mapped[Optional[str]] = mapped_column(String(512)) match_prefix: Mapped[Optional[str]] = mapped_column(String(512)) sort_order: Mapped[int] = mapped_column( Integer, nullable=False, default=100) icon: Mapped[Optional[str]] = mapped_column(String(64)) tooltip: Mapped[Optional[str]] = mapped_column(String(255)) required_roles: Mapped[list[str]] = mapped_column( MutableList.as_mutable(JSON), nullable=False, default=list ) is_enabled: Mapped[bool] = mapped_column( Boolean, nullable=False, default=True) is_external: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False) 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() ) group: Mapped[NavigationGroup] = relationship( NavigationGroup, back_populates="links", ) parent: Mapped[Optional["NavigationLink"]] = relationship( "NavigationLink", remote_side="NavigationLink.id", back_populates="children", ) children: Mapped[List["NavigationLink"]] = relationship( "NavigationLink", back_populates="parent", cascade="all, delete-orphan", order_by="NavigationLink.sort_order", ) def is_visible_for_roles(self, roles: list[str]) -> bool: if not self.required_roles: return True role_set = set(roles) return any(role in role_set for role in self.required_roles) def __repr__(self) -> str: # pragma: no cover return f"NavigationLink(id={self.id!r}, slug={self.slug!r})"