refactor: migrate messaging and media routes to modules
Messaging module (communication): - vendor_messages.py: Conversation and message management - vendor_notifications.py: Vendor notifications - vendor_email_settings.py: SMTP and provider configuration - vendor_email_templates.py: Email template customization CMS module (content management): - vendor_media.py: Media library management - vendor_content_pages.py: Content page overrides All routes auto-discovered via is_self_contained=True. Deleted 5 legacy files from app/api/v1/vendor/. app/api/v1/vendor/ now empty except for __init__.py (auto-discovery only). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,278 +1,25 @@
|
||||
# app/modules/cms/routes/api/vendor.py
|
||||
"""
|
||||
Vendor Content Pages API
|
||||
CMS module vendor API routes.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
|
||||
Vendors can:
|
||||
- View their content pages (includes platform defaults)
|
||||
- Create/edit/delete their own content page overrides
|
||||
- Preview pages before publishing
|
||||
Aggregates all vendor CMS routes:
|
||||
- /content-pages/* - Content page management
|
||||
- /media/* - Media library management
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
from app.modules.cms.exceptions import ContentPageNotFoundException
|
||||
from app.modules.cms.schemas import (
|
||||
VendorContentPageCreate,
|
||||
VendorContentPageUpdate,
|
||||
ContentPageResponse,
|
||||
CMSUsageResponse,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
|
||||
from models.database.user import User
|
||||
from .vendor_content_pages import vendor_content_pages_router
|
||||
from .vendor_media import vendor_media_router
|
||||
|
||||
# Route configuration for auto-discovery
|
||||
ROUTE_CONFIG = {
|
||||
"prefix": "/content-pages",
|
||||
"tags": ["vendor-content-pages"],
|
||||
"priority": 100, # Register last (CMS has catch-all slug routes)
|
||||
}
|
||||
|
||||
vendor_service = VendorService()
|
||||
vendor_router = APIRouter()
|
||||
router = vendor_router # Alias for discovery compatibility
|
||||
|
||||
router = APIRouter()
|
||||
vendor_router = router # Alias for discovery compatibility
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR CONTENT PAGES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ContentPageResponse])
|
||||
def list_vendor_pages(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all content pages available for this vendor.
|
||||
|
||||
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
|
||||
"""
|
||||
pages = content_page_service.list_pages_for_vendor(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
|
||||
|
||||
@router.get("/overrides", response_model=list[ContentPageResponse])
|
||||
def list_vendor_overrides(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List only vendor-specific content pages (no platform defaults).
|
||||
|
||||
Shows what the vendor has customized.
|
||||
"""
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
|
||||
|
||||
@router.get("/usage", response_model=CMSUsageResponse)
|
||||
def get_cms_usage(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get CMS usage statistics for the vendor.
|
||||
|
||||
Returns page counts and limits based on subscription tier.
|
||||
"""
|
||||
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
|
||||
if not vendor:
|
||||
return CMSUsageResponse(
|
||||
total_pages=0,
|
||||
custom_pages=0,
|
||||
override_pages=0,
|
||||
pages_limit=3,
|
||||
custom_pages_limit=0,
|
||||
can_create_page=False,
|
||||
can_create_custom=False,
|
||||
usage_percent=0,
|
||||
custom_usage_percent=0,
|
||||
)
|
||||
|
||||
# Get vendor's pages
|
||||
vendor_pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=True
|
||||
)
|
||||
|
||||
total_pages = len(vendor_pages)
|
||||
override_pages = sum(1 for p in vendor_pages if p.is_vendor_override)
|
||||
custom_pages = total_pages - override_pages
|
||||
|
||||
# Get limits from subscription tier
|
||||
pages_limit = None
|
||||
custom_pages_limit = None
|
||||
if vendor.subscription and vendor.subscription.tier:
|
||||
pages_limit = vendor.subscription.tier.cms_pages_limit
|
||||
custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit
|
||||
|
||||
# Calculate can_create flags
|
||||
can_create_page = pages_limit is None or total_pages < pages_limit
|
||||
can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit
|
||||
|
||||
# Calculate usage percentages
|
||||
usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100
|
||||
custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100
|
||||
|
||||
return CMSUsageResponse(
|
||||
total_pages=total_pages,
|
||||
custom_pages=custom_pages,
|
||||
override_pages=override_pages,
|
||||
pages_limit=pages_limit,
|
||||
custom_pages_limit=custom_pages_limit,
|
||||
can_create_page=can_create_page,
|
||||
can_create_custom=can_create_custom,
|
||||
usage_percent=usage_percent,
|
||||
custom_usage_percent=custom_usage_percent,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/platform-default/{slug}", response_model=ContentPageResponse)
|
||||
def get_platform_default(
|
||||
slug: str,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get the platform default content for a slug.
|
||||
|
||||
Useful for vendors to view the original before/after overriding.
|
||||
"""
|
||||
# Get vendor's platform
|
||||
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:
|
||||
platform_id = vendor.platforms[0].id
|
||||
|
||||
# Get platform default (vendor_id=None)
|
||||
page = content_page_service.get_vendor_default_page(
|
||||
db, platform_id=platform_id, slug=slug, include_unpublished=True
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(slug)
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@router.get("/{slug}", response_model=ContentPageResponse)
|
||||
def get_page(
|
||||
slug: str,
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific content page by slug.
|
||||
|
||||
Returns vendor override if exists, otherwise platform default.
|
||||
"""
|
||||
page = content_page_service.get_page_for_vendor_or_raise(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@router.post("/", response_model=ContentPageResponse, status_code=201)
|
||||
def create_vendor_page(
|
||||
page_data: VendorContentPageCreate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a vendor-specific content page override.
|
||||
|
||||
This will be shown instead of the platform default for this vendor.
|
||||
"""
|
||||
page = content_page_service.create_page(
|
||||
db,
|
||||
slug=page_data.slug,
|
||||
title=page_data.title,
|
||||
content=page_data.content,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
content_format=page_data.content_format,
|
||||
meta_description=page_data.meta_description,
|
||||
meta_keywords=page_data.meta_keywords,
|
||||
is_published=page_data.is_published,
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
show_in_legal=page_data.show_in_legal,
|
||||
display_order=page_data.display_order,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@router.put("/{page_id}", response_model=ContentPageResponse)
|
||||
def update_vendor_page(
|
||||
page_id: int,
|
||||
page_data: VendorContentPageUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a vendor-specific content page.
|
||||
|
||||
Can only update pages owned by this vendor.
|
||||
"""
|
||||
# Update with ownership check in service layer
|
||||
page = content_page_service.update_vendor_page(
|
||||
db,
|
||||
page_id=page_id,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
title=page_data.title,
|
||||
content=page_data.content,
|
||||
content_format=page_data.content_format,
|
||||
meta_description=page_data.meta_description,
|
||||
meta_keywords=page_data.meta_keywords,
|
||||
is_published=page_data.is_published,
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
show_in_legal=page_data.show_in_legal,
|
||||
display_order=page_data.display_order,
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@router.delete("/{page_id}", status_code=204)
|
||||
def delete_vendor_page(
|
||||
page_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete a vendor-specific content page.
|
||||
|
||||
Can only delete pages owned by this vendor.
|
||||
After deletion, platform default will be shown (if exists).
|
||||
"""
|
||||
# Delete with ownership check in service layer
|
||||
content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id)
|
||||
db.commit()
|
||||
# Aggregate all CMS vendor routes
|
||||
vendor_router.include_router(vendor_content_pages_router, tags=["vendor-content-pages"])
|
||||
vendor_router.include_router(vendor_media_router, tags=["vendor-media"])
|
||||
|
||||
270
app/modules/cms/routes/api/vendor_content_pages.py
Normal file
270
app/modules/cms/routes/api/vendor_content_pages.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# app/modules/cms/routes/api/vendor_content_pages.py
|
||||
"""
|
||||
Vendor Content Pages API
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
|
||||
Vendors can:
|
||||
- View their content pages (includes platform defaults)
|
||||
- Create/edit/delete their own content page overrides
|
||||
- Preview pages before publishing
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
from app.modules.cms.exceptions import ContentPageNotFoundException
|
||||
from app.modules.cms.schemas import (
|
||||
VendorContentPageCreate,
|
||||
VendorContentPageUpdate,
|
||||
ContentPageResponse,
|
||||
CMSUsageResponse,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
|
||||
from models.database.user import User
|
||||
|
||||
vendor_service = VendorService()
|
||||
|
||||
vendor_content_pages_router = APIRouter(prefix="/content-pages")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VENDOR CONTENT PAGES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@vendor_content_pages_router.get("/", response_model=list[ContentPageResponse])
|
||||
def list_vendor_pages(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all content pages available for this vendor.
|
||||
|
||||
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
|
||||
"""
|
||||
pages = content_page_service.list_pages_for_vendor(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
|
||||
|
||||
@vendor_content_pages_router.get("/overrides", response_model=list[ContentPageResponse])
|
||||
def list_vendor_overrides(
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List only vendor-specific content pages (no platform defaults).
|
||||
|
||||
Shows what the vendor has customized.
|
||||
"""
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
|
||||
|
||||
@vendor_content_pages_router.get("/usage", response_model=CMSUsageResponse)
|
||||
def get_cms_usage(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get CMS usage statistics for the vendor.
|
||||
|
||||
Returns page counts and limits based on subscription tier.
|
||||
"""
|
||||
vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id)
|
||||
if not vendor:
|
||||
return CMSUsageResponse(
|
||||
total_pages=0,
|
||||
custom_pages=0,
|
||||
override_pages=0,
|
||||
pages_limit=3,
|
||||
custom_pages_limit=0,
|
||||
can_create_page=False,
|
||||
can_create_custom=False,
|
||||
usage_percent=0,
|
||||
custom_usage_percent=0,
|
||||
)
|
||||
|
||||
# Get vendor's pages
|
||||
vendor_pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=current_user.token_vendor_id, include_unpublished=True
|
||||
)
|
||||
|
||||
total_pages = len(vendor_pages)
|
||||
override_pages = sum(1 for p in vendor_pages if p.is_vendor_override)
|
||||
custom_pages = total_pages - override_pages
|
||||
|
||||
# Get limits from subscription tier
|
||||
pages_limit = None
|
||||
custom_pages_limit = None
|
||||
if vendor.subscription and vendor.subscription.tier:
|
||||
pages_limit = vendor.subscription.tier.cms_pages_limit
|
||||
custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit
|
||||
|
||||
# Calculate can_create flags
|
||||
can_create_page = pages_limit is None or total_pages < pages_limit
|
||||
can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit
|
||||
|
||||
# Calculate usage percentages
|
||||
usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100
|
||||
custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100
|
||||
|
||||
return CMSUsageResponse(
|
||||
total_pages=total_pages,
|
||||
custom_pages=custom_pages,
|
||||
override_pages=override_pages,
|
||||
pages_limit=pages_limit,
|
||||
custom_pages_limit=custom_pages_limit,
|
||||
can_create_page=can_create_page,
|
||||
can_create_custom=can_create_custom,
|
||||
usage_percent=usage_percent,
|
||||
custom_usage_percent=custom_usage_percent,
|
||||
)
|
||||
|
||||
|
||||
@vendor_content_pages_router.get("/platform-default/{slug}", response_model=ContentPageResponse)
|
||||
def get_platform_default(
|
||||
slug: str,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get the platform default content for a slug.
|
||||
|
||||
Useful for vendors to view the original before/after overriding.
|
||||
"""
|
||||
# Get vendor's platform
|
||||
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:
|
||||
platform_id = vendor.platforms[0].id
|
||||
|
||||
# Get platform default (vendor_id=None)
|
||||
page = content_page_service.get_vendor_default_page(
|
||||
db, platform_id=platform_id, slug=slug, include_unpublished=True
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(slug)
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@vendor_content_pages_router.get("/{slug}", response_model=ContentPageResponse)
|
||||
def get_page(
|
||||
slug: str,
|
||||
include_unpublished: bool = Query(False, description="Include draft pages"),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific content page by slug.
|
||||
|
||||
Returns vendor override if exists, otherwise platform default.
|
||||
"""
|
||||
page = content_page_service.get_page_for_vendor_or_raise(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@vendor_content_pages_router.post("/", response_model=ContentPageResponse, status_code=201)
|
||||
def create_vendor_page(
|
||||
page_data: VendorContentPageCreate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Create a vendor-specific content page override.
|
||||
|
||||
This will be shown instead of the platform default for this vendor.
|
||||
"""
|
||||
page = content_page_service.create_page(
|
||||
db,
|
||||
slug=page_data.slug,
|
||||
title=page_data.title,
|
||||
content=page_data.content,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
content_format=page_data.content_format,
|
||||
meta_description=page_data.meta_description,
|
||||
meta_keywords=page_data.meta_keywords,
|
||||
is_published=page_data.is_published,
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
show_in_legal=page_data.show_in_legal,
|
||||
display_order=page_data.display_order,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@vendor_content_pages_router.put("/{page_id}", response_model=ContentPageResponse)
|
||||
def update_vendor_page(
|
||||
page_id: int,
|
||||
page_data: VendorContentPageUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update a vendor-specific content page.
|
||||
|
||||
Can only update pages owned by this vendor.
|
||||
"""
|
||||
# Update with ownership check in service layer
|
||||
page = content_page_service.update_vendor_page(
|
||||
db,
|
||||
page_id=page_id,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
title=page_data.title,
|
||||
content=page_data.content,
|
||||
content_format=page_data.content_format,
|
||||
meta_description=page_data.meta_description,
|
||||
meta_keywords=page_data.meta_keywords,
|
||||
is_published=page_data.is_published,
|
||||
show_in_footer=page_data.show_in_footer,
|
||||
show_in_header=page_data.show_in_header,
|
||||
show_in_legal=page_data.show_in_legal,
|
||||
display_order=page_data.display_order,
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@vendor_content_pages_router.delete("/{page_id}", status_code=204)
|
||||
def delete_vendor_page(
|
||||
page_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete a vendor-specific content page.
|
||||
|
||||
Can only delete pages owned by this vendor.
|
||||
After deletion, platform default will be shown (if exists).
|
||||
"""
|
||||
# Delete with ownership check in service layer
|
||||
content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id)
|
||||
db.commit()
|
||||
304
app/modules/cms/routes/api/vendor_media.py
Normal file
304
app/modules/cms/routes/api/vendor_media.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# app/modules/cms/routes/api/vendor_media.py
|
||||
"""
|
||||
Vendor media and file management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Query, UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions.media import MediaOptimizationException
|
||||
from app.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.media import (
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
MediaMetadataUpdate,
|
||||
MediaUploadResponse,
|
||||
MediaUsageResponse,
|
||||
MultipleUploadResponse,
|
||||
OptimizationResultResponse,
|
||||
UploadedFileInfo,
|
||||
FailedFileInfo,
|
||||
)
|
||||
|
||||
vendor_media_router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@vendor_media_router.get("", response_model=MediaListResponse)
|
||||
def get_media_library(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
media_type: str | None = Query(None, description="image, video, document"),
|
||||
folder: str | None = Query(None, description="Filter by folder"),
|
||||
search: str | None = Query(None),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get vendor media library.
|
||||
|
||||
- Get all media files for vendor
|
||||
- Filter by type (image, video, document)
|
||||
- Filter by folder
|
||||
- Search by filename
|
||||
- Support pagination
|
||||
"""
|
||||
media_files, total = media_service.get_media_library(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
media_type=media_type,
|
||||
folder=folder,
|
||||
search=search,
|
||||
)
|
||||
|
||||
return MediaListResponse(
|
||||
media=[MediaItemResponse.model_validate(m) for m in media_files],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@vendor_media_router.post("/upload", response_model=MediaUploadResponse)
|
||||
async def upload_media(
|
||||
file: UploadFile = File(...),
|
||||
folder: str | None = Query("general", description="products, general, etc."),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload media file.
|
||||
|
||||
- Accept file upload
|
||||
- Validate file type and size
|
||||
- Store file in vendor-specific directory
|
||||
- Generate thumbnails for images
|
||||
- Save metadata to database
|
||||
- Return file URL
|
||||
"""
|
||||
# Read file content
|
||||
file_content = await file.read()
|
||||
|
||||
# Upload using service (exceptions will propagate to handler)
|
||||
media_file = await media_service.upload_file(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
file_content=file_content,
|
||||
filename=file.filename or "unnamed",
|
||||
folder=folder or "general",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return MediaUploadResponse(
|
||||
id=media_file.id,
|
||||
file_url=media_file.file_url,
|
||||
thumbnail_url=media_file.thumbnail_url,
|
||||
filename=media_file.original_filename,
|
||||
file_size=media_file.file_size,
|
||||
media_type=media_file.media_type,
|
||||
message="File uploaded successfully",
|
||||
)
|
||||
|
||||
|
||||
@vendor_media_router.post("/upload/multiple", response_model=MultipleUploadResponse)
|
||||
async def upload_multiple_media(
|
||||
files: list[UploadFile] = File(...),
|
||||
folder: str | None = Query("general"),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload multiple media files at once.
|
||||
|
||||
- Accept multiple files
|
||||
- Process each file
|
||||
- Return list of uploaded file URLs
|
||||
- Handle errors gracefully
|
||||
"""
|
||||
uploaded = []
|
||||
failed = []
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
file_content = await file.read()
|
||||
|
||||
media_file = await media_service.upload_file(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
file_content=file_content,
|
||||
filename=file.filename or "unnamed",
|
||||
folder=folder or "general",
|
||||
)
|
||||
|
||||
uploaded.append(UploadedFileInfo(
|
||||
id=media_file.id,
|
||||
filename=media_file.original_filename or media_file.filename,
|
||||
file_url=media_file.file_url,
|
||||
thumbnail_url=media_file.thumbnail_url,
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to upload {file.filename}: {e}")
|
||||
failed.append(FailedFileInfo(
|
||||
filename=file.filename or "unnamed",
|
||||
error=str(e),
|
||||
))
|
||||
|
||||
db.commit()
|
||||
|
||||
return MultipleUploadResponse(
|
||||
uploaded_files=uploaded,
|
||||
failed_files=failed,
|
||||
total_uploaded=len(uploaded),
|
||||
total_failed=len(failed),
|
||||
message=f"Uploaded {len(uploaded)} files, {len(failed)} failed",
|
||||
)
|
||||
|
||||
|
||||
@vendor_media_router.get("/{media_id}", response_model=MediaDetailResponse)
|
||||
def get_media_details(
|
||||
media_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get media file details.
|
||||
|
||||
- Get file metadata
|
||||
- Return file URL
|
||||
- Return basic info
|
||||
"""
|
||||
# Service will raise MediaNotFoundException if not found
|
||||
media = media_service.get_media(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
media_id=media_id,
|
||||
)
|
||||
|
||||
return MediaDetailResponse.model_validate(media)
|
||||
|
||||
|
||||
@vendor_media_router.put("/{media_id}", response_model=MediaDetailResponse)
|
||||
def update_media_metadata(
|
||||
media_id: int,
|
||||
metadata: MediaMetadataUpdate,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Update media file metadata.
|
||||
|
||||
- Update filename
|
||||
- Update alt text
|
||||
- Update description
|
||||
- Move to different folder
|
||||
"""
|
||||
# Service will raise MediaNotFoundException if not found
|
||||
media = media_service.update_media_metadata(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
media_id=media_id,
|
||||
filename=metadata.filename,
|
||||
alt_text=metadata.alt_text,
|
||||
description=metadata.description,
|
||||
folder=metadata.folder,
|
||||
metadata=metadata.metadata,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return MediaDetailResponse.model_validate(media)
|
||||
|
||||
|
||||
@vendor_media_router.delete("/{media_id}", response_model=MediaDetailResponse)
|
||||
def delete_media(
|
||||
media_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete media file.
|
||||
|
||||
- Verify file belongs to vendor
|
||||
- Delete file from storage
|
||||
- Delete database record
|
||||
- Return success/error
|
||||
"""
|
||||
# Service will raise MediaNotFoundException if not found
|
||||
media_service.delete_media(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
media_id=media_id,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return MediaDetailResponse(message="Media file deleted successfully")
|
||||
|
||||
|
||||
@vendor_media_router.get("/{media_id}/usage", response_model=MediaUsageResponse)
|
||||
def get_media_usage(
|
||||
media_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get where this media file is being used.
|
||||
|
||||
- Check products using this media
|
||||
- Return list of usage
|
||||
"""
|
||||
# Service will raise MediaNotFoundException if not found
|
||||
usage = media_service.get_media_usage(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
media_id=media_id,
|
||||
)
|
||||
|
||||
return MediaUsageResponse(**usage)
|
||||
|
||||
|
||||
@vendor_media_router.post("/optimize/{media_id}", response_model=OptimizationResultResponse)
|
||||
def optimize_media(
|
||||
media_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Optimize media file (compress, resize, etc.).
|
||||
|
||||
Note: Image optimization requires PIL/Pillow to be installed.
|
||||
"""
|
||||
# Service will raise MediaNotFoundException if not found
|
||||
media = media_service.get_media(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
media_id=media_id,
|
||||
)
|
||||
|
||||
if media.media_type != "image":
|
||||
raise MediaOptimizationException("Only images can be optimized")
|
||||
|
||||
# For now, return current state - optimization is done on upload
|
||||
return OptimizationResultResponse(
|
||||
media_id=media_id,
|
||||
original_size=media.file_size,
|
||||
optimized_size=media.optimized_size or media.file_size,
|
||||
savings_percent=0.0 if not media.optimized_size else
|
||||
round((1 - media.optimized_size / media.file_size) * 100, 1),
|
||||
optimized_url=media.file_url,
|
||||
message="Image optimization applied on upload" if media.is_optimized
|
||||
else "Image not yet optimized",
|
||||
)
|
||||
@@ -2,38 +2,15 @@
|
||||
"""
|
||||
CMS module vendor routes.
|
||||
|
||||
This module wraps the existing vendor content pages and media routes
|
||||
and adds module-based access control. Routes are re-exported from the
|
||||
original location with the module access dependency.
|
||||
Re-exports routes from the API routes for backwards compatibility
|
||||
with the lazy router attachment pattern.
|
||||
|
||||
Includes:
|
||||
- /content-pages/* - Content page management
|
||||
- /media/* - Media library
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
# Re-export vendor_router from API routes
|
||||
from app.modules.cms.routes.api.vendor import vendor_router
|
||||
|
||||
from app.api.deps import require_module_access
|
||||
|
||||
# Import original routers (direct import to avoid circular dependency)
|
||||
from app.api.v1.vendor.content_pages import router as content_original_router
|
||||
from app.api.v1.vendor.media import router as media_original_router
|
||||
|
||||
# Create module-aware router for content pages
|
||||
vendor_router = APIRouter(
|
||||
prefix="/content-pages",
|
||||
dependencies=[Depends(require_module_access("cms"))],
|
||||
)
|
||||
|
||||
# Re-export all routes from the original content pages module
|
||||
for route in content_original_router.routes:
|
||||
vendor_router.routes.append(route)
|
||||
|
||||
# Create separate router for media library
|
||||
vendor_media_router = APIRouter(
|
||||
prefix="/media",
|
||||
dependencies=[Depends(require_module_access("cms"))],
|
||||
)
|
||||
|
||||
for route in media_original_router.routes:
|
||||
vendor_media_router.routes.append(route)
|
||||
__all__ = ["vendor_router"]
|
||||
|
||||
Reference in New Issue
Block a user