feat: complete CMS as fully autonomous self-contained module

Transform CMS from a thin wrapper into a fully self-contained module with
all code living within app/modules/cms/:

Module Structure:
- models/: ContentPage model (canonical location with dynamic discovery)
- schemas/: Pydantic schemas for API validation
- services/: ContentPageService business logic
- exceptions/: Module-specific exceptions
- routes/api/: REST API endpoints (admin, vendor, shop)
- routes/pages/: HTML page routes (admin, vendor)
- templates/cms/: Jinja2 templates (namespaced)
- static/: JavaScript files (admin/vendor)
- locales/: i18n translations (en, fr, de, lb)

Key Changes:
- Move ContentPage model to module with dynamic model discovery
- Create Pydantic schemas package for request/response validation
- Extract API routes from app/api/v1/*/ to module
- Extract page routes from admin_pages.py/vendor_pages.py to module
- Move static JS files to module with dedicated mount point
- Update templates to use cms_static for module assets
- Add module static file mounting in main.py
- Delete old scattered files (no shims - hard errors on old imports)

This establishes the pattern for migrating other modules to be
fully autonomous and independently deployable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 22:42:46 +01:00
parent 8ff9c39845
commit ec4ec045fc
40 changed files with 878 additions and 695 deletions

View File

@@ -1,5 +1,27 @@
# models/database/__init__.py
"""Database models package."""
"""
Database models package.
This package imports all SQLAlchemy models to ensure they are registered
with Base.metadata. This includes:
1. Core models (defined in this directory)
2. Module models (discovered from app/modules/<module>/models/)
Module Model Discovery:
- Modules can define their own models in app/modules/<module>/models/
- These are automatically imported when this package loads
- Module models must use `from app.core.database import Base`
"""
import importlib
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
# ============================================================================
# CORE MODELS (always loaded)
# ============================================================================
from .admin import (
AdminAuditLog,
@@ -18,7 +40,6 @@ from .architecture_scan import (
)
from .base import Base
from .company import Company
from .content_page import ContentPage
from .platform import Platform
from .platform_module import PlatformModule
from .vendor_platform import VendorPlatform
@@ -82,6 +103,49 @@ from .vendor import Role, Vendor, VendorUser
from .vendor_domain import VendorDomain
from .vendor_theme import VendorTheme
# ============================================================================
# MODULE MODELS (dynamically discovered)
# ============================================================================
def _discover_module_models():
"""
Discover and import models from app/modules/<module>/models/ directories.
This ensures module models are registered with Base.metadata for:
1. Alembic migrations
2. SQLAlchemy queries
Module models must:
- Be in app/modules/<module>/models/__init__.py or individual files
- Import Base from app.core.database
"""
modules_dir = Path(__file__).parent.parent.parent / "app" / "modules"
if not modules_dir.exists():
return
for module_dir in sorted(modules_dir.iterdir()):
if not module_dir.is_dir():
continue
models_init = module_dir / "models" / "__init__.py"
if models_init.exists():
module_name = f"app.modules.{module_dir.name}.models"
try:
importlib.import_module(module_name)
logger.debug(f"[Models] Loaded module models: {module_name}")
except ImportError as e:
logger.warning(f"[Models] Failed to import {module_name}: {e}")
# Run discovery at import time
_discover_module_models()
# ============================================================================
# EXPORTS
# ============================================================================
__all__ = [
# Admin-specific models
"AdminAuditLog",
@@ -113,8 +177,6 @@ __all__ = [
"Role",
"VendorDomain",
"VendorTheme",
# Content
"ContentPage",
# Platform
"Platform",
"PlatformModule",