Transform CMS from a thin wrapper into a fully self-contained module with all code living within app/modules/cms/: Module Structure: - models/: ContentPage model (canonical location with dynamic discovery) - schemas/: Pydantic schemas for API validation - services/: ContentPageService business logic - exceptions/: Module-specific exceptions - routes/api/: REST API endpoints (admin, vendor, shop) - routes/pages/: HTML page routes (admin, vendor) - templates/cms/: Jinja2 templates (namespaced) - static/: JavaScript files (admin/vendor) - locales/: i18n translations (en, fr, de, lb) Key Changes: - Move ContentPage model to module with dynamic model discovery - Create Pydantic schemas package for request/response validation - Extract API routes from app/api/v1/*/ to module - Extract page routes from admin_pages.py/vendor_pages.py to module - Move static JS files to module with dedicated mount point - Update templates to use cms_static for module assets - Add module static file mounting in main.py - Delete old scattered files (no shims - hard errors on old imports) This establishes the pattern for migrating other modules to be fully autonomous and independently deployable. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
202 lines
6.5 KiB
Python
202 lines
6.5 KiB
Python
# app/modules/cms/schemas/content_page.py
|
|
"""
|
|
Content Page Pydantic schemas for API request/response validation.
|
|
|
|
Schemas are organized by context:
|
|
- Admin: Full CRUD with platform-level access
|
|
- Vendor: Vendor-scoped CRUD with usage limits
|
|
- Public/Shop: Read-only public access
|
|
"""
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ============================================================================
|
|
# ADMIN SCHEMAS
|
|
# ============================================================================
|
|
|
|
|
|
class ContentPageCreate(BaseModel):
|
|
"""Schema for creating a content page (admin)."""
|
|
|
|
slug: str = Field(
|
|
...,
|
|
max_length=100,
|
|
description="URL-safe identifier (about, faq, contact, etc.)",
|
|
)
|
|
title: str = Field(..., max_length=200, description="Page title")
|
|
content: str = Field(..., description="HTML or Markdown content")
|
|
content_format: str = Field(
|
|
default="html", description="Content format: html or markdown"
|
|
)
|
|
template: str = Field(
|
|
default="default",
|
|
max_length=50,
|
|
description="Template name (default, minimal, modern)",
|
|
)
|
|
meta_description: str | None = Field(
|
|
None, max_length=300, description="SEO meta description"
|
|
)
|
|
meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords")
|
|
is_published: bool = Field(default=False, description="Publish immediately")
|
|
show_in_footer: bool = Field(default=True, description="Show in footer navigation")
|
|
show_in_header: bool = Field(default=False, description="Show in header navigation")
|
|
show_in_legal: bool = Field(
|
|
default=False, description="Show in legal/bottom bar (next to copyright)"
|
|
)
|
|
display_order: int = Field(default=0, description="Display order (lower = first)")
|
|
vendor_id: int | None = Field(
|
|
None, description="Vendor ID (None for platform default)"
|
|
)
|
|
|
|
|
|
class ContentPageUpdate(BaseModel):
|
|
"""Schema for updating a content page (admin)."""
|
|
|
|
title: str | None = Field(None, max_length=200)
|
|
content: str | None = None
|
|
content_format: str | None = None
|
|
template: str | None = Field(None, max_length=50)
|
|
meta_description: str | None = Field(None, max_length=300)
|
|
meta_keywords: str | None = Field(None, max_length=300)
|
|
is_published: bool | None = None
|
|
show_in_footer: bool | None = None
|
|
show_in_header: bool | None = None
|
|
show_in_legal: bool | None = None
|
|
display_order: int | None = None
|
|
|
|
|
|
class ContentPageResponse(BaseModel):
|
|
"""Schema for content page response (admin/vendor)."""
|
|
|
|
id: int
|
|
platform_id: int | None = None
|
|
platform_code: str | None = None
|
|
platform_name: str | None = None
|
|
vendor_id: int | None
|
|
vendor_name: str | None
|
|
slug: str
|
|
title: str
|
|
content: str
|
|
content_format: str
|
|
template: str | None = None
|
|
meta_description: str | None
|
|
meta_keywords: str | None
|
|
is_published: bool
|
|
published_at: str | None
|
|
display_order: int
|
|
show_in_footer: bool
|
|
show_in_header: bool
|
|
show_in_legal: bool
|
|
is_platform_page: bool = False
|
|
is_platform_default: bool = False # Deprecated: use is_platform_page
|
|
is_vendor_default: bool = False
|
|
is_vendor_override: bool = False
|
|
page_tier: str | None = None
|
|
created_at: str
|
|
updated_at: str
|
|
created_by: int | None
|
|
updated_by: int | None
|
|
|
|
|
|
class HomepageSectionsResponse(BaseModel):
|
|
"""Response containing homepage sections with platform language info."""
|
|
|
|
sections: dict | None = None
|
|
supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
|
|
default_language: str = "fr"
|
|
|
|
|
|
class SectionUpdateResponse(BaseModel):
|
|
"""Response after updating sections."""
|
|
|
|
message: str
|
|
sections: dict | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# VENDOR SCHEMAS
|
|
# ============================================================================
|
|
|
|
|
|
class VendorContentPageCreate(BaseModel):
|
|
"""Schema for creating a vendor content page."""
|
|
|
|
slug: str = Field(
|
|
...,
|
|
max_length=100,
|
|
description="URL-safe identifier (about, faq, contact, etc.)",
|
|
)
|
|
title: str = Field(..., max_length=200, description="Page title")
|
|
content: str = Field(..., description="HTML or Markdown content")
|
|
content_format: str = Field(
|
|
default="html", description="Content format: html or markdown"
|
|
)
|
|
meta_description: str | None = Field(
|
|
None, max_length=300, description="SEO meta description"
|
|
)
|
|
meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords")
|
|
is_published: bool = Field(default=False, description="Publish immediately")
|
|
show_in_footer: bool = Field(default=True, description="Show in footer navigation")
|
|
show_in_header: bool = Field(default=False, description="Show in header navigation")
|
|
show_in_legal: bool = Field(
|
|
default=False, description="Show in legal/bottom bar (next to copyright)"
|
|
)
|
|
display_order: int = Field(default=0, description="Display order (lower = first)")
|
|
|
|
|
|
class VendorContentPageUpdate(BaseModel):
|
|
"""Schema for updating a vendor content page."""
|
|
|
|
title: str | None = Field(None, max_length=200)
|
|
content: str | None = None
|
|
content_format: str | None = None
|
|
meta_description: str | None = Field(None, max_length=300)
|
|
meta_keywords: str | None = Field(None, max_length=300)
|
|
is_published: bool | None = None
|
|
show_in_footer: bool | None = None
|
|
show_in_header: bool | None = None
|
|
show_in_legal: bool | None = None
|
|
display_order: int | None = None
|
|
|
|
|
|
class CMSUsageResponse(BaseModel):
|
|
"""Schema for CMS usage statistics."""
|
|
|
|
total_pages: int
|
|
custom_pages: int
|
|
override_pages: int
|
|
pages_limit: int | None
|
|
custom_pages_limit: int | None
|
|
can_create_page: bool
|
|
can_create_custom: bool
|
|
usage_percent: float
|
|
custom_usage_percent: float
|
|
|
|
|
|
# ============================================================================
|
|
# PUBLIC/SHOP SCHEMAS
|
|
# ============================================================================
|
|
|
|
|
|
class PublicContentPageResponse(BaseModel):
|
|
"""Public content page response (no internal IDs)."""
|
|
|
|
slug: str
|
|
title: str
|
|
content: str
|
|
content_format: str
|
|
meta_description: str | None
|
|
meta_keywords: str | None
|
|
published_at: str | None
|
|
|
|
|
|
class ContentPageListItem(BaseModel):
|
|
"""Content page list item for navigation."""
|
|
|
|
slug: str
|
|
title: str
|
|
show_in_footer: bool
|
|
show_in_header: bool
|
|
display_order: int
|