"""Localization, directionality & media-ref aggregates — Phase 11.5. SiteLocaleBinding = bind a locale profile (+ concrete RTL/LTR) to a site. ExperienceLocalization = multi-locale content shells for experience targets. ExperienceMediaRef = Storage file references only (no binaries in experience_db). No forms/surveys/appointments engine in this phase. """ from __future__ import annotations import uuid from sqlalchemy import Boolean, Index, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( GUID, LocalizationStatus, LocalizationTarget, MediaKind, MediaRefStatus, TextDirection, ) class SiteLocaleBinding( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Pin a locale profile (+ concrete direction) onto a site.""" __tablename__ = "experience_site_locale_bindings" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) locale_profile_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) locale_code: Mapped[str] = mapped_column(String(16), nullable=False) text_direction: Mapped[TextDirection] = mapped_column( default=TextDirection.RTL, nullable=False ) is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", "locale_profile_id", name="uq_experience_site_locale_bindings_profile", ), Index("ix_experience_site_locale_bindings_site", "tenant_id", "site_id"), Index( "ix_experience_site_locale_bindings_profile", "tenant_id", "locale_profile_id", ), Index( "ix_experience_site_locale_bindings_locale", "tenant_id", "locale_code", ), Index( "ix_experience_site_locale_bindings_tenant_deleted", "tenant_id", "is_deleted", ), ) class ExperienceLocalization( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Multi-locale content shell for experience targets (RTL/LTR first-class).""" __tablename__ = "experience_localizations" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) target_type: Mapped[LocalizationTarget] = mapped_column(nullable=False) target_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) locale: Mapped[str] = mapped_column(String(16), nullable=False) text_direction: Mapped[TextDirection] = mapped_column( default=TextDirection.RTL, nullable=False ) title: Mapped[str | None] = mapped_column(String(255), nullable=True) summary: Mapped[str | None] = mapped_column(Text, nullable=True) content_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) status: Mapped[LocalizationStatus] = mapped_column( default=LocalizationStatus.DRAFT, nullable=False ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "target_type", "target_id", "locale", name="uq_experience_localizations_locale", ), Index( "ix_experience_localizations_target", "tenant_id", "target_type", "target_id", ), Index("ix_experience_localizations_locale", "tenant_id", "locale"), Index("ix_experience_localizations_status", "tenant_id", "status"), Index( "ix_experience_localizations_tenant_deleted", "tenant_id", "is_deleted" ), ) class ExperienceMediaRef( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Media reference shell — Storage file refs only (no binaries).""" __tablename__ = "experience_media_refs" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) page_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) owner_type: Mapped[str | None] = mapped_column(String(50), nullable=True) owner_id: Mapped[uuid.UUID | None] = mapped_column(GUID(), nullable=True) kind: Mapped[MediaKind] = mapped_column(default=MediaKind.IMAGE, nullable=False) storage_file_ref: Mapped[str] = mapped_column(String(255), nullable=False) alt_text: Mapped[str | None] = mapped_column(String(255), nullable=True) caption: Mapped[str | None] = mapped_column(String(500), nullable=True) locale: Mapped[str | None] = mapped_column(String(16), nullable=True) text_direction: Mapped[TextDirection | None] = mapped_column(nullable=True) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) status: Mapped[MediaRefStatus] = mapped_column( default=MediaRefStatus.ACTIVE, nullable=False ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( Index("ix_experience_media_refs_workspace", "tenant_id", "workspace_id"), Index("ix_experience_media_refs_site", "tenant_id", "site_id"), Index("ix_experience_media_refs_page", "tenant_id", "page_id"), Index("ix_experience_media_refs_owner", "tenant_id", "owner_type", "owner_id"), Index("ix_experience_media_refs_kind", "tenant_id", "kind"), Index("ix_experience_media_refs_locale", "tenant_id", "locale"), Index("ix_experience_media_refs_status", "tenant_id", "status"), Index( "ix_experience_media_refs_tenant_deleted", "tenant_id", "is_deleted" ), )