Ver Fonte

added docs and more tests

John Anderson há 6 anos atrás
pai
commit
67565ca191

+ 71 - 0
docs/api/filtering.md

@@ -0,0 +1,71 @@
+# API Filtering
+
+The NetBox API supports robust filtering of results based on the fields of each model.
+Generally speaking you are able to filter based on the attributes (fields) present in
+the response body. Please note however that certain read-only or metadata fields are not
+filterable.
+
+Filtering is achieved by passing HTTP query parameters and the parameter name is the
+name of the field you wish to filter on and the value is the field value.
+
+E.g. filtering based on a device's name:
+```
+/api/dcim/devices/?name=DC-SPINE-1
+```
+
+## Multi Value Logic
+
+While you are able to filter based on an arbitrary number of fields, you are also able to
+pass multiple values for the same field. In most cases filtering on multiple values is
+implemented as a logical OR operation. A notible exception is the `tag` filter which
+is a logical AND. Passing multiple values for one field, can be combined with other fields.
+
+For example, filtering for devices with either the name of DC-SPINE-1 _or_ DC-LEAF-4:
+```
+/api/dcim/devices/?name=DC-SPINE-1&name=DC-LEAF-4
+```
+
+Filtering for devices with tag `router` and `customer-a` will return only devices with
+_both_ of those tags applied:
+```
+/api/dcim/devices/?tag=router&tag=customer-a
+```
+
+## Lookup Expressions
+
+Certain model fields also support filtering using additonal lookup expressions. This allows
+for negation and other context specific filtering.
+
+These lookup expressions can be applied by adding a suffix to the desired field's name.
+E.g. `mac_address__n`. In this case, the filter expression is for negation and it is seperated
+by two underscores. Below are the lookup expressions that are supported across different field
+types.
+
+### Numeric Fields
+
+Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
+
+- `n` - not equal (negation)
+- `lt` - less than
+- `lte` - less than or equal
+- `gt` - greater than
+- `gte` - greater than or equal
+
+### String Fields
+
+String based (char) fields (Name, Address, etc) support these lookup expressions:
+
+- `n` - not equal (negation)
+- `ic` - case insensitive contains
+- `nic` - negated case insensitive contains
+- `isw` - case insensitive starts with
+- `nisw` - negated case insensitive starts with
+- `iew` - case insensitive ends with
+- `niew` - negated case insensitive ends with
+- `ie` - case sensitive exact match
+- `nie` - negated case sensitive exact match
+
+### Foreign Keys & Other Fields
+
+Certain other fields, namely foreign key relationships support just the negation
+expression: `n`.

+ 2 - 0
docs/api/overview.md

@@ -62,6 +62,8 @@ Lists of objects can be filtered using a set of query parameters. For example, t
 GET /api/dcim/interfaces/?device_id=123
 ```
 
+See [filtering](filtering.md) for more details.
+
 # Serialization
 
 The NetBox API employs three types of serializers to represent model data:

+ 1 - 0
mkdocs.yml

@@ -50,6 +50,7 @@ pages:
         - Authentication: 'api/authentication.md'
         - Working with Secrets: 'api/working-with-secrets.md'
         - Examples: 'api/examples.md'
+        - Filtering: 'api/filtering.md'
     - Development:
         - Introduction: 'development/index.md'
         - Style Guide: 'development/style-guide.md'

+ 0 - 13
netbox/utilities/constants.py

@@ -57,19 +57,6 @@ FILTER_NEGATION_LOOKUP_MAP = dict(
     n='exact'
 )
 
-FILTER_LOOKUP_HELP_TEXT_MAP = dict(
-    icontains='case insensitive contains',
-    iendswith='case insensitive ends with',
-    istartswith='case insensitive starts with',
-    iexact='case insensitive exact',
-    exact='case sensitive exact',
-    lt='less than',
-    lte='less than or equal',
-    gt='greater than',
-    gte='greater than or equal',
-    n='negated'
-)
-
 
 # Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
 # the advisory_lock contextmanager. When a lock is acquired,

+ 1 - 2
netbox/utilities/filters.py

@@ -8,8 +8,7 @@ from django_filters.utils import get_model_field, resolve_field
 
 from extras.models import Tag
 from utilities.constants import (
-    FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_LOOKUP_HELP_TEXT_MAP, FILTER_NEGATION_LOOKUP_MAP,
-    FILTER_NUMERIC_BASED_LOOKUP_MAP
+    FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_NUMERIC_BASED_LOOKUP_MAP
 )
 
 

+ 156 - 30
netbox/utilities/tests/test_filters.py

@@ -2,8 +2,12 @@ from django.conf import settings
 from django.test import TestCase
 import django_filters
 
-from dcim.filters import SiteFilterSet
-from dcim.models import Region, Site
+from dcim.filters import DeviceFilterSet, SiteFilterSet
+from dcim.choices import *
+from dcim.models import (
+    Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
+)
+from ipam.models import IPAddress
 from utilities.filters import BaseFilterSet, TreeNodeMultipleChoiceFilter
 
 
@@ -68,67 +72,189 @@ class DynamicFilterLookupExpressionTest(TestCase):
     These tests ensure of the utilities.filters.BaseFilterSet.get_filters() method
     correctly generates dynamic filter expressions
     """
+    device_queryset = Device.objects.all()
+    device_filterset = DeviceFilterSet
+    site_queryset = Site.objects.all()
+    site_filterset = SiteFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+
+        manufacturers = (
+            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
+            Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
+            Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
+        )
+        Manufacturer.objects.bulk_create(manufacturers)
 
-    def setUp(self):
+        device_types = (
+            DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', is_full_depth=True),
+            DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', is_full_depth=True),
+            DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', is_full_depth=False),
+        )
+        DeviceType.objects.bulk_create(device_types)
 
-        super().setUp()
+        device_roles = (
+            DeviceRole(name='Device Role 1', slug='device-role-1'),
+            DeviceRole(name='Device Role 2', slug='device-role-2'),
+            DeviceRole(name='Device Role 3', slug='device-role-3'),
+        )
+        DeviceRole.objects.bulk_create(device_roles)
 
-        self.region1 = Region.objects.create(name='Test Region 1', slug='test-region-1')
-        self.region2 = Region.objects.create(name='Test Region 2', slug='test-region-2')
-        self.site1 = Site.objects.create(region=self.region1, name='Test Site 1', slug='ABC-test-site1-ABC', asn=65001)
-        self.site2 = Site.objects.create(region=self.region2, name='Test Site 2', slug='def-test-site2-def', asn=65101)
-        self.site3 = Site.objects.create(region=None, name='Test Site 3', slug='ghi-test-site3-ghi', asn=65201)
+        platforms = (
+            Platform(name='Platform 1', slug='platform-1'),
+            Platform(name='Platform 2', slug='platform-2'),
+            Platform(name='Platform 3', slug='platform-3'),
+        )
+        Platform.objects.bulk_create(platforms)
 
-        self.queryset = Site.objects.all()
+        regions = (
+            Region(name='Region 1', slug='region-1'),
+            Region(name='Region 2', slug='region-2'),
+            Region(name='Region 3', slug='region-3'),
+        )
+        for region in regions:
+            region.save()
+
+        sites = (
+            Site(name='Site 1', slug='abc-site-1', region=regions[0], asn=65001),
+            Site(name='Site 2', slug='def-site-2', region=regions[1], asn=65101),
+            Site(name='Site 3', slug='ghi-site-3', region=regions[2], asn=65201),
+        )
+        Site.objects.bulk_create(sites)
+
+        racks = (
+            Rack(name='Rack 1', site=sites[0]),
+            Rack(name='Rack 2', site=sites[1]),
+            Rack(name='Rack 3', site=sites[2]),
+        )
+        Rack.objects.bulk_create(racks)
+
+        devices = (
+            Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, local_context_data={"foo": 123}),
+            Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED),
+            Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED),
+        )
+        Device.objects.bulk_create(devices)
+
+        interfaces = (
+            Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
+            Interface(device=devices[0], name='Interface 2', mac_address='aa-00-00-00-00-01'),
+            Interface(device=devices[1], name='Interface 3', mac_address='00-00-00-00-00-02'),
+            Interface(device=devices[1], name='Interface 4', mac_address='bb-00-00-00-00-02'),
+            Interface(device=devices[2], name='Interface 5', mac_address='00-00-00-00-00-03'),
+            Interface(device=devices[2], name='Interface 6', mac_address='cc-00-00-00-00-03'),
+        )
+        Interface.objects.bulk_create(interfaces)
 
     def test_site_name_negation(self):
-        params = {'name__n': ['Test Site 1']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        params = {'name__n': ['Site 1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_slug_icontains(self):
-        params = {'slug__ic': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 1)
+        params = {'slug__ic': ['-1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
 
     def test_site_slug_icontains_negation(self):
-        params = {'slug__nic': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        params = {'slug__nic': ['-1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_slug_startswith(self):
         params = {'slug__isw': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 1)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
 
     def test_site_slug_startswith_negation(self):
         params = {'slug__nisw': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_slug_endswith(self):
-        params = {'slug__iew': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 1)
+        params = {'slug__iew': ['-1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
 
     def test_site_slug_endswith_negation(self):
-        params = {'slug__niew': ['abc']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        params = {'slug__niew': ['-1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_asn_lt(self):
         params = {'asn__lt': [65101]}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 1)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
 
     def test_site_asn_lte(self):
         params = {'asn__lte': [65101]}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_asn_gt(self):
         params = {'asn__lt': [65101]}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 1)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
 
     def test_site_asn_gte(self):
         params = {'asn__gte': [65101]}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_region_negation(self):
-        params = {'region__n': ['test-region-1']}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        params = {'region__n': ['region-1']}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
 
     def test_site_region_id_negation(self):
-        params = {'region_id__n': [self.region1.pk]}
-        self.assertEqual(SiteFilterSet(params, self.queryset).qs.count(), 2)
+        params = {'region_id__n': [Region.objects.first().pk]}
+        self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
+
+    def test_device_name_eq(self):
+        params = {'name': ['Device 1']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
+
+    def test_device_name_negation(self):
+        params = {'name__n': ['Device 1']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_name_startswith(self):
+        params = {'name__isw': ['Device']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 3)
+
+    def test_device_name_startswith_negation(self):
+        params = {'name__nisw': ['Device 1']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_name_endswith(self):
+        params = {'name__iew': [' 1']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
+
+    def test_device_name_endswith_negation(self):
+        params = {'name__niew': [' 1']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_name_icontains(self):
+        params = {'name__ic': [' 2']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
+
+    def test_device_name_icontains_negation(self):
+        params = {'name__nic': [' ']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 0)
+
+    def test_device_mac_address_negation(self):
+        params = {'mac_address__n': ['00-00-00-00-00-01', 'aa-00-00-00-00-01']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_mac_address_startswith(self):
+        params = {'mac_address__isw': ['aa:']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
+
+    def test_device_mac_address_startswith_negation(self):
+        params = {'mac_address__nisw': ['aa:']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_mac_address_endswith(self):
+        params = {'mac_address__iew': [':02']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
+
+    def test_device_mac_address_endswith_negation(self):
+        params = {'mac_address__niew': [':02']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_mac_address_icontains(self):
+        params = {'mac_address__ic': ['aa:', 'bb']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
+
+    def test_device_mac_address_icontains_negation(self):
+        params = {'mac_address__nic': ['aa:', 'bb']}
+        self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)