Files
2026-07-13 13:08:41 +08:00

395 lines
13 KiB
Python

from datetime import datetime
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
from models.sql.template import TemplateModel
from services.database import get_async_session
LAYOUT_MANAGEMENT_ROUTER = APIRouter(
prefix="/template-management", tags=["template-management"]
)
class LayoutData(BaseModel):
presentation: UUID
layout_id: str
layout_name: str
layout_code: str
fonts: Optional[List[str]] = None
class SaveLayoutsRequest(BaseModel):
layouts: list[LayoutData]
class SaveLayoutsResponse(BaseModel):
success: bool
saved_count: int
message: Optional[str] = None
class GetLayoutsResponse(BaseModel):
success: bool
layouts: list[LayoutData]
message: Optional[str] = None
template: Optional[dict] = None
fonts: Optional[List[str]] = None
class PresentationSummary(BaseModel):
presentation_id: UUID
layout_count: int
last_updated_at: Optional[datetime] = None
template: Optional[dict] = None
class GetPresentationSummaryResponse(BaseModel):
success: bool
presentations: List[PresentationSummary]
total_presentations: int
total_layouts: int
message: Optional[str] = None
class ErrorResponse(BaseModel):
success: bool = False
detail: str
error_code: Optional[str] = None
class TemplateCreateRequest(BaseModel):
id: UUID
name: str
description: Optional[str] = None
class TemplateCreateResponse(BaseModel):
success: bool
template: dict
message: Optional[str] = None
@LAYOUT_MANAGEMENT_ROUTER.post(
"/save-templates",
response_model=SaveLayoutsResponse,
responses={
400: {"model": ErrorResponse, "description": "Validation error"},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def save_layouts(
request: SaveLayoutsRequest, session: AsyncSession = Depends(get_async_session)
):
"""
Save multiple layouts for presentations.
Args:
request: JSON request containing array of layout data
session: Database session
Returns:
SaveLayoutsResponse with success status and count of saved layouts
Raises:
HTTPException: 400 for validation errors, 500 for server errors
"""
try:
if not request.layouts:
raise HTTPException(status_code=400, detail="Layouts array cannot be empty")
if len(request.layouts) > 50:
raise HTTPException(
status_code=400, detail="Cannot save more than 50 layouts at once"
)
saved_count = 0
for i, layout_data in enumerate(request.layouts):
if (
not layout_data.presentation
or not str(layout_data.presentation).strip()
):
raise HTTPException(
status_code=400,
detail=f"Layout {i+1}: presentation_id cannot be empty",
)
if not layout_data.layout_id or not layout_data.layout_id.strip():
raise HTTPException(
status_code=400, detail=f"Layout {i+1}: layout_id cannot be empty"
)
if not layout_data.layout_name or not layout_data.layout_name.strip():
raise HTTPException(
status_code=400, detail=f"Layout {i+1}: layout_name cannot be empty"
)
if not layout_data.layout_code or not layout_data.layout_code.strip():
raise HTTPException(
status_code=400, detail=f"Layout {i+1}: layout_code cannot be empty"
)
stmt = select(PresentationLayoutCodeModel).where(
PresentationLayoutCodeModel.presentation == layout_data.presentation,
PresentationLayoutCodeModel.layout_id == layout_data.layout_id,
)
result = await session.execute(stmt)
existing_layout = result.scalar_one_or_none()
if existing_layout:
existing_layout.layout_name = layout_data.layout_name
existing_layout.layout_code = layout_data.layout_code
existing_layout.fonts = layout_data.fonts
existing_layout.updated_at = datetime.now()
else:
new_layout = PresentationLayoutCodeModel(
presentation=layout_data.presentation,
layout_id=layout_data.layout_id,
layout_name=layout_data.layout_name,
layout_code=layout_data.layout_code,
fonts=layout_data.fonts,
)
session.add(new_layout)
saved_count += 1
await session.commit()
return SaveLayoutsResponse(
success=True,
saved_count=saved_count,
message=f"Successfully saved {saved_count} layout(s)",
)
except HTTPException:
await session.rollback()
raise
except Exception as e:
await session.rollback()
print(f"Unexpected error saving layouts: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Internal server error while saving layouts: {str(e)}",
)
@LAYOUT_MANAGEMENT_ROUTER.get(
"/get-templates/{presentation}",
response_model=GetLayoutsResponse,
responses={
400: {"model": ErrorResponse, "description": "Invalid presentation ID"},
404: {
"model": ErrorResponse,
"description": "No layouts found for presentation",
},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def get_layouts(
presentation: UUID, session: AsyncSession = Depends(get_async_session)
):
"""
Retrieve all layouts for a specific presentation.
"""
try:
if not presentation or len(str(presentation).strip()) == 0:
raise HTTPException(
status_code=400, detail="Presentation ID cannot be empty"
)
stmt = select(PresentationLayoutCodeModel).where(
PresentationLayoutCodeModel.presentation == presentation
)
result = await session.execute(stmt)
layouts_db = result.scalars().all()
if not layouts_db:
raise HTTPException(
status_code=404,
detail=f"No layouts found for presentation ID: {presentation}",
)
layouts = [
LayoutData(
presentation=layout.presentation,
layout_id=layout.layout_id,
layout_name=layout.layout_name,
layout_code=layout.layout_code,
fonts=layout.fonts,
)
for layout in layouts_db
]
aggregated_fonts: set[str] = set()
for layout in layouts_db:
if layout.fonts:
aggregated_fonts.update([f for f in layout.fonts if isinstance(f, str)])
fonts_list = sorted(list(aggregated_fonts)) if aggregated_fonts else None
template_meta = await session.get(TemplateModel, presentation)
template = None
if template_meta:
template = {
"id": template_meta.id,
"name": template_meta.name,
"description": template_meta.description,
"created_at": template_meta.created_at,
}
return GetLayoutsResponse(
success=True,
layouts=layouts,
message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation}",
template=template,
fonts=fonts_list,
)
except HTTPException:
raise
except Exception as e:
print(f"Error retrieving layouts for presentation {presentation}: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Internal server error while retrieving layouts: {str(e)}",
)
@LAYOUT_MANAGEMENT_ROUTER.get(
"/summary",
response_model=GetPresentationSummaryResponse,
summary="Get all presentations with layout counts",
description="Retrieve a summary of all presentations and the number of layouts in each",
responses={
200: {
"model": GetPresentationSummaryResponse,
"description": "Presentations summary retrieved successfully",
},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def get_presentations_summary(
session: AsyncSession = Depends(get_async_session),
):
"""Get summary of all presentations with their layout counts."""
try:
stmt = select(
PresentationLayoutCodeModel.presentation,
func.count(PresentationLayoutCodeModel.id).label("layout_count"),
func.max(PresentationLayoutCodeModel.updated_at).label("last_updated_at"),
).group_by(PresentationLayoutCodeModel.presentation)
result = await session.execute(stmt)
presentation_data = result.all()
presentations = []
for row in presentation_data:
template_meta = await session.get(TemplateModel, row.presentation)
template = None
if template_meta:
template = {
"id": template_meta.id,
"name": template_meta.name,
"description": template_meta.description,
"created_at": template_meta.created_at,
}
presentations.append(
PresentationSummary(
presentation_id=row.presentation,
layout_count=row.layout_count,
last_updated_at=row.last_updated_at,
template=template,
)
)
total_presentations = len(presentations)
total_layouts = sum(p.layout_count for p in presentations)
return GetPresentationSummaryResponse(
success=True,
presentations=presentations,
total_presentations=total_presentations,
total_layouts=total_layouts,
message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)",
)
except Exception as e:
print(f"Error retrieving presentations summary: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Internal server error while retrieving presentations summary: {str(e)}",
)
@LAYOUT_MANAGEMENT_ROUTER.post(
"/templates",
response_model=TemplateCreateResponse,
responses={
400: {"model": ErrorResponse, "description": "Validation error"},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def create_template(
request: TemplateCreateRequest,
session: AsyncSession = Depends(get_async_session),
):
try:
if not request.id or not request.name:
raise HTTPException(status_code=400, detail="id and name are required")
existing = await session.get(TemplateModel, request.id)
if existing:
existing.name = request.name
existing.description = request.description
else:
session.add(
TemplateModel(
id=request.id, name=request.name, description=request.description
)
)
await session.commit()
template = await session.get(TemplateModel, request.id)
return TemplateCreateResponse(
success=True,
template={
"id": template.id,
"name": template.name,
"description": template.description,
"created_at": template.created_at,
},
message="Template saved",
)
except HTTPException:
await session.rollback()
raise
except Exception as e:
await session.rollback()
raise HTTPException(
status_code=500, detail=f"Failed to save template: {str(e)}"
)
@LAYOUT_MANAGEMENT_ROUTER.delete("/delete-templates/{template_id}", status_code=204)
async def delete_template(
template_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
try:
await session.execute(
delete(TemplateModel).where(TemplateModel.id == template_id)
)
await session.execute(
delete(PresentationLayoutCodeModel).where(
PresentationLayoutCodeModel.presentation == template_id,
)
)
await session.commit()
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete template")