diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py index 70a1a6258..7d5e21f20 100644 --- a/netbox/core/api/views.py +++ b/netbox/core/api/views.py @@ -1,7 +1,6 @@ from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ -from django_filters.rest_framework import DjangoFilterBackend from django_rq.queues import get_redis_connection from django_rq.settings import get_queues_list from django_rq.utils import get_statistics @@ -20,7 +19,6 @@ from core.jobs import SyncDataSourceJob from core.models import * from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired -from netbox.api.filter_backends import OrderingFilter from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import LimitOffsetListPagination from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet @@ -73,11 +71,6 @@ class JobViewSet(NetBoxReadOnlyModelViewSet): queryset = Job.objects.all() serializer_class = serializers.JobSerializer filterset_class = filtersets.JobFilterSet - filter_backends = (DjangoFilterBackend, OrderingFilter) - # Order by elapsed time for jobs which are still running, matching the jobs table in the UI - ordering_expressions = { - 'execution_time': Job.elapsed_time_expression(), - } class ObjectChangeViewSet(NetBoxReadOnlyModelViewSet): diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index 75eb4acb5..c043e2948 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -212,7 +212,7 @@ class JobTestCase( ) def test_list_objects_by_execution_time(self): - """The Job list endpoint supports filtering by execution_time.""" + """The Job list endpoint supports filtering and ordering by execution_time.""" self.add_permissions('core.view_job') url = reverse('core-api:job-list') @@ -221,30 +221,10 @@ class JobTestCase( self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['count'], 1) - def test_ordering_by_execution_time(self): - """ - Ordering by execution_time must place jobs with no execution time last in both directions, - and must rank a running job by its elapsed time (matching the jobs table in the UI). - """ - self.add_permissions('core.view_job') - url = reverse('core-api:job-list') - - # 'Job 2' is running; give it a start time so it has an elapsed time exceeding Job 3's 90s - Job.objects.filter(name='Job 2').update(started=timezone.now() - timezone.timedelta(hours=1)) - - response = self.client.get(f'{url}?ordering=-execution_time', **self.header) - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual( - [job['name'] for job in response.data['results']], - ['Job 2', 'Job 3', 'Job 1'], - ) - + # Ordering by execution_time should be accepted (NULLs sort to one end) response = self.client.get(f'{url}?ordering=execution_time', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual( - [job['name'] for job in response.data['results']], - ['Job 3', 'Job 2', 'Job 1'], - ) + self.assertEqual(response.data['count'], 3) class BackgroundTaskTestCase(RQQueueTestMixin, TestCase): diff --git a/netbox/netbox/api/filter_backends.py b/netbox/netbox/api/filter_backends.py deleted file mode 100644 index b64e1c503..000000000 --- a/netbox/netbox/api/filter_backends.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.db.models import F -from rest_framework import filters - -__all__ = ( - 'OrderingFilter', -) - - -class OrderingFilter(filters.OrderingFilter): - """ - Extends DRF's OrderingFilter to sort null values last irrespective of the sort direction, and to - append a stable tiebreaker so that paginating through tied rows cannot skip or repeat them. - (PostgreSQL sorts nulls first when ordering descending, which pushes rows with no value to the - top of a descending sort.) - - A viewset may map a field name to a query expression via `ordering_expressions` to order by - something other than the named column; this is used where the value presented to the user is - computed rather than stored. - """ - def filter_queryset(self, request, queryset, view): - if not (ordering := self.get_ordering(request, queryset, view)): - return queryset - - expressions = getattr(view, 'ordering_expressions', {}) - terms = [] - for term in ordering: - if descending := term.startswith('-'): - term = term[1:] - expression = expressions[term] if term in expressions else F(term) - terms.append( - expression.desc(nulls_last=True) if descending else expression.asc(nulls_last=True) - ) - - return queryset.order_by(*terms, 'pk')