mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #3165 from digitalocean/3038-filtering-improvements
Closes #3038: Filtering improvements
This commit is contained in:
commit
c24fb8df84
@ -186,6 +186,7 @@ functionality provided by the front end UI.
|
|||||||
* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add `comments` field for tags
|
* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add `comments` field for tags
|
||||||
* [#2920](https://github.com/digitalocean/netbox/issues/2920) - Rename Interface `form_factor` to `type` (backward-compatible until v2.7)
|
* [#2920](https://github.com/digitalocean/netbox/issues/2920) - Rename Interface `form_factor` to `type` (backward-compatible until v2.7)
|
||||||
* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add change logging to the Tag model
|
* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add change logging to the Tag model
|
||||||
|
* [#3038](https://github.com/digitalocean/netbox/issues/3038) - OR logic now used when multiple values of a query filter are passed
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
@ -193,6 +194,7 @@ functionality provided by the front end UI.
|
|||||||
* New API endpoint for custom field choices: `/api/extras/_custom_field_choices/`
|
* New API endpoint for custom field choices: `/api/extras/_custom_field_choices/`
|
||||||
* ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object.
|
* ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object.
|
||||||
* Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`.
|
* Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`.
|
||||||
|
* The `id__in` filter is now deprecated and will be removed in v2.7. (Begin using the `?id=1&id=2` format instead.)
|
||||||
* Added a `description` field for all device components.
|
* Added a `description` field for all device components.
|
||||||
* dcim.Device: The devices list endpoint now includes rendered context data.
|
* dcim.Device: The devices list endpoint now includes rendered context data.
|
||||||
* dcim.DeviceType: `instance_count` has been renamed to `device_count`.
|
* dcim.DeviceType: `instance_count` has been renamed to `device_count`.
|
||||||
|
@ -274,12 +274,31 @@ A list of objects retrieved via the API can be filtered by passing one or more q
|
|||||||
GET /api/ipam/prefixes/?status=1
|
GET /api/ipam/prefixes/?status=1
|
||||||
```
|
```
|
||||||
|
|
||||||
Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
|
The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`:
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/ipam/prefixes/?status=1&status=2
|
"prefix:status": [
|
||||||
|
{
|
||||||
|
"label": "Container",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Active",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Reserved",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Deprecated",
|
||||||
|
"value": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar".
|
||||||
|
|
||||||
## Custom Fields
|
## Custom Fields
|
||||||
|
|
||||||
To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
|
To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
|
||||||
|
@ -9,7 +9,7 @@ from .constants import CIRCUIT_STATUS_CHOICES
|
|||||||
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
from .models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
|
|
||||||
|
|
||||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class ProviderFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -51,10 +51,10 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class CircuitFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
@ -9,9 +8,7 @@ from netaddr.core import AddrFormatError
|
|||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
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 NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
||||||
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
|
||||||
)
|
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -37,7 +34,7 @@ class RegionFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class SiteFilter(CustomFieldFilterSet):
|
class SiteFilter(CustomFieldFilterSet):
|
||||||
@ -78,7 +75,10 @@ class SiteFilter(CustomFieldFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
fields = [
|
||||||
|
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
|
'contact_email',
|
||||||
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -115,14 +115,14 @@ class RackGroupFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['site_id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class RackRoleFilter(NameSlugSearchFilterSet):
|
class RackRoleFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
fields = ['name', 'slug', 'color']
|
fields = ['id', 'name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class RackFilter(CustomFieldFilterSet):
|
class RackFilter(CustomFieldFilterSet):
|
||||||
@ -134,7 +134,6 @@ class RackFilter(CustomFieldFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
facility_id = NullableCharFieldFilter()
|
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -179,14 +178,13 @@ class RackFilter(CustomFieldFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
asset_tag = NullableCharFieldFilter()
|
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
'id', 'name', 'facility_id', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units',
|
||||||
'outer_unit',
|
'outer_width', 'outer_depth', 'outer_unit',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -276,7 +274,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet):
|
||||||
@ -374,63 +372,63 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = ['name']
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = ['name']
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = ['name']
|
fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
|
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = ['name']
|
fields = ['id', 'name', 'feed_leg']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = ['name', 'type', 'mgmt_only']
|
fields = ['id', 'name', 'type', 'mgmt_only']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
|
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = ['name', 'type']
|
fields = ['id', 'name', 'type']
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
|
class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = ['name', 'type']
|
fields = ['id', 'name', 'type', 'positions']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
fields = ['name']
|
fields = ['id', 'name']
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleFilter(NameSlugSearchFilterSet):
|
class DeviceRoleFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ['name', 'slug', 'color', 'vm_role']
|
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||||
|
|
||||||
|
|
||||||
class PlatformFilter(NameSlugSearchFilterSet):
|
class PlatformFilter(NameSlugSearchFilterSet):
|
||||||
@ -448,7 +446,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug', 'napalm_driver']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilter(CustomFieldFilterSet):
|
class DeviceFilter(CustomFieldFilterSet):
|
||||||
@ -506,8 +504,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Platform (slug)',
|
label='Platform (slug)',
|
||||||
)
|
)
|
||||||
name = NullableCharFieldFilter()
|
|
||||||
asset_tag = NullableCharFieldFilter()
|
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='site__region__in',
|
field_name='site__region__in',
|
||||||
@ -539,10 +535,6 @@ class DeviceFilter(CustomFieldFilterSet):
|
|||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label='Rack (ID)',
|
label='Rack (ID)',
|
||||||
)
|
)
|
||||||
position = django_filters.ChoiceFilter(
|
|
||||||
choices=DEVICE_POSITION_CHOICES,
|
|
||||||
null_label='Non-racked'
|
|
||||||
)
|
|
||||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
label='VM cluster (ID)',
|
label='VM cluster (ID)',
|
||||||
@ -602,7 +594,7 @@ class DeviceFilter(CustomFieldFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['serial', 'face']
|
fields = ['id', 'name', 'serial', 'asset_tag', 'face', 'position', 'vc_position', 'vc_priority']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -693,7 +685,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['name', 'description', 'connection_status']
|
fields = ['id', 'name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||||
@ -705,7 +697,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['name', 'description', 'connection_status']
|
fields = ['id', 'name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilter(DeviceComponentFilterSet):
|
class PowerPortFilter(DeviceComponentFilterSet):
|
||||||
@ -717,7 +709,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['name', 'description', 'connection_status']
|
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||||
@ -729,7 +721,7 @@ class PowerOutletFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['name', 'description', 'connection_status']
|
fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(django_filters.FilterSet):
|
class InterfaceFilter(django_filters.FilterSet):
|
||||||
@ -784,7 +776,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'description']
|
fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -848,7 +840,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = ['name', 'type', 'description']
|
fields = ['id', 'name', 'type', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilter(DeviceComponentFilterSet):
|
class RearPortFilter(DeviceComponentFilterSet):
|
||||||
@ -860,14 +852,14 @@ class RearPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ['name', 'type', 'description']
|
fields = ['id', 'name', 'type', 'positions', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['name', 'description']
|
fields = ['id', 'name', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||||
@ -898,11 +890,10 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
asset_tag = NullableCharFieldFilter()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
fields = ['id', 'name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -948,7 +939,7 @@ class VirtualChassisFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['domain']
|
fields = ['id', 'domain']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -968,6 +959,9 @@ class CableFilter(django_filters.FilterSet):
|
|||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=CABLE_TYPE_CHOICES
|
choices=CABLE_TYPE_CHOICES
|
||||||
)
|
)
|
||||||
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES
|
||||||
|
)
|
||||||
color = django_filters.MultipleChoiceFilter(
|
color = django_filters.MultipleChoiceFilter(
|
||||||
choices=COLOR_CHOICES
|
choices=COLOR_CHOICES
|
||||||
)
|
)
|
||||||
@ -982,7 +976,7 @@ class CableFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = ['type', 'status', 'color', 'length', 'length_unit']
|
fields = ['id', 'label', 'length', 'length_unit']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -13,7 +13,7 @@ from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_
|
|||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VRFFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -59,7 +59,7 @@ class RIRFilter(NameSlugSearchFilterSet):
|
|||||||
fields = ['name', 'slug', 'is_private']
|
fields = ['name', 'slug', 'is_private']
|
||||||
|
|
||||||
|
|
||||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class AggregateFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -68,6 +68,10 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
prefix = django_filters.CharFilter(
|
||||||
|
method='filter_prefix',
|
||||||
|
label='Prefix',
|
||||||
|
)
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
label='RIR (ID)',
|
label='RIR (ID)',
|
||||||
@ -95,6 +99,15 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
def filter_prefix(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
query = str(netaddr.IPNetwork(value).cidr)
|
||||||
|
return queryset.filter(prefix=query)
|
||||||
|
except ValidationError:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
|
||||||
class RoleFilter(NameSlugSearchFilterSet):
|
class RoleFilter(NameSlugSearchFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@ -104,10 +117,10 @@ class RoleFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class PrefixFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -254,7 +267,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset.filter(prefix__net_mask_length=value)
|
return queryset.filter(prefix__net_mask_length=value)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class IPAddressFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -392,10 +405,10 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VLANFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
@ -494,7 +507,7 @@ class ServiceFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = ['name', 'protocol', 'port']
|
fields = ['id', 'name', 'protocol', 'port']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -11,10 +11,10 @@ class SecretRoleFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class SecretFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class SecretFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
|
@ -10,10 +10,10 @@ class TenantGroupFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TenantGroup
|
model = TenantGroup
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class TenantFilter(CustomFieldFilterSet):
|
||||||
id__in = NumericInFilter(
|
id__in = NumericInFilter(
|
||||||
field_name='id',
|
field_name='id',
|
||||||
lookup_expr='in'
|
lookup_expr='in'
|
||||||
|
@ -1,10 +1,51 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db import models
|
||||||
|
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
|
def multivalue_field_factory(field_class):
|
||||||
|
"""
|
||||||
|
Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
|
||||||
|
filter values while maintaining the field's built-in vlaidation. Example: GET /api/dcim/devices/?name=foo&name=bar
|
||||||
|
"""
|
||||||
|
class NewField(field_class):
|
||||||
|
widget = forms.SelectMultiple
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
return [super(field_class, self).to_python(v) for v in value]
|
||||||
|
|
||||||
|
return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Filters
|
||||||
|
#
|
||||||
|
|
||||||
|
class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.DateField)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.DateTimeField)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.IntegerField)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(forms.TimeField)
|
||||||
|
|
||||||
|
|
||||||
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
||||||
"""
|
"""
|
||||||
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
||||||
@ -48,6 +89,10 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# FilterSets
|
||||||
|
#
|
||||||
|
|
||||||
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
|
||||||
@ -61,6 +106,57 @@ class NameSlugSearchFilterSet(django_filters.FilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
models.Q(name__icontains=value) |
|
||||||
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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
@ -16,14 +15,14 @@ class ClusterTypeFilter(NameSlugSearchFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterType
|
model = ClusterType
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupFilter(NameSlugSearchFilterSet):
|
class ClusterGroupFilter(NameSlugSearchFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ClusterGroup
|
model = ClusterGroup
|
||||||
fields = ['name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class ClusterFilter(CustomFieldFilterSet):
|
class ClusterFilter(CustomFieldFilterSet):
|
||||||
@ -175,7 +174,7 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = ['name', 'cluster']
|
fields = ['id', 'name', 'cluster', 'vcpus', 'memory', 'disk']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@ -209,7 +208,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name', 'enabled', 'mtu']
|
fields = ['id', 'name', 'enabled', 'mtu']
|
||||||
|
|
||||||
def _mac_address(self, queryset, name, value):
|
def _mac_address(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
@ -464,7 +464,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
def test_config_context_included_by_default_in_list_view(self):
|
def test_config_context_included_by_default_in_list_view(self):
|
||||||
|
|
||||||
url = reverse('virtualization-api:virtualmachine-list')
|
url = reverse('virtualization-api:virtualmachine-list')
|
||||||
url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk)
|
url = '{}?id={}'.format(url, self.virtualmachine_with_context_data.pk)
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
|
self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
|
||||||
|
Loading…
Reference in New Issue
Block a user