Add super-admin-only backend endpoints for toggling is_gallery_featured and gallery_sort_order on flows and scripts, plus a frontend GalleryManagementPage with toggle switches, editable sort order fields, and name/featured filters. 13 integration tests; all pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
"""Admin gallery curation endpoints.
|
|
|
|
Allows super admins to toggle is_gallery_featured and update gallery_sort_order
|
|
on Tree (flows) and ScriptTemplate (scripts) records.
|
|
"""
|
|
from typing import Annotated
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import require_admin
|
|
from app.core.database import get_db
|
|
from app.models.script_template import ScriptTemplate
|
|
from app.models.tree import Tree
|
|
from app.models.user import User
|
|
|
|
router = APIRouter(prefix="/admin/gallery", tags=["admin-gallery"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class FeatureToggle(BaseModel):
|
|
is_gallery_featured: bool
|
|
|
|
|
|
class SortOrderUpdate(BaseModel):
|
|
gallery_sort_order: int
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Response helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _flow_summary(tree: Tree) -> dict:
|
|
return {
|
|
"id": str(tree.id),
|
|
"name": tree.name,
|
|
"tree_type": tree.tree_type,
|
|
"is_gallery_featured": tree.is_gallery_featured,
|
|
"gallery_sort_order": tree.gallery_sort_order,
|
|
"visibility": tree.visibility,
|
|
}
|
|
|
|
|
|
def _script_summary(script: ScriptTemplate) -> dict:
|
|
return {
|
|
"id": str(script.id),
|
|
"name": script.name,
|
|
"is_gallery_featured": script.is_gallery_featured,
|
|
"gallery_sort_order": script.gallery_sort_order,
|
|
"is_active": script.is_active,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@router.get("/featured")
|
|
async def list_featured(
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""List all featured flows and scripts (super admin only)."""
|
|
flows_result = await db.execute(
|
|
select(Tree)
|
|
.where(Tree.is_gallery_featured == True) # noqa: E712
|
|
.order_by(Tree.gallery_sort_order.asc(), Tree.name.asc())
|
|
)
|
|
flows = flows_result.scalars().all()
|
|
|
|
scripts_result = await db.execute(
|
|
select(ScriptTemplate)
|
|
.where(ScriptTemplate.is_gallery_featured == True) # noqa: E712
|
|
.order_by(ScriptTemplate.gallery_sort_order.asc(), ScriptTemplate.name.asc())
|
|
)
|
|
scripts = scripts_result.scalars().all()
|
|
|
|
return {
|
|
"flows": [_flow_summary(f) for f in flows],
|
|
"scripts": [_script_summary(s) for s in scripts],
|
|
}
|
|
|
|
|
|
@router.get("/items")
|
|
async def list_all_items(
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""List ALL flows and scripts with their gallery status (super admin only)."""
|
|
flows_result = await db.execute(
|
|
select(Tree)
|
|
.where(Tree.visibility == "public")
|
|
.order_by(Tree.gallery_sort_order.asc(), Tree.name.asc())
|
|
)
|
|
flows = flows_result.scalars().all()
|
|
|
|
scripts_result = await db.execute(
|
|
select(ScriptTemplate)
|
|
.order_by(ScriptTemplate.gallery_sort_order.asc(), ScriptTemplate.name.asc())
|
|
)
|
|
scripts = scripts_result.scalars().all()
|
|
|
|
return {
|
|
"flows": [_flow_summary(f) for f in flows],
|
|
"scripts": [_script_summary(s) for s in scripts],
|
|
}
|
|
|
|
|
|
@router.patch("/flows/{flow_id}/feature")
|
|
async def toggle_flow_featured(
|
|
flow_id: UUID,
|
|
body: FeatureToggle,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""Toggle is_gallery_featured on a flow (super admin only)."""
|
|
result = await db.execute(select(Tree).where(Tree.id == flow_id))
|
|
tree = result.scalar_one_or_none()
|
|
if not tree:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Flow not found")
|
|
|
|
tree.is_gallery_featured = body.is_gallery_featured
|
|
await db.commit()
|
|
await db.refresh(tree)
|
|
return _flow_summary(tree)
|
|
|
|
|
|
@router.patch("/flows/{flow_id}/sort-order")
|
|
async def update_flow_sort_order(
|
|
flow_id: UUID,
|
|
body: SortOrderUpdate,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""Update gallery_sort_order on a flow (super admin only)."""
|
|
result = await db.execute(select(Tree).where(Tree.id == flow_id))
|
|
tree = result.scalar_one_or_none()
|
|
if not tree:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Flow not found")
|
|
|
|
tree.gallery_sort_order = body.gallery_sort_order
|
|
await db.commit()
|
|
await db.refresh(tree)
|
|
return _flow_summary(tree)
|
|
|
|
|
|
@router.patch("/scripts/{script_id}/feature")
|
|
async def toggle_script_featured(
|
|
script_id: UUID,
|
|
body: FeatureToggle,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""Toggle is_gallery_featured on a script (super admin only)."""
|
|
result = await db.execute(select(ScriptTemplate).where(ScriptTemplate.id == script_id))
|
|
script = result.scalar_one_or_none()
|
|
if not script:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
|
|
|
|
script.is_gallery_featured = body.is_gallery_featured
|
|
await db.commit()
|
|
await db.refresh(script)
|
|
return _script_summary(script)
|
|
|
|
|
|
@router.patch("/scripts/{script_id}/sort-order")
|
|
async def update_script_sort_order(
|
|
script_id: UUID,
|
|
body: SortOrderUpdate,
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
current_user: Annotated[User, Depends(require_admin)],
|
|
):
|
|
"""Update gallery_sort_order on a script (super admin only)."""
|
|
result = await db.execute(select(ScriptTemplate).where(ScriptTemplate.id == script_id))
|
|
script = result.scalar_one_or_none()
|
|
if not script:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
|
|
|
|
script.gallery_sort_order = body.gallery_sort_order
|
|
await db.commit()
|
|
await db.refresh(script)
|
|
return _script_summary(script)
|