feat(api): implement proper pagination for file list API

- Change default per_page from 50 to 25
- Rename total_items → total, total_pages → pages in pagination response
- Add next/previous URL fields to pagination response
- Update view and template to use new field names
- Update tests and API docs

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-01 17:31:56 +00:00
parent 82c524a788
commit af34ce88df
6 changed files with 37 additions and 27 deletions
+15 -7
View File
@@ -49,7 +49,7 @@ def list_files_api(
request: Request, request: Request,
db: DbSession, db: DbSession,
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
per_page: int = Query(50, ge=1, le=200, description="Items per page"), per_page: int = Query(25, ge=1, le=200, description="Items per page"),
sort_by: str = Query( sort_by: str = Query(
"created_at", "created_at",
description="Sort field: id, original_filename, file_size, mime_type, created_at, status", description="Sort field: id, original_filename, file_size, mime_type, created_at, status",
@@ -69,7 +69,7 @@ def list_files_api(
Query Parameters: Query Parameters:
- page: Page number (default: 1) - page: Page number (default: 1)
- per_page: Items per page (default: 50, max: 200) - per_page: Items per page (default: 25, max: 200)
- sort_by: Field to sort by (default: created_at) - sort_by: Field to sort by (default: created_at)
- sort_order: asc or desc (default: desc) - sort_order: asc or desc (default: desc)
- search: Search in filename - search: Search in filename
@@ -85,9 +85,11 @@ def list_files_api(
"files": [...], "files": [...],
"pagination": { "pagination": {
"page": 1, "page": 1,
"per_page": 50, "per_page": 25,
"total_items": 150, "total": 150,
"total_pages": 3 "pages": 6,
"next": "http://host/api/files?page=2",
"previous": null
} }
} }
""" """
@@ -205,13 +207,19 @@ def list_files_api(
# Calculate pagination info # Calculate pagination info
total_pages = (total_items + per_page - 1) // per_page total_pages = (total_items + per_page - 1) // per_page
# Build next / previous page URLs by replacing the page query parameter
next_url = str(request.url.include_query_params(page=page + 1)) if page < total_pages else None
previous_url = str(request.url.include_query_params(page=page - 1)) if page > 1 else None
return { return {
"files": result, "files": result,
"pagination": { "pagination": {
"page": page, "page": page,
"per_page": per_page, "per_page": per_page,
"total_items": total_items, "total": total_items,
"total_pages": total_pages, "pages": total_pages,
"next": next_url,
"previous": previous_url,
}, },
} }
+4 -4
View File
@@ -25,7 +25,7 @@ def files_page(
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=200), per_page: int = Query(25, ge=1, le=200),
sort_by: str = Query("created_at"), sort_by: str = Query("created_at"),
sort_order: str = Query("desc"), sort_order: str = Query("desc"),
search: Optional[str] = Query(None), search: Optional[str] = Query(None),
@@ -168,8 +168,8 @@ def files_page(
"pagination": { "pagination": {
"page": page, "page": page,
"per_page": per_page, "per_page": per_page,
"total_items": total_items, "total": total_items,
"total_pages": total_pages, "pages": total_pages,
}, },
"sort_by": sort_by, "sort_by": sort_by,
"sort_order": sort_order, "sort_order": sort_order,
@@ -196,7 +196,7 @@ def files_page(
{ {
"request": request, "request": request,
"files": [], "files": [],
"pagination": {"page": 1, "per_page": per_page, "total_items": 0, "total_pages": 0}, "pagination": {"page": 1, "per_page": per_page, "total": 0, "pages": 0},
"error": str(e), "error": str(e),
"upload_concurrency": settings.upload_concurrency, "upload_concurrency": settings.upload_concurrency,
"upload_queue_delay_ms": settings.upload_queue_delay_ms, "upload_queue_delay_ms": settings.upload_queue_delay_ms,
+6 -4
View File
@@ -285,7 +285,7 @@ Retrieve a paginated list of processed files with advanced filtering and sorting
**Query Parameters**: **Query Parameters**:
- `page` (optional, default: 1): Page number - `page` (optional, default: 1): Page number
- `per_page` (optional, default: 50, max: 200): Items per page - `per_page` (optional, default: 25, max: 200): Items per page
- `sort_by` (optional, default: created_at): Sort field (`id`, `original_filename`, `file_size`, `mime_type`, `created_at`) - `sort_by` (optional, default: created_at): Sort field (`id`, `original_filename`, `file_size`, `mime_type`, `created_at`)
- `sort_order` (optional, default: desc): Sort order (`asc` or `desc`) - `sort_order` (optional, default: desc): Sort order (`asc` or `desc`)
- `search` (optional): Search in filename (partial match) - `search` (optional): Search in filename (partial match)
@@ -324,9 +324,11 @@ GET /api/files?status=completed&mime_type=application/pdf&tags=invoice&date_from
], ],
"pagination": { "pagination": {
"page": 1, "page": 1,
"per_page": 50, "per_page": 25,
"total_items": 150, "total": 150,
"total_pages": 3 "pages": 6,
"next": "http://host/api/files?page=2",
"previous": null
} }
} }
``` ```
+6 -6
View File
@@ -783,12 +783,12 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{% if pagination.total_pages > 1 %} {% if pagination.pages > 1 %}
<nav class="pagination flex-wrap gap-2" aria-label="File list pagination"> <nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<div class="pagination-info" aria-live="polite"> <div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} - Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }} {{ min(pagination.page * pagination.per_page, pagination.total) }}
of {{ pagination.total_items }} files of {{ pagination.total }} files
</div> </div>
<div class="pagination-buttons flex-wrap"> <div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %} {% if pagination.page > 1 %}
@@ -796,15 +796,15 @@
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button> <button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button>
{% endif %} {% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %} {% for p in range(max(1, pagination.page - 2), min(pagination.pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="Page {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;"> <button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="Page {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;">
{{ p }} {{ p }}
</button> </button>
{% endfor %} {% endfor %}
{% if pagination.page < pagination.total_pages %} {% if pagination.page < pagination.pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="Go to next page" style="min-height:44px;">Next</button> <button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="Go to next page" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button> <button class="pagination-button" onclick="goToPage({{ pagination.pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button>
{% endif %} {% endif %}
</div> </div>
</nav> </nav>
+3 -3
View File
@@ -27,7 +27,7 @@ class TestListFilesAPI:
assert "files" in data assert "files" in data
assert "pagination" in data assert "pagination" in data
assert len(data["files"]) == 0 assert len(data["files"]) == 0
assert data["pagination"]["total_items"] == 0 assert data["pagination"]["total"] == 0
def test_list_files_with_data(self, client: TestClient, db_session): def test_list_files_with_data(self, client: TestClient, db_session):
"""Test listing files with existing data.""" """Test listing files with existing data."""
@@ -54,7 +54,7 @@ class TestListFilesAPI:
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["files"]) == 2 assert len(data["files"]) == 2
assert data["pagination"]["total_items"] == 2 assert data["pagination"]["total"] == 2
def test_list_files_with_pagination(self, client: TestClient, db_session): def test_list_files_with_pagination(self, client: TestClient, db_session):
"""Test pagination parameters.""" """Test pagination parameters."""
@@ -77,7 +77,7 @@ class TestListFilesAPI:
assert len(data["files"]) == 5 assert len(data["files"]) == 5
assert data["pagination"]["page"] == 1 assert data["pagination"]["page"] == 1
assert data["pagination"]["per_page"] == 5 assert data["pagination"]["per_page"] == 5
assert data["pagination"]["total_pages"] == 2 assert data["pagination"]["pages"] == 2
# Request page 2 # Request page 2
response = client.get("/api/files?page=2&per_page=5") response = client.get("/api/files?page=2&per_page=5")
+3 -3
View File
@@ -22,7 +22,7 @@ class TestFileListingPagination:
assert "pagination" in data assert "pagination" in data
assert isinstance(data["files"], list) assert isinstance(data["files"], list)
assert len(data["files"]) == 0 assert len(data["files"]) == 0
assert data["pagination"]["total_items"] == 0 assert data["pagination"]["total"] == 0
def test_list_files_with_data(self, client: TestClient, db_session): def test_list_files_with_data(self, client: TestClient, db_session):
"""Test listing files with sample data.""" """Test listing files with sample data."""
@@ -42,7 +42,7 @@ class TestFileListingPagination:
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["files"]) == 5 assert len(data["files"]) == 5
assert data["pagination"]["total_items"] == 5 assert data["pagination"]["total"] == 5
assert data["pagination"]["page"] == 1 assert data["pagination"]["page"] == 1
def test_pagination_works(self, client: TestClient, db_session): def test_pagination_works(self, client: TestClient, db_session):
@@ -65,7 +65,7 @@ class TestFileListingPagination:
data = response.json() data = response.json()
assert len(data["files"]) == 5 assert len(data["files"]) == 5
assert data["pagination"]["page"] == 1 assert data["pagination"]["page"] == 1
assert data["pagination"]["total_pages"] == 2 assert data["pagination"]["pages"] == 2
# Get second page # Get second page
response = client.get("/api/files?page=2&per_page=5") response = client.get("/api/files?page=2&per_page=5")