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,
db: DbSession,
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(
"created_at",
description="Sort field: id, original_filename, file_size, mime_type, created_at, status",
@@ -69,7 +69,7 @@ def list_files_api(
Query Parameters:
- 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_order: asc or desc (default: desc)
- search: Search in filename
@@ -85,9 +85,11 @@ def list_files_api(
"files": [...],
"pagination": {
"page": 1,
"per_page": 50,
"total_items": 150,
"total_pages": 3
"per_page": 25,
"total": 150,
"pages": 6,
"next": "http://host/api/files?page=2",
"previous": null
}
}
"""
@@ -205,13 +207,19 @@ def list_files_api(
# Calculate pagination info
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 {
"files": result,
"pagination": {
"page": page,
"per_page": per_page,
"total_items": total_items,
"total_pages": total_pages,
"total": total_items,
"pages": total_pages,
"next": next_url,
"previous": previous_url,
},
}
+4 -4
View File
@@ -25,7 +25,7 @@ def files_page(
request: Request,
db: Session = Depends(get_db),
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_order: str = Query("desc"),
search: Optional[str] = Query(None),
@@ -168,8 +168,8 @@ def files_page(
"pagination": {
"page": page,
"per_page": per_page,
"total_items": total_items,
"total_pages": total_pages,
"total": total_items,
"pages": total_pages,
},
"sort_by": sort_by,
"sort_order": sort_order,
@@ -196,7 +196,7 @@ def files_page(
{
"request": request,
"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),
"upload_concurrency": settings.upload_concurrency,
"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**:
- `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_order` (optional, default: desc): Sort order (`asc` or `desc`)
- `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": {
"page": 1,
"per_page": 50,
"total_items": 150,
"total_pages": 3
"per_page": 25,
"total": 150,
"pages": 6,
"next": "http://host/api/files?page=2",
"previous": null
}
}
```
+6 -6
View File
@@ -783,12 +783,12 @@
</div>
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
{% if pagination.pages > 1 %}
<nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files
{{ min(pagination.page * pagination.per_page, pagination.total) }}
of {{ pagination.total }} files
</div>
<div class="pagination-buttons flex-wrap">
{% 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>
{% 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;">
{{ p }}
</button>
{% 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.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 %}
</div>
</nav>
+3 -3
View File
@@ -27,7 +27,7 @@ class TestListFilesAPI:
assert "files" in data
assert "pagination" in data
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):
"""Test listing files with existing data."""
@@ -54,7 +54,7 @@ class TestListFilesAPI:
assert response.status_code == 200
data = response.json()
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):
"""Test pagination parameters."""
@@ -77,7 +77,7 @@ class TestListFilesAPI:
assert len(data["files"]) == 5
assert data["pagination"]["page"] == 1
assert data["pagination"]["per_page"] == 5
assert data["pagination"]["total_pages"] == 2
assert data["pagination"]["pages"] == 2
# Request page 2
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 isinstance(data["files"], list)
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):
"""Test listing files with sample data."""
@@ -42,7 +42,7 @@ class TestFileListingPagination:
assert response.status_code == 200
data = response.json()
assert len(data["files"]) == 5
assert data["pagination"]["total_items"] == 5
assert data["pagination"]["total"] == 5
assert data["pagination"]["page"] == 1
def test_pagination_works(self, client: TestClient, db_session):
@@ -65,7 +65,7 @@ class TestFileListingPagination:
data = response.json()
assert len(data["files"]) == 5
assert data["pagination"]["page"] == 1
assert data["pagination"]["total_pages"] == 2
assert data["pagination"]["pages"] == 2
# Get second page
response = client.get("/api/files?page=2&per_page=5")