Fixes #21895: Fix missing pagination controls for Job Log entries (#22252)

This commit is contained in:
Martin Hauser 2026-06-03 13:14:38 +02:00 committed by GitHub
parent 583ab535e8
commit 62837089b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 5 deletions

View File

@ -1,7 +1,7 @@
import json
import urllib.parse
import uuid
from datetime import datetime
from datetime import UTC, datetime
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
@ -152,6 +152,77 @@ class JobTestCase(
)
class JobLogViewTestCase(TestCase):
user_permissions = (
'core.view_job',
)
@classmethod
def setUpTestData(cls):
cls.job = Job.objects.create(
name='Test Job',
job_id=uuid.uuid4(),
)
cls.job.log_entries = [
{
'level': 'info',
'message': f'log line {i}',
'timestamp': datetime(2026, 1, 1, tzinfo=UTC),
}
for i in range(120)
]
cls.job.save()
def setUp(self):
super().setUp()
# UserConfig.set() mutates self.data in place, which can mutate DEFAULT_USER_PREFERENCES
# (the signal in users/signals.py initializes data with a shared reference). Assign a
# fresh literal instead. Pin per_page so page-boundary assertions don't depend on PAGINATE_COUNT.
self.user.config.data = {'pagination': {'per_page': 50}}
self.user.config.save()
def test_log_page_renders_table_inline(self):
"""The full page renders the first log page inside an HTMX container."""
url = reverse('core:job_log', kwargs={'pk': self.job.pk})
response = self.client.get(url)
self.assertHttpStatus(response, 200)
self.assertContains(response, 'htmx-container')
self.assertContains(response, 'log line 0')
self.assertContains(response, 'Showing 1-50 of 120')
def test_log_page_table_is_embedded(self):
"""The embedded table never pushes page/per_page into the browser URL."""
url = reverse('core:job_log', kwargs={'pk': self.job.pk})
response = self.client.get(url)
self.assertHttpStatus(response, 200)
self.assertNotContains(response, 'hx-push-url="true"')
def test_log_table_htmx_renders_partial(self):
"""An HTMX request returns the paginated table partial."""
url = reverse('core:job_log', kwargs={'pk': self.job.pk})
response = self.client.get(url, headers={'hx-request': 'true'})
self.assertHttpStatus(response, 200)
self.assertContains(response, 'log line 0')
self.assertContains(response, 'Showing 1-50 of 120')
self.assertContains(response, 'Per Page')
def test_log_table_htmx_page_navigation(self):
"""`?page=2` advances the embedded table to the second page."""
url = reverse('core:job_log', kwargs={'pk': self.job.pk})
response = self.client.get(f'{url}?page=2', headers={'hx-request': 'true'})
self.assertHttpStatus(response, 200)
self.assertContains(response, 'log line 50')
self.assertNotContains(response, 'log line 49')
def test_log_table_htmx_per_page(self):
"""`?per_page=100` widens the embedded table page size."""
url = reverse('core:job_log', kwargs={'pk': self.job.pk})
response = self.client.get(f'{url}?per_page=100', headers={'hx-request': 'true'})
self.assertHttpStatus(response, 200)
self.assertContains(response, 'log line 99')
self.assertNotContains(response, 'log line 100')
# TODO: Convert to StandardTestCases.Views
class ObjectChangeTestCase(TestCase):
user_permissions = (

View File

@ -39,7 +39,6 @@ from netbox.plugins.utils import get_installed_plugins
from netbox.ui import layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
PluginContentPanel,
@ -269,7 +268,7 @@ class JobLogView(generic.ObjectView):
layout = layout.Layout(
layout.Row(
layout.Column(
ContextTablePanel('table', title=_('Log Entries')),
TemplatePanel('core/job/log_entries.html', title=_('Log Entries')),
PluginContentPanel('left_page'),
),
),
@ -280,13 +279,27 @@ class JobLogView(generic.ObjectView):
),
)
def get_extra_context(self, request, instance):
def get_table(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
table.embedded = True
table.htmx_url = reverse('core:job_log', kwargs={'pk': instance.pk})
table.configure(request)
return table
def get_extra_context(self, request, instance):
return {
'table': table,
'table': self.get_table(request, instance),
}
def get(self, request, **kwargs):
if htmx_partial(request):
instance = self.get_object(**kwargs)
return render(request, 'htmx/table.html', {
'object': instance,
'table': self.get_table(request, instance),
})
return super().get(request, **kwargs)
@register_model_view(Job, 'delete')
class JobDeleteView(generic.ObjectDeleteView):

View File

@ -0,0 +1,5 @@
{% extends 'ui/panels/_base.html' %}
{% block panel_content %}
{% include 'htmx/table.html' %}
{% endblock panel_content %}