Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
357 lines
11 KiB
Python
357 lines
11 KiB
Python
# app/modules/cms/exceptions.py
|
|
"""
|
|
CMS module exceptions.
|
|
|
|
This module provides exception classes for CMS operations including:
|
|
- Content page management
|
|
- Media file handling
|
|
- Vendor theme customization
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from app.exceptions.base import (
|
|
AuthorizationException,
|
|
BusinessLogicException,
|
|
ConflictException,
|
|
ResourceNotFoundException,
|
|
ValidationException,
|
|
)
|
|
|
|
__all__ = [
|
|
# Content page exceptions
|
|
"ContentPageNotFoundException",
|
|
"ContentPageAlreadyExistsException",
|
|
"ContentPageSlugReservedException",
|
|
"ContentPageNotPublishedException",
|
|
"UnauthorizedContentPageAccessException",
|
|
"VendorNotAssociatedException",
|
|
"ContentPageValidationException",
|
|
# Media exceptions
|
|
"MediaNotFoundException",
|
|
"MediaUploadException",
|
|
"MediaValidationException",
|
|
"UnsupportedMediaTypeException",
|
|
"MediaFileTooLargeException",
|
|
"MediaOptimizationException",
|
|
"MediaDeleteException",
|
|
# Theme exceptions
|
|
"VendorThemeNotFoundException",
|
|
"InvalidThemeDataException",
|
|
"ThemePresetNotFoundException",
|
|
"ThemeValidationException",
|
|
"ThemePresetAlreadyAppliedException",
|
|
"InvalidColorFormatException",
|
|
"InvalidFontFamilyException",
|
|
"ThemeOperationException",
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# Content Page Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class ContentPageNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a content page is not found."""
|
|
|
|
def __init__(self, identifier: str | int | None = None):
|
|
if identifier:
|
|
message = f"Content page not found: {identifier}"
|
|
else:
|
|
message = "Content page not found"
|
|
super().__init__(
|
|
message=message,
|
|
resource_type="ContentPage",
|
|
identifier=str(identifier) if identifier else "unknown",
|
|
error_code="CONTENT_PAGE_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class ContentPageAlreadyExistsException(ConflictException):
|
|
"""Raised when a content page with the same slug already exists."""
|
|
|
|
def __init__(self, slug: str, vendor_id: int | None = None):
|
|
if vendor_id:
|
|
message = f"Content page with slug '{slug}' already exists for this vendor"
|
|
else:
|
|
message = f"Platform content page with slug '{slug}' already exists"
|
|
super().__init__(
|
|
message=message,
|
|
error_code="CONTENT_PAGE_ALREADY_EXISTS",
|
|
details={"slug": slug, "vendor_id": vendor_id} if vendor_id else {"slug": slug},
|
|
)
|
|
|
|
|
|
class ContentPageSlugReservedException(ValidationException):
|
|
"""Raised when trying to use a reserved slug."""
|
|
|
|
def __init__(self, slug: str):
|
|
super().__init__(
|
|
message=f"Content page slug '{slug}' is reserved",
|
|
field="slug",
|
|
details={"slug": slug},
|
|
)
|
|
self.error_code = "CONTENT_PAGE_SLUG_RESERVED"
|
|
|
|
|
|
class ContentPageNotPublishedException(BusinessLogicException):
|
|
"""Raised when trying to access an unpublished content page."""
|
|
|
|
def __init__(self, slug: str):
|
|
super().__init__(
|
|
message=f"Content page '{slug}' is not published",
|
|
error_code="CONTENT_PAGE_NOT_PUBLISHED",
|
|
details={"slug": slug},
|
|
)
|
|
|
|
|
|
class UnauthorizedContentPageAccessException(AuthorizationException):
|
|
"""Raised when a user tries to access/modify a content page they don't own."""
|
|
|
|
def __init__(self, action: str = "access"):
|
|
super().__init__(
|
|
message=f"Cannot {action} content pages from other vendors",
|
|
error_code="CONTENT_PAGE_ACCESS_DENIED",
|
|
details={"required_permission": f"content_page:{action}"},
|
|
)
|
|
|
|
|
|
class VendorNotAssociatedException(AuthorizationException):
|
|
"""Raised when a user is not associated with a vendor."""
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
message="User is not associated with a vendor",
|
|
error_code="VENDOR_NOT_ASSOCIATED",
|
|
details={"required_permission": "vendor:member"},
|
|
)
|
|
|
|
|
|
class ContentPageValidationException(ValidationException):
|
|
"""Raised when content page data validation fails."""
|
|
|
|
def __init__(self, field: str, message: str, value: str | None = None):
|
|
details = {}
|
|
if value:
|
|
details["value"] = value
|
|
super().__init__(message=message, field=field, details=details if details else None)
|
|
self.error_code = "CONTENT_PAGE_VALIDATION_FAILED"
|
|
|
|
|
|
# =============================================================================
|
|
# Media Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class MediaNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a media file is not found."""
|
|
|
|
def __init__(self, media_id: int | str | None = None):
|
|
if media_id:
|
|
message = f"Media file '{media_id}' not found"
|
|
else:
|
|
message = "Media file not found"
|
|
super().__init__(
|
|
resource_type="MediaFile",
|
|
identifier=str(media_id) if media_id else "unknown",
|
|
message=message,
|
|
error_code="MEDIA_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class MediaUploadException(BusinessLogicException):
|
|
"""Raised when media upload fails."""
|
|
|
|
def __init__(self, message: str = "Media upload failed", details: dict[str, Any] | None = None):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="MEDIA_UPLOAD_FAILED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
class MediaValidationException(ValidationException):
|
|
"""Raised when media validation fails (file type, size, etc.)."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Media validation failed",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(message=message, field=field, details=details)
|
|
self.error_code = "MEDIA_VALIDATION_FAILED"
|
|
|
|
|
|
class UnsupportedMediaTypeException(ValidationException):
|
|
"""Raised when media file type is not supported."""
|
|
|
|
def __init__(self, file_type: str, allowed_types: list[str] | None = None):
|
|
details = {"file_type": file_type}
|
|
if allowed_types:
|
|
details["allowed_types"] = allowed_types
|
|
|
|
super().__init__(
|
|
message=f"Unsupported media type: {file_type}",
|
|
field="file",
|
|
details=details,
|
|
)
|
|
self.error_code = "UNSUPPORTED_MEDIA_TYPE"
|
|
|
|
|
|
class MediaFileTooLargeException(ValidationException):
|
|
"""Raised when media file exceeds size limit."""
|
|
|
|
def __init__(self, file_size: int, max_size: int, media_type: str = "file"):
|
|
super().__init__(
|
|
message=f"File size ({file_size} bytes) exceeds maximum allowed ({max_size} bytes) for {media_type}",
|
|
field="file",
|
|
details={
|
|
"file_size": file_size,
|
|
"max_size": max_size,
|
|
"media_type": media_type,
|
|
},
|
|
)
|
|
self.error_code = "MEDIA_FILE_TOO_LARGE"
|
|
|
|
|
|
class MediaOptimizationException(BusinessLogicException):
|
|
"""Raised when media optimization fails."""
|
|
|
|
def __init__(self, message: str = "Only images can be optimized"):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="MEDIA_OPTIMIZATION_FAILED",
|
|
)
|
|
|
|
|
|
class MediaDeleteException(BusinessLogicException):
|
|
"""Raised when media deletion fails."""
|
|
|
|
def __init__(self, message: str, details: dict[str, Any] | None = None):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="MEDIA_DELETE_FAILED",
|
|
details=details,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Theme Exceptions
|
|
# =============================================================================
|
|
|
|
|
|
class VendorThemeNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a vendor theme is not found."""
|
|
|
|
def __init__(self, vendor_identifier: str):
|
|
super().__init__(
|
|
resource_type="VendorTheme",
|
|
identifier=vendor_identifier,
|
|
message=f"Theme for vendor '{vendor_identifier}' not found",
|
|
error_code="VENDOR_THEME_NOT_FOUND",
|
|
)
|
|
|
|
|
|
class InvalidThemeDataException(ValidationException):
|
|
"""Raised when theme data is invalid."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Invalid theme data",
|
|
field: str | None = None,
|
|
details: dict[str, Any] | None = None,
|
|
):
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "INVALID_THEME_DATA"
|
|
|
|
|
|
class ThemePresetNotFoundException(ResourceNotFoundException):
|
|
"""Raised when a theme preset is not found."""
|
|
|
|
def __init__(self, preset_name: str, available_presets: list | None = None):
|
|
super().__init__(
|
|
resource_type="ThemePreset",
|
|
identifier=preset_name,
|
|
message=f"Theme preset '{preset_name}' not found",
|
|
error_code="THEME_PRESET_NOT_FOUND",
|
|
)
|
|
if available_presets:
|
|
self.details["available_presets"] = available_presets
|
|
|
|
|
|
class ThemeValidationException(ValidationException):
|
|
"""Raised when theme validation fails."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str = "Theme validation failed",
|
|
field: str | None = None,
|
|
validation_errors: dict[str, str] | None = None,
|
|
):
|
|
details = {}
|
|
if validation_errors:
|
|
details["validation_errors"] = validation_errors
|
|
|
|
super().__init__(
|
|
message=message,
|
|
field=field,
|
|
details=details,
|
|
)
|
|
self.error_code = "THEME_VALIDATION_FAILED"
|
|
|
|
|
|
class ThemePresetAlreadyAppliedException(BusinessLogicException):
|
|
"""Raised when trying to apply the same preset that's already active."""
|
|
|
|
def __init__(self, preset_name: str, vendor_code: str):
|
|
super().__init__(
|
|
message=f"Preset '{preset_name}' is already applied to vendor '{vendor_code}'",
|
|
error_code="THEME_PRESET_ALREADY_APPLIED",
|
|
details={"preset_name": preset_name, "vendor_code": vendor_code},
|
|
)
|
|
|
|
|
|
class InvalidColorFormatException(ValidationException):
|
|
"""Raised when color format is invalid."""
|
|
|
|
def __init__(self, color_value: str, field: str):
|
|
super().__init__(
|
|
message=f"Invalid color format: {color_value}",
|
|
field=field,
|
|
details={"color_value": color_value},
|
|
)
|
|
self.error_code = "INVALID_COLOR_FORMAT"
|
|
|
|
|
|
class InvalidFontFamilyException(ValidationException):
|
|
"""Raised when font family is invalid."""
|
|
|
|
def __init__(self, font_value: str, field: str):
|
|
super().__init__(
|
|
message=f"Invalid font family: {font_value}",
|
|
field=field,
|
|
details={"font_value": font_value},
|
|
)
|
|
self.error_code = "INVALID_FONT_FAMILY"
|
|
|
|
|
|
class ThemeOperationException(BusinessLogicException):
|
|
"""Raised when theme operation fails."""
|
|
|
|
def __init__(self, operation: str, vendor_code: str, reason: str):
|
|
super().__init__(
|
|
message=f"Theme operation '{operation}' failed for vendor '{vendor_code}': {reason}",
|
|
error_code="THEME_OPERATION_FAILED",
|
|
details={
|
|
"operation": operation,
|
|
"vendor_code": vendor_code,
|
|
"reason": reason,
|
|
},
|
|
)
|