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>
109 lines
3.9 KiB
Python
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
|