Explorar o código

functional dynamic filter lookups

John Anderson %!s(int64=6) %!d(string=hai) anos
pai
achega
a6b43b30e9

+ 8 - 4
netbox/circuits/filters.py

@@ -29,12 +29,14 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilte
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='circuits__terminations__site__region__in',
+        field_name='circuits__terminations__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='circuits__terminations__site__region__in',
+        field_name='circuits__terminations__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -120,12 +122,14 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='terminations__site__region__in',
+        field_name='terminations__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='terminations__site__region__in',
+        field_name='terminations__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )

+ 56 - 38
netbox/dcim/filters.py

@@ -7,7 +7,7 @@ from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 from utilities.constants import COLOR_CHOICES
 from utilities.filters import (
 from utilities.filters import (
     BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
     BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
-    BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
+    NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from .choices import *
 from .choices import *
@@ -92,12 +92,14 @@ class SiteFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='region__in',
+        field_name='region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='region__in',
+        field_name='region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -134,12 +136,14 @@ class SiteFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
 class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -177,12 +181,14 @@ class RackFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -402,7 +408,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
         return queryset.exclude(device_bay_templates__isnull=value)
         return queryset.exclude(device_bay_templates__isnull=value)
 
 
 
 
-class DeviceTypeComponentFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
     devicetype_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         field_name='device_type_id',
         field_name='device_type_id',
@@ -410,56 +416,56 @@ class DeviceTypeComponentFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     )
     )
 
 
 
 
-class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet):
+class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
+class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
+class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = PowerPortTemplate
         model = PowerPortTemplate
         fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
         fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
 
 
 
 
-class PowerOutletTemplateFilterSet(DeviceTypeComponentFilterSet):
+class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = PowerOutletTemplate
         model = PowerOutletTemplate
         fields = ['id', 'name', 'type', 'feed_leg']
         fields = ['id', 'name', 'type', 'feed_leg']
 
 
 
 
-class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet):
+class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
         fields = ['id', 'name', 'type', 'mgmt_only']
         fields = ['id', 'name', 'type', 'mgmt_only']
 
 
 
 
-class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet):
+class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet):
+class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = ['id', 'name', 'type', 'positions']
         fields = ['id', 'name', 'type', 'positions']
 
 
 
 
-class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet):
+class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBayTemplate
         model = DeviceBayTemplate
@@ -538,12 +544,14 @@ class DeviceFilterSet(BaseFilterSet, LocalConfigContextFilterSet, CustomFieldFil
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -690,19 +698,21 @@ class DeviceFilterSet(BaseFilterSet, LocalConfigContextFilterSet, CustomFieldFil
         return queryset.exclude(device_bays__isnull=value)
         return queryset.exclude(device_bays__isnull=value)
 
 
 
 
-class DeviceComponentFilterSet(BaseFilterSet):
+class DeviceComponentFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='device__site__region__in',
+        field_name='device__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='device__site__region__in',
+        field_name='device__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -738,7 +748,7 @@ class DeviceComponentFilterSet(BaseFilterSet):
         )
         )
 
 
 
 
-class ConsolePortFilterSet(DeviceComponentFilterSet):
+class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -754,7 +764,7 @@ class ConsolePortFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'description', 'connection_status']
         fields = ['id', 'name', 'description', 'connection_status']
 
 
 
 
-class ConsoleServerPortFilterSet(DeviceComponentFilterSet):
+class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=ConsolePortTypeChoices,
         choices=ConsolePortTypeChoices,
         null_value=None
         null_value=None
@@ -770,7 +780,7 @@ class ConsoleServerPortFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'description', 'connection_status']
         fields = ['id', 'name', 'description', 'connection_status']
 
 
 
 
-class PowerPortFilterSet(DeviceComponentFilterSet):
+class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerPortTypeChoices,
         choices=PowerPortTypeChoices,
         null_value=None
         null_value=None
@@ -786,7 +796,7 @@ class PowerPortFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
         fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
 
 
 
 
-class PowerOutletFilterSet(DeviceComponentFilterSet):
+class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
         choices=PowerOutletTypeChoices,
         choices=PowerOutletTypeChoices,
         null_value=None
         null_value=None
@@ -802,7 +812,7 @@ class PowerOutletFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
         fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
 
 
 
 
-class InterfaceFilterSet(DeviceComponentFilterSet):
+class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -900,7 +910,7 @@ class InterfaceFilterSet(DeviceComponentFilterSet):
         }.get(value, queryset.none())
         }.get(value, queryset.none())
 
 
 
 
-class FrontPortFilterSet(DeviceComponentFilterSet):
+class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -912,7 +922,7 @@ class FrontPortFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'type', 'description']
         fields = ['id', 'name', 'type', 'description']
 
 
 
 
-class RearPortFilterSet(DeviceComponentFilterSet):
+class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -924,26 +934,28 @@ class RearPortFilterSet(DeviceComponentFilterSet):
         fields = ['id', 'name', 'type', 'positions', 'description']
         fields = ['id', 'name', 'type', 'positions', 'description']
 
 
 
 
-class DeviceBayFilterSet(DeviceComponentFilterSet):
+class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = ['id', 'name', 'description']
         fields = ['id', 'name', 'description']
 
 
 
 
-class InventoryItemFilterSet(DeviceComponentFilterSet):
+class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='device__site__region__in',
+        field_name='device__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='device__site__region__in',
+        field_name='device__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -1009,12 +1021,14 @@ class VirtualChassisFilterSet(BaseFilterSet):
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='master__site__region__in',
+        field_name='master__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='master__site__region__in',
+        field_name='master__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -1226,12 +1240,14 @@ class PowerPanelFilterSet(BaseFilterSet):
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -1275,12 +1291,14 @@ class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='power_panel__site__region__in',
+        field_name='power_panel__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='power_panel__site__region__in',
+        field_name='power_panel__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )

+ 1 - 1
netbox/extras/filters.py

@@ -235,7 +235,7 @@ class ConfigContextFilterSet(BaseFilterSet):
 # Filter for Local Config Context Data
 # Filter for Local Config Context Data
 #
 #
 
 
-class LocalConfigContextFilterSet(BaseFilterSet):
+class LocalConfigContextFilterSet(django_filters.FilterSet):
     local_context_data = django_filters.BooleanFilter(
     local_context_data = django_filters.BooleanFilter(
         method='_local_context_data',
         method='_local_context_data',
         label='Has local config context data',
         label='Has local config context data',

+ 6 - 6
netbox/extras/tests/test_filters.py

@@ -28,8 +28,8 @@ class GraphTestCase(TestCase):
         Graph.objects.bulk_create(graphs)
         Graph.objects.bulk_create(graphs)
 
 
     def test_name(self):
     def test_name(self):
-        params = {'name': 'Graph 1'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'name': ['Graph 1', 'Graph 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_type(self):
     def test_type(self):
         content_type = ContentType.objects.filter(GRAPH_MODELS).first()
         content_type = ContentType.objects.filter(GRAPH_MODELS).first()
@@ -59,8 +59,8 @@ class ExportTemplateTestCase(TestCase):
         ExportTemplate.objects.bulk_create(export_templates)
         ExportTemplate.objects.bulk_create(export_templates)
 
 
     def test_name(self):
     def test_name(self):
-        params = {'name': 'Export Template 1'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'name': ['Export Template 1', 'Export Template 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_content_type(self):
     def test_content_type(self):
         params = {'content_type': ContentType.objects.get(model='site').pk}
         params = {'content_type': ContentType.objects.get(model='site').pk}
@@ -154,8 +154,8 @@ class ConfigContextTestCase(TestCase):
             c.tenants.set([tenants[i]])
             c.tenants.set([tenants[i]])
 
 
     def test_name(self):
     def test_name(self):
-        params = {'name': 'Config Context 1'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'name': ['Config Context 1', 'Config Context 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_is_active(self):
     def test_is_active(self):
         params = {'is_active': True}
         params = {'is_active': True}

+ 23 - 16
netbox/ipam/filters.py

@@ -8,7 +8,8 @@ from dcim.models import Device, Interface, Region, Site
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
 from utilities.filters import (
-    MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
+    BaseFilterSet, MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
+    TreeNodeMultipleChoiceFilter,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from .choices import *
 from .choices import *
@@ -28,7 +29,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -53,7 +54,7 @@ class VRFFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterS
         fields = ['name', 'rd', 'enforce_unique']
         fields = ['name', 'rd', 'enforce_unique']
 
 
 
 
-class RIRFilterSet(NameSlugSearchFilterSet):
+class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -64,7 +65,7 @@ class RIRFilterSet(NameSlugSearchFilterSet):
         fields = ['name', 'slug', 'is_private']
         fields = ['name', 'slug', 'is_private']
 
 
 
 
-class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class AggregateFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -114,7 +115,7 @@ class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
             return queryset.none()
             return queryset.none()
 
 
 
 
-class RoleFilterSet(NameSlugSearchFilterSet):
+class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -125,7 +126,7 @@ class RoleFilterSet(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -166,12 +167,14 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -273,7 +276,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt
         return queryset.filter(prefix__net_mask_length=value)
         return queryset.filter(prefix__net_mask_length=value)
 
 
 
 
-class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -395,15 +398,17 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
         return queryset.exclude(interface__isnull=value)
         return queryset.exclude(interface__isnull=value)
 
 
 
 
-class VLANGroupFilterSet(NameSlugSearchFilterSet):
+class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -423,7 +428,7 @@ class VLANGroupFilterSet(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -434,12 +439,14 @@ class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -494,7 +501,7 @@ class VLANFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class ServiceFilterSet(CreatedUpdatedFilterSet):
+class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 3 - 3
netbox/secrets/filters.py

@@ -3,7 +3,7 @@ from django.db.models import Q
 
 
 from dcim.models import Device
 from dcim.models import Device
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
-from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
+from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter
 from .models import Secret, SecretRole
 from .models import Secret, SecretRole
 
 
 
 
@@ -13,14 +13,14 @@ __all__ = (
 )
 )
 
 
 
 
-class SecretRoleFilterSet(NameSlugSearchFilterSet):
+class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = SecretRole
         model = SecretRole
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class SecretFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'

+ 2 - 2
netbox/tenancy/filters.py

@@ -13,14 +13,14 @@ __all__ = (
 )
 )
 
 
 
 
-class TenantGroupFilterSet(NameSlugSearchFilterSet):
+class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class TenantFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class TenantFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'

+ 69 - 65
netbox/utilities/filters.py

@@ -1,9 +1,10 @@
 import django_filters
 import django_filters
+from copy import deepcopy
 from dcim.forms import MACAddressField
 from dcim.forms import MACAddressField
 from django import forms
 from django import forms
 from django.conf import settings
 from django.conf import settings
 from django.db import models
 from django.db import models
-from django_filters.utils import get_model_field
+from django_filters.utils import get_model_field, resolve_field
 
 
 from extras.models import Tag
 from extras.models import Tag
 from utilities.constants import (
 from utilities.constants import (
@@ -120,13 +121,62 @@ class BaseFilterSet(django_filters.FilterSet):
     """
     """
     A base filterset which provides common functionaly to all NetBox filtersets
     A base filterset which provides common functionaly to all NetBox filtersets
     """
     """
+    FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
+    FILTER_DEFAULTS.update({
+        models.AutoField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.CharField: {
+            'filter_class': MultiValueCharFilter
+        },
+        models.DateField: {
+            'filter_class': MultiValueDateFilter
+        },
+        models.DateTimeField: {
+            'filter_class': MultiValueDateTimeFilter
+        },
+        models.DecimalField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.EmailField: {
+            'filter_class': MultiValueCharFilter
+        },
+        models.FloatField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.IntegerField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.PositiveIntegerField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.PositiveSmallIntegerField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.SlugField: {
+            'filter_class': MultiValueCharFilter
+        },
+        models.SmallIntegerField: {
+            'filter_class': MultiValueNumberFilter
+        },
+        models.TimeField: {
+            'filter_class': MultiValueTimeFilter
+        },
+        models.URLField: {
+            'filter_class': MultiValueCharFilter
+        },
+        MACAddressField: {
+            'filter_class': MultiValueMACAddressFilter
+        },
+    })
+
     @classmethod
     @classmethod
     def get_filters(cls):
     def get_filters(cls):
         """
         """
         Override filter generation to support dynamic lookup expressions for certain filter types.
         Override filter generation to support dynamic lookup expressions for certain filter types.
 
 
         For specific filter types, new filters are created based on defined lookup expressions in
         For specific filter types, new filters are created based on defined lookup expressions in
-        the form `<field_name>_<lookup_expr>`
+        the form `<field_name>__<lookup_expr>`
         """
         """
         filters = super().get_filters()
         filters = super().get_filters()
 
 
@@ -136,7 +186,7 @@ class BaseFilterSet(django_filters.FilterSet):
 
 
             # It the filter makes use of a custom filter method or lookup expression skip it
             # It the filter makes use of a custom filter method or lookup expression skip it
             # as we cannot sanely handle these cases in a generic mannor
             # as we cannot sanely handle these cases in a generic mannor
-            if existing_filter.method is not None or existing_filter.lookup_expr != 'exact':
+            if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
                 continue
                 continue
 
 
             # Choose the lookup expression map based on the filter type
             # Choose the lookup expression map based on the filter type
@@ -169,7 +219,7 @@ class BaseFilterSet(django_filters.FilterSet):
                 )
                 )
 
 
             else:
             else:
-                # Do no augment any other filter types with more lookup expressions
+                # Do not augment any other filter types with more lookup expressions
                 continue
                 continue
 
 
             # Get properties of the existing filter for later use
             # Get properties of the existing filter for later use
@@ -178,24 +228,29 @@ class BaseFilterSet(django_filters.FilterSet):
 
 
             # Create new filters for each lookup expression in the map
             # Create new filters for each lookup expression in the map
             for lookup_name, lookup_expr in lookup_map.items():
             for lookup_name, lookup_expr in lookup_map.items():
-                new_filter_name = '{}_{}'.format(existing_filter_name, lookup_name)
-                
+                new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
+
                 try:
                 try:
-                    print(existing_filter_name)
-                    new_filter = cls.filter_for_field(field, field_name, lookup_expr)
+                    if existing_filter_name in cls.declared_filters:
+                        resolve_field(field, lookup_expr)  # Will raise FieldLookupError if the lookup is invalid
+                        new_filter = type(existing_filter)(
+                            field_name=field_name,
+                            lookup_expr=lookup_expr,
+                            label=existing_filter.label,
+                            exclude=existing_filter.exclude,
+                            distinct=existing_filter.distinct,
+                            **existing_filter.extra
+                        )
+                    else:
+                        new_filter = cls.filter_for_field(field, field_name, lookup_expr)
                 except django_filters.exceptions.FieldLookupError:
                 except django_filters.exceptions.FieldLookupError:
-                    # The filter could not be created because the lookup expression is not supported
+                    # The filter could not be created because the lookup expression is not supported on the field
                     continue
                     continue
 
 
-                help_text = FILTER_LOOKUP_HELP_TEXT_MAP[lookup_expr]
-                
                 if lookup_name.startswith('n'):
                 if lookup_name.startswith('n'):
                     # This is a negation filter which requires a queryselt.exclud() clause
                     # This is a negation filter which requires a queryselt.exclud() clause
                     new_filter.exclude = True
                     new_filter.exclude = True
-                    help_text = 'negated {}'.format(help_text)
 
 
-                new_filter.extra = existing_filter.extra
-                new_filter.extra['help_text'] = '{} - {}'.format(field_name, help_text)
                 new_filters[new_filter_name] = new_filter
                 new_filters[new_filter_name] = new_filter
 
 
         filters.update(new_filters)
         filters.update(new_filters)
@@ -218,54 +273,3 @@ class NameSlugSearchFilterSet(django_filters.FilterSet):
             models.Q(name__icontains=value) |
             models.Q(name__icontains=value) |
             models.Q(slug__icontains=value)
             models.Q(slug__icontains=value)
         )
         )
-
-
-#
-# Update default filters
-#
-
-FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
-FILTER_DEFAULTS.update({
-    models.AutoField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.CharField: {
-        'filter_class': MultiValueCharFilter
-    },
-    models.DateField: {
-        'filter_class': MultiValueDateFilter
-    },
-    models.DateTimeField: {
-        'filter_class': MultiValueDateTimeFilter
-    },
-    models.DecimalField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.EmailField: {
-        'filter_class': MultiValueCharFilter
-    },
-    models.FloatField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.IntegerField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.PositiveIntegerField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.PositiveSmallIntegerField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.SlugField: {
-        'filter_class': MultiValueCharFilter
-    },
-    models.SmallIntegerField: {
-        'filter_class': MultiValueNumberFilter
-    },
-    models.TimeField: {
-        'filter_class': MultiValueTimeFilter
-    },
-    models.URLField: {
-        'filter_class': MultiValueCharFilter
-    },
-})

+ 15 - 9
netbox/virtualization/filters.py

@@ -6,7 +6,8 @@ from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalC
 from tenancy.filters import TenancyFilterSet
 from tenancy.filters import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.filters import (
 from utilities.filters import (
-    MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
+    BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
+    TreeNodeMultipleChoiceFilter,
 )
 )
 from .choices import *
 from .choices import *
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -20,21 +21,21 @@ __all__ = (
 )
 )
 
 
 
 
-class ClusterTypeFilterSet(NameSlugSearchFilterSet):
+class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = ClusterType
         model = ClusterType
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class ClusterGroupFilterSet(NameSlugSearchFilterSet):
+class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = ClusterGroup
         model = ClusterGroup
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class ClusterFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -45,12 +46,14 @@ class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='site__region__in',
+        field_name='site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -104,6 +107,7 @@ class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
 
 
 
 
 class VirtualMachineFilterSet(
 class VirtualMachineFilterSet(
+    BaseFilterSet,
     LocalConfigContextFilterSet,
     LocalConfigContextFilterSet,
     TenancyFilterSet,
     TenancyFilterSet,
     CustomFieldFilterSet,
     CustomFieldFilterSet,
@@ -149,12 +153,14 @@ class VirtualMachineFilterSet(
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='cluster__site__region__in',
+        field_name='cluster__site__region',
+        lookup_expr='in',
         label='Region (ID)',
         label='Region (ID)',
     )
     )
     region = TreeNodeMultipleChoiceFilter(
     region = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
-        field_name='cluster__site__region__in',
+        field_name='cluster__site__region',
+        lookup_expr='in',
         to_field_name='slug',
         to_field_name='slug',
         label='Region (slug)',
         label='Region (slug)',
     )
     )
@@ -208,7 +214,7 @@ class VirtualMachineFilterSet(
         )
         )
 
 
 
 
-class InterfaceFilterSet(django_filters.FilterSet):
+class InterfaceFilterSet(BaseFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',