- Remove |safe from |tojson in HTML attributes (x-data) - quotes must become " for browsers to parse correctly - Update LANG-002 and LANG-003 architecture rules to document correct |tojson usage patterns: - HTML attributes: |tojson (no |safe) - Script blocks: |tojson|safe - Fix validator to warn when |tojson|safe is used in x-data (breaks HTML attribute parsing) - Improve code quality across services, APIs, and tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
235 lines
7.7 KiB
Python
235 lines
7.7 KiB
Python
# app/api/v1/admin/vendor_themes.py
|
|
"""
|
|
Vendor theme management endpoints for admin.
|
|
|
|
These endpoints allow admins to:
|
|
- View vendor themes
|
|
- Apply theme presets
|
|
- Customize theme settings
|
|
- Reset themes to default
|
|
|
|
All operations use the service layer for business logic.
|
|
All exceptions are handled by the global exception handler.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, Path
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_current_admin_api, get_db
|
|
from app.services.vendor_theme_service import vendor_theme_service
|
|
from models.database.user import User
|
|
from models.schema.vendor_theme import (
|
|
ThemeDeleteResponse,
|
|
ThemePresetListResponse,
|
|
ThemePresetResponse,
|
|
VendorThemeResponse,
|
|
VendorThemeUpdate,
|
|
)
|
|
|
|
router = APIRouter(prefix="/vendor-themes")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# PRESET ENDPOINTS
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/presets", response_model=ThemePresetListResponse)
|
|
async def get_theme_presets(current_admin: User = Depends(get_current_admin_api)):
|
|
"""
|
|
Get all available theme presets with preview information.
|
|
|
|
Returns list of presets that can be applied to vendor themes.
|
|
Each preset includes color palette, fonts, and layout configuration.
|
|
|
|
**Permissions:** Admin only
|
|
|
|
**Returns:**
|
|
- List of available theme presets with preview data
|
|
"""
|
|
logger.info("Getting theme presets")
|
|
|
|
presets = vendor_theme_service.get_available_presets()
|
|
return ThemePresetListResponse(presets=presets)
|
|
|
|
|
|
# ============================================================================
|
|
# THEME RETRIEVAL
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/{vendor_code}", response_model=VendorThemeResponse)
|
|
async def get_vendor_theme(
|
|
vendor_code: str = Path(..., description="Vendor code"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Get theme configuration for a vendor.
|
|
|
|
Returns the vendor's custom theme if exists, otherwise returns default theme.
|
|
|
|
**Path Parameters:**
|
|
- `vendor_code`: Vendor code (e.g., VENDOR001)
|
|
|
|
**Permissions:** Admin only
|
|
|
|
**Returns:**
|
|
- Complete theme configuration including colors, fonts, layout, and branding
|
|
|
|
**Errors:**
|
|
- `404`: Vendor not found (VendorNotFoundException)
|
|
"""
|
|
logger.info(f"Getting theme for vendor: {vendor_code}")
|
|
|
|
# Service raises VendorNotFoundException if vendor not found
|
|
# Global exception handler converts it to HTTP 404
|
|
theme = vendor_theme_service.get_theme(db, vendor_code)
|
|
return theme
|
|
|
|
|
|
# ============================================================================
|
|
# THEME UPDATE
|
|
# ============================================================================
|
|
|
|
|
|
@router.put("/{vendor_code}", response_model=VendorThemeResponse)
|
|
async def update_vendor_theme(
|
|
vendor_code: str = Path(..., description="Vendor code"),
|
|
theme_data: VendorThemeUpdate = None,
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Update or create theme for a vendor.
|
|
|
|
Accepts partial updates - only provided fields are updated.
|
|
If vendor has no theme, a new one is created.
|
|
|
|
**Path Parameters:**
|
|
- `vendor_code`: Vendor code (e.g., VENDOR001)
|
|
|
|
**Request Body:**
|
|
- `theme_name`: Optional theme name
|
|
- `colors`: Optional color palette (primary, secondary, accent, etc.)
|
|
- `fonts`: Optional font settings (heading, body)
|
|
- `layout`: Optional layout settings (style, header, product_card)
|
|
- `branding`: Optional branding assets (logo, favicon, etc.)
|
|
- `custom_css`: Optional custom CSS rules
|
|
- `social_links`: Optional social media links
|
|
|
|
**Permissions:** Admin only
|
|
|
|
**Returns:**
|
|
- Updated theme configuration
|
|
|
|
**Errors:**
|
|
- `404`: Vendor not found (VendorNotFoundException)
|
|
- `422`: Validation error (ThemeValidationException, InvalidColorFormatException, etc.)
|
|
- `500`: Operation failed (ThemeOperationException)
|
|
"""
|
|
logger.info(f"Updating theme for vendor: {vendor_code}")
|
|
|
|
# Service handles all validation and raises appropriate exceptions
|
|
# Global exception handler converts them to proper HTTP responses
|
|
theme = vendor_theme_service.update_theme(db, vendor_code, theme_data)
|
|
db.commit()
|
|
return VendorThemeResponse(**theme.to_dict())
|
|
|
|
|
|
# ============================================================================
|
|
# PRESET APPLICATION
|
|
# ============================================================================
|
|
|
|
|
|
@router.post("/{vendor_code}/preset/{preset_name}", response_model=ThemePresetResponse)
|
|
async def apply_theme_preset(
|
|
vendor_code: str = Path(..., description="Vendor code"),
|
|
preset_name: str = Path(..., description="Preset name"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Apply a theme preset to a vendor.
|
|
|
|
Replaces the vendor's current theme with the selected preset.
|
|
Available presets can be retrieved from the `/presets` endpoint.
|
|
|
|
**Path Parameters:**
|
|
- `vendor_code`: Vendor code (e.g., VENDOR001)
|
|
- `preset_name`: Name of preset to apply (e.g., 'modern', 'classic')
|
|
|
|
**Available Presets:**
|
|
- `default`: Clean and professional
|
|
- `modern`: Contemporary tech-inspired
|
|
- `classic`: Traditional and trustworthy
|
|
- `minimal`: Ultra-clean black and white
|
|
- `vibrant`: Bold and energetic
|
|
- `elegant`: Sophisticated gray tones
|
|
- `nature`: Fresh and eco-friendly
|
|
|
|
**Permissions:** Admin only
|
|
|
|
**Returns:**
|
|
- Success message and applied theme configuration
|
|
|
|
**Errors:**
|
|
- `404`: Vendor not found (VendorNotFoundException) or preset not found (ThemePresetNotFoundException)
|
|
- `500`: Operation failed (ThemeOperationException)
|
|
"""
|
|
logger.info(f"Applying preset '{preset_name}' to vendor {vendor_code}")
|
|
|
|
# Service validates preset name and applies it
|
|
# Raises ThemePresetNotFoundException if preset doesn't exist
|
|
# Global exception handler converts to HTTP 404
|
|
theme = vendor_theme_service.apply_theme_preset(db, vendor_code, preset_name)
|
|
db.commit()
|
|
|
|
return ThemePresetResponse(
|
|
message=f"Applied {preset_name} preset successfully",
|
|
theme=VendorThemeResponse(**theme.to_dict()),
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# THEME DELETION
|
|
# ============================================================================
|
|
|
|
|
|
@router.delete("/{vendor_code}", response_model=ThemeDeleteResponse)
|
|
async def delete_vendor_theme(
|
|
vendor_code: str = Path(..., description="Vendor code"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Delete custom theme for a vendor.
|
|
|
|
Removes the vendor's custom theme. After deletion, the vendor
|
|
will revert to using the default platform theme.
|
|
|
|
**Path Parameters:**
|
|
- `vendor_code`: Vendor code (e.g., VENDOR001)
|
|
|
|
**Permissions:** Admin only
|
|
|
|
**Returns:**
|
|
- Success message
|
|
|
|
**Errors:**
|
|
- `404`: Vendor not found (VendorNotFoundException) or no custom theme (VendorThemeNotFoundException)
|
|
- `500`: Operation failed (ThemeOperationException)
|
|
"""
|
|
logger.info(f"Deleting theme for vendor: {vendor_code}")
|
|
|
|
# Service handles deletion and raises exceptions if needed
|
|
# Global exception handler converts them to proper HTTP responses
|
|
result = vendor_theme_service.delete_theme(db, vendor_code)
|
|
db.commit()
|
|
return ThemeDeleteResponse(
|
|
message=result.get("message", "Theme deleted successfully")
|
|
)
|