فهرست منبع

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

Closes #3038: Filtering improvements
Jeremy Stretch 6 سال پیش
والد
کامیت
c24fb8df84

+ 2 - 0
CHANGELOG.md

@@ -186,6 +186,7 @@ functionality provided by the front end UI.
 * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add `comments` field for tags
 * [#2920](https://github.com/digitalocean/netbox/issues/2920) - Rename Interface `form_factor` to `type` (backward-compatible until v2.7)
 * [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add change logging to the Tag model
+* [#3038](https://github.com/digitalocean/netbox/issues/3038) - OR logic now used when multiple values of a query filter are passed
 
 ## API Changes
 
@@ -193,6 +194,7 @@ functionality provided by the front end UI.
 * New API endpoint for custom field choices: `/api/extras/_custom_field_choices/`
 * ForeignKey fields now accept either the related object PK or a dictionary of attributes describing the related object.
 * Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`.
+* The `id__in` filter is now deprecated and will be removed in v2.7. (Begin using the `?id=1&id=2` format instead.)
 * Added a `description` field for all device components.
 * dcim.Device: The devices list endpoint now includes rendered context data.
 * dcim.DeviceType: `instance_count` has been renamed to `device_count`.

+ 21 - 2
docs/api/overview.md

@@ -274,12 +274,31 @@ A list of objects retrieved via the API can be filtered by passing one or more q
 GET /api/ipam/prefixes/?status=1
 ```
 
-Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
+The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`:
 
 ```
-GET /api/ipam/prefixes/?status=1&status=2
+"prefix:status": [
+    {
+        "label": "Container",
+        "value": 0
+    },
+    {
+        "label": "Active",
+        "value": 1
+    },
+    {
+        "label": "Reserved",
+        "value": 2
+    },
+    {
+        "label": "Deprecated",
+        "value": 3
+    }
+],
 ```
 
+For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar".
+
 ## Custom Fields
 
 To filter on a custom field, prepend `cf_` to the field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:

+ 3 - 3
netbox/circuits/filters.py

@@ -9,7 +9,7 @@ from .constants import CIRCUIT_STATUS_CHOICES
 from .models import Provider, Circuit, CircuitTermination, CircuitType
 
 
-class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class ProviderFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -51,10 +51,10 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = CircuitType
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
-class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class CircuitFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 36 - 42
netbox/dcim/filters.py

@@ -1,6 +1,5 @@
 import django_filters
 from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from netaddr import EUI
@@ -9,9 +8,7 @@ from netaddr.core import AddrFormatError
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
-from utilities.filters import (
-    NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
-)
+from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from virtualization.models import Cluster
 from .constants import *
 from .models import (
@@ -37,7 +34,7 @@ class RegionFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = Region
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
 class SiteFilter(CustomFieldFilterSet):
@@ -78,7 +75,10 @@ class SiteFilter(CustomFieldFilterSet):
 
     class Meta:
         model = Site
-        fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
+        fields = [
+            'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
+            'contact_email',
+        ]
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -115,14 +115,14 @@ class RackGroupFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = RackGroup
-        fields = ['site_id', 'name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
 class RackRoleFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = RackRole
-        fields = ['name', 'slug', 'color']
+        fields = ['id', 'name', 'slug', 'color']
 
 
 class RackFilter(CustomFieldFilterSet):
@@ -134,7 +134,6 @@ class RackFilter(CustomFieldFilterSet):
         method='search',
         label='Search',
     )
-    facility_id = NullableCharFieldFilter()
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
@@ -179,14 +178,13 @@ class RackFilter(CustomFieldFilterSet):
         to_field_name='slug',
         label='Role (slug)',
     )
-    asset_tag = NullableCharFieldFilter()
     tag = TagFilter()
 
     class Meta:
         model = Rack
         fields = [
-            'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
-            'outer_unit',
+            'id', 'name', 'facility_id', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units',
+            'outer_width', 'outer_depth', 'outer_unit',
         ]
 
     def search(self, queryset, name, value):
@@ -276,7 +274,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = Manufacturer
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
 class DeviceTypeFilter(CustomFieldFilterSet):
@@ -374,63 +372,63 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = ConsolePortTemplate
-        fields = ['name']
+        fields = ['id', 'name']
 
 
 class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = ConsoleServerPortTemplate
-        fields = ['name']
+        fields = ['id', 'name']
 
 
 class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = PowerPortTemplate
-        fields = ['name']
+        fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
 
 
 class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = PowerOutletTemplate
-        fields = ['name']
+        fields = ['id', 'name', 'feed_leg']
 
 
 class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = InterfaceTemplate
-        fields = ['name', 'type', 'mgmt_only']
+        fields = ['id', 'name', 'type', 'mgmt_only']
 
 
 class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = FrontPortTemplate
-        fields = ['name', 'type']
+        fields = ['id', 'name', 'type']
 
 
 class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = RearPortTemplate
-        fields = ['name', 'type']
+        fields = ['id', 'name', 'type', 'positions']
 
 
 class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
 
     class Meta:
         model = DeviceBayTemplate
-        fields = ['name']
+        fields = ['id', 'name']
 
 
 class DeviceRoleFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = DeviceRole
-        fields = ['name', 'slug', 'color', 'vm_role']
+        fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
 class PlatformFilter(NameSlugSearchFilterSet):
@@ -448,7 +446,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = Platform
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug', 'napalm_driver']
 
 
 class DeviceFilter(CustomFieldFilterSet):
@@ -506,8 +504,6 @@ class DeviceFilter(CustomFieldFilterSet):
         to_field_name='slug',
         label='Platform (slug)',
     )
-    name = NullableCharFieldFilter()
-    asset_tag = NullableCharFieldFilter()
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region__in',
@@ -539,10 +535,6 @@ class DeviceFilter(CustomFieldFilterSet):
         queryset=Rack.objects.all(),
         label='Rack (ID)',
     )
-    position = django_filters.ChoiceFilter(
-        choices=DEVICE_POSITION_CHOICES,
-        null_label='Non-racked'
-    )
     cluster_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Cluster.objects.all(),
         label='VM cluster (ID)',
@@ -602,7 +594,7 @@ class DeviceFilter(CustomFieldFilterSet):
 
     class Meta:
         model = Device
-        fields = ['serial', 'face']
+        fields = ['id', 'name', 'serial', 'asset_tag', 'face', 'position', 'vc_position', 'vc_priority']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -693,7 +685,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = ConsolePort
-        fields = ['name', 'description', 'connection_status']
+        fields = ['id', 'name', 'description', 'connection_status']
 
 
 class ConsoleServerPortFilter(DeviceComponentFilterSet):
@@ -705,7 +697,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = ConsoleServerPort
-        fields = ['name', 'description', 'connection_status']
+        fields = ['id', 'name', 'description', 'connection_status']
 
 
 class PowerPortFilter(DeviceComponentFilterSet):
@@ -717,7 +709,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = PowerPort
-        fields = ['name', 'description', 'connection_status']
+        fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
 
 
 class PowerOutletFilter(DeviceComponentFilterSet):
@@ -729,7 +721,7 @@ class PowerOutletFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = PowerOutlet
-        fields = ['name', 'description', 'connection_status']
+        fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
 
 
 class InterfaceFilter(django_filters.FilterSet):
@@ -784,7 +776,7 @@ class InterfaceFilter(django_filters.FilterSet):
 
     class Meta:
         model = Interface
-        fields = ['name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'description']
+        fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -848,7 +840,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = FrontPort
-        fields = ['name', 'type', 'description']
+        fields = ['id', 'name', 'type', 'description']
 
 
 class RearPortFilter(DeviceComponentFilterSet):
@@ -860,14 +852,14 @@ class RearPortFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = RearPort
-        fields = ['name', 'type', 'description']
+        fields = ['id', 'name', 'type', 'positions', 'description']
 
 
 class DeviceBayFilter(DeviceComponentFilterSet):
 
     class Meta:
         model = DeviceBay
-        fields = ['name', 'description']
+        fields = ['id', 'name', 'description']
 
 
 class InventoryItemFilter(DeviceComponentFilterSet):
@@ -898,11 +890,10 @@ class InventoryItemFilter(DeviceComponentFilterSet):
         to_field_name='slug',
         label='Manufacturer (slug)',
     )
-    asset_tag = NullableCharFieldFilter()
 
     class Meta:
         model = InventoryItem
-        fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
+        fields = ['id', 'name', 'part_id', 'serial', 'asset_tag', 'discovered']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -948,7 +939,7 @@ class VirtualChassisFilter(django_filters.FilterSet):
 
     class Meta:
         model = VirtualChassis
-        fields = ['domain']
+        fields = ['id', 'domain']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -968,6 +959,9 @@ class CableFilter(django_filters.FilterSet):
     type = django_filters.MultipleChoiceFilter(
         choices=CABLE_TYPE_CHOICES
     )
+    status = django_filters.MultipleChoiceFilter(
+        choices=CONNECTION_STATUS_CHOICES
+    )
     color = django_filters.MultipleChoiceFilter(
         choices=COLOR_CHOICES
     )
@@ -982,7 +976,7 @@ class CableFilter(django_filters.FilterSet):
 
     class Meta:
         model = Cable
-        fields = ['type', 'status', 'color', 'length', 'length_unit']
+        fields = ['id', 'label', 'length', 'length_unit']
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 21 - 8
netbox/ipam/filters.py

@@ -13,7 +13,7 @@ from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
-class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class VRFFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -59,7 +59,7 @@ class RIRFilter(NameSlugSearchFilterSet):
         fields = ['name', 'slug', 'is_private']
 
 
-class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class AggregateFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -68,6 +68,10 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
+    prefix = django_filters.CharFilter(
+        method='filter_prefix',
+        label='Prefix',
+    )
     rir_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RIR.objects.all(),
         label='RIR (ID)',
@@ -95,6 +99,15 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
         return queryset.filter(qs_filter)
 
+    def filter_prefix(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        try:
+            query = str(netaddr.IPNetwork(value).cidr)
+            return queryset.filter(prefix=query)
+        except ValidationError:
+            return queryset.none()
+
 
 class RoleFilter(NameSlugSearchFilterSet):
     q = django_filters.CharFilter(
@@ -104,10 +117,10 @@ class RoleFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = Role
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
-class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class PrefixFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -254,7 +267,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         return queryset.filter(prefix__net_mask_length=value)
 
 
-class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class IPAddressFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -392,10 +405,10 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = VLANGroup
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
-class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class VLANFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'
@@ -494,7 +507,7 @@ class ServiceFilter(django_filters.FilterSet):
 
     class Meta:
         model = Service
-        fields = ['name', 'protocol', 'port']
+        fields = ['id', 'name', 'protocol', 'port']
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 2 - 2
netbox/secrets/filters.py

@@ -11,10 +11,10 @@ class SecretRoleFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = SecretRole
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
-class SecretFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class SecretFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 2 - 2
netbox/tenancy/filters.py

@@ -10,10 +10,10 @@ class TenantGroupFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = TenantGroup
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
-class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
+class TenantFilter(CustomFieldFilterSet):
     id__in = NumericInFilter(
         field_name='id',
         lookup_expr='in'

+ 99 - 3
netbox/utilities/filters.py

@@ -1,10 +1,51 @@
 import django_filters
+from django import forms
 from django.conf import settings
-from django.db.models import Q
+from django.db import models
 
 from extras.models import Tag
 
 
+def multivalue_field_factory(field_class):
+    """
+    Given a form field class, return a subclass capable of accepting multiple values. This allows us to OR on multiple
+    filter values while maintaining the field's built-in vlaidation. Example: GET /api/dcim/devices/?name=foo&name=bar
+    """
+    class NewField(field_class):
+        widget = forms.SelectMultiple
+
+        def to_python(self, value):
+            if not value:
+                return []
+            return [super(field_class, self).to_python(v) for v in value]
+
+    return type('MultiValue{}'.format(field_class.__name__), (NewField,), dict())
+
+
+#
+# Filters
+#
+
+class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(forms.CharField)
+
+
+class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(forms.DateField)
+
+
+class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(forms.DateTimeField)
+
+
+class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(forms.IntegerField)
+
+
+class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
+    field_class = multivalue_field_factory(forms.TimeField)
+
+
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]
@@ -48,6 +89,10 @@ class TagFilter(django_filters.ModelMultipleChoiceFilter):
         super().__init__(*args, **kwargs)
 
 
+#
+# FilterSets
+#
+
 class NameSlugSearchFilterSet(django_filters.FilterSet):
     """
     A base class for adding the search method to models which only expose the `name` and `slug` fields
@@ -61,6 +106,57 @@ class NameSlugSearchFilterSet(django_filters.FilterSet):
         if not value.strip():
             return queryset
         return queryset.filter(
-            Q(name__icontains=value) |
-            Q(slug__icontains=value)
+            models.Q(name__icontains=value) |
+            models.Q(slug__icontains=value)
         )
+
+
+#
+# Update default filters
+#
+
+FILTER_DEFAULTS = django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS
+FILTER_DEFAULTS.update({
+    models.AutoField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.CharField: {
+        'filter_class': MultiValueCharFilter
+    },
+    models.DateField: {
+        'filter_class': MultiValueDateFilter
+    },
+    models.DateTimeField: {
+        'filter_class': MultiValueDateTimeFilter
+    },
+    models.DecimalField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.EmailField: {
+        'filter_class': MultiValueCharFilter
+    },
+    models.FloatField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.IntegerField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.PositiveIntegerField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.PositiveSmallIntegerField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.SlugField: {
+        'filter_class': MultiValueCharFilter
+    },
+    models.SmallIntegerField: {
+        'filter_class': MultiValueNumberFilter
+    },
+    models.TimeField: {
+        'filter_class': MultiValueTimeFilter
+    },
+    models.URLField: {
+        'filter_class': MultiValueCharFilter
+    },
+})

+ 4 - 5
netbox/virtualization/filters.py

@@ -1,5 +1,4 @@
 import django_filters
-from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q
 from netaddr import EUI
 from netaddr.core import AddrFormatError
@@ -16,14 +15,14 @@ class ClusterTypeFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = ClusterType
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
 class ClusterGroupFilter(NameSlugSearchFilterSet):
 
     class Meta:
         model = ClusterGroup
-        fields = ['name', 'slug']
+        fields = ['id', 'name', 'slug']
 
 
 class ClusterFilter(CustomFieldFilterSet):
@@ -175,7 +174,7 @@ class VirtualMachineFilter(CustomFieldFilterSet):
 
     class Meta:
         model = VirtualMachine
-        fields = ['name', 'cluster']
+        fields = ['id', 'name', 'cluster', 'vcpus', 'memory', 'disk']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -209,7 +208,7 @@ class InterfaceFilter(django_filters.FilterSet):
 
     class Meta:
         model = Interface
-        fields = ['name', 'enabled', 'mtu']
+        fields = ['id', 'name', 'enabled', 'mtu']
 
     def _mac_address(self, queryset, name, value):
         value = value.strip()

+ 1 - 1
netbox/virtualization/tests/test_api.py

@@ -464,7 +464,7 @@ class VirtualMachineTest(APITestCase):
     def test_config_context_included_by_default_in_list_view(self):
 
         url = reverse('virtualization-api:virtualmachine-list')
-        url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk)
+        url = '{}?id={}'.format(url, self.virtualmachine_with_context_data.pk)
         response = self.client.get(url, **self.header)
 
         self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)