Files
resolutionflow/backend/app/api/endpoints/admin_gallery.py
chihlasm 12a373a2a2 feat(gallery): add admin gallery curation endpoints and management page (Task 6)
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>
2026-03-19 20:04:40 +00:00

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)