feat: Add pagination, search, and filter functionality to user retrieval endpoint

This commit is contained in:
JSC
2025-08-17 11:44:15 +02:00
parent 99c757a073
commit e6f796a3c9
2 changed files with 130 additions and 9 deletions

View File

@@ -1,8 +1,8 @@
"""Admin users endpoints."""
from typing import Annotated
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.database import get_db
@@ -10,7 +10,7 @@ from app.core.dependencies import get_admin_user
from app.models.plan import Plan
from app.models.user import User
from app.repositories.plan import PlanRepository
from app.repositories.user import UserRepository
from app.repositories.user import UserRepository, UserSortField, SortOrder, UserStatus
from app.schemas.auth import UserResponse
from app.schemas.user import UserUpdate
@@ -45,13 +45,33 @@ def _user_to_response(user: User) -> UserResponse:
@router.get("/")
async def list_users(
session: Annotated[AsyncSession, Depends(get_db)],
limit: int = 100,
offset: int = 0,
) -> list[UserResponse]:
"""Get all users (admin only)."""
page: Annotated[int, Query(description="Page number", ge=1)] = 1,
limit: Annotated[int, Query(description="Items per page", ge=1, le=100)] = 50,
search: Annotated[str | None, Query(description="Search in name or email")] = None,
sort_by: Annotated[UserSortField, Query(description="Sort by field")] = UserSortField.NAME,
sort_order: Annotated[SortOrder, Query(description="Sort order")] = SortOrder.ASC,
status_filter: Annotated[UserStatus, Query(description="Filter by status")] = UserStatus.ALL,
) -> dict[str, Any]:
"""Get all users with pagination, search, and filters (admin only)."""
user_repo = UserRepository(session)
users = await user_repo.get_all_with_plan(limit=limit, offset=offset)
return [_user_to_response(user) for user in users]
users, total_count = await user_repo.get_all_with_plan_paginated(
page=page,
limit=limit,
search=search,
sort_by=sort_by,
sort_order=sort_order,
status_filter=status_filter,
)
total_pages = (total_count + limit - 1) // limit # Ceiling division
return {
"users": [_user_to_response(user) for user in users],
"total": total_count,
"page": page,
"limit": limit,
"total_pages": total_pages,
}
@router.get("/{user_id}")

View File

@@ -1,7 +1,9 @@
"""User repository."""
from typing import Any
from enum import Enum
from sqlalchemy import func
from sqlalchemy.orm import selectinload
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -14,6 +16,28 @@ from app.repositories.base import BaseRepository
logger = get_logger(__name__)
class UserSortField(str, Enum):
"""User sort fields."""
NAME = "name"
EMAIL = "email"
ROLE = "role"
CREDITS = "credits"
CREATED_AT = "created_at"
class SortOrder(str, Enum):
"""Sort order."""
ASC = "asc"
DESC = "desc"
class UserStatus(str, Enum):
"""User status filter."""
ALL = "all"
ACTIVE = "active"
INACTIVE = "inactive"
class UserRepository(BaseRepository[User]):
"""Repository for user operations."""
@@ -40,6 +64,83 @@ class UserRepository(BaseRepository[User]):
logger.exception("Failed to get all users with plan")
raise
async def get_all_with_plan_paginated(
self,
page: int = 1,
limit: int = 50,
search: str | None = None,
sort_by: UserSortField = UserSortField.NAME,
sort_order: SortOrder = SortOrder.ASC,
status_filter: UserStatus = UserStatus.ALL,
) -> tuple[list[User], int]:
"""Get all users with plan relationship loaded and return total count."""
try:
# Calculate offset
offset = (page - 1) * limit
# Build base query
base_query = select(User).options(selectinload(User.plan))
count_query = select(func.count(User.id))
# Apply search filter
if search and search.strip():
search_pattern = f"%{search.strip().lower()}%"
search_condition = (
func.lower(User.name).like(search_pattern) |
func.lower(User.email).like(search_pattern)
)
base_query = base_query.where(search_condition)
count_query = count_query.where(search_condition)
# Apply status filter
if status_filter == UserStatus.ACTIVE:
base_query = base_query.where(User.is_active == True) # noqa: E712
count_query = count_query.where(User.is_active == True) # noqa: E712
elif status_filter == UserStatus.INACTIVE:
base_query = base_query.where(User.is_active == False) # noqa: E712
count_query = count_query.where(User.is_active == False) # noqa: E712
# Apply sorting
if sort_by == UserSortField.EMAIL:
if sort_order == SortOrder.DESC:
base_query = base_query.order_by(User.email.desc())
else:
base_query = base_query.order_by(User.email.asc())
elif sort_by == UserSortField.ROLE:
if sort_order == SortOrder.DESC:
base_query = base_query.order_by(User.role.desc())
else:
base_query = base_query.order_by(User.role.asc())
elif sort_by == UserSortField.CREDITS:
if sort_order == SortOrder.DESC:
base_query = base_query.order_by(User.credits.desc())
else:
base_query = base_query.order_by(User.credits.asc())
elif sort_by == UserSortField.CREATED_AT:
if sort_order == SortOrder.DESC:
base_query = base_query.order_by(User.created_at.desc())
else:
base_query = base_query.order_by(User.created_at.asc())
else: # Default to name
if sort_order == SortOrder.DESC:
base_query = base_query.order_by(User.name.desc())
else:
base_query = base_query.order_by(User.name.asc())
# Get total count
count_result = await self.session.exec(count_query)
total_count = count_result.one()
# Apply pagination and get results
paginated_query = base_query.limit(limit).offset(offset)
result = await self.session.exec(paginated_query)
users = list(result.all())
return users, total_count
except Exception:
logger.exception("Failed to get paginated users with plan")
raise
async def get_by_id_with_plan(self, entity_id: int) -> User | None:
"""Get a user by ID with plan relationship loaded."""
try: