Kaynağa Gözat

Merge pull request #22778 from netbox-community/22745-script2

22766 - Map GraphQL array length lookup to Django's len transform
bctiemann 1 gün önce
ebeveyn
işleme
f951ba0219

+ 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.')

+ 73 - 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,68 @@ 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_array_lookups(self):
+        """
+        The ``contains``, ``contained_by``, and ``overlap`` array lookups share their name with the
+        corresponding ORM transform. Verify they still resolve after #22766 replaced the auto-generated
+        filter fields with a manual ``filter()`` method.
+        """
+        self.add_permissions('dcim.view_rackreservation')
+        url = reverse('graphql')
+
+        site = Site.objects.first()
+        rack = Rack.objects.create(name='Array Lookup Rack', site=site)
+        RackReservation.objects.create(rack=rack, units=[1, 2], user=self.user, description='Low units')
+        RackReservation.objects.create(rack=rack, units=[3, 4, 5], user=self.user, description='High units')
+
+        def run(lookup):
+            query = f"""
+            {{
+                rack_reservation_list(filters: {{units: {lookup}}}) {{
+                    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)
+            return [r['units'] for r in data['data']['rack_reservation_list']]
+
+        # contains: arrays that include all of the given elements
+        self.assertEqual(run('{contains: [1]}'), [[1, 2]])
+        # contained_by: arrays whose elements all fall within the given set
+        self.assertEqual(run('{contained_by: [1, 2, 3]}'), [[1, 2]])
+        # overlap: arrays sharing at least one element with the given set
+        self.assertCountEqual(run('{overlap: [2, 3]}'), [[1, 2], [3, 4, 5]])
+
     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')