Files
orion/app/services/content_page_service.py
Samir Boulahtit 238c1ec9b8 refactor: modernize code quality tooling with Ruff
- Replace black, isort, and flake8 with Ruff (all-in-one linter and formatter)
- Add comprehensive pyproject.toml configuration
- Simplify Makefile code quality targets
- Configure exclusions for venv/.venv in pyproject.toml
- Auto-fix 1,359 linting issues across codebase

Benefits:
- Much faster builds (Ruff is written in Rust)
- Single tool replaces multiple tools
- More comprehensive rule set (UP, B, C4, SIM, PIE, RET, Q)
- All configuration centralized in pyproject.toml
- Better import sorting and formatting consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 19:37:38 +01:00

378 lines
11 KiB
Python

# app/services/content_page_service.py
"""
Content Page Service
Business logic for managing content pages with platform defaults
and vendor-specific overrides.
Lookup Strategy:
1. Check for vendor-specific override (vendor_id + slug + published)
2. If not found, check for platform default (slug + published)
3. If neither exists, return None
This allows:
- Platform admin to create default content for all vendors
- Vendors to override specific pages with custom content
- Fallback to platform defaults when vendor hasn't customized
"""
import logging
from datetime import UTC, datetime
from sqlalchemy import and_
from sqlalchemy.orm import Session
from models.database.content_page import ContentPage
logger = logging.getLogger(__name__)
class ContentPageService:
"""Service for content page operations."""
@staticmethod
def get_page_for_vendor(
db: Session,
slug: str,
vendor_id: int | None = None,
include_unpublished: bool = False,
) -> ContentPage | None:
"""
Get content page for a vendor with fallback to platform default.
Lookup order:
1. Vendor-specific override (vendor_id + slug)
2. Platform default (vendor_id=NULL + slug)
Args:
db: Database session
slug: Page slug (about, faq, contact, etc.)
vendor_id: Vendor ID (None for platform defaults only)
include_unpublished: Include draft pages (for preview)
Returns:
ContentPage or None
"""
filters = [ContentPage.slug == slug]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
# Try vendor-specific override first
if vendor_id:
vendor_page = (
db.query(ContentPage)
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
.first()
)
if vendor_page:
logger.debug(
f"Found vendor-specific page: {slug} for vendor_id={vendor_id}"
)
return vendor_page
# Fallback to platform default
platform_page = (
db.query(ContentPage)
.filter(and_(ContentPage.vendor_id == None, *filters))
.first()
)
if platform_page:
logger.debug(f"Using platform default page: {slug}")
else:
logger.debug(f"No page found for slug: {slug}")
return platform_page
@staticmethod
def list_pages_for_vendor(
db: Session,
vendor_id: int | None = None,
include_unpublished: bool = False,
footer_only: bool = False,
header_only: bool = False,
) -> list[ContentPage]:
"""
List all available pages for a vendor (includes vendor overrides + platform defaults).
Merges vendor-specific overrides with platform defaults, prioritizing vendor overrides.
Args:
db: Database session
vendor_id: Vendor ID (None for platform pages only)
include_unpublished: Include draft pages
footer_only: Only pages marked for footer display
header_only: Only pages marked for header display
Returns:
List of ContentPage objects
"""
filters = []
if not include_unpublished:
filters.append(ContentPage.is_published == True)
if footer_only:
filters.append(ContentPage.show_in_footer == True)
if header_only:
filters.append(ContentPage.show_in_header == True)
# Get vendor-specific pages
vendor_pages = []
if vendor_id:
vendor_pages = (
db.query(ContentPage)
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
.order_by(ContentPage.display_order, ContentPage.title)
.all()
)
# Get platform defaults
platform_pages = (
db.query(ContentPage)
.filter(and_(ContentPage.vendor_id == None, *filters))
.order_by(ContentPage.display_order, ContentPage.title)
.all()
)
# Merge: vendor overrides take precedence
vendor_slugs = {page.slug for page in vendor_pages}
all_pages = vendor_pages + [
page for page in platform_pages if page.slug not in vendor_slugs
]
# Sort by display_order
all_pages.sort(key=lambda p: (p.display_order, p.title))
return all_pages
@staticmethod
def create_page(
db: Session,
slug: str,
title: str,
content: str,
vendor_id: int | None = None,
content_format: str = "html",
template: str = "default",
meta_description: str | None = None,
meta_keywords: str | None = None,
is_published: bool = False,
show_in_footer: bool = True,
show_in_header: bool = False,
display_order: int = 0,
created_by: int | None = None,
) -> ContentPage:
"""
Create a new content page.
Args:
db: Database session
slug: URL-safe identifier
title: Page title
content: HTML or Markdown content
vendor_id: Vendor ID (None for platform default)
content_format: "html" or "markdown"
template: Template name for homepage/landing pages (default, minimal, modern, etc.)
meta_description: SEO description
meta_keywords: SEO keywords
is_published: Publish immediately
show_in_footer: Show in footer navigation
show_in_header: Show in header navigation
display_order: Sort order
created_by: User ID who created it
Returns:
Created ContentPage
"""
page = ContentPage(
vendor_id=vendor_id,
slug=slug,
title=title,
content=content,
content_format=content_format,
template=template,
meta_description=meta_description,
meta_keywords=meta_keywords,
is_published=is_published,
published_at=datetime.now(UTC) if is_published else None,
show_in_footer=show_in_footer,
show_in_header=show_in_header,
display_order=display_order,
created_by=created_by,
updated_by=created_by,
)
db.add(page)
db.commit()
db.refresh(page)
logger.info(
f"Created content page: {slug} (vendor_id={vendor_id}, id={page.id})"
)
return page
@staticmethod
def update_page(
db: Session,
page_id: int,
title: str | None = None,
content: str | None = None,
content_format: str | None = None,
template: str | None = None,
meta_description: str | None = None,
meta_keywords: str | None = None,
is_published: bool | None = None,
show_in_footer: bool | None = None,
show_in_header: bool | None = None,
display_order: int | None = None,
updated_by: int | None = None,
) -> ContentPage | None:
"""
Update an existing content page.
Args:
db: Database session
page_id: Page ID
title: New title
content: New content
content_format: New format
template: New template name
meta_description: New SEO description
meta_keywords: New SEO keywords
is_published: New publish status
show_in_footer: New footer visibility
show_in_header: New header visibility
display_order: New sort order
updated_by: User ID who updated it
Returns:
Updated ContentPage or None if not found
"""
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
if not page:
logger.warning(f"Content page not found: id={page_id}")
return None
# Update fields if provided
if title is not None:
page.title = title
if content is not None:
page.content = content
if content_format is not None:
page.content_format = content_format
if template is not None:
page.template = template
if meta_description is not None:
page.meta_description = meta_description
if meta_keywords is not None:
page.meta_keywords = meta_keywords
if is_published is not None:
page.is_published = is_published
if is_published and not page.published_at:
page.published_at = datetime.now(UTC)
if show_in_footer is not None:
page.show_in_footer = show_in_footer
if show_in_header is not None:
page.show_in_header = show_in_header
if display_order is not None:
page.display_order = display_order
if updated_by is not None:
page.updated_by = updated_by
db.commit()
db.refresh(page)
logger.info(f"Updated content page: id={page_id}, slug={page.slug}")
return page
@staticmethod
def delete_page(db: Session, page_id: int) -> bool:
"""
Delete a content page.
Args:
db: Database session
page_id: Page ID
Returns:
True if deleted, False if not found
"""
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
if not page:
logger.warning(f"Content page not found for deletion: id={page_id}")
return False
db.delete(page)
db.commit()
logger.info(f"Deleted content page: id={page_id}, slug={page.slug}")
return True
@staticmethod
def get_page_by_id(db: Session, page_id: int) -> ContentPage | None:
"""Get content page by ID."""
return db.query(ContentPage).filter(ContentPage.id == page_id).first()
@staticmethod
def list_all_vendor_pages(
db: Session, vendor_id: int, include_unpublished: bool = False
) -> list[ContentPage]:
"""
List only vendor-specific pages (no platform defaults).
Args:
db: Database session
vendor_id: Vendor ID
include_unpublished: Include draft pages
Returns:
List of vendor-specific ContentPage objects
"""
filters = [ContentPage.vendor_id == vendor_id]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
return (
db.query(ContentPage)
.filter(and_(*filters))
.order_by(ContentPage.display_order, ContentPage.title)
.all()
)
@staticmethod
def list_all_platform_pages(
db: Session, include_unpublished: bool = False
) -> list[ContentPage]:
"""
List only platform default pages.
Args:
db: Database session
include_unpublished: Include draft pages
Returns:
List of platform default ContentPage objects
"""
filters = [ContentPage.vendor_id == None]
if not include_unpublished:
filters.append(ContentPage.is_published == True)
return (
db.query(ContentPage)
.filter(and_(*filters))
.order_by(ContentPage.display_order, ContentPage.title)
.all()
)
# Singleton instance
content_page_service = ContentPageService()