Adding pagination

This commit is contained in:
Ian Roddis committed 2026-07-02 14:48:21 -03:00
1 parent a50fccd56d
commit e923250177
8 files changed
+269 -46

No files matched your search

+133 -41
View File
@@ -1,5 +1,8 @@
"""HTMX-driven admin dashboard for users, roles, and thread standards."""
import math
import urllib.parse
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from sqlalchemy.ext.asyncio import AsyncSession
@@ -31,6 +34,40 @@ from app.web.templates import templates
router = APIRouter(prefix="/admin", include_in_schema=False)
_PAGE_SIZE = 50
def _paginate(
base_path: str,
filter_params: dict,
page: int,
page_size: int,
total: int,
target_id: str,
) -> dict:
total_pages = max(1, math.ceil(total / page_size))
page = max(1, min(page, total_pages))
skip = (page - 1) * page_size
def _url(p: int) -> str:
params = {k: v for k, v in filter_params.items() if v not in (None, "", 0)}
params["page"] = p
params["page_size"] = page_size
return f"{base_path}?{urllib.parse.urlencode(params)}"
return {
"page": page,
"page_size": page_size,
"total": total,
"total_pages": total_pages,
"skip": skip,
"showing_from": skip + 1 if total > 0 else 0,
"showing_to": min(skip + page_size, total),
"prev_url": _url(page - 1) if page > 1 else None,
"next_url": _url(page + 1) if page < total_pages else None,
"target_id": target_id,
}
def web_require(*perms: str):
"""Web dependency: allow superusers or holders of all given permissions."""
@@ -1045,12 +1082,13 @@ async def imported_parts_page(
"username": config.username if config else None,
"has_password": bool(config and config.password),
}
parts = await imported_part_crud.list_all(db)
total = await imported_part_crud.count(db)
parts = await imported_part_crud.list_filtered(db, limit=_PAGE_SIZE)
pagination = _paginate("/admin/imported-parts/filter", {}, 1, _PAGE_SIZE, total, "#imported-parts-table-card")
return templates.TemplateResponse(
request,
"admin/imported_parts.html",
{"user": user, "config": config_view, "parts": parts, "total": total},
{"user": user, "config": config_view, "parts": parts, "pagination": pagination},
)
@@ -1087,13 +1125,17 @@ async def refresh_imported_parts(
actor: User = Depends(web_require("imported_parts:write")),
):
config = await imported_part_config_crud.get(db)
if not config or not config.odata_url:
parts = await imported_part_crud.list_all(db)
async def _imported_table_ctx(sync_result=None, sync_error=None):
total = await imported_part_crud.count(db)
parts = await imported_part_crud.list_filtered(db, limit=_PAGE_SIZE)
pagination = _paginate("/admin/imported-parts/filter", {}, 1, _PAGE_SIZE, total, "#imported-parts-table-card")
return {"parts": parts, "pagination": pagination, "sync_result": sync_result, "sync_error": sync_error}
if not config or not config.odata_url:
return templates.TemplateResponse(
request,
"admin/_imported_parts_table.html",
{"parts": parts, "total": total, "sync_error": "OData URL is not configured."},
request, "admin/_imported_parts_table.html",
await _imported_table_ctx(sync_error="OData URL is not configured."),
)
try:
records = await odata_service.fetch(config.odata_url, config.username, config.password)
@@ -1103,24 +1145,14 @@ async def refresh_imported_parts(
resource_id=None,
description=f"OData sync: {created} created, {updated} updated ({len(records)} total records)",
)
parts = await imported_part_crud.list_all(db)
total = await imported_part_crud.count(db)
return templates.TemplateResponse(
request,
"admin/_imported_parts_table.html",
{
"parts": parts,
"total": total,
"sync_result": f"Sync complete: {created} created, {updated} updated.",
},
request, "admin/_imported_parts_table.html",
await _imported_table_ctx(sync_result=f"Sync complete: {created} created, {updated} updated."),
)
except Exception as exc:
parts = await imported_part_crud.list_all(db)
total = await imported_part_crud.count(db)
return templates.TemplateResponse(
request,
"admin/_imported_parts_table.html",
{"parts": parts, "total": total, "sync_error": f"Sync failed: {exc}"},
request, "admin/_imported_parts_table.html",
await _imported_table_ctx(sync_error=f"Sync failed: {exc}"),
)
@@ -1128,20 +1160,35 @@ async def refresh_imported_parts(
async def filter_imported_parts(
request: Request,
description: str = "",
page: int = 1,
page_size: int = _PAGE_SIZE,
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("imported_parts:read")),
):
parts = await imported_part_crud.list_filtered(db, description=description)
total = await imported_part_crud.count_filtered(db, description=description)
pagination = _paginate(
"/admin/imported-parts/filter",
{"description": description},
page, page_size, total,
"#imported-parts-table-card",
)
parts = await imported_part_crud.list_filtered(
db, description=description, skip=pagination["skip"], limit=page_size,
)
return templates.TemplateResponse(
request,
"admin/_imported_parts_table_card.html",
{"parts": parts},
{"parts": parts, "pagination": pagination},
)
# --- Augmented Parts ---------------------------------------------------------
async def _skipped_colours(db: AsyncSession) -> list[str]:
return await augmented_part_crud.distinct_unlinked_colours(db)
async def _augmented_part_form_ctx(db: AsyncSession) -> dict:
return {
"part_colours": await part_colour_crud.list_all(db),
@@ -1159,8 +1206,10 @@ async def augmented_parts_page(
user: User = Depends(web_require("augmented_parts:read")),
):
ctx = await _augmented_part_form_ctx(db)
total = await augmented_part_crud.count_filtered(db)
ctx["augmented_parts"] = await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE)
ctx["pagination"] = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
ctx["user"] = user
ctx["augmented_parts"] = await augmented_part_crud.list_all(db)
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
@@ -1195,10 +1244,12 @@ async def create_augmented_part(
),
)
form_ctx = await _augmented_part_form_ctx(db)
total = await augmented_part_crud.count_filtered(db)
pagination = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
return templates.TemplateResponse(
request,
"admin/_augmented_parts_table.html",
{**form_ctx, "augmented_parts": await augmented_part_crud.list_all(db)},
{**form_ctx, "augmented_parts": await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE), "pagination": pagination},
)
@@ -1228,12 +1279,15 @@ async def sync_augmented_parts(
sync_result = None
sync_error = f"Sync failed: {exc}"
form_ctx = await _augmented_part_form_ctx(db)
total = await augmented_part_crud.count_filtered(db)
pagination = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
return templates.TemplateResponse(
request,
"admin/_augmented_parts_table.html",
{
**form_ctx,
"augmented_parts": await augmented_part_crud.list_all(db),
"augmented_parts": await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE),
"pagination": pagination,
"sync_result": sync_result,
"sync_error": sync_error,
},
@@ -1249,22 +1303,49 @@ async def filter_augmented_parts(
scope_model_id: str = "",
part_type_id: str = "",
sub_part_type_id: str = "",
page: int = 1,
page_size: int = _PAGE_SIZE,
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("augmented_parts:read")),
):
colour_id_int = int(colour_id) if colour_id else None
manufacturer_id_int = int(manufacturer_id) if manufacturer_id else None
scope_model_id_int = int(scope_model_id) if scope_model_id else None
part_type_id_int = int(part_type_id) if part_type_id else None
sub_part_type_id_int = int(sub_part_type_id) if sub_part_type_id else None
total = await augmented_part_crud.count_filtered(
db,
description=description,
colour_id=colour_id_int,
manufacturer_id=manufacturer_id_int,
scope_model_id=scope_model_id_int,
part_type_id=part_type_id_int,
sub_part_type_id=sub_part_type_id_int,
)
filter_params = {
"description": description,
"colour_id": colour_id,
"manufacturer_id": manufacturer_id,
"scope_model_id": scope_model_id,
"part_type_id": part_type_id,
"sub_part_type_id": sub_part_type_id,
}
pagination = _paginate("/admin/augmented-parts/filter", filter_params, page, page_size, total, "#augmented-parts-table-card")
parts = await augmented_part_crud.list_filtered(
db,
description=description,
colour_id=int(colour_id) if colour_id else None,
manufacturer_id=int(manufacturer_id) if manufacturer_id else None,
scope_model_id=int(scope_model_id) if scope_model_id else None,
part_type_id=int(part_type_id) if part_type_id else None,
sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None,
colour_id=colour_id_int,
manufacturer_id=manufacturer_id_int,
scope_model_id=scope_model_id_int,
part_type_id=part_type_id_int,
sub_part_type_id=sub_part_type_id_int,
skip=pagination["skip"],
limit=page_size,
)
return templates.TemplateResponse(
request,
"admin/_augmented_parts_table_card.html",
{"augmented_parts": parts},
{"augmented_parts": parts, "pagination": pagination},
)
@@ -1275,8 +1356,10 @@ async def skipped_parts_page(
user: User = Depends(web_require("augmented_parts:read")),
):
form_ctx = await _augmented_part_form_ctx(db)
unlinked = await augmented_part_crud.get_unlinked_imported_parts(db)
skipped_colours = sorted({p.TBXColour for p in unlinked if p.TBXColour})
total_unlinked = await augmented_part_crud.count_unlinked_filtered(db)
skipped_colours = await _skipped_colours(db)
pagination = _paginate("/admin/augmented-parts/skipped/filter", {}, 1, _PAGE_SIZE, total_unlinked, "#skipped-parts-table-card")
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(db, limit=_PAGE_SIZE)
return templates.TemplateResponse(
request,
"admin/augmented_parts_skipped.html",
@@ -1284,10 +1367,11 @@ async def skipped_parts_page(
"user": user,
**form_ctx,
"unlinked_parts": unlinked,
"any_unlinked": bool(unlinked),
"any_unlinked": bool(total_unlinked),
"skipped_colours": skipped_colours,
"filter_description": "",
"filter_colour": "",
"pagination": pagination,
},
)
@@ -1340,18 +1424,21 @@ async def sync_skipped_parts(
sync_result = f"{synced} part(s) synced successfully."
form_ctx = await _augmented_part_form_ctx(db)
unlinked = await augmented_part_crud.get_unlinked_imported_parts(db)
skipped_colours = sorted({p.TBXColour for p in unlinked if p.TBXColour})
total_unlinked = await augmented_part_crud.count_unlinked_filtered(db)
colours = await _skipped_colours(db)
pagination = _paginate("/admin/augmented-parts/skipped/filter", {}, 1, _PAGE_SIZE, total_unlinked, "#skipped-parts-table-card")
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(db, limit=_PAGE_SIZE)
return templates.TemplateResponse(
request,
"admin/_skipped_parts_table.html",
{
**form_ctx,
"unlinked_parts": unlinked,
"any_unlinked": bool(unlinked),
"skipped_colours": skipped_colours,
"any_unlinked": bool(total_unlinked),
"skipped_colours": colours,
"filter_description": "",
"filter_colour": "",
"pagination": pagination,
"sync_result": sync_result,
"sync_error": sync_error,
},
@@ -1363,16 +1450,21 @@ async def filter_skipped_parts(
request: Request,
description: str = "",
colour: str = "",
page: int = 1,
page_size: int = _PAGE_SIZE,
db: AsyncSession = Depends(get_db),
_: User = Depends(web_require("augmented_parts:read")),
):
total = await augmented_part_crud.count_unlinked_filtered(db, description=description, colour=colour)
filter_params = {"description": description, "colour": colour}
pagination = _paginate("/admin/augmented-parts/skipped/filter", filter_params, page, page_size, total, "#skipped-parts-table-card")
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(
db, description=description, colour=colour
db, description=description, colour=colour, skip=pagination["skip"], limit=page_size,
)
return templates.TemplateResponse(
request,
"admin/_skipped_parts_table_card.html",
{"unlinked_parts": unlinked},
{"unlinked_parts": unlinked, "pagination": pagination},
)