Merge pull request #4189 from netbox-community/4121-filter-lookup-expressions

4121 filter lookup expressions
This commit is contained in:
Jeremy Stretch 2020-03-04 11:51:19 -05:00 committed by GitHub
commit 1dd07337fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 885 additions and 161 deletions

71
docs/api/filtering.md Normal file
View File

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

View File

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

View File

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

View File

@ -4,7 +4,9 @@ from django.db.models import Q
from dcim.models import Region, Site from dcim.models import 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 NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import (
BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
)
from .choices import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -16,7 +18,7 @@ __all__ = (
) )
class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -27,12 +29,14 @@ class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
) )
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)',
) )
@ -65,14 +69,14 @@ class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
) )
class CircuitTypeFilterSet(NameSlugSearchFilterSet): class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -118,12 +122,14 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil
) )
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)',
) )
@ -146,7 +152,7 @@ class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFil
).distinct() ).distinct()
class CircuitTerminationFilterSet(django_filters.FilterSet): class CircuitTerminationFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -6,8 +6,8 @@ from tenancy.filters import TenancyFilterSet
from tenancy.models import Tenant 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 (
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
TagFilter, TreeNodeMultipleChoiceFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import * from .choices import *
@ -60,7 +60,7 @@ __all__ = (
) )
class RegionFilterSet(NameSlugSearchFilterSet): class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@ -77,7 +77,7 @@ class RegionFilterSet(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -92,12 +92,14 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
) )
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)',
) )
@ -131,15 +133,17 @@ class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RackGroupFilterSet(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)',
) )
@ -159,14 +163,14 @@ class RackGroupFilterSet(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class RackRoleFilterSet(NameSlugSearchFilterSet): class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -177,12 +181,14 @@ class RackFilterSet(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)',
) )
@ -244,7 +250,7 @@ class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilter
) )
class RackReservationFilterSet(TenancyFilterSet): class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -305,14 +311,14 @@ class RackReservationFilterSet(TenancyFilterSet):
) )
class ManufacturerFilterSet(NameSlugSearchFilterSet): class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -410,70 +416,70 @@ class DeviceTypeComponentFilterSet(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
fields = ['id', 'name'] fields = ['id', 'name']
class DeviceRoleFilterSet(NameSlugSearchFilterSet): class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role'] fields = ['id', 'name', 'slug', 'color', 'vm_role']
class PlatformFilterSet(NameSlugSearchFilterSet): class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer', field_name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -491,7 +497,13 @@ class PlatformFilterSet(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceFilterSet(
BaseFilterSet,
TenancyFilterSet,
LocalConfigContextFilterSet,
CustomFieldFilterSet,
CreatedUpdatedFilterSet
):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -538,12 +550,14 @@ class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomField
) )
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)',
) )
@ -697,12 +711,14 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
) )
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 +754,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
) )
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 +770,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 +786,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 +802,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 +818,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 +916,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 +928,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 +940,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)',
) )
@ -1002,19 +1020,21 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class VirtualChassisFilterSet(django_filters.FilterSet): class VirtualChassisFilterSet(BaseFilterSet):
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='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)',
) )
@ -1056,7 +1076,7 @@ class VirtualChassisFilterSet(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class CableFilterSet(django_filters.FilterSet): class CableFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1119,7 +1139,7 @@ class CableFilterSet(django_filters.FilterSet):
return queryset return queryset
class ConsoleConnectionFilterSet(django_filters.FilterSet): class ConsoleConnectionFilterSet(BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1150,7 +1170,7 @@ class ConsoleConnectionFilterSet(django_filters.FilterSet):
) )
class PowerConnectionFilterSet(django_filters.FilterSet): class PowerConnectionFilterSet(BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1181,7 +1201,7 @@ class PowerConnectionFilterSet(django_filters.FilterSet):
) )
class InterfaceConnectionFilterSet(django_filters.FilterSet): class InterfaceConnectionFilterSet(BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1215,7 +1235,7 @@ class InterfaceConnectionFilterSet(django_filters.FilterSet):
) )
class PowerPanelFilterSet(django_filters.FilterSet): class PowerPanelFilterSet(BaseFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -1226,12 +1246,14 @@ class PowerPanelFilterSet(django_filters.FilterSet):
) )
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)',
) )
@ -1264,7 +1286,7 @@ class PowerPanelFilterSet(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -1275,12 +1297,14 @@ class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
) )
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

@ -4,6 +4,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.filters import BaseFilterSet
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
from .choices import * from .choices import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
@ -89,21 +90,21 @@ class CustomFieldFilterSet(django_filters.FilterSet):
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
class GraphFilterSet(django_filters.FilterSet): class GraphFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Graph model = Graph
fields = ['type', 'name', 'template_language'] fields = ['type', 'name', 'template_language']
class ExportTemplateFilterSet(django_filters.FilterSet): class ExportTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['content_type', 'name', 'template_language'] fields = ['content_type', 'name', 'template_language']
class TagFilterSet(django_filters.FilterSet): class TagFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -122,7 +123,7 @@ class TagFilterSet(django_filters.FilterSet):
) )
class ConfigContextFilterSet(django_filters.FilterSet): class ConfigContextFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -244,7 +245,7 @@ class LocalConfigContextFilterSet(django_filters.FilterSet):
return queryset.exclude(local_context_data__isnull=value) return queryset.exclude(local_context_data__isnull=value)
class ObjectChangeFilterSet(django_filters.FilterSet): class ObjectChangeFilterSet(BaseFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

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, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, 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'
@ -397,15 +400,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)',
) )
@ -425,7 +430,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'
@ -436,12 +441,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)',
) )
@ -496,7 +503,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

@ -2,7 +2,7 @@ import django_filters
from django.db.models import Q from django.db.models import Q
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 Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -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

@ -28,12 +28,47 @@ COLOR_CHOICES = (
('ffffff', 'White'), ('ffffff', 'White'),
) )
#
# Filter lookup expressions
#
FILTER_CHAR_BASED_LOOKUP_MAP = dict(
n='exact',
ic='icontains',
nic='icontains',
iew='iendswith',
niew='iendswith',
isw='istartswith',
nisw='istartswith',
ie='iexact',
nie='iexact'
)
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
n='exact',
lte='lte',
lt='lt',
gte='gte',
gt='gt'
)
FILTER_NEGATION_LOOKUP_MAP = dict(
n='exact'
)
FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
n='in'
)
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by # Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
# the advisory_lock contextmanager. When a lock is acquired, # the advisory_lock contextmanager. When a lock is acquired,
# one of these keys will be used to identify said lock. # one of these keys will be used to identify said lock.
# #
# When adding a new key, pick something arbitrary and unique so # When adding a new key, pick something arbitrary and unique so
# that it is easily searchable in query logs. # that it is easily searchable in query logs.
ADVISORY_LOCK_KEYS = { ADVISORY_LOCK_KEYS = {
'available-prefixes': 100100, 'available-prefixes': 100100,
'available-ips': 100200, 'available-ips': 100200,

View File

@ -1,9 +1,16 @@
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, resolve_field
from extras.models import Tag from extras.models import Tag
from utilities.constants import (
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
FILTER_NUMERIC_BASED_LOOKUP_MAP
)
def multivalue_field_factory(field_class): def multivalue_field_factory(field_class):
@ -111,6 +118,165 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
# FilterSets # FilterSets
# #
class BaseFilterSet(django_filters.FilterSet):
"""
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
},
})
@staticmethod
def _get_filter_lookup_dict(existing_filter):
# Choose the lookup expression map based on the filter type
if isinstance(existing_filter, (
MultiValueDateFilter,
MultiValueDateTimeFilter,
MultiValueNumberFilter,
MultiValueTimeFilter
)):
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
elif isinstance(existing_filter, (
TreeNodeMultipleChoiceFilter,
)):
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.ModelChoiceFilter,
django_filters.ModelMultipleChoiceFilter,
TagFilter
)) or existing_filter.extra.get('choices'):
# These filter types support only negation
lookup_map = FILTER_NEGATION_LOOKUP_MAP
elif isinstance(existing_filter, (
django_filters.filters.CharFilter,
django_filters.MultipleChoiceFilter,
MultiValueCharFilter,
MultiValueMACAddressFilter
)):
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
else:
lookup_map = None
return lookup_map
@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>`
"""
# TODO: once 3.6 is the minimum required version of python, change this to a bare super() call
# We have to do it this way in py3.5 becuase of django_filters.FilterSet's use of a metaclass
filters = super(django_filters.FilterSet, cls).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
# If 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
lookup_map = cls._get_filter_lookup_dict(existing_filter)
if lookup_map is None:
# Do not augment this filter type 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:
# The filter field has been explicity defined on the filterset class so we must manually
# create the new filter with the same type because there is no guarantee the defined type
# is the same as the default type for the field
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:
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
# Will raise FieldLookupError if the lookup is invalid
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 queryset.exclude() clause
# Of course setting the negation of the existing filter's exclude attribute handles both cases
new_filter.exclude = not existing_filter.exclude
new_filters[new_filter_name] = new_filter
filters.update(new_filters)
return filters
class NameSlugSearchFilterSet(django_filters.FilterSet): class NameSlugSearchFilterSet(django_filters.FilterSet):
""" """
A base class for adding the search method to models which only expose the `name` and `slug` fields A base class for adding the search method to models which only expose the `name` and `slug` fields
@ -127,54 +293,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
},
})

View File

@ -1,9 +1,21 @@
from django.conf import settings
from django.test import TestCase
import django_filters import django_filters
from django.conf import settings
from django.db import models
from django.test import TestCase
from mptt.fields import TreeForeignKey
from taggit.managers import TaggableManager
from dcim.models import Region, Site from dcim.choices import *
from utilities.filters import TreeNodeMultipleChoiceFilter from dcim.fields import MACAddressField
from dcim.filters import DeviceFilterSet, SiteFilterSet
from dcim.models import (
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
)
from extras.models import TaggedItem
from utilities.filters import (
BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter,
MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
)
class TreeNodeMultipleChoiceFilterTest(TestCase): class TreeNodeMultipleChoiceFilterTest(TestCase):
@ -60,3 +72,447 @@ class TreeNodeMultipleChoiceFilterTest(TestCase):
self.assertEqual(qs.count(), 2) self.assertEqual(qs.count(), 2)
self.assertEqual(qs[0], self.site1) self.assertEqual(qs[0], self.site1)
self.assertEqual(qs[1], self.site3) self.assertEqual(qs[1], self.site3)
class DummyModel(models.Model):
"""
Dummy model used by BaseFilterSetTest for filter validation. Should never appear in a schema migration.
"""
charfield = models.CharField(
max_length=10
)
choicefield = models.IntegerField(
choices=(('A', 1), ('B', 2), ('C', 3))
)
datefield = models.DateField()
datetimefield = models.DateTimeField()
integerfield = models.IntegerField()
macaddressfield = MACAddressField()
timefield = models.TimeField()
treeforeignkeyfield = TreeForeignKey(
to='self',
on_delete=models.CASCADE
)
tags = TaggableManager(through=TaggedItem)
class BaseFilterSetTest(TestCase):
"""
Ensure that a BaseFilterSet automatically creates the expected set of filters for each filter type.
"""
class DummyFilterSet(BaseFilterSet):
charfield = django_filters.CharFilter()
macaddressfield = MACAddressFilter()
modelchoicefield = django_filters.ModelChoiceFilter(
field_name='integerfield', # We're pretending this is a ForeignKey field
queryset=Site.objects.all()
)
modelmultiplechoicefield = django_filters.ModelMultipleChoiceFilter(
field_name='integerfield', # We're pretending this is a ForeignKey field
queryset=Site.objects.all()
)
multiplechoicefield = django_filters.MultipleChoiceFilter(
field_name='choicefield'
)
multivaluecharfield = MultiValueCharFilter(
field_name='charfield'
)
tagfield = TagFilter()
treeforeignkeyfield = TreeNodeMultipleChoiceFilter(
queryset=DummyModel.objects.all()
)
class Meta:
model = DummyModel
fields = (
'charfield',
'choicefield',
'datefield',
'datetimefield',
'integerfield',
'macaddressfield',
'modelchoicefield',
'modelmultiplechoicefield',
'multiplechoicefield',
'tagfield',
'timefield',
'treeforeignkeyfield',
)
@classmethod
def setUpTestData(cls):
cls.filters = cls.DummyFilterSet().filters
def test_char_filter(self):
self.assertIsInstance(self.filters['charfield'], django_filters.CharFilter)
self.assertEqual(self.filters['charfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['charfield'].exclude, False)
self.assertEqual(self.filters['charfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['charfield__n'].exclude, True)
self.assertEqual(self.filters['charfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['charfield__ie'].exclude, False)
self.assertEqual(self.filters['charfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['charfield__nie'].exclude, True)
self.assertEqual(self.filters['charfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['charfield__ic'].exclude, False)
self.assertEqual(self.filters['charfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['charfield__nic'].exclude, True)
self.assertEqual(self.filters['charfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['charfield__isw'].exclude, False)
self.assertEqual(self.filters['charfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['charfield__nisw'].exclude, True)
self.assertEqual(self.filters['charfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['charfield__iew'].exclude, False)
self.assertEqual(self.filters['charfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['charfield__niew'].exclude, True)
def test_mac_address_filter(self):
self.assertIsInstance(self.filters['macaddressfield'], MACAddressFilter)
self.assertEqual(self.filters['macaddressfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['macaddressfield'].exclude, False)
self.assertEqual(self.filters['macaddressfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['macaddressfield__n'].exclude, True)
self.assertEqual(self.filters['macaddressfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['macaddressfield__ie'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['macaddressfield__nie'].exclude, True)
self.assertEqual(self.filters['macaddressfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['macaddressfield__ic'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['macaddressfield__nic'].exclude, True)
self.assertEqual(self.filters['macaddressfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['macaddressfield__isw'].exclude, False)
self.assertEqual(self.filters['macaddressfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['macaddressfield__nisw'].exclude, True)
self.assertEqual(self.filters['macaddressfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['macaddressfield__iew'].exclude, False)
self.assertEqual(self.filters['macaddressfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['macaddressfield__niew'].exclude, True)
def test_model_choice_filter(self):
self.assertIsInstance(self.filters['modelchoicefield'], django_filters.ModelChoiceFilter)
self.assertEqual(self.filters['modelchoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelchoicefield'].exclude, False)
self.assertEqual(self.filters['modelchoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelchoicefield__n'].exclude, True)
def test_model_multiple_choice_filter(self):
self.assertIsInstance(self.filters['modelmultiplechoicefield'], django_filters.ModelMultipleChoiceFilter)
self.assertEqual(self.filters['modelmultiplechoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelmultiplechoicefield'].exclude, False)
self.assertEqual(self.filters['modelmultiplechoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['modelmultiplechoicefield__n'].exclude, True)
def test_multi_value_char_filter(self):
self.assertIsInstance(self.filters['multivaluecharfield'], MultiValueCharFilter)
self.assertEqual(self.filters['multivaluecharfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['multivaluecharfield'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['multivaluecharfield__n'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multivaluecharfield__ie'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multivaluecharfield__nie'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multivaluecharfield__ic'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multivaluecharfield__nic'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multivaluecharfield__isw'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multivaluecharfield__nisw'].exclude, True)
self.assertEqual(self.filters['multivaluecharfield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multivaluecharfield__iew'].exclude, False)
self.assertEqual(self.filters['multivaluecharfield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multivaluecharfield__niew'].exclude, True)
def test_multi_value_date_filter(self):
self.assertIsInstance(self.filters['datefield'], MultiValueDateFilter)
self.assertEqual(self.filters['datefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['datefield'].exclude, False)
self.assertEqual(self.filters['datefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['datefield__n'].exclude, True)
self.assertEqual(self.filters['datefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['datefield__lt'].exclude, False)
self.assertEqual(self.filters['datefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['datefield__lte'].exclude, False)
self.assertEqual(self.filters['datefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['datefield__gt'].exclude, False)
self.assertEqual(self.filters['datefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['datefield__gte'].exclude, False)
def test_multi_value_datetime_filter(self):
self.assertIsInstance(self.filters['datetimefield'], MultiValueDateTimeFilter)
self.assertEqual(self.filters['datetimefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['datetimefield'].exclude, False)
self.assertEqual(self.filters['datetimefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['datetimefield__n'].exclude, True)
self.assertEqual(self.filters['datetimefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['datetimefield__lt'].exclude, False)
self.assertEqual(self.filters['datetimefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['datetimefield__lte'].exclude, False)
self.assertEqual(self.filters['datetimefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['datetimefield__gt'].exclude, False)
self.assertEqual(self.filters['datetimefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['datetimefield__gte'].exclude, False)
def test_multi_value_number_filter(self):
self.assertIsInstance(self.filters['integerfield'], MultiValueNumberFilter)
self.assertEqual(self.filters['integerfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['integerfield'].exclude, False)
self.assertEqual(self.filters['integerfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['integerfield__n'].exclude, True)
self.assertEqual(self.filters['integerfield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['integerfield__lt'].exclude, False)
self.assertEqual(self.filters['integerfield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['integerfield__lte'].exclude, False)
self.assertEqual(self.filters['integerfield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['integerfield__gt'].exclude, False)
self.assertEqual(self.filters['integerfield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['integerfield__gte'].exclude, False)
def test_multi_value_time_filter(self):
self.assertIsInstance(self.filters['timefield'], MultiValueTimeFilter)
self.assertEqual(self.filters['timefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['timefield'].exclude, False)
self.assertEqual(self.filters['timefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['timefield__n'].exclude, True)
self.assertEqual(self.filters['timefield__lt'].lookup_expr, 'lt')
self.assertEqual(self.filters['timefield__lt'].exclude, False)
self.assertEqual(self.filters['timefield__lte'].lookup_expr, 'lte')
self.assertEqual(self.filters['timefield__lte'].exclude, False)
self.assertEqual(self.filters['timefield__gt'].lookup_expr, 'gt')
self.assertEqual(self.filters['timefield__gt'].exclude, False)
self.assertEqual(self.filters['timefield__gte'].lookup_expr, 'gte')
self.assertEqual(self.filters['timefield__gte'].exclude, False)
def test_multiple_choice_filter(self):
self.assertIsInstance(self.filters['multiplechoicefield'], django_filters.MultipleChoiceFilter)
self.assertEqual(self.filters['multiplechoicefield'].lookup_expr, 'exact')
self.assertEqual(self.filters['multiplechoicefield'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['multiplechoicefield__n'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__ie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multiplechoicefield__ie'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nie'].lookup_expr, 'iexact')
self.assertEqual(self.filters['multiplechoicefield__nie'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__ic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multiplechoicefield__ic'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nic'].lookup_expr, 'icontains')
self.assertEqual(self.filters['multiplechoicefield__nic'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__isw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multiplechoicefield__isw'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__nisw'].lookup_expr, 'istartswith')
self.assertEqual(self.filters['multiplechoicefield__nisw'].exclude, True)
self.assertEqual(self.filters['multiplechoicefield__iew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multiplechoicefield__iew'].exclude, False)
self.assertEqual(self.filters['multiplechoicefield__niew'].lookup_expr, 'iendswith')
self.assertEqual(self.filters['multiplechoicefield__niew'].exclude, True)
def test_tag_filter(self):
self.assertIsInstance(self.filters['tagfield'], TagFilter)
self.assertEqual(self.filters['tagfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['tagfield'].exclude, False)
self.assertEqual(self.filters['tagfield__n'].lookup_expr, 'exact')
self.assertEqual(self.filters['tagfield__n'].exclude, True)
def test_tree_node_multiple_choice_filter(self):
self.assertIsInstance(self.filters['treeforeignkeyfield'], TreeNodeMultipleChoiceFilter)
# TODO: lookup_expr different for negation?
self.assertEqual(self.filters['treeforeignkeyfield'].lookup_expr, 'exact')
self.assertEqual(self.filters['treeforeignkeyfield'].exclude, False)
self.assertEqual(self.filters['treeforeignkeyfield__n'].lookup_expr, 'in')
self.assertEqual(self.filters['treeforeignkeyfield__n'].exclude, True)
class DynamicFilterLookupExpressionTest(TestCase):
"""
Validate function of automatically generated filters using the Device model as an example.
"""
device_queryset = Device.objects.all()
device_filterset = DeviceFilterSet
site_queryset = Site.objects.all()
site_filterset = SiteFilterSet
@classmethod
def setUpTestData(cls):
manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
device_types = (
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', is_full_depth=True),
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', is_full_depth=True),
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', is_full_depth=False),
)
DeviceType.objects.bulk_create(device_types)
device_roles = (
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
)
DeviceRole.objects.bulk_create(device_roles)
platforms = (
Platform(name='Platform 1', slug='platform-1'),
Platform(name='Platform 2', slug='platform-2'),
Platform(name='Platform 3', slug='platform-3'),
)
Platform.objects.bulk_create(platforms)
regions = (
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for region in regions:
region.save()
sites = (
Site(name='Site 1', slug='abc-site-1', region=regions[0], asn=65001),
Site(name='Site 2', slug='def-site-2', region=regions[1], asn=65101),
Site(name='Site 3', slug='ghi-site-3', region=regions[2], asn=65201),
)
Site.objects.bulk_create(sites)
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),
Rack(name='Rack 3', site=sites[2]),
)
Rack.objects.bulk_create(racks)
devices = (
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, local_context_data={"foo": 123}),
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED),
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED),
)
Device.objects.bulk_create(devices)
interfaces = (
Interface(device=devices[0], name='Interface 1', mac_address='00-00-00-00-00-01'),
Interface(device=devices[0], name='Interface 2', mac_address='aa-00-00-00-00-01'),
Interface(device=devices[1], name='Interface 3', mac_address='00-00-00-00-00-02'),
Interface(device=devices[1], name='Interface 4', mac_address='bb-00-00-00-00-02'),
Interface(device=devices[2], name='Interface 5', mac_address='00-00-00-00-00-03'),
Interface(device=devices[2], name='Interface 6', mac_address='cc-00-00-00-00-03'),
)
Interface.objects.bulk_create(interfaces)
def test_site_name_negation(self):
params = {'name__n': ['Site 1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_icontains(self):
params = {'slug__ic': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_icontains_negation(self):
params = {'slug__nic': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_startswith(self):
params = {'slug__isw': ['abc']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_startswith_negation(self):
params = {'slug__nisw': ['abc']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_slug_endswith(self):
params = {'slug__iew': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_slug_endswith_negation(self):
params = {'slug__niew': ['-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_asn_lt(self):
params = {'asn__lt': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_asn_lte(self):
params = {'asn__lte': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_asn_gt(self):
params = {'asn__lt': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1)
def test_site_asn_gte(self):
params = {'asn__gte': [65101]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_region_negation(self):
params = {'region__n': ['region-1']}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_site_region_id_negation(self):
params = {'region_id__n': [Region.objects.first().pk]}
self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2)
def test_device_name_eq(self):
params = {'name': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_negation(self):
params = {'name__n': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_startswith(self):
params = {'name__isw': ['Device']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 3)
def test_device_name_startswith_negation(self):
params = {'name__nisw': ['Device 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_endswith(self):
params = {'name__iew': [' 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_endswith_negation(self):
params = {'name__niew': [' 1']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_name_icontains(self):
params = {'name__ic': [' 2']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_name_icontains_negation(self):
params = {'name__nic': [' ']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 0)
def test_device_mac_address_negation(self):
params = {'mac_address__n': ['00-00-00-00-00-01', 'aa-00-00-00-00-01']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_startswith(self):
params = {'mac_address__isw': ['aa:']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_mac_address_startswith_negation(self):
params = {'mac_address__nisw': ['aa:']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_endswith(self):
params = {'mac_address__iew': [':02']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)
def test_device_mac_address_endswith_negation(self):
params = {'mac_address__niew': [':02']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_icontains(self):
params = {'mac_address__ic': ['aa:', 'bb']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2)
def test_device_mac_address_icontains_negation(self):
params = {'mac_address__nic': ['aa:', 'bb']}
self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1)

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(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, 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(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
) )
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)',
) )
@ -100,6 +103,7 @@ class ClusterFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
class VirtualMachineFilterSet( class VirtualMachineFilterSet(
BaseFilterSet,
LocalConfigContextFilterSet, LocalConfigContextFilterSet,
TenancyFilterSet, TenancyFilterSet,
CustomFieldFilterSet, CustomFieldFilterSet,
@ -145,12 +149,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)',
) )
@ -204,7 +210,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',