functional dynamic filter lookups

This commit is contained in:
John Anderson 2020-02-09 17:46:21 -05:00
parent a311002141
commit a6b43b30e9
9 changed files with 183 additions and 144 deletions

View File

@ -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)',
) )

View File

@ -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)',
) )

View File

@ -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',

View File

@ -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'} params = {'name': ['Graph 1', 'Graph 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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'} params = {'name': ['Export Template 1', 'Export Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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'} params = {'name': ['Config Context 1', 'Config Context 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) 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}

View File

@ -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',

View File

@ -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'

View File

@ -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'

View File

@ -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,112 +121,8 @@ 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
""" """
@classmethod FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
def get_filters(cls): FILTER_DEFAULTS.update({
"""
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
the form `<field_name>_<lookup_expr>`
"""
filters = super().get_filters()
new_filters = {}
for existing_filter_name, existing_filter in filters.items():
# Loop over existing filters to extract metadata by which to create new filters
# 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
if existing_filter.method is not None or existing_filter.lookup_expr != 'exact':
continue
# Choose the lookup expression map based on the filter type
if isinstance(existing_filter, (
django_filters.filters.CharFilter,
django_filters.MultipleChoiceFilter,
MultiValueCharFilter,
MultiValueMACAddressFilter,
TagFilter
)):
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
MultiValueDateFilter,
MultiValueDateTimeFilter,
MultiValueNumberFilter,
MultiValueTimeFilter
)):
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
NumericInFilter,
TreeNodeMultipleChoiceFilter,
)):
# These filter types support only negation
lookup_map = dict(
n='exact'
)
else:
# Do no augment any other filter types with more lookup expressions
continue
# Get properties of the existing filter for later use
field_name = existing_filter.field_name
field = get_model_field(cls._meta.model, field_name)
# Create new filters for each lookup expression in the map
for lookup_name, lookup_expr in lookup_map.items():
new_filter_name = '{}_{}'.format(existing_filter_name, lookup_name)
try:
print(existing_filter_name)
new_filter = cls.filter_for_field(field, field_name, lookup_expr)
except django_filters.exceptions.FieldLookupError:
# The filter could not be created because the lookup expression is not supported
continue
help_text = FILTER_LOOKUP_HELP_TEXT_MAP[lookup_expr]
if lookup_name.startswith('n'):
# This is a negation filter which requires a queryselt.exclud() clause
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
filters.update(new_filters)
return filters
class NameSlugSearchFilterSet(django_filters.FilterSet):
"""
A base class for adding the search method to models which only expose the `name` and `slug` fields
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
models.Q(name__icontains=value) |
models.Q(slug__icontains=value)
)
#
# Update default filters
#
FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
FILTER_DEFAULTS.update({
models.AutoField: { models.AutoField: {
'filter_class': MultiValueNumberFilter 'filter_class': MultiValueNumberFilter
}, },
@ -268,4 +165,111 @@ FILTER_DEFAULTS.update({
models.URLField: { models.URLField: {
'filter_class': MultiValueCharFilter 'filter_class': MultiValueCharFilter
}, },
}) MACAddressField: {
'filter_class': MultiValueMACAddressFilter
},
})
@classmethod
def get_filters(cls):
"""
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
the form `<field_name>__<lookup_expr>`
"""
filters = super().get_filters()
new_filters = {}
for existing_filter_name, existing_filter in filters.items():
# Loop over existing filters to extract metadata by which to create new filters
# 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
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
continue
# Choose the lookup expression map based on the filter type
if isinstance(existing_filter, (
django_filters.filters.CharFilter,
django_filters.MultipleChoiceFilter,
MultiValueCharFilter,
MultiValueMACAddressFilter,
TagFilter
)):
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
MultiValueDateFilter,
MultiValueDateTimeFilter,
MultiValueNumberFilter,
MultiValueTimeFilter
)):
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
NumericInFilter,
TreeNodeMultipleChoiceFilter,
)):
# These filter types support only negation
lookup_map = dict(
n='exact'
)
else:
# Do not augment any other filter types with more lookup expressions
continue
# Get properties of the existing filter for later use
field_name = existing_filter.field_name
field = get_model_field(cls._meta.model, field_name)
# Create new filters for each lookup expression in the map
for lookup_name, lookup_expr in lookup_map.items():
new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
try:
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:
# The filter could not be created because the lookup expression is not supported on the field
continue
if lookup_name.startswith('n'):
# This is a negation filter which requires a queryselt.exclud() clause
new_filter.exclude = True
new_filters[new_filter_name] = new_filter
filters.update(new_filters)
return filters
class NameSlugSearchFilterSet(django_filters.FilterSet):
"""
A base class for adding the search method to models which only expose the `name` and `slug` fields
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
models.Q(name__icontains=value) |
models.Q(slug__icontains=value)
)

View File

@ -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',