Arthur hai 1 día
pai
achega
1cde8ded5d

+ 8 - 1
netbox/dcim/tests/test_api.py

@@ -4361,7 +4361,14 @@ class InterfaceTestCase(Mixins.FieldPrefetchMixin, Mixins.ComponentTraceMixin, A
             name='Interfaces', template_code='{% for interface in queryset %}{{ interface.name }},{% endfor %}'
         )
         export_template.object_types.set([ObjectType.objects.get_for_model(Interface)])
-        url = f'{self._get_list_url()}?ordering=-_count_ipaddresses'
+        url = f'{self._get_list_url()}?ordering=-count_ipaddresses'
+
+        # The count's source isn't a valid ordering field
+        response = self.client.get(f'{self._get_list_url()}?ordering=-_count_ipaddresses', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(
+            [row['name'] for row in response.data['results']], ['Interface 1', 'Interface 2', 'Interface 3']
+        )
 
         # Responses without the count annotate it for the ordering alone
         for params in ('', '&fields=id,name,count_ipaddresses', '&brief=1', '&fields=id,name', '&export=Interfaces'):

+ 36 - 16
netbox/netbox/api/filters.py

@@ -10,28 +10,48 @@ __all__ = (
 
 class OrderingFilter(filters.OrderingFilter):
     """
-    Extends DRF's OrderingFilter to annotate a RelatedObjectCountField named by the ordering when the response leaves
-    it out, since its annotation is otherwise applied only when the field is included in the response.
+    Extends DRF's OrderingFilter to order by a RelatedObjectCountField under its field name rather than its source,
+    annotating the count when the response leaves it out, since its annotation is otherwise applied only when the field
+    is included in the response.
     """
+    @staticmethod
+    def _get_serializer_class(view):
+        if hasattr(view, 'get_serializer_class'):
+            return view.get_serializer_class()
+        return view.serializer_class
+
+    def _get_count_sources(self, view):
+        """Map the name of each RelatedObjectCountField on the view's serializer to its source."""
+        return {
+            field_name: field.source or field_name
+            for field_name, field in self._get_serializer_class(view)._declared_fields.items()
+            if isinstance(field, RelatedObjectCountField)
+        }
+
+    def get_default_valid_fields(self, queryset, view, context=None):
+        field_names = {source: field_name for field_name, source in self._get_count_sources(view).items()}
+        return [
+            (field_names.get(key, key), label)
+            for key, label in super().get_default_valid_fields(queryset, view, context)
+        ]
+
     def filter_queryset(self, request, queryset, view):
         ordering = self.get_ordering(request, queryset, view)
         if not ordering:
             return queryset
 
-        if hasattr(view, 'get_serializer_class'):
-            serializer_class = view.get_serializer_class()
-        else:
-            serializer_class = view.serializer_class
-        count_fields = {
-            field.source or field_name: field_name
-            for field_name, field in serializer_class._declared_fields.items()
-            if isinstance(field, RelatedObjectCountField)
-        }
-        missing = [
-            count_fields[source] for source in (term.lstrip('-') for term in ordering)
-            if source in count_fields and source not in queryset.query.annotations
-        ]
+        count_sources = self._get_count_sources(view)
+        terms = []
+        missing = []
+        for term in ordering:
+            if isinstance(term, str) and (field_name := term.lstrip('-')) in count_sources:
+                source = count_sources[field_name]
+                if source not in queryset.query.annotations:
+                    missing.append(field_name)
+                term = f'-{source}' if term.startswith('-') else source
+            terms.append(term)
         if missing:
+            serializer_class = self._get_serializer_class(view)
             queryset = queryset.annotate(**get_annotations_for_serializer(serializer_class, fields=missing))
 
-        return queryset.order_by(*ordering)
+        return queryset.order_by(*terms)

+ 8 - 1
netbox/virtualization/tests/test_api.py

@@ -1151,7 +1151,14 @@ class VMInterfaceTestCase(APIViewTestCases.APIViewTestCase):
             FHRPGroupAssignment(group=groups[1], interface=interfaces['Interface 2'], priority=100),
             FHRPGroupAssignment(group=groups[0], interface=interfaces['Interface 3'], priority=100),
         ))
-        url = f'{self._get_list_url()}?ordering=-_count_fhrp_groups'
+        url = f'{self._get_list_url()}?ordering=-count_fhrp_groups'
+
+        # The count's source isn't a valid ordering field
+        response = self.client.get(f'{self._get_list_url()}?ordering=-_count_fhrp_groups', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(
+            [row['name'] for row in response.data['results']], ['Interface 1', 'Interface 2', 'Interface 3']
+        )
 
         # Responses without the count annotate it for the ordering alone
         for params in ('', '&brief=1', '&fields=id,name'):