From 6cb5173e273a2dfd52e00b51b89adc8985414f50 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 May 2019 12:25:33 -0400 Subject: [PATCH 1/7] Update query filters to OR multiple values --- netbox/circuits/filters.py | 4 +- netbox/ipam/filters.py | 10 ++-- netbox/secrets/filters.py | 2 +- netbox/tenancy/filters.py | 2 +- netbox/utilities/filters.py | 102 ++++++++++++++++++++++++++++++++++-- 5 files changed, 108 insertions(+), 12 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 12955eeca..1ee0a9cf2 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -9,7 +9,7 @@ from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType -class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): +class ProviderFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): +class CircuitFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 2df680240..71597cfda 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -13,7 +13,7 @@ from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_ from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): +class VRFFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -59,7 +59,7 @@ class RIRFilter(NameSlugSearchFilterSet): fields = ['name', 'slug', 'is_private'] -class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): +class AggregateFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -107,7 +107,7 @@ class RoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): +class PrefixFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -254,7 +254,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): return queryset.filter(prefix__net_mask_length=value) -class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): +class IPAddressFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -395,7 +395,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): +class VLANFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 6548708b5..4be77cb15 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -14,7 +14,7 @@ class SecretRoleFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class SecretFilter(CustomFieldFilterSet, django_filters.FilterSet): +class SecretFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 2610b3ec0..f3acb62a4 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -13,7 +13,7 @@ class TenantGroupFilter(NameSlugSearchFilterSet): fields = ['name', 'slug'] -class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): +class TenantFilter(CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 6e2116751..614c09902 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,10 +1,51 @@ import django_filters +from django import forms from django.conf import settings -from django.db.models import Q +from django.db import models from extras.models import Tag +def multivalue_field_factory(field_class): + """ + Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple + filter values while maintaining the field's built-in vlaidation. Example: GET /api/dcim/devices/?name=foo&name=bar + """ + class NewField(field_class): + widget = forms.SelectMultiple + + def to_python(self, value): + if not value: + return [] + return [super(field_class, self).to_python(v) for v in value] + + return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict()) + + +# +# Filters +# + +class MultiValueCharFilter(django_filters.MultipleChoiceFilter): + field_class = multivalue_field_factory(forms.CharField) + + +class MultiValueDateFilter(django_filters.MultipleChoiceFilter): + field_class = multivalue_field_factory(forms.DateField) + + +class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter): + field_class = multivalue_field_factory(forms.DateTimeField) + + +class MultiValueNumberFilter(django_filters.MultipleChoiceFilter): + field_class = multivalue_field_factory(forms.IntegerField) + + +class MultiValueTimeFilter(django_filters.MultipleChoiceFilter): + field_class = multivalue_field_factory(forms.TimeField) + + class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): """ Filters for a set of Models, including all descendant models within a Tree. Example: [,] @@ -48,6 +89,10 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter): super().__init__(*args, **kwargs) +# +# FilterSets +# + class NameSlugSearchFilterSet(django_filters.FilterSet): """ A base class for adding the search method to models which only expose the `name` and `slug` fields @@ -61,6 +106,57 @@ class NameSlugSearchFilterSet(django_filters.FilterSet): if not value.strip(): return queryset return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) + 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: { + '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 + }, +}) From 3d616baf754ff96990438d0cc88f836aee7e5232 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 May 2019 13:07:01 -0400 Subject: [PATCH 2/7] Remove NumericInFilter and id__in filters --- netbox/circuits/filters.py | 14 ++-------- netbox/dcim/filters.py | 47 ++++++-------------------------- netbox/ipam/filters.py | 44 +++++++----------------------- netbox/secrets/filters.py | 8 ++---- netbox/tenancy/filters.py | 8 ++---- netbox/utilities/filters.py | 7 ----- netbox/virtualization/filters.py | 14 ++-------- 7 files changed, 29 insertions(+), 113 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 1ee0a9cf2..8fe0ba50c 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -4,16 +4,12 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType class ProviderFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -33,7 +29,7 @@ class ProviderFilter(CustomFieldFilterSet): class Meta: model = Provider - fields = ['name', 'slug', 'asn', 'account'] + fields = ['id', 'name', 'slug', 'asn', 'account'] def search(self, queryset, name, value): if not value.strip(): @@ -55,10 +51,6 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): class CircuitFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -112,7 +104,7 @@ class CircuitFilter(CustomFieldFilterSet): class Meta: model = Circuit - fields = ['cid', 'install_date', 'commit_rate'] + fields = ['id', 'cid', 'install_date', 'commit_rate'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 53163e7bd..f75686f88 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,5 @@ import django_filters from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI @@ -10,7 +9,7 @@ from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( - NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter + NameSlugSearchFilterSet, NullableCharFieldFilter, TagFilter, TreeNodeMultipleChoiceFilter ) from virtualization.models import Cluster from .constants import * @@ -41,10 +40,6 @@ class RegionFilter(NameSlugSearchFilterSet): class SiteFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -78,7 +73,7 @@ class SiteFilter(CustomFieldFilterSet): class Meta: model = Site - fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] + fields = ['id', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] def search(self, queryset, name, value): if not value.strip(): @@ -126,10 +121,6 @@ class RackRoleFilter(NameSlugSearchFilterSet): class RackFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -185,8 +176,8 @@ class RackFilter(CustomFieldFilterSet): class Meta: model = Rack fields = [ - 'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', - 'outer_unit', + 'id', 'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', ] def search(self, queryset, name, value): @@ -202,10 +193,6 @@ class RackFilter(CustomFieldFilterSet): class RackReservationFilter(django_filters.FilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -259,7 +246,7 @@ class RackReservationFilter(django_filters.FilterSet): class Meta: model = RackReservation - fields = ['created'] + fields = ['id', 'created'] def search(self, queryset, name, value): if not value.strip(): @@ -280,10 +267,6 @@ class ManufacturerFilter(NameSlugSearchFilterSet): class DeviceTypeFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -327,7 +310,7 @@ class DeviceTypeFilter(CustomFieldFilterSet): class Meta: model = DeviceType fields = [ - 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', ] def search(self, queryset, name, value): @@ -452,10 +435,6 @@ class PlatformFilter(NameSlugSearchFilterSet): class DeviceFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -602,7 +581,7 @@ class DeviceFilter(CustomFieldFilterSet): class Meta: model = Device - fields = ['serial', 'face'] + fields = ['id', 'serial', 'face'] def search(self, queryset, name, value): if not value.strip(): @@ -1088,10 +1067,6 @@ class InterfaceConnectionFilter(django_filters.FilterSet): class PowerPanelFilter(django_filters.FilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -1114,7 +1089,7 @@ class PowerPanelFilter(django_filters.FilterSet): class Meta: model = PowerPanel - fields = ['name'] + fields = ['id', 'name'] def search(self, queryset, name, value): if not value.strip(): @@ -1126,10 +1101,6 @@ class PowerPanelFilter(django_filters.FilterSet): class PowerFeedFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -1158,7 +1129,7 @@ class PowerFeedFilter(CustomFieldFilterSet): class Meta: model = PowerFeed - fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor'] + fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 71597cfda..9145d4ca3 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -7,17 +7,13 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF class VRFFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -34,6 +30,10 @@ class VRFFilter(CustomFieldFilterSet): ) tag = TagFilter() + class Meta: + model = VRF + fields = ['id', 'name', 'rd', 'enforce_unique'] + def search(self, queryset, name, value): if not value.strip(): return queryset @@ -43,27 +43,15 @@ class VRFFilter(CustomFieldFilterSet): Q(description__icontains=value) ) - class Meta: - model = VRF - fields = ['name', 'rd', 'enforce_unique'] - class RIRFilter(NameSlugSearchFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) class Meta: model = RIR - fields = ['name', 'slug', 'is_private'] + fields = ['id', 'name', 'slug', 'is_private'] class AggregateFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -82,7 +70,7 @@ class AggregateFilter(CustomFieldFilterSet): class Meta: model = Aggregate - fields = ['family', 'date_added'] + fields = ['id', 'family', 'date_added'] def search(self, queryset, name, value): if not value.strip(): @@ -108,10 +96,6 @@ class RoleFilter(NameSlugSearchFilterSet): class PrefixFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -192,7 +176,7 @@ class PrefixFilter(CustomFieldFilterSet): class Meta: model = Prefix - fields = ['family', 'is_pool'] + fields = ['id', 'family', 'is_pool'] def search(self, queryset, name, value): if not value.strip(): @@ -255,10 +239,6 @@ class PrefixFilter(CustomFieldFilterSet): class IPAddressFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -331,7 +311,7 @@ class IPAddressFilter(CustomFieldFilterSet): class Meta: model = IPAddress - fields = ['family', 'dns_name'] + fields = ['id', 'family', 'dns_name'] def search(self, queryset, name, value): if not value.strip(): @@ -396,10 +376,6 @@ class VLANGroupFilter(NameSlugSearchFilterSet): class VLANFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -452,7 +428,7 @@ class VLANFilter(CustomFieldFilterSet): class Meta: model = VLAN - fields = ['vid', 'name'] + fields = ['id', 'vid', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 4be77cb15..ad299a2d9 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Device from extras.filters import CustomFieldFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, TagFilter from .models import Secret, SecretRole @@ -15,10 +15,6 @@ class SecretRoleFilter(NameSlugSearchFilterSet): class SecretFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -47,7 +43,7 @@ class SecretFilter(CustomFieldFilterSet): class Meta: model = Secret - fields = ['name'] + fields = ['id', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index f3acb62a4..e193738a9 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -2,7 +2,7 @@ import django_filters from django.db.models import Q from extras.filters import CustomFieldFilterSet -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter +from utilities.filters import NameSlugSearchFilterSet, TagFilter from .models import Tenant, TenantGroup @@ -14,10 +14,6 @@ class TenantGroupFilter(NameSlugSearchFilterSet): class TenantFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -36,7 +32,7 @@ class TenantFilter(CustomFieldFilterSet): class Meta: model = Tenant - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 614c09902..b5db210de 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -55,13 +55,6 @@ class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): return super().filter(qs, value) -class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): - """ - Filters for a set of numeric values. Example: id__in=100,200,300 - """ - pass - - class NullableCharFieldFilter(django_filters.CharFilter): """ Allow matching on null field values by passing a special string used to signify NULL. diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 0e5ff6cd2..441783a31 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -27,10 +27,6 @@ class ClusterGroupFilter(NameSlugSearchFilterSet): class ClusterFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -69,7 +65,7 @@ class ClusterFilter(CustomFieldFilterSet): class Meta: model = Cluster - fields = ['name'] + fields = ['id', 'name'] def search(self, queryset, name, value): if not value.strip(): @@ -81,10 +77,6 @@ class ClusterFilter(CustomFieldFilterSet): class VirtualMachineFilter(CustomFieldFilterSet): - id__in = NumericInFilter( - field_name='id', - lookup_expr='in' - ) q = django_filters.CharFilter( method='search', label='Search', @@ -175,7 +167,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): class Meta: model = VirtualMachine - fields = ['name', 'cluster'] + fields = ['id', 'name', 'cluster'] def search(self, queryset, name, value): if not value.strip(): From 205adeb2e9008e36d8076fb7b4c212f9c2e9faf3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 May 2019 13:55:48 -0400 Subject: [PATCH 3/7] Remove NullableCharFieldFilter; add missing filter fields --- netbox/circuits/filters.py | 2 +- netbox/dcim/filters.py | 73 ++++++++++++------------- netbox/ipam/filters.py | 19 ++++++- netbox/secrets/filters.py | 2 +- netbox/tenancy/filters.py | 2 +- netbox/virtualization/filters.py | 9 ++- netbox/virtualization/tests/test_api.py | 2 +- 7 files changed, 60 insertions(+), 49 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 8fe0ba50c..64ba2a1cb 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -47,7 +47,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): class Meta: model = CircuitType - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class CircuitFilter(CustomFieldFilterSet): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index f75686f88..94224bf3c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,9 +8,7 @@ from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES -from utilities.filters import ( - NameSlugSearchFilterSet, NullableCharFieldFilter, TagFilter, TreeNodeMultipleChoiceFilter -) +from utilities.filters import NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter from virtualization.models import Cluster from .constants import * from .models import ( @@ -36,7 +34,7 @@ class RegionFilter(NameSlugSearchFilterSet): class Meta: model = Region - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class SiteFilter(CustomFieldFilterSet): @@ -73,7 +71,10 @@ class SiteFilter(CustomFieldFilterSet): class Meta: model = Site - fields = ['id', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] + fields = [ + 'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', + ] def search(self, queryset, name, value): if not value.strip(): @@ -110,14 +111,14 @@ class RackGroupFilter(NameSlugSearchFilterSet): class Meta: model = RackGroup - fields = ['site_id', 'name', 'slug'] + fields = ['id', 'name', 'slug'] class RackRoleFilter(NameSlugSearchFilterSet): class Meta: model = RackRole - fields = ['name', 'slug', 'color'] + fields = ['id', 'name', 'slug', 'color'] class RackFilter(CustomFieldFilterSet): @@ -125,7 +126,6 @@ class RackFilter(CustomFieldFilterSet): method='search', label='Search', ) - facility_id = NullableCharFieldFilter() site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -170,14 +170,13 @@ class RackFilter(CustomFieldFilterSet): to_field_name='slug', label='Role (slug)', ) - asset_tag = NullableCharFieldFilter() tag = TagFilter() class Meta: model = Rack fields = [ - 'id', 'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', - 'outer_depth', 'outer_unit', + 'id', 'name', 'facility_id', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', + 'outer_width', 'outer_depth', 'outer_unit', ] def search(self, queryset, name, value): @@ -263,7 +262,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet): class Meta: model = Manufacturer - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class DeviceTypeFilter(CustomFieldFilterSet): @@ -357,63 +356,63 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate - fields = ['name'] + fields = ['id', 'name'] class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate - fields = ['name'] + fields = ['id', 'name'] class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = PowerPortTemplate - fields = ['name'] + fields = ['id', 'name', 'maximum_draw', 'allocated_draw'] class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = PowerOutletTemplate - fields = ['name'] + fields = ['id', 'name', 'feed_leg'] class InterfaceTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = InterfaceTemplate - fields = ['name', 'type', 'mgmt_only'] + fields = ['id', 'name', 'type', 'mgmt_only'] class FrontPortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = FrontPortTemplate - fields = ['name', 'type'] + fields = ['id', 'name', 'type'] class RearPortTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = RearPortTemplate - fields = ['name', 'type'] + fields = ['id', 'name', 'type', 'positions'] class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate - fields = ['name'] + fields = ['id', 'name'] class DeviceRoleFilter(NameSlugSearchFilterSet): class Meta: model = DeviceRole - fields = ['name', 'slug', 'color', 'vm_role'] + fields = ['id', 'name', 'slug', 'color', 'vm_role'] class PlatformFilter(NameSlugSearchFilterSet): @@ -431,7 +430,7 @@ class PlatformFilter(NameSlugSearchFilterSet): class Meta: model = Platform - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug', 'napalm_driver'] class DeviceFilter(CustomFieldFilterSet): @@ -485,8 +484,6 @@ class DeviceFilter(CustomFieldFilterSet): to_field_name='slug', label='Platform (slug)', ) - name = NullableCharFieldFilter() - asset_tag = NullableCharFieldFilter() region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region__in', @@ -581,7 +578,7 @@ class DeviceFilter(CustomFieldFilterSet): class Meta: model = Device - fields = ['id', 'serial', 'face'] + fields = ['id', 'name', 'serial', 'asset_tag', 'face', 'vc_position', 'vc_priority'] def search(self, queryset, name, value): if not value.strip(): @@ -672,7 +669,7 @@ class ConsolePortFilter(DeviceComponentFilterSet): class Meta: model = ConsolePort - fields = ['name', 'description', 'connection_status'] + fields = ['id', 'name', 'description', 'connection_status'] class ConsoleServerPortFilter(DeviceComponentFilterSet): @@ -684,7 +681,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): class Meta: model = ConsoleServerPort - fields = ['name', 'description', 'connection_status'] + fields = ['id', 'name', 'description', 'connection_status'] class PowerPortFilter(DeviceComponentFilterSet): @@ -696,7 +693,7 @@ class PowerPortFilter(DeviceComponentFilterSet): class Meta: model = PowerPort - fields = ['name', 'description', 'connection_status'] + fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] class PowerOutletFilter(DeviceComponentFilterSet): @@ -708,7 +705,7 @@ class PowerOutletFilter(DeviceComponentFilterSet): class Meta: model = PowerOutlet - fields = ['name', 'description', 'connection_status'] + fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] class InterfaceFilter(django_filters.FilterSet): @@ -763,7 +760,7 @@ class InterfaceFilter(django_filters.FilterSet): class Meta: model = Interface - fields = ['name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'description'] + fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] def search(self, queryset, name, value): if not value.strip(): @@ -827,7 +824,7 @@ class FrontPortFilter(DeviceComponentFilterSet): class Meta: model = FrontPort - fields = ['name', 'type', 'description'] + fields = ['id', 'name', 'type', 'description'] class RearPortFilter(DeviceComponentFilterSet): @@ -839,14 +836,14 @@ class RearPortFilter(DeviceComponentFilterSet): class Meta: model = RearPort - fields = ['name', 'type', 'description'] + fields = ['id', 'name', 'type', 'positions', 'description'] class DeviceBayFilter(DeviceComponentFilterSet): class Meta: model = DeviceBay - fields = ['name', 'description'] + fields = ['id', 'name', 'description'] class InventoryItemFilter(DeviceComponentFilterSet): @@ -877,11 +874,10 @@ class InventoryItemFilter(DeviceComponentFilterSet): to_field_name='slug', label='Manufacturer (slug)', ) - asset_tag = NullableCharFieldFilter() class Meta: model = InventoryItem - fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered'] + fields = ['id', 'name', 'part_id', 'serial', 'asset_tag', 'discovered'] def search(self, queryset, name, value): if not value.strip(): @@ -927,7 +923,7 @@ class VirtualChassisFilter(django_filters.FilterSet): class Meta: model = VirtualChassis - fields = ['domain'] + fields = ['id', 'domain'] def search(self, queryset, name, value): if not value.strip(): @@ -947,6 +943,9 @@ class CableFilter(django_filters.FilterSet): type = django_filters.MultipleChoiceFilter( choices=CABLE_TYPE_CHOICES ) + status = django_filters.MultipleChoiceFilter( + choices=CONNECTION_STATUS_CHOICES + ) color = django_filters.MultipleChoiceFilter( choices=COLOR_CHOICES ) @@ -961,7 +960,7 @@ class CableFilter(django_filters.FilterSet): class Meta: model = Cable - fields = ['type', 'status', 'color', 'length', 'length_unit'] + fields = ['id', 'label', 'length', 'length_unit'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 9145d4ca3..b4e64262a 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -56,6 +56,10 @@ class AggregateFilter(CustomFieldFilterSet): method='search', label='Search', ) + prefix = django_filters.CharFilter( + method='filter_prefix', + label='Prefix', + ) rir_id = django_filters.ModelMultipleChoiceFilter( queryset=RIR.objects.all(), label='RIR (ID)', @@ -83,6 +87,15 @@ class AggregateFilter(CustomFieldFilterSet): pass return queryset.filter(qs_filter) + def filter_prefix(self, queryset, name, value): + if not value.strip(): + return queryset + try: + query = str(netaddr.IPNetwork(value).cidr) + return queryset.filter(prefix=query) + except ValidationError: + return queryset.none() + class RoleFilter(NameSlugSearchFilterSet): q = django_filters.CharFilter( @@ -92,7 +105,7 @@ class RoleFilter(NameSlugSearchFilterSet): class Meta: model = Role - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class PrefixFilter(CustomFieldFilterSet): @@ -372,7 +385,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet): class Meta: model = VLANGroup - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class VLANFilter(CustomFieldFilterSet): @@ -470,7 +483,7 @@ class ServiceFilter(django_filters.FilterSet): class Meta: model = Service - fields = ['name', 'protocol', 'port'] + fields = ['id', 'name', 'protocol', 'port'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index ad299a2d9..a06791498 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -11,7 +11,7 @@ class SecretRoleFilter(NameSlugSearchFilterSet): class Meta: model = SecretRole - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class SecretFilter(CustomFieldFilterSet): diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index e193738a9..52e13425c 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -10,7 +10,7 @@ class TenantGroupFilter(NameSlugSearchFilterSet): class Meta: model = TenantGroup - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class TenantFilter(CustomFieldFilterSet): diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 441783a31..69e1493cd 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -1,5 +1,4 @@ import django_filters -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -16,14 +15,14 @@ class ClusterTypeFilter(NameSlugSearchFilterSet): class Meta: model = ClusterType - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class ClusterGroupFilter(NameSlugSearchFilterSet): class Meta: model = ClusterGroup - fields = ['name', 'slug'] + fields = ['id', 'name', 'slug'] class ClusterFilter(CustomFieldFilterSet): @@ -167,7 +166,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): class Meta: model = VirtualMachine - fields = ['id', 'name', 'cluster'] + fields = ['id', 'name', 'cluster', 'vcpus', 'memory', 'disk'] def search(self, queryset, name, value): if not value.strip(): @@ -201,7 +200,7 @@ class InterfaceFilter(django_filters.FilterSet): class Meta: model = Interface - fields = ['name', 'enabled', 'mtu'] + fields = ['id', 'name', 'enabled', 'mtu'] def _mac_address(self, queryset, name, value): value = value.strip() diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 814e05854..f1e372dd4 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -464,7 +464,7 @@ class VirtualMachineTest(APITestCase): def test_config_context_included_by_default_in_list_view(self): url = reverse('virtualization-api:virtualmachine-list') - url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk) + url = '{}?id={}'.format(url, self.virtualmachine_with_context_data.pk) response = self.client.get(url, **self.header) self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1) From 8e8c9822ea42371dea0723aaf635fb1bc5ba4a71 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 May 2019 14:12:56 -0400 Subject: [PATCH 4/7] Correct Device.position filter --- netbox/dcim/filters.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 94224bf3c..d57e44882 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -515,10 +515,6 @@ class DeviceFilter(CustomFieldFilterSet): queryset=Rack.objects.all(), label='Rack (ID)', ) - position = django_filters.ChoiceFilter( - choices=DEVICE_POSITION_CHOICES, - null_label='Non-racked' - ) cluster_id = django_filters.ModelMultipleChoiceFilter( queryset=Cluster.objects.all(), label='VM cluster (ID)', @@ -578,7 +574,7 @@ class DeviceFilter(CustomFieldFilterSet): class Meta: model = Device - fields = ['id', 'name', 'serial', 'asset_tag', 'face', 'vc_position', 'vc_priority'] + fields = ['id', 'name', 'serial', 'asset_tag', 'face', 'position', 'vc_position', 'vc_priority'] def search(self, queryset, name, value): if not value.strip(): From ffa34c61332d0ec5db918e29ccd029a63a9f28d9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 May 2019 14:23:02 -0400 Subject: [PATCH 5/7] Updated documentation --- CHANGELOG.md | 2 ++ docs/api/overview.md | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2b7ee83..36ab646f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,6 +186,7 @@ functionality provided by the front end UI. * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add `comments` field for tags * [#2920](https://github.com/digitalocean/netbox/issues/2920) - Rename Interface `form_factor` to `type` (backward-compatible until v2.7) * [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add change logging to the Tag model +* [#3038](https://github.com/digitalocean/netbox/issues/3038) - OR logic now used when multiple values of a query filter are passed ## API Changes @@ -193,6 +194,7 @@ functionality provided by the front end UI. * New API endpoint for custom field choices: `/api/extras/_custom_field_choices/` * ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object. * Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`. +* Removed the `id__in` filter from all models. (Use `?id=1&id=2` format instead.) * Added a `description` field for all device components. * dcim.Device: The devices list endpoint now includes rendered context data. * dcim.DeviceType: `instance_count` has been renamed to `device_count`. diff --git a/docs/api/overview.md b/docs/api/overview.md index 6b9a1a429..e74a12371 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -274,12 +274,31 @@ A list of objects retrieved via the API can be filtered by passing one or more q GET /api/ipam/prefixes/?status=1 ``` -Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes: +The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`: ``` -GET /api/ipam/prefixes/?status=1&status=2 +"prefix:status": [ + { + "label": "Container", + "value": 0 + }, + { + "label": "Active", + "value": 1 + }, + { + "label": "Reserved", + "value": 2 + }, + { + "label": "Deprecated", + "value": 3 + } +], ``` +For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar". + ## Custom Fields To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123: From dfffd1ea943f8c71b8ad8912110d2e3390e5c6d6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 8 May 2019 21:08:35 -0400 Subject: [PATCH 6/7] Restore id__in filters to retain backward compatability until v2.7 --- netbox/circuits/filters.py | 14 +++++++--- netbox/dcim/filters.py | 38 +++++++++++++++++++++++---- netbox/ipam/filters.py | 44 ++++++++++++++++++++++++-------- netbox/secrets/filters.py | 8 ++++-- netbox/tenancy/filters.py | 8 ++++-- netbox/utilities/filters.py | 7 +++++ netbox/virtualization/filters.py | 12 +++++++-- 7 files changed, 107 insertions(+), 24 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 64ba2a1cb..02e95019a 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -4,12 +4,16 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType class ProviderFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -29,7 +33,7 @@ class ProviderFilter(CustomFieldFilterSet): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'asn', 'account'] + fields = ['name', 'slug', 'asn', 'account'] def search(self, queryset, name, value): if not value.strip(): @@ -51,6 +55,10 @@ class CircuitTypeFilter(NameSlugSearchFilterSet): class CircuitFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -104,7 +112,7 @@ class CircuitFilter(CustomFieldFilterSet): class Meta: model = Circuit - fields = ['id', 'cid', 'install_date', 'commit_rate'] + fields = ['cid', 'install_date', 'commit_rate'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index d57e44882..48c38dd30 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -8,7 +8,7 @@ from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES -from utilities.filters import NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from virtualization.models import Cluster from .constants import * from .models import ( @@ -38,6 +38,10 @@ class RegionFilter(NameSlugSearchFilterSet): class SiteFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -122,6 +126,10 @@ class RackRoleFilter(NameSlugSearchFilterSet): class RackFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -192,6 +200,10 @@ class RackFilter(CustomFieldFilterSet): class RackReservationFilter(django_filters.FilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -245,7 +257,7 @@ class RackReservationFilter(django_filters.FilterSet): class Meta: model = RackReservation - fields = ['id', 'created'] + fields = ['created'] def search(self, queryset, name, value): if not value.strip(): @@ -266,6 +278,10 @@ class ManufacturerFilter(NameSlugSearchFilterSet): class DeviceTypeFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -309,7 +325,7 @@ class DeviceTypeFilter(CustomFieldFilterSet): class Meta: model = DeviceType fields = [ - 'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', ] def search(self, queryset, name, value): @@ -434,6 +450,10 @@ class PlatformFilter(NameSlugSearchFilterSet): class DeviceFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -1062,6 +1082,10 @@ class InterfaceConnectionFilter(django_filters.FilterSet): class PowerPanelFilter(django_filters.FilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -1084,7 +1108,7 @@ class PowerPanelFilter(django_filters.FilterSet): class Meta: model = PowerPanel - fields = ['id', 'name'] + fields = ['name'] def search(self, queryset, name, value): if not value.strip(): @@ -1096,6 +1120,10 @@ class PowerPanelFilter(django_filters.FilterSet): class PowerFeedFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -1124,7 +1152,7 @@ class PowerFeedFilter(CustomFieldFilterSet): class Meta: model = PowerFeed - fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor'] + fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index b4e64262a..5558094eb 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -7,13 +7,17 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF class VRFFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -30,10 +34,6 @@ class VRFFilter(CustomFieldFilterSet): ) tag = TagFilter() - class Meta: - model = VRF - fields = ['id', 'name', 'rd', 'enforce_unique'] - def search(self, queryset, name, value): if not value.strip(): return queryset @@ -43,15 +43,27 @@ class VRFFilter(CustomFieldFilterSet): Q(description__icontains=value) ) + class Meta: + model = VRF + fields = ['name', 'rd', 'enforce_unique'] + class RIRFilter(NameSlugSearchFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) class Meta: model = RIR - fields = ['id', 'name', 'slug', 'is_private'] + fields = ['name', 'slug', 'is_private'] class AggregateFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -74,7 +86,7 @@ class AggregateFilter(CustomFieldFilterSet): class Meta: model = Aggregate - fields = ['id', 'family', 'date_added'] + fields = ['family', 'date_added'] def search(self, queryset, name, value): if not value.strip(): @@ -109,6 +121,10 @@ class RoleFilter(NameSlugSearchFilterSet): class PrefixFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -189,7 +205,7 @@ class PrefixFilter(CustomFieldFilterSet): class Meta: model = Prefix - fields = ['id', 'family', 'is_pool'] + fields = ['family', 'is_pool'] def search(self, queryset, name, value): if not value.strip(): @@ -252,6 +268,10 @@ class PrefixFilter(CustomFieldFilterSet): class IPAddressFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -324,7 +344,7 @@ class IPAddressFilter(CustomFieldFilterSet): class Meta: model = IPAddress - fields = ['id', 'family', 'dns_name'] + fields = ['family', 'dns_name'] def search(self, queryset, name, value): if not value.strip(): @@ -389,6 +409,10 @@ class VLANGroupFilter(NameSlugSearchFilterSet): class VLANFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -441,7 +465,7 @@ class VLANFilter(CustomFieldFilterSet): class Meta: model = VLAN - fields = ['id', 'vid', 'name'] + fields = ['vid', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index a06791498..628d716db 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -3,7 +3,7 @@ from django.db.models import Q from dcim.models import Device from extras.filters import CustomFieldFilterSet -from utilities.filters import NameSlugSearchFilterSet, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .models import Secret, SecretRole @@ -15,6 +15,10 @@ class SecretRoleFilter(NameSlugSearchFilterSet): class SecretFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -43,7 +47,7 @@ class SecretFilter(CustomFieldFilterSet): class Meta: model = Secret - fields = ['id', 'name'] + fields = ['name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 52e13425c..acb0fa0cc 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -2,7 +2,7 @@ import django_filters from django.db.models import Q from extras.filters import CustomFieldFilterSet -from utilities.filters import NameSlugSearchFilterSet, TagFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from .models import Tenant, TenantGroup @@ -14,6 +14,10 @@ class TenantGroupFilter(NameSlugSearchFilterSet): class TenantFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -32,7 +36,7 @@ class TenantFilter(CustomFieldFilterSet): class Meta: model = Tenant - fields = ['id', 'name', 'slug'] + fields = ['name', 'slug'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index b5db210de..614c09902 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -55,6 +55,13 @@ class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter): return super().filter(qs, value) +class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter): + """ + Filters for a set of numeric values. Example: id__in=100,200,300 + """ + pass + + class NullableCharFieldFilter(django_filters.CharFilter): """ Allow matching on null field values by passing a special string used to signify NULL. diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 69e1493cd..ec6487704 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -6,7 +6,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter +from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -26,6 +26,10 @@ class ClusterGroupFilter(NameSlugSearchFilterSet): class ClusterFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', @@ -64,7 +68,7 @@ class ClusterFilter(CustomFieldFilterSet): class Meta: model = Cluster - fields = ['id', 'name'] + fields = ['name'] def search(self, queryset, name, value): if not value.strip(): @@ -76,6 +80,10 @@ class ClusterFilter(CustomFieldFilterSet): class VirtualMachineFilter(CustomFieldFilterSet): + id__in = NumericInFilter( + field_name='id', + lookup_expr='in' + ) q = django_filters.CharFilter( method='search', label='Search', From 53b5fed8ae5400a3ad5209bf15d63b5a5c793f47 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 8 May 2019 21:10:49 -0400 Subject: [PATCH 7/7] Tweak docs to indicate deprecation of id__in filter --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ab646f4..19225f7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,7 +194,7 @@ functionality provided by the front end UI. * New API endpoint for custom field choices: `/api/extras/_custom_field_choices/` * ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object. * Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`. -* Removed the `id__in` filter from all models. (Use `?id=1&id=2` format instead.) +* The `id__in` filter is now deprecated and will be removed in v2.7. (Begin using the `?id=1&id=2` format instead.) * Added a `description` field for all device components. * dcim.Device: The devices list endpoint now includes rendered context data. * dcim.DeviceType: `instance_count` has been renamed to `device_count`.