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