feat(cms): CMS-driven homepages, products section, placeholder resolution
Some checks failed
Some checks failed
- 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user