Files
orion/app/api/v1/admin/content_pages.py
Samir Boulahtit b8a46e1746 fix: protect critical re-export imports from linter removal
Problem:
- Ruff removed 'from app.core.database import Base' from models/database/base.py
- Import appeared "unused" (F401) but was actually a critical re-export
- Caused ImportError: cannot import name 'Base' at runtime
- Re-export pattern: import in one file to export from package

Solution:
1. Added F401 ignore for models/database/base.py in pyproject.toml
2. Created scripts/verify_critical_imports.py verification script
3. Integrated verification into make check and CI pipeline
4. Updated documentation with explanation

New Verification Script:
- Checks all critical re-export imports exist
- Detects import variations (parentheses, 'as' clauses)
- Handles SQLAlchemy declarative_base alternatives
- Runs as part of make check automatically

Protected Files:
- models/database/base.py - Re-exports Base for all models
- models/__init__.py - Exports Base for Alembic
- models/database/__init__.py - Exports Base from package
- All __init__.py files (already protected)

Makefile Changes:
- make verify-imports - Run import verification
- make check - Now includes verify-imports
- make ci - Includes verify-imports in pipeline

Documentation Updated:
- Code quality guide explains re-export protection
- Pre-commit workflow includes verification
- Examples of why re-exports matter

This prevents future issues where linters remove seemingly
"unused" imports that are actually critical for application structure.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 20:10:22 +01:00

257 lines
8.0 KiB
Python

# 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, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, get_db
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")
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
display_order: int | None = None
class ContentPageResponse(BaseModel):
"""Schema for content page response."""
id: int
vendor_id: int | None
vendor_name: str | None
slug: str
title: str
content: str
content_format: str
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
is_platform_default: bool
is_vendor_override: bool
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,
display_order=page_data.display_order,
created_by=current_user.id,
)
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.
"""
if vendor_id:
pages = content_page_service.list_all_vendor_pages(
db, vendor_id=vendor_id, include_unpublished=include_unpublished
)
else:
# Get all pages (both platform and vendor)
from sqlalchemy import and_
from models.database.content_page import ContentPage
filters = []
if not include_unpublished:
filters.append(ContentPage.is_published == True)
pages = (
db.query(ContentPage)
.filter(and_(*filters) if filters else True)
.order_by(
ContentPage.vendor_id, ContentPage.display_order, ContentPage.title
)
.all()
)
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(db, page_id)
if not page:
raise HTTPException(status_code=404, detail="Content page not found")
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(
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,
display_order=page_data.display_order,
updated_by=current_user.id,
)
if not page:
raise HTTPException(status_code=404, detail="Content page not found")
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."""
success = content_page_service.delete_page(db, page_id)
if not success:
raise HTTPException(status_code=404, detail="Content page not found")
return