Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions app/routes/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from app.schemas import (
ActivityHistoryAdminItem,
ActivityHistoryStatus,
CursorPage,
Page,
Response,
UserBrief,
)
Expand All @@ -18,25 +18,19 @@

@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.",
page: int = Query(default=1, ge=1, description="Page number (1-based)."),
size: int = Query(
default=10, ge=1, le=100, description="Number of items per page."
),
limit: int = Query(default=20, ge=1, le=100),
_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]
)
Expand Down Expand Up @@ -69,5 +63,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),
)
3 changes: 2 additions & 1 deletion app/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -93,6 +93,7 @@
"CertificateOptions",
"CertificateSummary",
"CursorPage",
"Page",
"DevSigninRequest",
"DraftCertificateCreate",
"GoogleTokenRequest",
Expand Down
14 changes: 14 additions & 0 deletions app/schemas/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
24 changes: 12 additions & 12 deletions app/services/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 7 additions & 4 deletions tests/test_activity_history_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading