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>
70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
"""Filtering, search and sorting specifications — Phase 11.10 analytics."""
|
|
from dataclasses import dataclass
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import asc, desc, or_
|
|
|
|
from app.models.analytics import (
|
|
ExperienceAnalyticsReportDefinition,
|
|
ExperienceAnalyticsSnapshot,
|
|
)
|
|
|
|
REPORT_DEFINITION_SORT = frozenset(
|
|
{"created_at", "updated_at", "code", "name", "status"}
|
|
)
|
|
SNAPSHOT_SORT = frozenset(
|
|
{"created_at", "updated_at", "status", "period_start", "period_end"}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AnalyticsListSpec:
|
|
workspace_id: UUID | None = None
|
|
site_id: UUID | None = None
|
|
report_id: UUID | None = None
|
|
status: str | None = None
|
|
q: str | None = None
|
|
sort_by: str = "created_at"
|
|
sort_dir: str = "desc"
|
|
model = None
|
|
sort_fields = frozenset()
|
|
|
|
def filter_clauses(self):
|
|
m = self.model
|
|
clauses = []
|
|
for field in ("workspace_id", "site_id", "report_id", "status"):
|
|
value = getattr(self, field)
|
|
if value is not None and hasattr(m, field):
|
|
clauses.append(getattr(m, field) == value)
|
|
if self.q:
|
|
term = f"%{self.q.strip()}%"
|
|
searchable = [
|
|
getattr(m, name).ilike(term)
|
|
for name in ("code", "name", "period_start", "period_end")
|
|
if hasattr(m, name)
|
|
]
|
|
if searchable:
|
|
clauses.append(or_(*searchable))
|
|
return clauses
|
|
|
|
def apply(self, stmt):
|
|
for clause in self.filter_clauses():
|
|
stmt = stmt.where(clause)
|
|
direction = desc if self.sort_dir.lower() == "desc" else asc
|
|
sort_col = getattr(self.model, self.sort_by, self.model.created_at)
|
|
if self.sort_by not in self.sort_fields:
|
|
sort_col = self.model.created_at
|
|
return stmt.order_by(direction(sort_col))
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AnalyticsReportDefinitionListSpec(AnalyticsListSpec):
|
|
model = ExperienceAnalyticsReportDefinition
|
|
sort_fields = REPORT_DEFINITION_SORT
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AnalyticsSnapshotListSpec(AnalyticsListSpec):
|
|
model = ExperienceAnalyticsSnapshot
|
|
sort_fields = SNAPSHOT_SORT
|