Merge pull request #3165 from digitalocean/3038-filtering-improvements

Closes #3038: Filtering improvements
This commit is contained in:
Jeremy Stretch 2019-05-08 21:13:42 -04:00 committed by GitHub
commit c24fb8df84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 191 additions and 68 deletions

View File

@ -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`.

View File

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

View File

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

View File

@ -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():

View File

@ -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():

View File

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

View File

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

View File

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

View File

@ -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()

View File

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