Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""Theme & layout query specifications — Phase 11.3 (versioned)."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import Select, asc, desc, or_
|
|
from sqlalchemy.sql import ColumnElement
|
|
|
|
from app.models.themes import (
|
|
ExperienceLayout,
|
|
ExperienceLayoutVersion,
|
|
ExperienceTheme,
|
|
ExperienceThemeVersion,
|
|
PageLayoutBinding,
|
|
SiteThemeBinding,
|
|
)
|
|
from app.models.types import (
|
|
DirectionMode,
|
|
LayoutKind,
|
|
LayoutStatus,
|
|
LayoutVersionStatus,
|
|
ThemeStatus,
|
|
ThemeVersionStatus,
|
|
)
|
|
|
|
THEME_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "code", "name", "status", "direction_mode"}
|
|
)
|
|
THEME_VERSION_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "version_number", "status"}
|
|
)
|
|
LAYOUT_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "code", "name", "status", "direction_mode", "kind"}
|
|
)
|
|
LAYOUT_VERSION_SORT_FIELDS = frozenset(
|
|
{"created_at", "updated_at", "version_number", "status"}
|
|
)
|
|
SITE_THEME_BINDING_SORT_FIELDS = frozenset({"created_at", "updated_at"})
|
|
PAGE_LAYOUT_BINDING_SORT_FIELDS = frozenset({"created_at", "updated_at"})
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ThemeListSpec:
|
|
workspace_id: UUID | None = None
|
|
status: ThemeStatus | str | None = None
|
|
direction_mode: DirectionMode | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(ExperienceTheme.workspace_id == self.workspace_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, ThemeStatus)
|
|
else ThemeStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceTheme.status == status)
|
|
if self.direction_mode is not None:
|
|
mode = (
|
|
self.direction_mode
|
|
if isinstance(self.direction_mode, DirectionMode)
|
|
else DirectionMode(str(self.direction_mode))
|
|
)
|
|
clauses.append(ExperienceTheme.direction_mode == mode)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
ExperienceTheme.code.ilike(term),
|
|
ExperienceTheme.name.ilike(term),
|
|
ExperienceTheme.description.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = self.sort_by if self.sort_by in THEME_SORT_FIELDS else "created_at"
|
|
column = getattr(ExperienceTheme, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ThemeVersionListSpec:
|
|
theme_id: UUID | None = None
|
|
status: ThemeVersionStatus | str | None = None
|
|
sort_by: str = "version_number"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.theme_id is not None:
|
|
clauses.append(ExperienceThemeVersion.theme_id == self.theme_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, ThemeVersionStatus)
|
|
else ThemeVersionStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceThemeVersion.status == status)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by if self.sort_by in THEME_VERSION_SORT_FIELDS else "version_number"
|
|
)
|
|
column = getattr(ExperienceThemeVersion, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LayoutListSpec:
|
|
workspace_id: UUID | None = None
|
|
status: LayoutStatus | str | None = None
|
|
direction_mode: DirectionMode | str | None = None
|
|
kind: LayoutKind | str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.workspace_id is not None:
|
|
clauses.append(ExperienceLayout.workspace_id == self.workspace_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, LayoutStatus)
|
|
else LayoutStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceLayout.status == status)
|
|
if self.direction_mode is not None:
|
|
mode = (
|
|
self.direction_mode
|
|
if isinstance(self.direction_mode, DirectionMode)
|
|
else DirectionMode(str(self.direction_mode))
|
|
)
|
|
clauses.append(ExperienceLayout.direction_mode == mode)
|
|
if self.kind is not None:
|
|
kind = (
|
|
self.kind if isinstance(self.kind, LayoutKind) else LayoutKind(str(self.kind))
|
|
)
|
|
clauses.append(ExperienceLayout.kind == kind)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
clauses.append(
|
|
or_(
|
|
ExperienceLayout.code.ilike(term),
|
|
ExperienceLayout.name.ilike(term),
|
|
ExperienceLayout.description.ilike(term),
|
|
)
|
|
)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by if self.sort_by in LAYOUT_SORT_FIELDS else "created_at"
|
|
)
|
|
column = getattr(ExperienceLayout, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LayoutVersionListSpec:
|
|
layout_id: UUID | None = None
|
|
status: LayoutVersionStatus | str | None = None
|
|
sort_by: str = "version_number"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.layout_id is not None:
|
|
clauses.append(ExperienceLayoutVersion.layout_id == self.layout_id)
|
|
if self.status is not None:
|
|
status = (
|
|
self.status
|
|
if isinstance(self.status, LayoutVersionStatus)
|
|
else LayoutVersionStatus(str(self.status))
|
|
)
|
|
clauses.append(ExperienceLayoutVersion.status == status)
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by
|
|
if self.sort_by in LAYOUT_VERSION_SORT_FIELDS
|
|
else "version_number"
|
|
)
|
|
column = getattr(ExperienceLayoutVersion, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SiteThemeBindingListSpec:
|
|
site_id: UUID | None = None
|
|
theme_id: UUID | None = None
|
|
theme_version_id: UUID | None = None
|
|
workspace_id: UUID | None = None
|
|
is_active: bool | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.site_id is not None:
|
|
clauses.append(SiteThemeBinding.site_id == self.site_id)
|
|
if self.theme_id is not None:
|
|
clauses.append(SiteThemeBinding.theme_id == self.theme_id)
|
|
if self.theme_version_id is not None:
|
|
clauses.append(SiteThemeBinding.theme_version_id == self.theme_version_id)
|
|
if self.workspace_id is not None:
|
|
clauses.append(SiteThemeBinding.workspace_id == self.workspace_id)
|
|
if self.is_active is not None:
|
|
clauses.append(SiteThemeBinding.is_active.is_(self.is_active))
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by
|
|
if self.sort_by in SITE_THEME_BINDING_SORT_FIELDS
|
|
else "created_at"
|
|
)
|
|
column = getattr(SiteThemeBinding, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PageLayoutBindingListSpec:
|
|
page_id: UUID | None = None
|
|
layout_id: UUID | None = None
|
|
layout_version_id: UUID | None = None
|
|
site_id: UUID | None = None
|
|
workspace_id: UUID | None = None
|
|
is_active: bool | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
|
|
def filter_clauses(self) -> list[ColumnElement]:
|
|
clauses: list[ColumnElement] = []
|
|
if self.page_id is not None:
|
|
clauses.append(PageLayoutBinding.page_id == self.page_id)
|
|
if self.layout_id is not None:
|
|
clauses.append(PageLayoutBinding.layout_id == self.layout_id)
|
|
if self.layout_version_id is not None:
|
|
clauses.append(PageLayoutBinding.layout_version_id == self.layout_version_id)
|
|
if self.site_id is not None:
|
|
clauses.append(PageLayoutBinding.site_id == self.site_id)
|
|
if self.workspace_id is not None:
|
|
clauses.append(PageLayoutBinding.workspace_id == self.workspace_id)
|
|
if self.is_active is not None:
|
|
clauses.append(PageLayoutBinding.is_active.is_(self.is_active))
|
|
return clauses
|
|
|
|
def apply(self, stmt: Select) -> Select:
|
|
clauses = self.filter_clauses()
|
|
if clauses:
|
|
stmt = stmt.where(*clauses)
|
|
sort_key = (
|
|
self.sort_by
|
|
if self.sort_by in PAGE_LAYOUT_BINDING_SORT_FIELDS
|
|
else "created_at"
|
|
)
|
|
column = getattr(PageLayoutBinding, sort_key)
|
|
order = desc(column) if self.sort_dir.lower() != "asc" else asc(column)
|
|
return stmt.order_by(order)
|