feat(cms): CMS-driven homepages, products section, placeholder resolution
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

- 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>
This commit is contained in:
2026-03-01 12:12:20 +01:00
parent ef9ea29643
commit adbecd360b
11 changed files with 1132 additions and 916 deletions

View File

@@ -142,8 +142,8 @@ class ContentPageService:
db.query(ContentPage)
.filter(
and_(
ContentPage.store_id is None,
ContentPage.is_platform_page == False,
ContentPage.store_id.is_(None),
ContentPage.is_platform_page.is_(False),
*base_filters,
)
)
@@ -182,12 +182,12 @@ class ContentPageService:
filters = [
ContentPage.platform_id == platform_id,
ContentPage.slug == slug,
ContentPage.store_id is None,
ContentPage.is_platform_page == True,
ContentPage.store_id.is_(None),
ContentPage.is_platform_page.is_(True),
]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
filters.append(ContentPage.is_published.is_(True))
page = db.query(ContentPage).filter(and_(*filters)).first()
@@ -255,8 +255,8 @@ class ContentPageService:
db.query(ContentPage)
.filter(
and_(
ContentPage.store_id is None,
ContentPage.is_platform_page == False,
ContentPage.store_id.is_(None),
ContentPage.is_platform_page.is_(False),
*base_filters,
)
)
@@ -298,12 +298,12 @@ class ContentPageService:
"""
filters = [
ContentPage.platform_id == platform_id,
ContentPage.store_id is None,
ContentPage.is_platform_page == True,
ContentPage.store_id.is_(None),
ContentPage.is_platform_page.is_(True),
]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
filters.append(ContentPage.is_published.is_(True))
if footer_only:
filters.append(ContentPage.show_in_footer == True)
@@ -377,12 +377,12 @@ class ContentPageService:
"""
filters = [
ContentPage.platform_id == platform_id,
ContentPage.store_id is None,
ContentPage.is_platform_page == False,
ContentPage.store_id.is_(None),
ContentPage.is_platform_page.is_(False),
]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
filters.append(ContentPage.is_published.is_(True))
return (
db.query(ContentPage)
@@ -845,13 +845,13 @@ class ContentPageService:
filters.append(ContentPage.is_published == True)
if page_tier == "platform":
filters.append(ContentPage.is_platform_page == True)
filters.append(ContentPage.store_id is None)
filters.append(ContentPage.is_platform_page.is_(True))
filters.append(ContentPage.store_id.is_(None))
elif page_tier == "store_default":
filters.append(ContentPage.is_platform_page == False)
filters.append(ContentPage.store_id is None)
filters.append(ContentPage.is_platform_page.is_(False))
filters.append(ContentPage.store_id.is_(None))
elif page_tier == "store_override":
filters.append(ContentPage.store_id is not None)
filters.append(ContentPage.store_id.isnot(None))
return (
db.query(ContentPage)
@@ -958,6 +958,34 @@ class ContentPageService:
if not success:
raise ContentPageNotFoundException(identifier=page_id)
# =========================================================================
# Placeholder Resolution (for store default pages)
# =========================================================================
@staticmethod
def resolve_placeholders(content: str, store) -> str:
"""
Replace {{store_name}}, {{store_email}}, {{store_phone}} placeholders
in store default page content with actual store values.
Args:
content: HTML content with placeholders
store: Store object with name, contact_email, phone attributes
Returns:
Content with placeholders replaced
"""
if not content or not store:
return content or ""
replacements = {
"{{store_name}}": store.name or "Our Store",
"{{store_email}}": getattr(store, "contact_email", "") or "",
"{{store_phone}}": getattr(store, "phone", "") or "",
}
for placeholder, value in replacements.items():
content = content.replace(placeholder, value)
return content
# =========================================================================
# Homepage Sections Management
# =========================================================================
@@ -1032,10 +1060,12 @@ class ContentPageService:
FeaturesSection,
HeroSection,
PricingSection,
ProductsSection,
)
SECTION_SCHEMAS = {
"hero": HeroSection,
"products": ProductsSection,
"features": FeaturesSection,
"pricing": PricingSection,
"cta": CTASection,