fix: resolve architecture validation errors
- Create platform_service.py to move DB queries from platforms.py API - Create platform.py exceptions for PlatformNotFoundException - Update platforms.py API to use platform_service - Update vendor/content_pages.py to use vendor_service - Add get_vendor_by_id_optional method to VendorService - Fix platforms.js: add centralized logger and init guard - Fix content-page-edit.html: use modal macro instead of inline modal All 21 architecture validation errors resolved. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,16 +17,13 @@ Platforms are business offerings (OMS, Loyalty, Site Builder) with their own:
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_from_cookie_or_header, get_db
|
||||
from models.database.content_page import ContentPage
|
||||
from models.database.platform import Platform
|
||||
from app.services.platform_service import platform_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor_platform import VendorPlatform
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/platforms")
|
||||
@@ -107,140 +104,12 @@ class PlatformStatsResponse(BaseModel):
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=PlatformListResponse)
|
||||
async def list_platforms(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
include_inactive: bool = Query(False, description="Include inactive platforms"),
|
||||
):
|
||||
"""
|
||||
List all platforms with their statistics.
|
||||
|
||||
Returns all platforms (OMS, Loyalty, etc.) with vendor counts and page counts.
|
||||
"""
|
||||
query = db.query(Platform)
|
||||
|
||||
if not include_inactive:
|
||||
query = query.filter(Platform.is_active == True)
|
||||
|
||||
platforms = query.order_by(Platform.id).all()
|
||||
|
||||
# Build response with computed fields
|
||||
result = []
|
||||
for platform in platforms:
|
||||
# Count vendors on this platform
|
||||
vendor_count = (
|
||||
db.query(func.count(VendorPlatform.vendor_id))
|
||||
.filter(VendorPlatform.platform_id == platform.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count platform marketing pages
|
||||
platform_pages_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count vendor default pages
|
||||
vendor_defaults_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
platform_data = PlatformResponse(
|
||||
id=platform.id,
|
||||
code=platform.code,
|
||||
name=platform.name,
|
||||
description=platform.description,
|
||||
domain=platform.domain,
|
||||
path_prefix=platform.path_prefix,
|
||||
logo=platform.logo,
|
||||
logo_dark=platform.logo_dark,
|
||||
favicon=platform.favicon,
|
||||
theme_config=platform.theme_config or {},
|
||||
default_language=platform.default_language,
|
||||
supported_languages=platform.supported_languages or ["fr", "de", "en"],
|
||||
is_active=platform.is_active,
|
||||
is_public=platform.is_public,
|
||||
settings=platform.settings or {},
|
||||
created_at=platform.created_at.isoformat(),
|
||||
updated_at=platform.updated_at.isoformat(),
|
||||
vendor_count=vendor_count,
|
||||
platform_pages_count=platform_pages_count,
|
||||
vendor_defaults_count=vendor_defaults_count,
|
||||
)
|
||||
result.append(platform_data)
|
||||
|
||||
logger.info(f"[PLATFORMS] Listed {len(result)} platforms")
|
||||
|
||||
return PlatformListResponse(platforms=result, total=len(result))
|
||||
|
||||
|
||||
@router.get("/{code}", response_model=PlatformResponse)
|
||||
async def get_platform(
|
||||
code: str = Path(..., description="Platform code (oms, loyalty, etc.)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Get platform details by code.
|
||||
|
||||
Returns full platform configuration including statistics.
|
||||
"""
|
||||
platform = db.query(Platform).filter(Platform.code == code).first()
|
||||
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail=f"Platform not found: {code}")
|
||||
|
||||
# Count vendors on this platform
|
||||
vendor_count = (
|
||||
db.query(func.count(VendorPlatform.vendor_id))
|
||||
.filter(VendorPlatform.platform_id == platform.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count platform marketing pages
|
||||
platform_pages_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count vendor default pages
|
||||
vendor_defaults_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
def _build_platform_response(db: Session, platform) -> PlatformResponse:
|
||||
"""Build PlatformResponse from Platform model with computed fields."""
|
||||
return PlatformResponse(
|
||||
id=platform.id,
|
||||
code=platform.code,
|
||||
@@ -259,12 +128,52 @@ async def get_platform(
|
||||
settings=platform.settings or {},
|
||||
created_at=platform.created_at.isoformat(),
|
||||
updated_at=platform.updated_at.isoformat(),
|
||||
vendor_count=vendor_count,
|
||||
platform_pages_count=platform_pages_count,
|
||||
vendor_defaults_count=vendor_defaults_count,
|
||||
vendor_count=platform_service.get_vendor_count(db, platform.id),
|
||||
platform_pages_count=platform_service.get_platform_pages_count(db, platform.id),
|
||||
vendor_defaults_count=platform_service.get_vendor_defaults_count(db, platform.id),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=PlatformListResponse)
|
||||
async def list_platforms(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
include_inactive: bool = Query(False, description="Include inactive platforms"),
|
||||
):
|
||||
"""
|
||||
List all platforms with their statistics.
|
||||
|
||||
Returns all platforms (OMS, Loyalty, etc.) with vendor counts and page counts.
|
||||
"""
|
||||
platforms = platform_service.list_platforms(db, include_inactive=include_inactive)
|
||||
|
||||
result = [_build_platform_response(db, platform) for platform in platforms]
|
||||
|
||||
logger.info(f"[PLATFORMS] Listed {len(result)} platforms")
|
||||
|
||||
return PlatformListResponse(platforms=result, total=len(result))
|
||||
|
||||
|
||||
@router.get("/{code}", response_model=PlatformResponse)
|
||||
async def get_platform(
|
||||
code: str = Path(..., description="Platform code (oms, loyalty, etc.)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_from_cookie_or_header),
|
||||
):
|
||||
"""
|
||||
Get platform details by code.
|
||||
|
||||
Returns full platform configuration including statistics.
|
||||
"""
|
||||
platform = platform_service.get_platform_by_code(db, code)
|
||||
return _build_platform_response(db, platform)
|
||||
|
||||
|
||||
@router.put("/{code}", response_model=PlatformResponse)
|
||||
async def update_platform(
|
||||
update_data: PlatformUpdateRequest,
|
||||
@@ -277,24 +186,15 @@ async def update_platform(
|
||||
|
||||
Allows updating name, description, branding, and configuration.
|
||||
"""
|
||||
platform = db.query(Platform).filter(Platform.code == code).first()
|
||||
platform = platform_service.get_platform_by_code(db, code)
|
||||
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail=f"Platform not found: {code}")
|
||||
|
||||
# Update fields if provided
|
||||
update_dict = update_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_dict.items():
|
||||
if hasattr(platform, field):
|
||||
setattr(platform, field, value)
|
||||
platform = platform_service.update_platform(db, platform, update_dict)
|
||||
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
|
||||
logger.info(f"[PLATFORMS] Updated platform: {code}")
|
||||
|
||||
# Return updated platform with stats
|
||||
return await get_platform(code=code, db=db, current_user=current_user)
|
||||
return _build_platform_response(db, platform)
|
||||
|
||||
|
||||
@router.get("/{code}/stats", response_model=PlatformStatsResponse)
|
||||
@@ -308,84 +208,17 @@ async def get_platform_stats(
|
||||
|
||||
Returns counts for vendors, pages, and content breakdown.
|
||||
"""
|
||||
platform = db.query(Platform).filter(Platform.code == code).first()
|
||||
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail=f"Platform not found: {code}")
|
||||
|
||||
# Count vendors
|
||||
vendor_count = (
|
||||
db.query(func.count(VendorPlatform.vendor_id))
|
||||
.filter(VendorPlatform.platform_id == platform.id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count platform marketing pages
|
||||
platform_pages_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count vendor default pages
|
||||
vendor_defaults_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count vendor override pages
|
||||
vendor_overrides_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.vendor_id != None,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count published pages
|
||||
published_pages_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.is_published == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Count draft pages
|
||||
draft_pages_count = (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform.id,
|
||||
ContentPage.is_published == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
platform = platform_service.get_platform_by_code(db, code)
|
||||
stats = platform_service.get_platform_stats(db, platform)
|
||||
|
||||
return PlatformStatsResponse(
|
||||
platform_id=platform.id,
|
||||
platform_code=platform.code,
|
||||
platform_name=platform.name,
|
||||
vendor_count=vendor_count,
|
||||
platform_pages_count=platform_pages_count,
|
||||
vendor_defaults_count=vendor_defaults_count,
|
||||
vendor_overrides_count=vendor_overrides_count,
|
||||
published_pages_count=published_pages_count,
|
||||
draft_pages_count=draft_pages_count,
|
||||
platform_id=stats.platform_id,
|
||||
platform_code=stats.platform_code,
|
||||
platform_name=stats.platform_name,
|
||||
vendor_count=stats.vendor_count,
|
||||
platform_pages_count=stats.platform_pages_count,
|
||||
vendor_defaults_count=stats.vendor_defaults_count,
|
||||
vendor_overrides_count=stats.vendor_overrides_count,
|
||||
published_pages_count=stats.published_pages_count,
|
||||
draft_pages_count=stats.draft_pages_count,
|
||||
)
|
||||
|
||||
11
app/api/v1/vendor/content_pages.py
vendored
11
app/api/v1/vendor/content_pages.py
vendored
@@ -19,8 +19,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
from app.services.content_page_service import content_page_service
|
||||
from app.services.vendor_service import VendorService
|
||||
from models.database.user import User
|
||||
|
||||
vendor_service = VendorService()
|
||||
|
||||
router = APIRouter(prefix="/content-pages")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -272,9 +275,7 @@ def get_cms_usage(
|
||||
|
||||
Returns page counts and limits based on subscription tier.
|
||||
"""
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first()
|
||||
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
|
||||
if not vendor:
|
||||
return CMSUsageResponse(
|
||||
total_pages=0,
|
||||
@@ -336,10 +337,8 @@ def get_platform_default(
|
||||
|
||||
Useful for vendors to view the original before/after overriding.
|
||||
"""
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
# Get vendor's platform
|
||||
vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first()
|
||||
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
|
||||
platform_id = 1 # Default to OMS
|
||||
|
||||
if vendor and vendor.platforms:
|
||||
|
||||
44
app/exceptions/platform.py
Normal file
44
app/exceptions/platform.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# app/exceptions/platform.py
|
||||
"""
|
||||
Platform-related exceptions.
|
||||
|
||||
Custom exceptions for platform management operations.
|
||||
"""
|
||||
|
||||
from app.exceptions.base import WizamartException
|
||||
|
||||
|
||||
class PlatformNotFoundException(WizamartException):
|
||||
"""Raised when a platform is not found."""
|
||||
|
||||
def __init__(self, code: str):
|
||||
super().__init__(
|
||||
message=f"Platform not found: {code}",
|
||||
error_code="PLATFORM_NOT_FOUND",
|
||||
status_code=404,
|
||||
details={"platform_code": code},
|
||||
)
|
||||
|
||||
|
||||
class PlatformInactiveException(WizamartException):
|
||||
"""Raised when trying to access an inactive platform."""
|
||||
|
||||
def __init__(self, code: str):
|
||||
super().__init__(
|
||||
message=f"Platform is inactive: {code}",
|
||||
error_code="PLATFORM_INACTIVE",
|
||||
status_code=403,
|
||||
details={"platform_code": code},
|
||||
)
|
||||
|
||||
|
||||
class PlatformUpdateException(WizamartException):
|
||||
"""Raised when platform update fails."""
|
||||
|
||||
def __init__(self, code: str, reason: str):
|
||||
super().__init__(
|
||||
message=f"Failed to update platform {code}: {reason}",
|
||||
error_code="PLATFORM_UPDATE_FAILED",
|
||||
status_code=400,
|
||||
details={"platform_code": code, "reason": reason},
|
||||
)
|
||||
288
app/services/platform_service.py
Normal file
288
app/services/platform_service.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# app/services/platform_service.py
|
||||
"""
|
||||
Platform Service
|
||||
|
||||
Business logic for platform management in the Multi-Platform CMS.
|
||||
|
||||
Platforms represent different business offerings (OMS, Loyalty, Site Builder, Main Marketing).
|
||||
Each platform has its own:
|
||||
- Marketing pages (homepage, pricing, features)
|
||||
- Vendor defaults (about, terms, privacy)
|
||||
- Configuration and branding
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions.platform import (
|
||||
PlatformNotFoundException,
|
||||
)
|
||||
from models.database.content_page import ContentPage
|
||||
from models.database.platform import Platform
|
||||
from models.database.vendor_platform import VendorPlatform
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformStats:
|
||||
"""Platform statistics."""
|
||||
|
||||
platform_id: int
|
||||
platform_code: str
|
||||
platform_name: str
|
||||
vendor_count: int
|
||||
platform_pages_count: int
|
||||
vendor_defaults_count: int
|
||||
vendor_overrides_count: int = 0
|
||||
published_pages_count: int = 0
|
||||
draft_pages_count: int = 0
|
||||
|
||||
|
||||
class PlatformService:
|
||||
"""Service for platform operations."""
|
||||
|
||||
@staticmethod
|
||||
def get_platform_by_code(db: Session, code: str) -> Platform:
|
||||
"""
|
||||
Get platform by code.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
code: Platform code (oms, loyalty, main, etc.)
|
||||
|
||||
Returns:
|
||||
Platform object
|
||||
|
||||
Raises:
|
||||
PlatformNotFoundException: If platform not found
|
||||
"""
|
||||
platform = db.query(Platform).filter(Platform.code == code).first()
|
||||
|
||||
if not platform:
|
||||
raise PlatformNotFoundException(code)
|
||||
|
||||
return platform
|
||||
|
||||
@staticmethod
|
||||
def get_platform_by_code_optional(db: Session, code: str) -> Platform | None:
|
||||
"""
|
||||
Get platform by code, returns None if not found.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
code: Platform code
|
||||
|
||||
Returns:
|
||||
Platform object or None
|
||||
"""
|
||||
return db.query(Platform).filter(Platform.code == code).first()
|
||||
|
||||
@staticmethod
|
||||
def list_platforms(
|
||||
db: Session, include_inactive: bool = False
|
||||
) -> list[Platform]:
|
||||
"""
|
||||
List all platforms.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
include_inactive: Include inactive platforms
|
||||
|
||||
Returns:
|
||||
List of Platform objects
|
||||
"""
|
||||
query = db.query(Platform)
|
||||
|
||||
if not include_inactive:
|
||||
query = query.filter(Platform.is_active == True)
|
||||
|
||||
return query.order_by(Platform.id).all()
|
||||
|
||||
@staticmethod
|
||||
def get_vendor_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of vendors on a platform.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Vendor count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(VendorPlatform.vendor_id))
|
||||
.filter(VendorPlatform.platform_id == platform_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_platform_pages_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of platform marketing pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Platform pages count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_vendor_defaults_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of vendor default pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Vendor defaults count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_vendor_overrides_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of vendor override pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Vendor overrides count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.vendor_id != None,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_published_pages_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of published pages on a platform.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Published pages count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.is_published == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_draft_pages_count(db: Session, platform_id: int) -> int:
|
||||
"""
|
||||
Get count of draft pages on a platform.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
|
||||
Returns:
|
||||
Draft pages count
|
||||
"""
|
||||
return (
|
||||
db.query(func.count(ContentPage.id))
|
||||
.filter(
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.is_published == False,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_platform_stats(cls, db: Session, platform: Platform) -> PlatformStats:
|
||||
"""
|
||||
Get comprehensive statistics for a platform.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform: Platform object
|
||||
|
||||
Returns:
|
||||
PlatformStats dataclass
|
||||
"""
|
||||
return PlatformStats(
|
||||
platform_id=platform.id,
|
||||
platform_code=platform.code,
|
||||
platform_name=platform.name,
|
||||
vendor_count=cls.get_vendor_count(db, platform.id),
|
||||
platform_pages_count=cls.get_platform_pages_count(db, platform.id),
|
||||
vendor_defaults_count=cls.get_vendor_defaults_count(db, platform.id),
|
||||
vendor_overrides_count=cls.get_vendor_overrides_count(db, platform.id),
|
||||
published_pages_count=cls.get_published_pages_count(db, platform.id),
|
||||
draft_pages_count=cls.get_draft_pages_count(db, platform.id),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_platform(
|
||||
db: Session, platform: Platform, update_data: dict
|
||||
) -> Platform:
|
||||
"""
|
||||
Update platform fields.
|
||||
|
||||
Note: This method does NOT commit the transaction.
|
||||
The caller (API endpoint) is responsible for committing.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform: Platform to update
|
||||
update_data: Dictionary of fields to update
|
||||
|
||||
Returns:
|
||||
Updated Platform object (with pending changes)
|
||||
"""
|
||||
for field, value in update_data.items():
|
||||
if hasattr(platform, field):
|
||||
setattr(platform, field, value)
|
||||
|
||||
logger.info(f"[PLATFORMS] Updated platform: {platform.code}")
|
||||
|
||||
return platform
|
||||
|
||||
|
||||
# Singleton instance for convenience
|
||||
platform_service = PlatformService()
|
||||
@@ -257,6 +257,19 @@ class VendorService:
|
||||
|
||||
return vendor
|
||||
|
||||
def get_vendor_by_id_optional(self, db: Session, vendor_id: int) -> Vendor | None:
|
||||
"""
|
||||
Get vendor by ID, returns None if not found.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Vendor ID to find
|
||||
|
||||
Returns:
|
||||
Vendor object or None if not found
|
||||
"""
|
||||
return db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
|
||||
def get_active_vendor_by_code(self, db: Session, vendor_code: str) -> Vendor:
|
||||
"""
|
||||
Get active vendor by vendor_code for public access (no auth required).
|
||||
|
||||
51
app/templates/vendor/content-page-edit.html
vendored
51
app/templates/vendor/content-page-edit.html
vendored
@@ -3,6 +3,7 @@
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert_dynamic %}
|
||||
{% from 'shared/macros/headers.html' import back_button %}
|
||||
{% from 'shared/macros/inputs.html' import number_stepper %}
|
||||
{% from 'shared/macros/modals.html' import modal %}
|
||||
|
||||
{% block title %}{% if page_id %}Edit{% else %}Create{% endif %} Content Page{% endblock %}
|
||||
|
||||
@@ -71,46 +72,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Default Content Preview Modal -->
|
||||
<div
|
||||
x-show="showingDefaultPreview"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
||||
@click.self="showingDefaultPreview = false"
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full mx-4 max-h-[80vh] overflow-hidden">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Platform Default Content</h3>
|
||||
<button @click="showingDefaultPreview = false" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('x-mark', 'w-6 h-6')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[60vh]">
|
||||
<div x-show="loadingDefault" class="text-center py-8">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-sm text-gray-500">Loading default content...</p>
|
||||
</div>
|
||||
<div x-show="!loadingDefault && defaultContent">
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Title</h4>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-4" x-text="defaultContent?.title"></p>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Content</h4>
|
||||
<div class="prose dark:prose-invert max-w-none bg-gray-50 dark:bg-gray-700 rounded-lg p-4" x-html="defaultContent?.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<button
|
||||
@click="showingDefaultPreview = false"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{% call modal('defaultPreviewModal', 'Platform Default Content', 'showingDefaultPreview', size='lg', show_footer=false) %}
|
||||
<div x-show="loadingDefault" class="text-center py-8">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-sm text-gray-500">Loading default content...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="!loadingDefault && defaultContent">
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Title</h4>
|
||||
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-4" x-text="defaultContent?.title"></p>
|
||||
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Content</h4>
|
||||
<div class="prose dark:prose-invert max-w-none bg-gray-50 dark:bg-gray-700 rounded-lg p-4" x-html="defaultContent?.content"></div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Main Form -->
|
||||
<div x-show="!loading" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* Handles platform listing and management for multi-platform CMS.
|
||||
*/
|
||||
|
||||
const platformsLog = window.LogConfig.createLogger('PLATFORMS');
|
||||
|
||||
function platformsManager() {
|
||||
return {
|
||||
// State
|
||||
@@ -13,6 +15,10 @@ function platformsManager() {
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
// Duplicate initialization guard
|
||||
if (window._adminPlatformsInitialized) return;
|
||||
window._adminPlatformsInitialized = true;
|
||||
|
||||
this.currentPage = "platforms";
|
||||
await this.loadPlatforms();
|
||||
},
|
||||
@@ -25,9 +31,9 @@ function platformsManager() {
|
||||
try {
|
||||
const response = await apiClient.get("/admin/platforms");
|
||||
this.platforms = response.platforms || [];
|
||||
console.log(`[PLATFORMS] Loaded ${this.platforms.length} platforms`);
|
||||
platformsLog.info(`Loaded ${this.platforms.length} platforms`);
|
||||
} catch (err) {
|
||||
console.error("[PLATFORMS] Error loading platforms:", err);
|
||||
platformsLog.error("Error loading platforms:", err);
|
||||
this.error = err.message || "Failed to load platforms";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
|
||||
Reference in New Issue
Block a user