templates: support include_total for search and categories endpoints; update docs; add tests

This commit is contained in:
HotSwapp
2025-08-15 15:06:45 -05:00
parent e3a279dba7
commit 006ef3d7b1
3 changed files with 43 additions and 6 deletions

View File

@@ -11,7 +11,7 @@ Endpoints:
"""
from __future__ import annotations
from typing import List, Optional, Dict, Any
from typing import List, Optional, Dict, Any, Union
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, or_, exists
@@ -23,6 +23,7 @@ from app.models.user import User
from app.models.templates import DocumentTemplate, DocumentTemplateVersion, TemplateKeyword
from app.services.storage import get_default_storage
from app.services.template_merge import extract_tokens_from_bytes, build_context, resolve_tokens, render_docx
from app.services.query_utils import paginate_with_total
router = APIRouter()
@@ -84,6 +85,16 @@ class CategoryCount(BaseModel):
count: int
class PaginatedSearchResponse(BaseModel):
items: List[SearchResponseItem]
total: int
class PaginatedCategoriesResponse(BaseModel):
items: List[CategoryCount]
total: int
@router.post("/upload", response_model=TemplateResponse)
async def upload_template(
name: str = Form(...),
@@ -136,7 +147,7 @@ async def upload_template(
)
@router.get("/search", response_model=List[SearchResponseItem])
@router.get("/search", response_model=Union[List[SearchResponseItem], PaginatedSearchResponse])
async def search_templates(
q: Optional[str] = None,
category: Optional[List[str]] = Query(
@@ -160,6 +171,7 @@ async def search_templates(
sort_by: Optional[str] = Query("name", description="Sort by: name | category | updated"),
sort_dir: Optional[str] = Query("asc", description="Sort direction: asc or desc"),
active_only: bool = Query(True, description="When true (default), only active templates are returned"),
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -227,8 +239,8 @@ async def search_templates(
else:
query = query.order_by(order_col.desc())
# Pagination
templates = query.offset(skip).limit(limit).all()
# Pagination with optional total
templates, total = paginate_with_total(query, skip, limit, include_total)
items: List[SearchResponseItem] = []
for tpl in templates:
latest_version = None
@@ -245,12 +257,15 @@ async def search_templates(
latest_version=latest_version,
)
)
if include_total:
return {"items": items, "total": int(total or 0)}
return items
@router.get("/categories", response_model=List[CategoryCount])
@router.get("/categories", response_model=Union[List[CategoryCount], PaginatedCategoriesResponse])
async def list_template_categories(
active_only: bool = Query(True, description="When true (default), only active templates are counted"),
include_total: bool = Query(False, description="When true, returns {items, total} instead of a plain list"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -258,7 +273,10 @@ async def list_template_categories(
if active_only:
query = query.filter(DocumentTemplate.active == True)
rows = query.group_by(DocumentTemplate.category).order_by(DocumentTemplate.category.asc()).all()
return [CategoryCount(category=row[0], count=row[1]) for row in rows]
items = [CategoryCount(category=row[0], count=row[1]) for row in rows]
if include_total:
return {"items": items, "total": len(items)}
return items
@router.get("/{template_id}", response_model=TemplateResponse)