feat: Add pagination, search, and filter functionality to user retrieval endpoint
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"""Admin users endpoints."""
|
"""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 sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
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.plan import Plan
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.repositories.plan import PlanRepository
|
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.auth import UserResponse
|
||||||
from app.schemas.user import UserUpdate
|
from app.schemas.user import UserUpdate
|
||||||
|
|
||||||
@@ -45,13 +45,33 @@ def _user_to_response(user: User) -> UserResponse:
|
|||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_users(
|
async def list_users(
|
||||||
session: Annotated[AsyncSession, Depends(get_db)],
|
session: Annotated[AsyncSession, Depends(get_db)],
|
||||||
limit: int = 100,
|
page: Annotated[int, Query(description="Page number", ge=1)] = 1,
|
||||||
offset: int = 0,
|
limit: Annotated[int, Query(description="Items per page", ge=1, le=100)] = 50,
|
||||||
) -> list[UserResponse]:
|
search: Annotated[str | None, Query(description="Search in name or email")] = None,
|
||||||
"""Get all users (admin only)."""
|
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)
|
user_repo = UserRepository(session)
|
||||||
users = await user_repo.get_all_with_plan(limit=limit, offset=offset)
|
users, total_count = await user_repo.get_all_with_plan_paginated(
|
||||||
return [_user_to_response(user) for user in users]
|
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}")
|
@router.get("/{user_id}")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""User repository."""
|
"""User repository."""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -14,6 +16,28 @@ from app.repositories.base import BaseRepository
|
|||||||
logger = get_logger(__name__)
|
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]):
|
class UserRepository(BaseRepository[User]):
|
||||||
"""Repository for user operations."""
|
"""Repository for user operations."""
|
||||||
|
|
||||||
@@ -40,6 +64,83 @@ class UserRepository(BaseRepository[User]):
|
|||||||
logger.exception("Failed to get all users with plan")
|
logger.exception("Failed to get all users with plan")
|
||||||
raise
|
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:
|
async def get_by_id_with_plan(self, entity_id: int) -> User | None:
|
||||||
"""Get a user by ID with plan relationship loaded."""
|
"""Get a user by ID with plan relationship loaded."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user