TorbatYar/backend/services/experience/app/validators/templates.py
Mortezakoohjani 203671a7bf feat(experience): ship Experience Platform phases 11.0-11.10
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>
2026-07-27 11:43:10 +03:30

109 lines
3.9 KiB
Python

"""Template validators — Phase 11.4."""
from __future__ import annotations
from typing import Any
from app.models.types import (
PageType,
TemplateStatus,
TemplateVersionStatus,
)
from app.validators.foundation import ValidationError, validate_non_empty
from shared.exceptions import AppError
TEMPLATE_STATUS_TRANSITIONS: dict[TemplateStatus, frozenset[TemplateStatus]] = {
TemplateStatus.DRAFT: frozenset({TemplateStatus.ACTIVE, TemplateStatus.ARCHIVED}),
TemplateStatus.ACTIVE: frozenset(
{TemplateStatus.DEPRECATED, TemplateStatus.ARCHIVED}
),
TemplateStatus.DEPRECATED: frozenset(
{TemplateStatus.ACTIVE, TemplateStatus.ARCHIVED}
),
TemplateStatus.ARCHIVED: frozenset(),
}
TEMPLATE_VERSION_STATUS_TRANSITIONS: dict[
TemplateVersionStatus, frozenset[TemplateVersionStatus]
] = {
TemplateVersionStatus.DRAFT: frozenset({TemplateVersionStatus.PUBLISHED}),
TemplateVersionStatus.PUBLISHED: frozenset({TemplateVersionStatus.RETIRED}),
TemplateVersionStatus.RETIRED: frozenset(),
}
_ALLOWED_PAGE_TYPES = {p.value for p in PageType}
def validate_page_type(page_type: str) -> str:
page_type = validate_non_empty(page_type, "page_type").strip().lower()
if page_type not in _ALLOWED_PAGE_TYPES:
raise ValidationError(
"page_type نامعتبر است",
{"field": "page_type", "allowed": sorted(_ALLOWED_PAGE_TYPES)},
)
return page_type
def ensure_template_status_transition(
current: TemplateStatus, target: TemplateStatus
) -> None:
if current == target:
return
allowed = TEMPLATE_STATUS_TRANSITIONS.get(current, frozenset())
if target not in allowed:
raise AppError(
"گذار وضعیت قالب مجاز نیست",
error_code="invalid_template_status_transition",
status_code=409,
details={"current": current.value, "target": target.value},
)
def ensure_template_version_status_transition(
current: TemplateVersionStatus, target: TemplateVersionStatus
) -> None:
if current == target:
return
allowed = TEMPLATE_VERSION_STATUS_TRANSITIONS.get(current, frozenset())
if target not in allowed:
raise AppError(
"گذار وضعیت نسخه قالب مجاز نیست",
error_code="invalid_template_version_status_transition",
status_code=409,
details={"current": current.value, "target": target.value},
)
def ensure_template_version_mutable(version: Any) -> None:
if getattr(version, "is_immutable", False) or getattr(
version, "status", None
) == TemplateVersionStatus.PUBLISHED:
raise AppError(
"نسخه منتشرشده قالب قابل ویرایش نیست",
error_code="template_version_immutable",
status_code=409,
)
if getattr(version, "status", None) == TemplateVersionStatus.RETIRED:
raise AppError(
"نسخه بازنشسته‌شده قالب قابل ویرایش نیست",
error_code="template_version_retired",
status_code=409,
)
def ensure_blueprint_json(payload: dict | None) -> dict | None:
if payload is None:
return None
if not isinstance(payload, dict):
raise ValidationError(
"blueprint_json باید شیء JSON باشد", {"field": "blueprint_json"}
)
if "page_type" in payload and payload["page_type"] is not None:
payload = {**payload, "page_type": validate_page_type(str(payload["page_type"]))}
placements = payload.get("placement_shells")
if placements is not None and not isinstance(placements, list):
raise ValidationError(
"placement_shells باید آرایه باشد",
{"field": "blueprint_json.placement_shells"},
)
return payload