From 5fbd6d6185abf6e976ee60eb384eb2f486bd331a Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Thu, 28 Aug 2025 20:24:24 +0400 Subject: [PATCH] feat: pagination and query params for a deal end-point --- repositories/base.py | 7 +++++++ repositories/deal.py | 31 +++++++++++++++++++++++-------- routers/deal.py | 12 +++++++----- schemas/deal.py | 3 ++- services/deal.py | 24 +++++++++++++++++++++--- utils/pagination.py | 9 ++++++--- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/repositories/base.py b/repositories/base.py index e8ee54f..8a3bf6e 100644 --- a/repositories/base.py +++ b/repositories/base.py @@ -1,6 +1,13 @@ +from sqlalchemy import Select from sqlalchemy.ext.asyncio import AsyncSession class BaseRepository: def __init__(self, session: AsyncSession): self.session = session + + @staticmethod + def _apply_pagination(query: Select, page: int, items_per_page: int) -> Select: + offset = (page - 1) * items_per_page + query = query.offset(offset).limit(items_per_page) + return query diff --git a/repositories/deal.py b/repositories/deal.py index ed4516f..51c4aa4 100644 --- a/repositories/deal.py +++ b/repositories/deal.py @@ -2,20 +2,35 @@ from typing import Optional from sqlalchemy import select -from models import Deal, CardStatusHistory +from models import Deal, CardStatusHistory, Board from repositories.base import BaseRepository from schemas.deal import UpdateDealSchema, CreateDealSchema class DealRepository(BaseRepository): - async def get_all(self, board_id: int) -> list[Deal]: - stmt = ( - select(Deal) - .where(Deal.is_deleted.is_(False), Deal.board_id == board_id) - .order_by(Deal.lexorank) - ) + async def get_all( + self, + board_id: Optional[int], + project_id: Optional[int], + page: Optional[int], + items_per_page: Optional[int], + ) -> tuple[list[Deal], int]: + stmt = select(Deal).where(Deal.is_deleted.is_(False)) + + if board_id: + stmt = stmt.where(Deal.board_id == board_id) + if project_id: + stmt = stmt.join(Board).where(Board.project_id == project_id) + + total_items = len((await self.session.execute(stmt)).all()) + + stmt = stmt.order_by(Deal.lexorank) + + if page and items_per_page: + stmt = self._apply_pagination(stmt, page, items_per_page) + result = await self.session.execute(stmt) - return list(result.scalars().all()) + return list(result.scalars().all()), total_items async def get_by_id(self, deal_id: int) -> Optional[Deal]: stmt = select(Deal).where(Deal.id == deal_id, Deal.is_deleted.is_(False)) diff --git a/routers/deal.py b/routers/deal.py index f372e36..256abe9 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -1,6 +1,6 @@ -from fastapi import APIRouter, Path +from fastapi import APIRouter, Path, Query -from backend.dependecies import SessionDependency +from backend.dependecies import SessionDependency, PaginationDependency from schemas.deal import * from services import DealService @@ -10,15 +10,17 @@ deal_router = APIRouter( @deal_router.get( - "/{boardId}", + "/", response_model=GetDealsResponse, operation_id="get_deals", ) async def get_deals( session: SessionDependency, - board_id: int = Path(alias="boardId"), + pagination: PaginationDependency, + board_id: Optional[int] = Query(alias="boardId", default=None), + project_id: Optional[int] = Query(alias="projectId", default=None), ): - return await DealService(session).get_deals(board_id) + return await DealService(session).get_deals(pagination, board_id, project_id) @deal_router.post( diff --git a/schemas/deal.py b/schemas/deal.py index aee0fba..f617b4a 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional -from schemas.base import BaseSchema, BaseResponse +from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema # region Entities @@ -48,6 +48,7 @@ class UpdateDealRequest(BaseSchema): class GetDealsResponse(BaseSchema): deals: list[DealSchema] + pagination_info: PaginationInfoSchema class CreateDealResponse(BaseResponse): diff --git a/services/deal.py b/services/deal.py index 86bc6f8..b892650 100644 --- a/services/deal.py +++ b/services/deal.py @@ -1,7 +1,10 @@ +import math + from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from repositories import DealRepository +from schemas.base import PaginationSchema from schemas.deal import * @@ -9,10 +12,25 @@ class DealService: def __init__(self, session: AsyncSession): self.repository = DealRepository(session) - async def get_deals(self, board_id: int) -> GetDealsResponse: - deals = await self.repository.get_all(board_id) + async def get_deals( + self, + pagination: PaginationSchema, + board_id: Optional[int], + project_id: Optional[int], + ) -> GetDealsResponse: + deals, total_items = await self.repository.get_all( + board_id, project_id, pagination.page, pagination.items_per_page + ) + + total_pages = 1 + if pagination.items_per_page: + total_pages = math.ceil(total_items / pagination.items_per_page) + return GetDealsResponse( - deals=[DealSchema.model_validate(deal) for deal in deals] + deals=[DealSchema.model_validate(deal) for deal in deals], + pagination_info=PaginationInfoSchema( + total_pages=total_pages, total_items=total_items + ), ) async def create_deal(self, request: CreateDealRequest) -> CreateDealResponse: diff --git a/utils/pagination.py b/utils/pagination.py index d31d333..7451ce0 100644 --- a/utils/pagination.py +++ b/utils/pagination.py @@ -1,10 +1,13 @@ from typing import Optional +from fastapi import Query + from schemas.base import PaginationSchema async def pagination_parameters( - page: Optional[int] = None, items_per_page: Optional[int] = None + page: Optional[int] = Query(default=None), + items_per_page: Optional[int] = Query(default=None, alias="itemsPerPage"), ) -> PaginationSchema: return PaginationSchema(page=page, items_per_page=items_per_page) @@ -12,6 +15,6 @@ async def pagination_parameters( def is_valid_pagination(pagination: Optional[PaginationSchema]) -> bool: if not pagination: return False - return all( - [isinstance(pagination.items_per_page, int), isinstance(pagination.page, int)] + return isinstance(pagination.items_per_page, int) and isinstance( + pagination.page, int )