From 2198954f9de4a4f044ee7045043fdc645fa7ebd7 Mon Sep 17 00:00:00 2001 From: Jaemin Kim Date: Fri, 31 Jul 2026 11:35:30 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=ED=99=9C=EB=8F=99=EC=9D=B4=EB=A0=A5?= =?UTF-8?q?=EA=B4=80=EB=A6=AC(=EC=9A=B4=EC=98=81=EC=A7=84)=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=EC=9D=B4=EB=A0=A5=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/activities.py | 22 +++++++--------------- app/schemas/__init__.py | 3 ++- app/schemas/common.py | 14 ++++++++++++++ app/services/activity.py | 24 ++++++++++++------------ 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/app/routes/activities.py b/app/routes/activities.py index 22ea818..88f017f 100644 --- a/app/routes/activities.py +++ b/app/routes/activities.py @@ -7,7 +7,7 @@ from app.schemas import ( ActivityHistoryAdminItem, ActivityHistoryStatus, - CursorPage, + Page, Response, UserBrief, ) @@ -18,25 +18,17 @@ @router.get( "", - response_model=Response[CursorPage[ActivityHistoryAdminItem]], + response_model=Response[Page[ActivityHistoryAdminItem]], summary="List all users' activities", - description=( - "Returns persisted activities for all users using an ID-based cursor. " - "Admin only." - ), + description="Returns persisted activities for all users with offset-based pagination. Admin only.", ) async def list_activities( - cursor: int - | None = Query( - default=None, - ge=1, - description="Last activity ID from the previous page.", - ), - limit: int = Query(default=20, ge=1, le=100), + page: int = Query(default=1, ge=1, description="Page number (1-based)."), + size: int = Query(default=20, ge=1, le=100, description="Number of items per page."), _admin: User = Depends(require_admin), db: Session = Depends(get_db), ): - activities, next_cursor = ActivityService.list_all(db, cursor=cursor, limit=limit) + activities, total = ActivityService.list_all_paged(db, page=page, size=size) pending_updates = ActivityService.pending_updates_by_activity( db, [activity.id for activity in activities] ) @@ -69,5 +61,5 @@ async def list_activities( ] return Response( ok=True, - data=CursorPage(items=items, next_cursor=next_cursor), + data=Page(items=items, total=total, page=page, size=size), ) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 2f5455a..7b588ee 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -24,7 +24,7 @@ DraftCertificateCreate, SignatureDetail, ) -from app.schemas.common import CursorPage, Response, Website +from app.schemas.common import CursorPage, Page, Response, Website from app.schemas.project import ( MemberDetail, MemberInput, @@ -93,6 +93,7 @@ "CertificateOptions", "CertificateSummary", "CursorPage", + "Page", "DevSigninRequest", "DraftCertificateCreate", "GoogleTokenRequest", diff --git a/app/schemas/common.py b/app/schemas/common.py index cb09da5..8040f76 100644 --- a/app/schemas/common.py +++ b/app/schemas/common.py @@ -50,6 +50,20 @@ class CursorPage(BaseModel, Generic[T]): ) +class Page(BaseModel, Generic[T]): + """ + Offset-based pagination wrapper. + + Use `page` and `size` query parameters to navigate pages. + `total` is the total number of items matching the query. + """ + + items: list[T] = Field(description="List of items in the current page") + total: int = Field(description="Total number of items across all pages") + page: int = Field(description="Current page number (1-based)") + size: int = Field(description="Number of items per page") + + class Website(BaseModel): """External website or link associated with a user or project.""" diff --git a/app/services/activity.py b/app/services/activity.py index 28046eb..5f226f5 100644 --- a/app/services/activity.py +++ b/app/services/activity.py @@ -22,22 +22,22 @@ def list_by_user(db: Session, user_id: int) -> list[UserActivity]: ) @staticmethod - def list_all( - db: Session, *, cursor: int | None, limit: int - ) -> tuple[list[UserActivity], int | None]: - """List persisted activities using the indexed primary key as a cursor.""" + def list_all_paged( + db: Session, *, page: int, size: int + ) -> tuple[list[UserActivity], int]: + """List persisted activities using offset-based pagination.""" query = db.query(UserActivity).options( joinedload(UserActivity.user), joinedload(UserActivity.project), ) - if cursor is not None: - query = query.filter(UserActivity.id < cursor) - - items = query.order_by(UserActivity.id.desc()).limit(limit + 1).all() - has_more = len(items) > limit - page_items = items[:limit] - next_cursor = page_items[-1].id if has_more and page_items else None - return page_items, next_cursor + total = query.count() + items = ( + query.order_by(UserActivity.id.desc()) + .offset((page - 1) * size) + .limit(size) + .all() + ) + return items, total @staticmethod def pending_updates_by_activity( From 78c3e8347f93e43024f746c344ae3d1808a99502 Mon Sep 17 00:00:00 2001 From: Jaemin Kim Date: Fri, 31 Jul 2026 11:55:55 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=ED=99=9C=EB=8F=99=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=EA=B4=80=EB=A6=AC(=EC=9A=B4=EC=98=81=EC=A7=84)=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_activity_history_management.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_activity_history_management.py b/tests/test_activity_history_management.py index 2fe5ae2..8455592 100644 --- a/tests/test_activity_history_management.py +++ b/tests/test_activity_history_management.py @@ -364,22 +364,25 @@ def test_admin_can_paginate_all_users_activities_by_id( assert forbidden.status_code == 403 first_response = client.get( - "/activities?limit=2", + "/activities?page=1&size=2", headers=auth(admin_token), ) assert first_response.status_code == 200 first_page = first_response.json()["data"] + assert first_page["total"] == 3 + assert first_page["page"] == 1 + assert first_page["size"] == 2 assert [item["id"] for item in first_page["items"]] == list( reversed(created_ids[1:]) ) - assert first_page["next_cursor"] == created_ids[1] assert all(item["status"] == "active" for item in first_page["items"]) second_response = client.get( - f"/activities?limit=2&cursor={first_page['next_cursor']}", + "/activities?page=2&size=2", headers=auth(admin_token), ) assert second_response.status_code == 200 second_page = second_response.json()["data"] + assert second_page["total"] == 3 + assert second_page["page"] == 2 assert [item["id"] for item in second_page["items"]] == [created_ids[0]] - assert second_page["next_cursor"] is None From ffeb3a3f61b78088d7f6727f965f9568533eab97 Mon Sep 17 00:00:00 2001 From: Jaemin Kim Date: Fri, 31 Jul 2026 12:05:10 +0900 Subject: [PATCH 3/4] chore: pre-commit hooks --- app/routes/activities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routes/activities.py b/app/routes/activities.py index 88f017f..f08fdba 100644 --- a/app/routes/activities.py +++ b/app/routes/activities.py @@ -24,7 +24,9 @@ ) async def list_activities( page: int = Query(default=1, ge=1, description="Page number (1-based)."), - size: int = Query(default=20, ge=1, le=100, description="Number of items per page."), + size: int = Query( + default=20, ge=1, le=100, description="Number of items per page." + ), _admin: User = Depends(require_admin), db: Session = Depends(get_db), ): From 42106e21047355fd475309dd563866963fedf550 Mon Sep 17 00:00:00 2001 From: Jaemin Kim Date: Fri, 31 Jul 2026 14:34:32 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=ED=99=9C=EB=8F=99=EC=9D=B4=EB=A0=A5?= =?UTF-8?q?=EA=B4=80=EB=A6=AC(=EC=9A=B4=EC=98=81=EC=A7=84)=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=95=AD=EB=AA=A9=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=2020=EA=B0=9C=20->=2010=EA=B0=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/activities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/activities.py b/app/routes/activities.py index f08fdba..8e393aa 100644 --- a/app/routes/activities.py +++ b/app/routes/activities.py @@ -25,7 +25,7 @@ async def list_activities( page: int = Query(default=1, ge=1, description="Page number (1-based)."), size: int = Query( - default=20, ge=1, le=100, description="Number of items per page." + default=10, ge=1, le=100, description="Number of items per page." ), _admin: User = Depends(require_admin), db: Session = Depends(get_db),