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:
+15
-7
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user