mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge pull request #4189 from netbox-community/4121-filter-lookup-expressions
4121 filter lookup expressions
This commit is contained in:
commit
1dd07337fd
71
docs/api/filtering.md
Normal file
71
docs/api/filtering.md
Normal 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`.
|
@ -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:
|
||||||
|
@ -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'
|
||||||
|
@ -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',
|
||||||
|
@ -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)',
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
|
@ -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}
|
||||||
|
@ -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',
|
||||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||||
from .models import Secret, SecretRole
|
from .models import Secret, SecretRole
|
||||||
|
|
||||||
|
|
||||||
@ -13,14 +13,14 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleFilterSet(NameSlugSearchFilterSet):
|
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class SecretFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
class SecretFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
|
@ -2,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'
|
||||||
|
@ -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,
|
||||||
|
@ -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,30 +118,12 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
|||||||
# FilterSets
|
# FilterSets
|
||||||
#
|
#
|
||||||
|
|
||||||
class NameSlugSearchFilterSet(django_filters.FilterSet):
|
class BaseFilterSet(django_filters.FilterSet):
|
||||||
"""
|
"""
|
||||||
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
A base filterset which provides common functionaly to all NetBox filtersets
|
||||||
"""
|
"""
|
||||||
q = django_filters.CharFilter(
|
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||||
method='search',
|
FILTER_DEFAULTS.update({
|
||||||
label='Search',
|
|
||||||
)
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
|
||||||
if not value.strip():
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(
|
|
||||||
models.Q(name__icontains=value) |
|
|
||||||
models.Q(slug__icontains=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Update default filters
|
|
||||||
#
|
|
||||||
|
|
||||||
FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
|
|
||||||
FILTER_DEFAULTS.update({
|
|
||||||
models.AutoField: {
|
models.AutoField: {
|
||||||
'filter_class': MultiValueNumberFilter
|
'filter_class': MultiValueNumberFilter
|
||||||
},
|
},
|
||||||
@ -177,4 +166,130 @@ FILTER_DEFAULTS.update({
|
|||||||
models.URLField: {
|
models.URLField: {
|
||||||
'filter_class': MultiValueCharFilter
|
'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):
|
||||||
|
"""
|
||||||
|
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
||||||
|
"""
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
models.Q(name__icontains=value) |
|
||||||
|
models.Q(slug__icontains=value)
|
||||||
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user