feat: Add pagination, search, and filter functionality to user retrieval endpoint
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user