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
|
import logging
|
||||||
from typing import Any
|
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 pydantic import BaseModel, Field
|
||||||
from sqlalchemy import func
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_current_admin_from_cookie_or_header, get_db
|
from app.api.deps import get_current_admin_from_cookie_or_header, get_db
|
||||||
from models.database.content_page import ContentPage
|
from app.services.platform_service import platform_service
|
||||||
from models.database.platform import Platform
|
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.database.vendor_platform import VendorPlatform
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/platforms")
|
router = APIRouter(prefix="/platforms")
|
||||||
@@ -107,140 +104,12 @@ class PlatformStatsResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# API Endpoints
|
# Helper Functions
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=PlatformListResponse)
|
def _build_platform_response(db: Session, platform) -> PlatformResponse:
|
||||||
async def list_platforms(
|
"""Build PlatformResponse from Platform model with computed fields."""
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
return PlatformResponse(
|
return PlatformResponse(
|
||||||
id=platform.id,
|
id=platform.id,
|
||||||
code=platform.code,
|
code=platform.code,
|
||||||
@@ -259,12 +128,52 @@ async def get_platform(
|
|||||||
settings=platform.settings or {},
|
settings=platform.settings or {},
|
||||||
created_at=platform.created_at.isoformat(),
|
created_at=platform.created_at.isoformat(),
|
||||||
updated_at=platform.updated_at.isoformat(),
|
updated_at=platform.updated_at.isoformat(),
|
||||||
vendor_count=vendor_count,
|
vendor_count=platform_service.get_vendor_count(db, platform.id),
|
||||||
platform_pages_count=platform_pages_count,
|
platform_pages_count=platform_service.get_platform_pages_count(db, platform.id),
|
||||||
vendor_defaults_count=vendor_defaults_count,
|
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)
|
@router.put("/{code}", response_model=PlatformResponse)
|
||||||
async def update_platform(
|
async def update_platform(
|
||||||
update_data: PlatformUpdateRequest,
|
update_data: PlatformUpdateRequest,
|
||||||
@@ -277,24 +186,15 @@ async def update_platform(
|
|||||||
|
|
||||||
Allows updating name, description, branding, and configuration.
|
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)
|
update_dict = update_data.model_dump(exclude_unset=True)
|
||||||
for field, value in update_dict.items():
|
platform = platform_service.update_platform(db, platform, update_dict)
|
||||||
if hasattr(platform, field):
|
|
||||||
setattr(platform, field, value)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(platform)
|
db.refresh(platform)
|
||||||
|
|
||||||
logger.info(f"[PLATFORMS] Updated platform: {code}")
|
return _build_platform_response(db, platform)
|
||||||
|
|
||||||
# Return updated platform with stats
|
|
||||||
return await get_platform(code=code, db=db, current_user=current_user)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{code}/stats", response_model=PlatformStatsResponse)
|
@router.get("/{code}/stats", response_model=PlatformStatsResponse)
|
||||||
@@ -308,84 +208,17 @@ async def get_platform_stats(
|
|||||||
|
|
||||||
Returns counts for vendors, pages, and content breakdown.
|
Returns counts for vendors, pages, and content breakdown.
|
||||||
"""
|
"""
|
||||||
platform = db.query(Platform).filter(Platform.code == code).first()
|
platform = platform_service.get_platform_by_code(db, code)
|
||||||
|
stats = platform_service.get_platform_stats(db, platform)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
return PlatformStatsResponse(
|
return PlatformStatsResponse(
|
||||||
platform_id=platform.id,
|
platform_id=stats.platform_id,
|
||||||
platform_code=platform.code,
|
platform_code=stats.platform_code,
|
||||||
platform_name=platform.name,
|
platform_name=stats.platform_name,
|
||||||
vendor_count=vendor_count,
|
vendor_count=stats.vendor_count,
|
||||||
platform_pages_count=platform_pages_count,
|
platform_pages_count=stats.platform_pages_count,
|
||||||
vendor_defaults_count=vendor_defaults_count,
|
vendor_defaults_count=stats.vendor_defaults_count,
|
||||||
vendor_overrides_count=vendor_overrides_count,
|
vendor_overrides_count=stats.vendor_overrides_count,
|
||||||
published_pages_count=published_pages_count,
|
published_pages_count=stats.published_pages_count,
|
||||||
draft_pages_count=draft_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.api.deps import get_current_vendor_api, get_db
|
||||||
from app.services.content_page_service import content_page_service
|
from app.services.content_page_service import content_page_service
|
||||||
|
from app.services.vendor_service import VendorService
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
|
||||||
|
vendor_service = VendorService()
|
||||||
|
|
||||||
router = APIRouter(prefix="/content-pages")
|
router = APIRouter(prefix="/content-pages")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -272,9 +275,7 @@ def get_cms_usage(
|
|||||||
|
|
||||||
Returns page counts and limits based on subscription tier.
|
Returns page counts and limits based on subscription tier.
|
||||||
"""
|
"""
|
||||||
from models.database.vendor import Vendor
|
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
|
||||||
|
|
||||||
vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first()
|
|
||||||
if not vendor:
|
if not vendor:
|
||||||
return CMSUsageResponse(
|
return CMSUsageResponse(
|
||||||
total_pages=0,
|
total_pages=0,
|
||||||
@@ -336,10 +337,8 @@ def get_platform_default(
|
|||||||
|
|
||||||
Useful for vendors to view the original before/after overriding.
|
Useful for vendors to view the original before/after overriding.
|
||||||
"""
|
"""
|
||||||
from models.database.vendor import Vendor
|
|
||||||
|
|
||||||
# Get vendor's platform
|
# 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
|
platform_id = 1 # Default to OMS
|
||||||
|
|
||||||
if vendor and vendor.platforms:
|
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
|
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:
|
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).
|
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/alerts.html' import loading_state, error_state, alert_dynamic %}
|
||||||
{% from 'shared/macros/headers.html' import back_button %}
|
{% from 'shared/macros/headers.html' import back_button %}
|
||||||
{% from 'shared/macros/inputs.html' import number_stepper %}
|
{% 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 %}
|
{% block title %}{% if page_id %}Edit{% else %}Create{% endif %} Content Page{% endblock %}
|
||||||
|
|
||||||
@@ -71,46 +72,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Default Content Preview Modal -->
|
<!-- Default Content Preview Modal -->
|
||||||
<div
|
{% call modal('defaultPreviewModal', 'Platform Default Content', 'showingDefaultPreview', size='lg', show_footer=false) %}
|
||||||
x-show="showingDefaultPreview"
|
<div x-show="loadingDefault" class="text-center py-8">
|
||||||
x-transition:enter="transition ease-out duration-300"
|
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||||
x-transition:enter-start="opacity-0"
|
<p class="mt-2 text-sm text-gray-500">Loading default content...</p>
|
||||||
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>
|
|
||||||
</div>
|
</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 -->
|
<!-- Main Form -->
|
||||||
<div x-show="!loading" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
<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.
|
* Handles platform listing and management for multi-platform CMS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const platformsLog = window.LogConfig.createLogger('PLATFORMS');
|
||||||
|
|
||||||
function platformsManager() {
|
function platformsManager() {
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
@@ -13,6 +15,10 @@ function platformsManager() {
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
async init() {
|
async init() {
|
||||||
|
// Duplicate initialization guard
|
||||||
|
if (window._adminPlatformsInitialized) return;
|
||||||
|
window._adminPlatformsInitialized = true;
|
||||||
|
|
||||||
this.currentPage = "platforms";
|
this.currentPage = "platforms";
|
||||||
await this.loadPlatforms();
|
await this.loadPlatforms();
|
||||||
},
|
},
|
||||||
@@ -25,9 +31,9 @@ function platformsManager() {
|
|||||||
try {
|
try {
|
||||||
const response = await apiClient.get("/admin/platforms");
|
const response = await apiClient.get("/admin/platforms");
|
||||||
this.platforms = response.platforms || [];
|
this.platforms = response.platforms || [];
|
||||||
console.log(`[PLATFORMS] Loaded ${this.platforms.length} platforms`);
|
platformsLog.info(`Loaded ${this.platforms.length} platforms`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[PLATFORMS] Error loading platforms:", err);
|
platformsLog.error("Error loading platforms:", err);
|
||||||
this.error = err.message || "Failed to load platforms";
|
this.error = err.message || "Failed to load platforms";
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user