- Updated docker-compose.yml to allow disabling embedded Ollama via environment variable. - Refactored Dockerfile and Dockerfile.dev for improved dependency management and installation process. - Enhanced FastAPI migration scripts to handle orphaned Alembic revisions and added new database migration logic. - Improved error handling in background tasks and Codex authentication endpoints. - Added support for font file uploads with better validation and extraction of font names. - Introduced new image search functionality with support for Pexels and Pixabay APIs.
181 lines
5.5 KiB
Python
181 lines
5.5 KiB
Python
import copy
|
|
import uuid
|
|
from typing import Any, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import select
|
|
|
|
from models.sql.image_asset import ImageAsset
|
|
from models.sql.key_value import KeyValueSqlModel
|
|
from services.database import get_async_session
|
|
|
|
THEMES_ROUTER = APIRouter(prefix="/themes", tags=["Themes"])
|
|
THEMES_STORAGE_KEY = "presentation_custom_themes"
|
|
|
|
|
|
class ThemeRequest(BaseModel):
|
|
name: str
|
|
description: str
|
|
company_name: Optional[str] = None
|
|
logo: Optional[str] = None
|
|
logo_url: Optional[str] = None
|
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class ThemeUpdateRequest(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
company_name: Optional[str] = None
|
|
logo: Optional[str] = None
|
|
logo_url: Optional[str] = None
|
|
data: Optional[dict[str, Any]] = None
|
|
|
|
|
|
class ThemeResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str
|
|
user: str
|
|
logo: Optional[str] = None
|
|
logo_url: Optional[str] = None
|
|
company_name: Optional[str] = None
|
|
data: dict[str, Any]
|
|
|
|
|
|
def _normalize_theme(theme: dict[str, Any]) -> ThemeResponse:
|
|
return ThemeResponse(
|
|
id=str(theme["id"]),
|
|
name=theme["name"],
|
|
description=theme["description"],
|
|
user=theme.get("user", "local"),
|
|
logo=theme.get("logo"),
|
|
logo_url=theme.get("logo_url"),
|
|
company_name=theme.get("company_name"),
|
|
data=theme.get("data", {}),
|
|
)
|
|
|
|
|
|
async def _get_themes_row(sql_session: AsyncSession) -> Optional[KeyValueSqlModel]:
|
|
return await sql_session.scalar(
|
|
select(KeyValueSqlModel).where(KeyValueSqlModel.key == THEMES_STORAGE_KEY)
|
|
)
|
|
|
|
|
|
def _read_themes_from_row(row: Optional[KeyValueSqlModel]) -> list[dict[str, Any]]:
|
|
if not row:
|
|
return []
|
|
value = row.value if isinstance(row.value, dict) else {}
|
|
themes = value.get("themes", [])
|
|
if not isinstance(themes, list):
|
|
return []
|
|
return copy.deepcopy(themes)
|
|
|
|
|
|
async def _resolve_logo_url(
|
|
sql_session: AsyncSession, logo: Optional[str]
|
|
) -> Optional[str]:
|
|
if not logo:
|
|
return None
|
|
try:
|
|
logo_uuid = uuid.UUID(str(logo))
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail="Invalid logo id") from exc
|
|
|
|
image_asset = await sql_session.get(ImageAsset, logo_uuid)
|
|
if not image_asset:
|
|
raise HTTPException(status_code=404, detail="Logo not found")
|
|
return image_asset.path
|
|
|
|
|
|
@THEMES_ROUTER.get("/default", response_model=List[dict[str, Any]])
|
|
async def get_default_themes():
|
|
# Built-in themes are provided by Next.js constants in this project.
|
|
return []
|
|
|
|
|
|
@THEMES_ROUTER.get("/all", response_model=List[ThemeResponse])
|
|
async def get_themes(sql_session: AsyncSession = Depends(get_async_session)):
|
|
row = await _get_themes_row(sql_session)
|
|
themes = _read_themes_from_row(row)
|
|
return [_normalize_theme(theme) for theme in themes]
|
|
|
|
|
|
@THEMES_ROUTER.post("/create", response_model=ThemeResponse)
|
|
async def create_theme(
|
|
payload: ThemeRequest, sql_session: AsyncSession = Depends(get_async_session)
|
|
):
|
|
row = await _get_themes_row(sql_session)
|
|
themes = _read_themes_from_row(row)
|
|
logo_url = payload.logo_url or await _resolve_logo_url(sql_session, payload.logo)
|
|
|
|
theme = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": payload.name,
|
|
"description": payload.description,
|
|
"user": "local",
|
|
"logo": payload.logo,
|
|
"logo_url": logo_url,
|
|
"company_name": payload.company_name,
|
|
"data": payload.data,
|
|
}
|
|
themes.append(theme)
|
|
|
|
if row:
|
|
row.value = {"themes": themes}
|
|
sql_session.add(row)
|
|
else:
|
|
sql_session.add(KeyValueSqlModel(key=THEMES_STORAGE_KEY, value={"themes": themes}))
|
|
|
|
await sql_session.commit()
|
|
return _normalize_theme(theme)
|
|
|
|
|
|
@THEMES_ROUTER.patch("/update/{theme_id}", response_model=ThemeResponse)
|
|
async def update_theme(
|
|
theme_id: str,
|
|
payload: ThemeUpdateRequest,
|
|
sql_session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
row = await _get_themes_row(sql_session)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Theme not found")
|
|
|
|
themes = _read_themes_from_row(row)
|
|
theme = next((item for item in themes if item.get("id") == theme_id), None)
|
|
if not theme:
|
|
raise HTTPException(status_code=404, detail="Theme not found")
|
|
|
|
if payload.name is not None:
|
|
theme["name"] = payload.name
|
|
if payload.description is not None:
|
|
theme["description"] = payload.description
|
|
if payload.company_name is not None:
|
|
theme["company_name"] = payload.company_name
|
|
if payload.data is not None:
|
|
theme["data"] = payload.data
|
|
if payload.logo is not None:
|
|
theme["logo"] = payload.logo
|
|
theme["logo_url"] = await _resolve_logo_url(sql_session, payload.logo)
|
|
elif payload.logo_url is not None:
|
|
theme["logo_url"] = payload.logo_url
|
|
|
|
row.value = {"themes": themes}
|
|
sql_session.add(row)
|
|
await sql_session.commit()
|
|
return _normalize_theme(theme)
|
|
|
|
|
|
@THEMES_ROUTER.delete("/delete/{theme_id}", status_code=204)
|
|
async def delete_theme(
|
|
theme_id: str, sql_session: AsyncSession = Depends(get_async_session)
|
|
):
|
|
row = await _get_themes_row(sql_session)
|
|
if not row:
|
|
return
|
|
|
|
themes = _read_themes_from_row(row)
|
|
row.value = {"themes": [theme for theme in themes if theme.get("id") != theme_id]}
|
|
sql_session.add(row)
|
|
await sql_session.commit()
|