Explorar o código

22766 - Map GraphQL array length lookup to Django's len transform

Arthur hai 4 días
pai
achega
f8e7950004
Modificáronse 2 ficheiros con 56 adicións e 5 borrados
  1. 18 4
      netbox/netbox/graphql/filter_lookups.py
  2. 38 1
      netbox/netbox/tests/test_graphql.py

+ 18 - 4
netbox/netbox/graphql/filter_lookups.py

@@ -306,10 +306,24 @@ class ArrayLookup(Generic[T]):
     Class for Array field lookups
     """
 
-    contains: list[T] | None = strawberry_django.filter_field(description='Contains the value')
-    contained_by: list[T] | None = strawberry_django.filter_field(description='Contained by the value')
-    overlap: list[T] | None = strawberry_django.filter_field(description='Overlaps with the value')
-    length: int | None = strawberry_django.filter_field(description='Length of the array')
+    contains: list[T] | None = strawberry.field(default=strawberry.UNSET, description='Contains the value')
+    contained_by: list[T] | None = strawberry.field(default=strawberry.UNSET, description='Contained by the value')
+    overlap: list[T] | None = strawberry.field(default=strawberry.UNSET, description='Overlaps with the value')
+    length: int | None = strawberry.field(default=strawberry.UNSET, description='Length of the array')
+
+    @strawberry_django.filter_field
+    def filter(self, info: Info, queryset: QuerySet, prefix: str = '') -> tuple[QuerySet, Q]:
+        # Map the public GraphQL ``length`` field to Django's ``len`` array transform; the
+        # remaining lookups share their name with the corresponding ORM transform.
+        if self.contains is not strawberry.UNSET and self.contains is not None:
+            return queryset, Q(**{f'{prefix}contains': self.contains})
+        if self.contained_by is not strawberry.UNSET and self.contained_by is not None:
+            return queryset, Q(**{f'{prefix}contained_by': self.contained_by})
+        if self.overlap is not strawberry.UNSET and self.overlap is not None:
+            return queryset, Q(**{f'{prefix}overlap': self.overlap})
+        if self.length is not strawberry.UNSET and self.length is not None:
+            return queryset, Q(**{f'{prefix}len': self.length})
+        return queryset, Q()
 
 
 @strawberry.input(one_of=True, description='Lookup for Array fields. Only one of the lookup fields can be set.')

+ 38 - 1
netbox/netbox/tests/test_graphql.py

@@ -12,7 +12,17 @@ from strawberry.extensions import QueryDepthLimiter
 from strawberry.schema.config import StrawberryConfig
 
 from dcim.choices import LocationStatusChoices
-from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Site, VirtualChassis
+from dcim.models import (
+    Device,
+    DeviceRole,
+    DeviceType,
+    Location,
+    Manufacturer,
+    Rack,
+    RackReservation,
+    Site,
+    VirtualChassis,
+)
 from extras.models import TableConfig, Tag
 from netbox.graphql.scalars import BigInt, BigIntScalar
 from netbox.graphql.schema import Query, get_schema_extensions, schema
@@ -315,6 +325,33 @@ class GraphQLAPITestCase(APITestCase):
         self.assertNotIn('errors', data)
         self.assertEqual(len(data['data']['device_list']), 3)
 
+    def test_graphql_array_length_lookup(self):
+        """
+        The public GraphQL ``length`` array lookup must map to Django's ``len`` transform.
+        Regression test for #22766 using a standard array field (RackReservation.units).
+        """
+        self.add_permissions('dcim.view_rackreservation')
+        url = reverse('graphql')
+
+        site = Site.objects.first()
+        rack = Rack.objects.create(name='Reservation Rack', site=site)
+        RackReservation.objects.create(rack=rack, units=[1, 2], user=self.user, description='Two units')
+        RackReservation.objects.create(rack=rack, units=[3, 4, 5], user=self.user, description='Three units')
+
+        query = """
+        {
+            rack_reservation_list(filters: {units: {length: 2}}) {
+                id units
+            }
+        }
+        """
+        response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(len(data['data']['rack_reservation_list']), 1)
+        self.assertEqual(data['data']['rack_reservation_list'][0]['units'], [1, 2])
+
     def test_graphql_tableconfig_object_type_exposes_id(self):
         """TableConfigType.object_type must expose ContentType fields (e.g. id)."""
         self.add_permissions('extras.view_tableconfig')