feat: add media library picker for product images
- Add admin media API endpoints for vendor media management - Create reusable media_picker_modal macro in modals.html - Create mediaPickerMixin Alpine.js helper for media selection - Update product create/edit forms with media picker UI - Support main image + additional images selection - Add upload functionality within the picker modal - Update vendor_product_service to handle additional_images - Add additional_images field to Pydantic schemas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ from . import (
|
||||
letzshop,
|
||||
logs,
|
||||
marketplace,
|
||||
media,
|
||||
messages,
|
||||
monitoring,
|
||||
notifications,
|
||||
@@ -173,6 +174,9 @@ router.include_router(logs.router, tags=["admin-logs"])
|
||||
# Include image management endpoints
|
||||
router.include_router(images.router, tags=["admin-images"])
|
||||
|
||||
# Include media library management endpoints
|
||||
router.include_router(media.router, tags=["admin-media"])
|
||||
|
||||
# Include platform health endpoints
|
||||
router.include_router(
|
||||
platform_health.router, prefix="/platform", tags=["admin-platform-health"]
|
||||
|
||||
138
app/api/v1/admin/media.py
Normal file
138
app/api/v1/admin/media.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# app/api/v1/admin/media.py
|
||||
"""
|
||||
Admin media management endpoints for vendor media libraries.
|
||||
|
||||
Allows admins to manage media files on behalf of vendors.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Query, UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.media_service import media_service
|
||||
from models.database.user import User
|
||||
from models.schema.media import (
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
MediaUploadResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/vendors/{vendor_id}", response_model=MediaListResponse)
|
||||
def get_vendor_media_library(
|
||||
vendor_id: int,
|
||||
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_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get media library for a specific vendor.
|
||||
|
||||
Admin can browse any vendor's media library.
|
||||
"""
|
||||
media_files, total = media_service.get_media_library(
|
||||
db=db,
|
||||
vendor_id=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,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vendors/{vendor_id}/upload", response_model=MediaUploadResponse)
|
||||
async def upload_vendor_media(
|
||||
vendor_id: int,
|
||||
file: UploadFile = File(...),
|
||||
folder: str | None = Query("products", description="products, general, etc."),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload media file for a specific vendor.
|
||||
|
||||
Admin can upload media on behalf of any vendor.
|
||||
Files are stored in vendor-specific directories.
|
||||
"""
|
||||
# Read file content
|
||||
file_content = await file.read()
|
||||
|
||||
# Upload using service
|
||||
media_file = await media_service.upload_file(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
file_content=file_content,
|
||||
filename=file.filename or "unnamed",
|
||||
folder=folder or "products",
|
||||
)
|
||||
|
||||
logger.info(f"Admin uploaded media for vendor {vendor_id}: {media_file.id}")
|
||||
|
||||
return MediaUploadResponse(
|
||||
success=True,
|
||||
message="File uploaded successfully",
|
||||
media=MediaItemResponse.model_validate(media_file),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vendors/{vendor_id}/{media_id}", response_model=MediaDetailResponse)
|
||||
def get_vendor_media_detail(
|
||||
vendor_id: int,
|
||||
media_id: int,
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed info about a specific media file.
|
||||
"""
|
||||
media_file = media_service.get_media_by_id(db=db, media_id=media_id)
|
||||
|
||||
# Verify media belongs to the specified vendor
|
||||
if media_file.vendor_id != vendor_id:
|
||||
from app.exceptions.media import MediaNotFoundException
|
||||
raise MediaNotFoundException(media_id)
|
||||
|
||||
return MediaDetailResponse.model_validate(media_file)
|
||||
|
||||
|
||||
@router.delete("/vendors/{vendor_id}/{media_id}")
|
||||
def delete_vendor_media(
|
||||
vendor_id: int,
|
||||
media_id: int,
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Delete a media file for a vendor.
|
||||
"""
|
||||
media_file = media_service.get_media_by_id(db=db, media_id=media_id)
|
||||
|
||||
# Verify media belongs to the specified vendor
|
||||
if media_file.vendor_id != vendor_id:
|
||||
from app.exceptions.media import MediaNotFoundException
|
||||
raise MediaNotFoundException(media_id)
|
||||
|
||||
media_service.delete_media(db=db, media_id=media_id)
|
||||
|
||||
logger.info(f"Admin deleted media {media_id} for vendor {vendor_id}")
|
||||
|
||||
return {"success": True, "message": "Media deleted successfully"}
|
||||
Reference in New Issue
Block a user