feat: implement section-based homepage management system
Add structured JSON sections to ContentPage for multi-language homepage editing:
Database:
- Add `sections` JSON column to content_pages table
- Migration z8i9j0k1l2m3 adds the column
Schema:
- New models/schema/homepage_sections.py with Pydantic schemas
- TranslatableText for language-keyed translations
- HeroSection, FeaturesSection, PricingSection, CTASection
Templates:
- New section partials in app/templates/platform/sections/
- Updated homepage-default.html to render sections dynamically
- Fallback to placeholder content when sections not configured
Service:
- update_homepage_sections() - validate and save all sections
- update_single_section() - update individual section
- get_default_sections() - empty structure for new homepages
API:
- GET /{page_id}/sections - get sections with platform languages
- PUT /{page_id}/sections - update all sections
- PUT /{page_id}/sections/{section_name} - update single section
Admin UI:
- Section editor appears when editing homepage (slug='home')
- Language tabs from platform.supported_languages
- Accordion sections for Hero, Features, Pricing, CTA
- Button/feature card repeaters with add/remove
Also fixes broken line 181 in z4e5f6a7b8c9 migration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -872,6 +872,130 @@ class ContentPageService:
|
||||
if not success:
|
||||
raise ContentPageNotFoundException(identifier=page_id)
|
||||
|
||||
# =========================================================================
|
||||
# Homepage Sections Management
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def update_homepage_sections(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
sections: dict,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update homepage sections with validation.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Content page ID
|
||||
sections: Homepage sections dict (validated against HomepageSections schema)
|
||||
updated_by: User ID making the update
|
||||
|
||||
Returns:
|
||||
Updated ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
ValidationError: If sections schema invalid
|
||||
"""
|
||||
from models.schema.homepage_sections import HomepageSections
|
||||
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
# Validate sections against schema
|
||||
validated = HomepageSections(**sections)
|
||||
|
||||
# Update page
|
||||
page.sections = validated.model_dump()
|
||||
page.updated_by = updated_by
|
||||
|
||||
db.flush()
|
||||
db.refresh(page)
|
||||
|
||||
logger.info(f"[CMS] Updated homepage sections for page_id={page_id}")
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def update_single_section(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
section_name: str,
|
||||
section_data: dict,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update a single section within homepage sections.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Content page ID
|
||||
section_name: Section to update (hero, features, pricing, cta)
|
||||
section_data: Section configuration dict
|
||||
updated_by: User ID making the update
|
||||
|
||||
Returns:
|
||||
Updated ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
ValueError: If section name is invalid
|
||||
"""
|
||||
from models.schema.homepage_sections import (
|
||||
HeroSection,
|
||||
FeaturesSection,
|
||||
PricingSection,
|
||||
CTASection,
|
||||
)
|
||||
|
||||
SECTION_SCHEMAS = {
|
||||
"hero": HeroSection,
|
||||
"features": FeaturesSection,
|
||||
"pricing": PricingSection,
|
||||
"cta": CTASection,
|
||||
}
|
||||
|
||||
if section_name not in SECTION_SCHEMAS:
|
||||
raise ValueError(f"Invalid section name: {section_name}. Must be one of: {list(SECTION_SCHEMAS.keys())}")
|
||||
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
# Validate section data against its schema
|
||||
schema = SECTION_SCHEMAS[section_name]
|
||||
validated_section = schema(**section_data)
|
||||
|
||||
# Initialize sections if needed
|
||||
current_sections = page.sections or {}
|
||||
current_sections[section_name] = validated_section.model_dump()
|
||||
|
||||
page.sections = current_sections
|
||||
page.updated_by = updated_by
|
||||
|
||||
db.flush()
|
||||
db.refresh(page)
|
||||
|
||||
logger.info(f"[CMS] Updated section '{section_name}' for page_id={page_id}")
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def get_default_sections(languages: list[str] | None = None) -> dict:
|
||||
"""
|
||||
Get empty sections structure for new homepage.
|
||||
|
||||
Args:
|
||||
languages: List of language codes from platform.supported_languages.
|
||||
Defaults to ['fr', 'de', 'en'] if not provided.
|
||||
|
||||
Returns:
|
||||
Empty sections dict with language placeholders
|
||||
"""
|
||||
from models.schema.homepage_sections import HomepageSections
|
||||
|
||||
if languages is None:
|
||||
languages = ["fr", "de", "en"]
|
||||
|
||||
return HomepageSections.get_empty_structure(languages).model_dump()
|
||||
|
||||
|
||||
# Singleton instance
|
||||
content_page_service = ContentPageService()
|
||||
|
||||
Reference in New Issue
Block a user