Răsfoiți Sursa

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

Martin Hauser 1 lună în urmă
părinte
comite
62837089b4

+ 72 - 1
netbox/core/tests/test_views.py

@@ -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 = (

+ 17 - 4
netbox/core/views.py

@@ -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):

+ 5 - 0
netbox/templates/core/job/log_entries.html

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