# app/api/v1/admin/content_pages.py """ Admin Content Pages API Platform administrators can: - Create/edit/delete platform default content pages - View all vendor content pages - Override vendor content if needed """ import logging from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api, get_db from app.exceptions import ValidationException from app.services.content_page_service import content_page_service from models.database.user import User router = APIRouter() logger = logging.getLogger(__name__) # ============================================================================ # REQUEST/RESPONSE SCHEMAS # ============================================================================ class ContentPageCreate(BaseModel): """Schema for creating a 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" ) 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.""" 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.""" 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 # ============================================================================ # PLATFORM DEFAULT PAGES (vendor_id=NULL) # ============================================================================ @router.get("/platform", response_model=list[ContentPageResponse]) def list_platform_pages( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ List all platform default content pages. These are used as fallbacks when vendors haven't created custom pages. """ pages = content_page_service.list_all_platform_pages( db, include_unpublished=include_unpublished ) return [page.to_dict() for page in pages] @router.post("/platform", response_model=ContentPageResponse, status_code=201) def create_platform_page( page_data: ContentPageCreate, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ Create a new platform default content page. Platform defaults are shown to all vendors who haven't created their own version. """ # Force vendor_id to None for platform pages page = content_page_service.create_page( db, slug=page_data.slug, title=page_data.title, content=page_data.content, vendor_id=None, # Platform default content_format=page_data.content_format, template=page_data.template, meta_description=page_data.meta_description, meta_keywords=page_data.meta_keywords, is_published=page_data.is_published, show_in_footer=page_data.show_in_footer, show_in_header=page_data.show_in_header, show_in_legal=page_data.show_in_legal, display_order=page_data.display_order, created_by=current_user.id, ) db.commit() return page.to_dict() # ============================================================================ # VENDOR PAGES # ============================================================================ @router.post("/vendor", response_model=ContentPageResponse, status_code=201) def create_vendor_page( page_data: ContentPageCreate, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ Create a vendor-specific content page override. Vendor pages override platform defaults for a specific vendor. """ if not page_data.vendor_id: raise ValidationException( message="vendor_id is required for vendor pages. Use /platform for platform defaults.", field="vendor_id", ) page = content_page_service.create_page( db, slug=page_data.slug, title=page_data.title, content=page_data.content, vendor_id=page_data.vendor_id, content_format=page_data.content_format, template=page_data.template, meta_description=page_data.meta_description, meta_keywords=page_data.meta_keywords, is_published=page_data.is_published, show_in_footer=page_data.show_in_footer, show_in_header=page_data.show_in_header, show_in_legal=page_data.show_in_legal, display_order=page_data.display_order, created_by=current_user.id, ) db.commit() return page.to_dict() # ============================================================================ # ALL CONTENT PAGES (Platform + Vendors) # ============================================================================ @router.get("/", response_model=list[ContentPageResponse]) def list_all_pages( vendor_id: int | None = Query(None, description="Filter by vendor ID"), include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ List all content pages (platform defaults and vendor overrides). Filter by vendor_id to see specific vendor pages. """ pages = content_page_service.list_all_pages( db, vendor_id=vendor_id, include_unpublished=include_unpublished ) return [page.to_dict() for page in pages] @router.get("/{page_id}", response_model=ContentPageResponse) def get_page( page_id: int, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Get a specific content page by ID.""" page = content_page_service.get_page_by_id_or_raise(db, page_id) return page.to_dict() @router.put("/{page_id}", response_model=ContentPageResponse) def update_page( page_id: int, page_data: ContentPageUpdate, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Update a content page (platform or vendor).""" page = content_page_service.update_page_or_raise( db, page_id=page_id, title=page_data.title, content=page_data.content, content_format=page_data.content_format, template=page_data.template, meta_description=page_data.meta_description, meta_keywords=page_data.meta_keywords, is_published=page_data.is_published, show_in_footer=page_data.show_in_footer, show_in_header=page_data.show_in_header, show_in_legal=page_data.show_in_legal, display_order=page_data.display_order, updated_by=current_user.id, ) db.commit() return page.to_dict() @router.delete("/{page_id}", status_code=204) def delete_page( page_id: int, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """Delete a content page.""" content_page_service.delete_page_or_raise(db, page_id) db.commit() # ============================================================================ # HOMEPAGE SECTIONS MANAGEMENT # ============================================================================ 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 @router.get("/{page_id}/sections", response_model=HomepageSectionsResponse) def get_page_sections( page_id: int, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ Get homepage sections for a content page. Returns sections along with platform language settings for the editor. """ page = content_page_service.get_page_by_id_or_raise(db, page_id) # Get platform languages platform = page.platform supported_languages = ( platform.supported_languages if platform else ["fr", "de", "en"] ) default_language = platform.default_language if platform else "fr" return { "sections": page.sections, "supported_languages": supported_languages, "default_language": default_language, } @router.put("/{page_id}/sections", response_model=SectionUpdateResponse) def update_page_sections( page_id: int, sections: dict, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ Update all homepage sections at once. Expected structure: { "hero": { ... }, "features": { ... }, "pricing": { ... }, "cta": { ... } } """ page = content_page_service.update_homepage_sections( db, page_id=page_id, sections=sections, updated_by=current_user.id, ) db.commit() return { "message": "Sections updated successfully", "sections": page.sections, } @router.put("/{page_id}/sections/{section_name}", response_model=SectionUpdateResponse) def update_single_section( page_id: int, section_name: str, section_data: dict, current_user: User = Depends(get_current_admin_api), db: Session = Depends(get_db), ): """ Update a single section (hero, features, pricing, or cta). section_name must be one of: hero, features, pricing, cta """ if section_name not in ["hero", "features", "pricing", "cta"]: raise ValidationException( message=f"Invalid section name: {section_name}. Must be one of: hero, features, pricing, cta", field="section_name", ) page = content_page_service.update_single_section( db, page_id=page_id, section_name=section_name, section_data=section_data, updated_by=current_user.id, ) db.commit() return { "message": f"Section '{section_name}' updated successfully", "sections": page.sections, }