[Feature] Make JobLog Page Paginated (#403)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#403
This commit is contained in:
2026-05-28 23:01:27 +12:00
parent 1a9f1cf9af
commit c9b643fe6e
8 changed files with 187 additions and 8 deletions

View File

@ -0,0 +1,26 @@
from django.conf import settings as s
from ninja import Router
from ninja_extra.pagination import paginate
from epdb.models import JobLog
from ..pagination import EnhancedPageNumberPagination
from ..schemas import JobLogOutSchema
router = Router()
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
@paginate(
EnhancedPageNumberPagination,
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
)
def list_all_joblogs(request):
"""
List all JobLogs from reviewed packages.
"""
current_user = request.user
if current_user.is_superuser:
return JobLog.objects.all().order_by("-created")
else:
return JobLog.objects.filter(user=current_user).order_by("-created")

View File

@ -15,6 +15,7 @@ from .endpoints import (
additional_information, additional_information,
settings, settings,
groups, groups,
joblogs,
) )
# Main router with authentication # Main router with authentication
@ -37,6 +38,7 @@ router.add_router("", structure.router)
router.add_router("", additional_information.router) router.add_router("", additional_information.router)
router.add_router("", settings.router) router.add_router("", settings.router)
router.add_router("", groups.router) router.add_router("", groups.router)
router.add_router("", joblogs.router)
if s.IUCLID_EXPORT_ENABLED: if s.IUCLID_EXPORT_ENABLED:
from epiuclid.api import router as iuclid_router from epiuclid.api import router as iuclid_router

View File

@ -1,7 +1,10 @@
from ninja import FilterSchema, FilterLookup, Schema from datetime import datetime
from typing import Annotated, Optional, List, Dict, Any from typing import Annotated, Optional, List, Dict, Any
from uuid import UUID from uuid import UUID
from django.urls import reverse
from ninja import Field, FilterSchema, FilterLookup, Schema
# Filter schema for query parameters # Filter schema for query parameters
class ReviewStatusFilter(FilterSchema): class ReviewStatusFilter(FilterSchema):
@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
url: str = "" url: str = ""
name: str name: str
description: str description: str
class SimpleUserOutSchema(Schema):
uuid: UUID
url: str
name: str = Field(alias="username")
class JobLogOutSchema(Schema):
user: SimpleUserOutSchema
id: UUID = Field(alias="task_id")
url: str
name: str = Field(alias="job_name")
created: datetime = Field(alias="created")
status: str = Field(alias="status")
done: Optional[datetime] = Field(None, alias="done_at")
@staticmethod
def resolve_url(obj):
return reverse("job detail", kwargs={"job_uuid": obj.task_id})

View File

@ -3080,12 +3080,21 @@ def jobs(request):
{"Home": s.SERVER_URL}, {"Home": s.SERVER_URL},
{"Jobs": s.SERVER_URL + "/jobs"}, {"Jobs": s.SERVER_URL + "/jobs"},
] ]
if current_user.is_superuser: # if current_user.is_superuser:
context["jobs"] = JobLog.objects.all().order_by("-created") # context["jobs"] = JobLog.objects.all().order_by("-created")
else: # else:
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created") # context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
return render(request, "collections/joblog.html", context) # Context for paginated template
context["entity_type"] = "joblog"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
context["list_title"] = "joblog"
context["list_mode"] = "combined"
return render(request, "collections/joblog_paginated.html", context)
# return render(request, "collections/joblog.html", context)
elif request.method == "POST": elif request.method == "POST":
job_name = request.POST.get("job-name") job_name = request.POST.get("job-name")

View File

@ -0,0 +1,102 @@
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
{% load envipytags %}
{# Loading state #}
<div
x-show="isLoading"
class="mx-auto flex h-32 w-32 items-center justify-center"
>
{% include "components/loading-spinner.html" %}
</div>
{# Error state #}
<div
x-show="!isLoading && error"
class="alert alert-error/50 text-sm"
x-text="error"
></div>
{# Content #}
<template x-if="!isLoading && !error">
<div>
{# Empty state #}
<div
x-show="totalItems === 0"
class="text-base-content/70 py-8 text-center"
>
<p>No {{ empty_text|default:"items" }} found.</p>
</div>
{# Items list #}
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
<table class="table-zebra table">
<thead>
<tr>
<th>User</th>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Queued At</th>
<th>Done At</th>
</tr>
</thead>
<tbody>
<template x-for="obj in paginatedItems" :key="obj.url">
<tr>
<td>
<a :href="obj.user.url"><span x-text="obj.user.name"></span></a>
</td>
<td>
<a :href="obj.url"><span x-text="obj.id"></span></a>
</td>
<td><span x-text="obj.name"></span></td>
<td><span x-text="obj.status"></span></td>
<td><span x-text="obj.created"></span></td>
<td><span x-text="obj.done"></span></td>
</tr>
</template>
</tbody>
</table>
</ul>
{# Pagination controls #}
<div
x-show="totalPages > 1"
class="mt-4 flex items-center justify-between px-2"
>
<span class="text-base-content/70 text-sm">
Showing <span x-text="showingStart"></span>-<span
x-text="showingEnd"
></span>
of <span x-text="totalItems"></span>
</span>
<div class="join">
<button
class="join-item btn btn-sm"
:disabled="currentPage === 1"
@click="prevPage()"
>
«
</button>
<template x-for="item in pageNumbers" :key="item.key">
<button
class="join-item btn btn-sm"
:class="{ 'btn-active': item.page === currentPage }"
:disabled="item.isEllipsis"
@click="!item.isEllipsis && goToPage(item.page)"
x-text="item.page"
></button>
</template>
<button
class="join-item btn btn-sm"
:disabled="currentPage === totalPages"
@click="nextPage()"
>
»
</button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,13 @@
{% extends "collections/paginated_base.html" %}
{% block page_title %}Jobs{% endblock %}
{% block action_button %}
{% endblock action_button %}
{% block action_modals %}
{% endblock action_modals %}
{% block description %}
<p>List of Jobs submitted.</p>
{% endblock description %}

View File

@ -37,7 +37,11 @@
perPage: {{ per_page|default:50 }} perPage: {{ per_page|default:50 }}
})" })"
> >
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %} {% if entity_type == 'joblog' %}
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
{% else %}
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
{% endif %}
</div> </div>
{% else %} {% else %}
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #} {# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}

2
uv.lock generated
View File

@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
[[package]] [[package]]
name = "envipy-additional-information" name = "envipy-additional-information"
version = "0.4.2" version = "0.4.2"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#676dae1c5678539beac637b87e49b9dadfdfd85a" } source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#79285f522e0a6ed3f2e805dfeeb6b9fa5cea4323" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
] ]