Files
orion/app/modules/cms/schemas/homepage_sections.py
Samir Boulahtit adbecd360b
Some checks failed
CI / ruff (push) Successful in 11s
CI / pytest (push) Failing after 51m41s
CI / validate (push) Successful in 26s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat(cms): CMS-driven homepages, products section, placeholder resolution
- Add ProductCard/ProductsSection schema and _products.html section macro
- Rewrite seed script with 3-platform homepage sections (wizard, OMS, loyalty),
  platform marketing pages, and store defaults with {{store_name}} placeholders
- Add resolve_placeholders() to ContentPageService for store default pages
- Fix SQLAlchemy filter bugs: replace Python `is None` with `.is_(None)` across
  all ContentPageService query methods (was silently breaking all platform page lookups)
- Remove hardcoded orion fallback and delete homepage-orion.html
- Add placeholder hint box with click-to-copy in admin content page editor
- Export ProductCard/ProductsSection from cms schemas __init__

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:12:20 +01:00

199 lines
6.3 KiB
Python

"""
Homepage Section Schemas with Dynamic Multi-Language Support.
Language codes are NOT hardcoded - they come from platform.supported_languages.
The TranslatableText class stores translations as a dict where keys are language codes.
Example JSON structure:
{
"hero": {
"enabled": true,
"title": {"translations": {"fr": "Bienvenue", "en": "Welcome"}},
"subtitle": {"translations": {...}},
"buttons": [...]
},
"features": {...},
"pricing": {...},
"cta": {...}
}
"""
from pydantic import BaseModel, Field
class TranslatableText(BaseModel):
"""
Text field with translations stored as language-keyed dict.
Languages come from platform.supported_languages (not hardcoded).
Use .get(lang, default_lang) to retrieve translation with fallback.
"""
translations: dict[str, str] = Field(
default_factory=dict, description="Language code -> translated text mapping"
)
def get(self, lang: str, default_lang: str = "fr") -> str:
"""Get translation with fallback to default language."""
return self.translations.get(lang) or self.translations.get(default_lang) or ""
def set(self, lang: str, text: str) -> None:
"""Set translation for a language."""
self.translations[lang] = text
def has_translation(self, lang: str) -> bool:
"""Check if translation exists for language."""
return bool(self.translations.get(lang))
class HeroButton(BaseModel):
"""Button in hero or CTA section."""
text: TranslatableText = Field(default_factory=TranslatableText)
url: str = ""
style: str = Field(default="primary", description="primary, secondary, outline")
class HeroSection(BaseModel):
"""Hero section configuration."""
enabled: bool = True
badge_text: TranslatableText | None = None
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText = Field(default_factory=TranslatableText)
background_type: str = Field(
default="gradient", description="gradient, image, solid"
)
background_image: str | None = None
buttons: list[HeroButton] = Field(default_factory=list)
class FeatureCard(BaseModel):
"""Single feature in features section."""
icon: str = ""
title: TranslatableText = Field(default_factory=TranslatableText)
description: TranslatableText = Field(default_factory=TranslatableText)
class ProductCard(BaseModel):
"""Single product/offering card in products section."""
icon: str = ""
title: TranslatableText = Field(default_factory=TranslatableText)
description: TranslatableText = Field(default_factory=TranslatableText)
url: str = ""
badge: TranslatableText | None = None
class ProductsSection(BaseModel):
"""Product/offering showcase section (e.g. wizard.lu multi-product landing)."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText | None = None
products: list[ProductCard] = Field(default_factory=list)
class FeaturesSection(BaseModel):
"""Features section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText | None = None
features: list[FeatureCard] = Field(default_factory=list)
layout: str = Field(default="grid", description="grid, list, cards")
class PricingSection(BaseModel):
"""Pricing section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText | None = None
use_subscription_tiers: bool = Field(
default=True, description="Pull pricing from subscription_tiers table dynamically"
)
class CTASection(BaseModel):
"""Call-to-action section configuration."""
enabled: bool = True
title: TranslatableText = Field(default_factory=TranslatableText)
subtitle: TranslatableText | None = None
buttons: list[HeroButton] = Field(default_factory=list)
background_type: str = Field(
default="gradient", description="gradient, image, solid"
)
class HomepageSections(BaseModel):
"""Complete homepage sections structure."""
hero: HeroSection | None = None
products: ProductsSection | None = None
features: FeaturesSection | None = None
pricing: PricingSection | None = None
cta: CTASection | None = None
@classmethod
def get_empty_structure(cls, languages: list[str]) -> "HomepageSections":
"""
Create empty section structure with language placeholders.
Args:
languages: List of language codes from platform.supported_languages
Returns:
HomepageSections with empty translations for all languages
"""
def make_translatable(langs: list[str]) -> TranslatableText:
return TranslatableText(translations=dict.fromkeys(langs, ""))
return cls(
hero=HeroSection(
title=make_translatable(languages),
subtitle=make_translatable(languages),
buttons=[],
),
products=ProductsSection(
title=make_translatable(languages),
products=[],
),
features=FeaturesSection(
title=make_translatable(languages),
features=[],
),
pricing=PricingSection(
title=make_translatable(languages),
use_subscription_tiers=True,
),
cta=CTASection(
title=make_translatable(languages),
buttons=[],
),
)
# =============================================================================
# API Request/Response Schemas
# =============================================================================
class SectionUpdateRequest(BaseModel):
"""Request to update a single section."""
section_name: str = Field(..., description="hero, products, features, pricing, or cta")
section_data: dict = Field(..., description="Section configuration")
class HomepageSectionsResponse(BaseModel):
"""Response containing all homepage sections with platform language info."""
sections: HomepageSections | None = None
supported_languages: list[str] = Field(default_factory=lambda: ["fr", "de", "en"])
default_language: str = "fr"