Procházet zdrojové kódy

Refactor API views

jeremystretch před 4 roky
rodič
revize
efd5a73a18

+ 2 - 2
netbox/circuits/api/views.py

@@ -4,7 +4,7 @@ from circuits import filtersets
 from circuits.models import *
 from circuits.models import *
 from dcim.api.views import PassThroughPortMixin
 from dcim.api.views import PassThroughPortMixin
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from netbox.api.views import ModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
 from utilities.utils import count_related
 from utilities.utils import count_related
 from . import serializers
 from . import serializers
 
 
@@ -57,7 +57,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
 # Circuit Terminations
 # Circuit Terminations
 #
 #
 
 
-class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
+class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
     queryset = CircuitTermination.objects.prefetch_related(
     queryset = CircuitTermination.objects.prefetch_related(
         'circuit', 'site', 'provider_network', 'cable'
         'circuit', 'site', 'provider_network', 'cable'
     )
     )

+ 25 - 25
netbox/dcim/api/views.py

@@ -19,7 +19,7 @@ from ipam.models import Prefix, VLAN
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.exceptions import ServiceUnavailable
 from netbox.api.exceptions import ServiceUnavailable
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
-from netbox.api.views import ModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.config import get_config
 from netbox.config import get_config
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from utilities.utils import count_related
 from utilities.utils import count_related
@@ -250,7 +250,7 @@ class RackViewSet(CustomFieldModelViewSet):
 # Rack reservations
 # Rack reservations
 #
 #
 
 
-class RackReservationViewSet(ModelViewSet):
+class RackReservationViewSet(NetBoxModelViewSet):
     queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
     queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
     serializer_class = serializers.RackReservationSerializer
     serializer_class = serializers.RackReservationSerializer
     filterset_class = filtersets.RackReservationFilterSet
     filterset_class = filtersets.RackReservationFilterSet
@@ -296,61 +296,61 @@ class ModuleTypeViewSet(CustomFieldModelViewSet):
 # Device type components
 # Device type components
 #
 #
 
 
-class ConsolePortTemplateViewSet(ModelViewSet):
+class ConsolePortTemplateViewSet(NetBoxModelViewSet):
     queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsolePortTemplateSerializer
     serializer_class = serializers.ConsolePortTemplateSerializer
     filterset_class = filtersets.ConsolePortTemplateFilterSet
     filterset_class = filtersets.ConsolePortTemplateFilterSet
 
 
 
 
-class ConsoleServerPortTemplateViewSet(ModelViewSet):
+class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
     queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
     filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
     filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
 
 
 
 
-class PowerPortTemplateViewSet(ModelViewSet):
+class PowerPortTemplateViewSet(NetBoxModelViewSet):
     queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerPortTemplateSerializer
     serializer_class = serializers.PowerPortTemplateSerializer
     filterset_class = filtersets.PowerPortTemplateFilterSet
     filterset_class = filtersets.PowerPortTemplateFilterSet
 
 
 
 
-class PowerOutletTemplateViewSet(ModelViewSet):
+class PowerOutletTemplateViewSet(NetBoxModelViewSet):
     queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerOutletTemplateSerializer
     serializer_class = serializers.PowerOutletTemplateSerializer
     filterset_class = filtersets.PowerOutletTemplateFilterSet
     filterset_class = filtersets.PowerOutletTemplateFilterSet
 
 
 
 
-class InterfaceTemplateViewSet(ModelViewSet):
+class InterfaceTemplateViewSet(NetBoxModelViewSet):
     queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.InterfaceTemplateSerializer
     serializer_class = serializers.InterfaceTemplateSerializer
     filterset_class = filtersets.InterfaceTemplateFilterSet
     filterset_class = filtersets.InterfaceTemplateFilterSet
 
 
 
 
-class FrontPortTemplateViewSet(ModelViewSet):
+class FrontPortTemplateViewSet(NetBoxModelViewSet):
     queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.FrontPortTemplateSerializer
     serializer_class = serializers.FrontPortTemplateSerializer
     filterset_class = filtersets.FrontPortTemplateFilterSet
     filterset_class = filtersets.FrontPortTemplateFilterSet
 
 
 
 
-class RearPortTemplateViewSet(ModelViewSet):
+class RearPortTemplateViewSet(NetBoxModelViewSet):
     queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.RearPortTemplateSerializer
     serializer_class = serializers.RearPortTemplateSerializer
     filterset_class = filtersets.RearPortTemplateFilterSet
     filterset_class = filtersets.RearPortTemplateFilterSet
 
 
 
 
-class ModuleBayTemplateViewSet(ModelViewSet):
+class ModuleBayTemplateViewSet(NetBoxModelViewSet):
     queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ModuleBayTemplateSerializer
     serializer_class = serializers.ModuleBayTemplateSerializer
     filterset_class = filtersets.ModuleBayTemplateFilterSet
     filterset_class = filtersets.ModuleBayTemplateFilterSet
 
 
 
 
-class DeviceBayTemplateViewSet(ModelViewSet):
+class DeviceBayTemplateViewSet(NetBoxModelViewSet):
     queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.DeviceBayTemplateSerializer
     serializer_class = serializers.DeviceBayTemplateSerializer
     filterset_class = filtersets.DeviceBayTemplateFilterSet
     filterset_class = filtersets.DeviceBayTemplateFilterSet
 
 
 
 
-class InventoryItemTemplateViewSet(ModelViewSet):
+class InventoryItemTemplateViewSet(NetBoxModelViewSet):
     queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
     queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
     serializer_class = serializers.InventoryItemTemplateSerializer
     serializer_class = serializers.InventoryItemTemplateSerializer
     filterset_class = filtersets.InventoryItemTemplateFilterSet
     filterset_class = filtersets.InventoryItemTemplateFilterSet
@@ -544,7 +544,7 @@ class ModuleViewSet(CustomFieldModelViewSet):
 # Device components
 # Device components
 #
 #
 
 
-class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
+class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = ConsolePort.objects.prefetch_related(
     queryset = ConsolePort.objects.prefetch_related(
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
     )
     )
@@ -553,7 +553,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
+class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = ConsoleServerPort.objects.prefetch_related(
     queryset = ConsoleServerPort.objects.prefetch_related(
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
     )
     )
@@ -562,7 +562,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
+class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerPort.objects.prefetch_related(
     queryset = PowerPort.objects.prefetch_related(
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
     )
     )
@@ -571,7 +571,7 @@ class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
+class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerOutlet.objects.prefetch_related(
     queryset = PowerOutlet.objects.prefetch_related(
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
         'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
     )
     )
@@ -580,7 +580,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
+class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = Interface.objects.prefetch_related(
     queryset = Interface.objects.prefetch_related(
         'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
         'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
         'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
         'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
@@ -590,7 +590,7 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
+class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
     queryset = FrontPort.objects.prefetch_related(
     queryset = FrontPort.objects.prefetch_related(
         'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
         'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
     )
     )
@@ -599,7 +599,7 @@ class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
+class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
     queryset = RearPort.objects.prefetch_related(
     queryset = RearPort.objects.prefetch_related(
         'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
         'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
     )
     )
@@ -608,21 +608,21 @@ class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class ModuleBayViewSet(ModelViewSet):
+class ModuleBayViewSet(NetBoxModelViewSet):
     queryset = ModuleBay.objects.prefetch_related('tags')
     queryset = ModuleBay.objects.prefetch_related('tags')
     serializer_class = serializers.ModuleBaySerializer
     serializer_class = serializers.ModuleBaySerializer
     filterset_class = filtersets.ModuleBayFilterSet
     filterset_class = filtersets.ModuleBayFilterSet
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class DeviceBayViewSet(ModelViewSet):
+class DeviceBayViewSet(NetBoxModelViewSet):
     queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
     queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
     serializer_class = serializers.DeviceBaySerializer
     serializer_class = serializers.DeviceBaySerializer
     filterset_class = filtersets.DeviceBayFilterSet
     filterset_class = filtersets.DeviceBayFilterSet
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']
 
 
 
 
-class InventoryItemViewSet(ModelViewSet):
+class InventoryItemViewSet(NetBoxModelViewSet):
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
     serializer_class = serializers.InventoryItemSerializer
     serializer_class = serializers.InventoryItemSerializer
     filterset_class = filtersets.InventoryItemFilterSet
     filterset_class = filtersets.InventoryItemFilterSet
@@ -645,7 +645,7 @@ class InventoryItemRoleViewSet(CustomFieldModelViewSet):
 # Cables
 # Cables
 #
 #
 
 
-class CableViewSet(ModelViewSet):
+class CableViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = Cable.objects.prefetch_related(
     queryset = Cable.objects.prefetch_related(
         'termination_a', 'termination_b'
         'termination_a', 'termination_b'
@@ -658,7 +658,7 @@ class CableViewSet(ModelViewSet):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisViewSet(ModelViewSet):
+class VirtualChassisViewSet(NetBoxModelViewSet):
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
         member_count=count_related(Device, 'virtual_chassis')
         member_count=count_related(Device, 'virtual_chassis')
     )
     )
@@ -671,7 +671,7 @@ class VirtualChassisViewSet(ModelViewSet):
 # Power panels
 # Power panels
 #
 #
 
 
-class PowerPanelViewSet(ModelViewSet):
+class PowerPanelViewSet(NetBoxModelViewSet):
     queryset = PowerPanel.objects.prefetch_related(
     queryset = PowerPanel.objects.prefetch_related(
         'site', 'location'
         'site', 'location'
     ).annotate(
     ).annotate(

+ 10 - 10
netbox/extras/api/views.py

@@ -18,7 +18,7 @@ from extras.reports import get_report, get_reports, run_report
 from extras.scripts import get_script, get_scripts, run_script
 from extras.scripts import get_script, get_scripts, run_script
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
-from netbox.api.views import ModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
 from utilities.exceptions import RQWorkerNotRunningException
 from utilities.exceptions import RQWorkerNotRunningException
 from utilities.utils import copy_safe_request, count_related
 from utilities.utils import copy_safe_request, count_related
 from . import serializers
 from . import serializers
@@ -58,7 +58,7 @@ class ConfigContextQuerySetMixin:
 # Webhooks
 # Webhooks
 #
 #
 
 
-class WebhookViewSet(ModelViewSet):
+class WebhookViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = Webhook.objects.all()
     queryset = Webhook.objects.all()
     serializer_class = serializers.WebhookSerializer
     serializer_class = serializers.WebhookSerializer
@@ -69,14 +69,14 @@ class WebhookViewSet(ModelViewSet):
 # Custom fields
 # Custom fields
 #
 #
 
 
-class CustomFieldViewSet(ModelViewSet):
+class CustomFieldViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = CustomField.objects.all()
     queryset = CustomField.objects.all()
     serializer_class = serializers.CustomFieldSerializer
     serializer_class = serializers.CustomFieldSerializer
     filterset_class = filtersets.CustomFieldFilterSet
     filterset_class = filtersets.CustomFieldFilterSet
 
 
 
 
-class CustomFieldModelViewSet(ModelViewSet):
+class CustomFieldModelViewSet(NetBoxModelViewSet):
     """
     """
     Include the applicable set of CustomFields in the ModelViewSet context.
     Include the applicable set of CustomFields in the ModelViewSet context.
     """
     """
@@ -98,7 +98,7 @@ class CustomFieldModelViewSet(ModelViewSet):
 # Custom links
 # Custom links
 #
 #
 
 
-class CustomLinkViewSet(ModelViewSet):
+class CustomLinkViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = CustomLink.objects.all()
     queryset = CustomLink.objects.all()
     serializer_class = serializers.CustomLinkSerializer
     serializer_class = serializers.CustomLinkSerializer
@@ -109,7 +109,7 @@ class CustomLinkViewSet(ModelViewSet):
 # Export templates
 # Export templates
 #
 #
 
 
-class ExportTemplateViewSet(ModelViewSet):
+class ExportTemplateViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = ExportTemplate.objects.all()
     queryset = ExportTemplate.objects.all()
     serializer_class = serializers.ExportTemplateSerializer
     serializer_class = serializers.ExportTemplateSerializer
@@ -120,7 +120,7 @@ class ExportTemplateViewSet(ModelViewSet):
 # Tags
 # Tags
 #
 #
 
 
-class TagViewSet(ModelViewSet):
+class TagViewSet(NetBoxModelViewSet):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
         tagged_items=count_related(TaggedItem, 'tag')
         tagged_items=count_related(TaggedItem, 'tag')
     )
     )
@@ -132,7 +132,7 @@ class TagViewSet(ModelViewSet):
 # Image attachments
 # Image attachments
 #
 #
 
 
-class ImageAttachmentViewSet(ModelViewSet):
+class ImageAttachmentViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = ImageAttachment.objects.all()
     queryset = ImageAttachment.objects.all()
     serializer_class = serializers.ImageAttachmentSerializer
     serializer_class = serializers.ImageAttachmentSerializer
@@ -143,7 +143,7 @@ class ImageAttachmentViewSet(ModelViewSet):
 # Journal entries
 # Journal entries
 #
 #
 
 
-class JournalEntryViewSet(ModelViewSet):
+class JournalEntryViewSet(NetBoxModelViewSet):
     metadata_class = ContentTypeMetadata
     metadata_class = ContentTypeMetadata
     queryset = JournalEntry.objects.all()
     queryset = JournalEntry.objects.all()
     serializer_class = serializers.JournalEntrySerializer
     serializer_class = serializers.JournalEntrySerializer
@@ -154,7 +154,7 @@ class JournalEntryViewSet(ModelViewSet):
 # Config contexts
 # Config contexts
 #
 #
 
 
-class ConfigContextViewSet(ModelViewSet):
+class ConfigContextViewSet(NetBoxModelViewSet):
     queryset = ConfigContext.objects.prefetch_related(
     queryset = ConfigContext.objects.prefetch_related(
         'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
         'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
     )
     )

+ 1 - 1
netbox/ipam/api/views.py

@@ -13,7 +13,7 @@ from dcim.models import Site
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
 from ipam import filtersets
 from ipam import filtersets
 from ipam.models import *
 from ipam.models import *
-from netbox.api.views import ModelViewSet, ObjectValidationMixin
+from netbox.api.viewsets.mixins import ObjectValidationMixin
 from netbox.config import get_config
 from netbox.config import get_config
 from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.constants import ADVISORY_LOCK_KEYS
 from utilities.utils import count_related
 from utilities.utils import count_related

+ 1 - 1
netbox/netbox/api/serializers/__init__.py

@@ -9,7 +9,7 @@ from .nested import *
 # Base model serializers
 # Base model serializers
 #
 #
 
 
-class NetBoxModelSerializer(TaggableObjectSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
+class NetBoxModelSerializer(TaggableModelSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
     """
     """
     Adds support for custom fields and tags.
     Adds support for custom fields and tags.
     """
     """

+ 2 - 2
netbox/netbox/api/serializers/features.py

@@ -8,7 +8,7 @@ from .nested import NestedTagSerializer
 
 
 __all__ = (
 __all__ = (
     'CustomFieldModelSerializer',
     'CustomFieldModelSerializer',
-    'TaggableObjectSerializer',
+    'TaggableModelSerializer',
 )
 )
 
 
 
 
@@ -44,7 +44,7 @@ class CustomFieldModelSerializer(serializers.Serializer):
             instance.custom_fields[field.name] = instance.cf.get(field.name)
             instance.custom_fields[field.name] = instance.cf.get(field.name)
 
 
 
 
-class TaggableObjectSerializer(serializers.Serializer):
+class TaggableModelSerializer(serializers.Serializer):
     """
     """
     Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment
     Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment
     on create() and update().
     on create() and update().

+ 0 - 275
netbox/netbox/api/views.py

@@ -1,292 +1,17 @@
-import logging
 import platform
 import platform
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django import __version__ as DJANGO_VERSION
 from django import __version__ as DJANGO_VERSION
 from django.apps import apps
 from django.apps import apps
 from django.conf import settings
 from django.conf import settings
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
-from django.db import transaction
-from django.db.models import ProtectedError
-from django.shortcuts import get_object_or_404
 from django_rq.queues import get_connection
 from django_rq.queues import get_connection
-from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
 from rest_framework.reverse import reverse
 from rest_framework.views import APIView
 from rest_framework.views import APIView
-from rest_framework.viewsets import ModelViewSet as ModelViewSet_
 from rq.worker import Worker
 from rq.worker import Worker
 
 
-from extras.models import ExportTemplate
-from netbox.api import BulkOperationSerializer
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
-from netbox.api.exceptions import SerializerNotFound
-from utilities.api import get_serializer_for_model
 
 
-HTTP_ACTIONS = {
-    'GET': 'view',
-    'OPTIONS': None,
-    'HEAD': 'view',
-    'POST': 'add',
-    'PUT': 'change',
-    'PATCH': 'change',
-    'DELETE': 'delete',
-}
-
-
-#
-# Mixins
-#
-
-class BulkUpdateModelMixin:
-    """
-    Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
-    or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
-    For example:
-
-    PATCH /api/dcim/sites/
-    [
-        {
-            "id": 123,
-            "name": "New name"
-        },
-        {
-            "id": 456,
-            "status": "planned"
-        }
-    ]
-    """
-    def bulk_update(self, request, *args, **kwargs):
-        partial = kwargs.pop('partial', False)
-        serializer = BulkOperationSerializer(data=request.data, many=True)
-        serializer.is_valid(raise_exception=True)
-        qs = self.get_queryset().filter(
-            pk__in=[o['id'] for o in serializer.data]
-        )
-
-        # Map update data by object ID
-        update_data = {
-            obj.pop('id'): obj for obj in request.data
-        }
-
-        data = self.perform_bulk_update(qs, update_data, partial=partial)
-
-        return Response(data, status=status.HTTP_200_OK)
-
-    def perform_bulk_update(self, objects, update_data, partial):
-        with transaction.atomic():
-            data_list = []
-            for obj in objects:
-                data = update_data.get(obj.id)
-                if hasattr(obj, 'snapshot'):
-                    obj.snapshot()
-                serializer = self.get_serializer(obj, data=data, partial=partial)
-                serializer.is_valid(raise_exception=True)
-                self.perform_update(serializer)
-                data_list.append(serializer.data)
-
-            return data_list
-
-    def bulk_partial_update(self, request, *args, **kwargs):
-        kwargs['partial'] = True
-        return self.bulk_update(request, *args, **kwargs)
-
-
-class BulkDestroyModelMixin:
-    """
-    Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
-    or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
-
-    DELETE /api/dcim/sites/
-    [
-        {"id": 123},
-        {"id": 456}
-    ]
-    """
-    def bulk_destroy(self, request, *args, **kwargs):
-        serializer = BulkOperationSerializer(data=request.data, many=True)
-        serializer.is_valid(raise_exception=True)
-        qs = self.get_queryset().filter(
-            pk__in=[o['id'] for o in serializer.data]
-        )
-
-        self.perform_bulk_destroy(qs)
-
-        return Response(status=status.HTTP_204_NO_CONTENT)
-
-    def perform_bulk_destroy(self, objects):
-        with transaction.atomic():
-            for obj in objects:
-                if hasattr(obj, 'snapshot'):
-                    obj.snapshot()
-                self.perform_destroy(obj)
-
-
-class ObjectValidationMixin:
-
-    def _validate_objects(self, instance):
-        """
-        Check that the provided instance or list of instances are matched by the current queryset. This confirms that
-        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
-        """
-        if type(instance) is list:
-            # Check that all instances are still included in the view's queryset
-            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
-            if conforming_count != len(instance):
-                raise ObjectDoesNotExist
-        else:
-            # Check that the instance is matched by the view's queryset
-            self.queryset.get(pk=instance.pk)
-
-
-#
-# Viewsets
-#
-
-class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet_):
-    """
-    Extend DRF's ModelViewSet to support bulk update and delete functions.
-    """
-    brief = False
-    brief_prefetch_fields = []
-
-    def get_object_with_snapshot(self):
-        """
-        Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
-        record the "before" data in the changelog.
-        """
-        obj = super().get_object()
-        if hasattr(obj, 'snapshot'):
-            obj.snapshot()
-        return obj
-
-    def get_serializer(self, *args, **kwargs):
-
-        # If a list of objects has been provided, initialize the serializer with many=True
-        if isinstance(kwargs.get('data', {}), list):
-            kwargs['many'] = True
-
-        return super().get_serializer(*args, **kwargs)
-
-    def get_serializer_class(self):
-        logger = logging.getLogger('netbox.api.views.ModelViewSet')
-
-        # If using 'brief' mode, find and return the nested serializer for this model, if one exists
-        if self.brief:
-            logger.debug("Request is for 'brief' format; initializing nested serializer")
-            try:
-                serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
-                logger.debug(f"Using serializer {serializer}")
-                return serializer
-            except SerializerNotFound:
-                logger.debug(f"Nested serializer for {self.queryset.model} not found!")
-
-        # Fall back to the hard-coded serializer class
-        logger.debug(f"Using serializer {self.serializer_class}")
-        return self.serializer_class
-
-    def get_queryset(self):
-        # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
-        if self.brief:
-            return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
-
-        return super().get_queryset()
-
-    def initialize_request(self, request, *args, **kwargs):
-        # Check if brief=True has been passed
-        if request.method == 'GET' and request.GET.get('brief'):
-            self.brief = True
-
-        return super().initialize_request(request, *args, **kwargs)
-
-    def initial(self, request, *args, **kwargs):
-        super().initial(request, *args, **kwargs)
-
-        if not request.user.is_authenticated:
-            return
-
-        # Restrict the view's QuerySet to allow only the permitted objects
-        action = HTTP_ACTIONS[request.method]
-        if action:
-            self.queryset = self.queryset.restrict(request.user, action)
-
-    def dispatch(self, request, *args, **kwargs):
-        logger = logging.getLogger('netbox.api.views.ModelViewSet')
-
-        try:
-            return super().dispatch(request, *args, **kwargs)
-        except ProtectedError as e:
-            protected_objects = list(e.protected_objects)
-            msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
-            msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
-            logger.warning(msg)
-            return self.finalize_response(
-                request,
-                Response({'detail': msg}, status=409),
-                *args,
-                **kwargs
-            )
-
-    def list(self, request, *args, **kwargs):
-        """
-        Overrides ListModelMixin to allow processing ExportTemplates.
-        """
-        if 'export' in request.GET:
-            content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
-            et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
-            queryset = self.filter_queryset(self.get_queryset())
-            return et.render_to_response(queryset)
-
-        return super().list(request, *args, **kwargs)
-
-    def perform_create(self, serializer):
-        model = self.queryset.model
-        logger = logging.getLogger('netbox.api.views.ModelViewSet')
-        logger.info(f"Creating new {model._meta.verbose_name}")
-
-        # Enforce object-level permissions on save()
-        try:
-            with transaction.atomic():
-                instance = serializer.save()
-                self._validate_objects(instance)
-        except ObjectDoesNotExist:
-            raise PermissionDenied()
-
-    def update(self, request, *args, **kwargs):
-        # Hotwire get_object() to ensure we save a pre-change snapshot
-        self.get_object = self.get_object_with_snapshot
-        return super().update(request, *args, **kwargs)
-
-    def perform_update(self, serializer):
-        model = self.queryset.model
-        logger = logging.getLogger('netbox.api.views.ModelViewSet')
-        logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
-
-        # Enforce object-level permissions on save()
-        try:
-            with transaction.atomic():
-                instance = serializer.save()
-                self._validate_objects(instance)
-        except ObjectDoesNotExist:
-            raise PermissionDenied()
-
-    def destroy(self, request, *args, **kwargs):
-        # Hotwire get_object() to ensure we save a pre-change snapshot
-        self.get_object = self.get_object_with_snapshot
-        return super().destroy(request, *args, **kwargs)
-
-    def perform_destroy(self, instance):
-        model = self.queryset.model
-        logger = logging.getLogger('netbox.api.views.ModelViewSet')
-        logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
-
-        return super().perform_destroy(instance)
-
-
-#
-# Views
-#
 
 
 class APIRootView(APIView):
 class APIRootView(APIView):
     """
     """

+ 168 - 0
netbox/netbox/api/viewsets/__init__.py

@@ -0,0 +1,168 @@
+import logging
+
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
+from django.db import transaction
+from django.db.models import ProtectedError
+from django.shortcuts import get_object_or_404
+from rest_framework.response import Response
+from rest_framework.viewsets import ModelViewSet
+
+from extras.models import ExportTemplate
+from netbox.api.exceptions import SerializerNotFound
+from utilities.api import get_serializer_for_model
+from .mixins import *
+
+__all__ = (
+    'NetBoxModelViewSet',
+)
+
+HTTP_ACTIONS = {
+    'GET': 'view',
+    'OPTIONS': None,
+    'HEAD': 'view',
+    'POST': 'add',
+    'PUT': 'change',
+    'PATCH': 'change',
+    'DELETE': 'delete',
+}
+
+
+class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet):
+    """
+    Extend DRF's ModelViewSet to support bulk update and delete functions.
+    """
+    brief = False
+    brief_prefetch_fields = []
+
+    def get_object_with_snapshot(self):
+        """
+        Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
+        record the "before" data in the changelog.
+        """
+        obj = super().get_object()
+        if hasattr(obj, 'snapshot'):
+            obj.snapshot()
+        return obj
+
+    def get_serializer(self, *args, **kwargs):
+
+        # If a list of objects has been provided, initialize the serializer with many=True
+        if isinstance(kwargs.get('data', {}), list):
+            kwargs['many'] = True
+
+        return super().get_serializer(*args, **kwargs)
+
+    def get_serializer_class(self):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+
+        # If using 'brief' mode, find and return the nested serializer for this model, if one exists
+        if self.brief:
+            logger.debug("Request is for 'brief' format; initializing nested serializer")
+            try:
+                serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
+                logger.debug(f"Using serializer {serializer}")
+                return serializer
+            except SerializerNotFound:
+                logger.debug(f"Nested serializer for {self.queryset.model} not found!")
+
+        # Fall back to the hard-coded serializer class
+        logger.debug(f"Using serializer {self.serializer_class}")
+        return self.serializer_class
+
+    def get_queryset(self):
+        # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
+        if self.brief:
+            return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
+
+        return super().get_queryset()
+
+    def initialize_request(self, request, *args, **kwargs):
+        # Check if brief=True has been passed
+        if request.method == 'GET' and request.GET.get('brief'):
+            self.brief = True
+
+        return super().initialize_request(request, *args, **kwargs)
+
+    def initial(self, request, *args, **kwargs):
+        super().initial(request, *args, **kwargs)
+
+        if not request.user.is_authenticated:
+            return
+
+        # Restrict the view's QuerySet to allow only the permitted objects
+        action = HTTP_ACTIONS[request.method]
+        if action:
+            self.queryset = self.queryset.restrict(request.user, action)
+
+    def dispatch(self, request, *args, **kwargs):
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+
+        try:
+            return super().dispatch(request, *args, **kwargs)
+        except ProtectedError as e:
+            protected_objects = list(e.protected_objects)
+            msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
+            msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
+            logger.warning(msg)
+            return self.finalize_response(
+                request,
+                Response({'detail': msg}, status=409),
+                *args,
+                **kwargs
+            )
+
+    def list(self, request, *args, **kwargs):
+        """
+        Overrides ListModelMixin to allow processing ExportTemplates.
+        """
+        if 'export' in request.GET:
+            content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
+            et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
+            queryset = self.filter_queryset(self.get_queryset())
+            return et.render_to_response(queryset)
+
+        return super().list(request, *args, **kwargs)
+
+    def perform_create(self, serializer):
+        model = self.queryset.model
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Creating new {model._meta.verbose_name}")
+
+        # Enforce object-level permissions on save()
+        try:
+            with transaction.atomic():
+                instance = serializer.save()
+                self._validate_objects(instance)
+        except ObjectDoesNotExist:
+            raise PermissionDenied()
+
+    def update(self, request, *args, **kwargs):
+        # Hotwire get_object() to ensure we save a pre-change snapshot
+        self.get_object = self.get_object_with_snapshot
+        return super().update(request, *args, **kwargs)
+
+    def perform_update(self, serializer):
+        model = self.queryset.model
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
+
+        # Enforce object-level permissions on save()
+        try:
+            with transaction.atomic():
+                instance = serializer.save()
+                self._validate_objects(instance)
+        except ObjectDoesNotExist:
+            raise PermissionDenied()
+
+    def destroy(self, request, *args, **kwargs):
+        # Hotwire get_object() to ensure we save a pre-change snapshot
+        self.get_object = self.get_object_with_snapshot
+        return super().destroy(request, *args, **kwargs)
+
+    def perform_destroy(self, instance):
+        model = self.queryset.model
+        logger = logging.getLogger('netbox.api.views.ModelViewSet')
+        logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
+
+        return super().perform_destroy(instance)

+ 113 - 0
netbox/netbox/api/viewsets/mixins.py

@@ -0,0 +1,113 @@
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
+from rest_framework import status
+from rest_framework.response import Response
+
+from netbox.api.serializers import BulkOperationSerializer
+
+__all__ = (
+    'BulkUpdateModelMixin',
+    'BulkDestroyModelMixin',
+    'ObjectValidationMixin',
+)
+
+
+class BulkUpdateModelMixin:
+    """
+    Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
+    or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
+    For example:
+
+    PATCH /api/dcim/sites/
+    [
+        {
+            "id": 123,
+            "name": "New name"
+        },
+        {
+            "id": 456,
+            "status": "planned"
+        }
+    ]
+    """
+    def bulk_update(self, request, *args, **kwargs):
+        partial = kwargs.pop('partial', False)
+        serializer = BulkOperationSerializer(data=request.data, many=True)
+        serializer.is_valid(raise_exception=True)
+        qs = self.get_queryset().filter(
+            pk__in=[o['id'] for o in serializer.data]
+        )
+
+        # Map update data by object ID
+        update_data = {
+            obj.pop('id'): obj for obj in request.data
+        }
+
+        data = self.perform_bulk_update(qs, update_data, partial=partial)
+
+        return Response(data, status=status.HTTP_200_OK)
+
+    def perform_bulk_update(self, objects, update_data, partial):
+        with transaction.atomic():
+            data_list = []
+            for obj in objects:
+                data = update_data.get(obj.id)
+                if hasattr(obj, 'snapshot'):
+                    obj.snapshot()
+                serializer = self.get_serializer(obj, data=data, partial=partial)
+                serializer.is_valid(raise_exception=True)
+                self.perform_update(serializer)
+                data_list.append(serializer.data)
+
+            return data_list
+
+    def bulk_partial_update(self, request, *args, **kwargs):
+        kwargs['partial'] = True
+        return self.bulk_update(request, *args, **kwargs)
+
+
+class BulkDestroyModelMixin:
+    """
+    Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
+    or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
+
+    DELETE /api/dcim/sites/
+    [
+        {"id": 123},
+        {"id": 456}
+    ]
+    """
+    def bulk_destroy(self, request, *args, **kwargs):
+        serializer = BulkOperationSerializer(data=request.data, many=True)
+        serializer.is_valid(raise_exception=True)
+        qs = self.get_queryset().filter(
+            pk__in=[o['id'] for o in serializer.data]
+        )
+
+        self.perform_bulk_destroy(qs)
+
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    def perform_bulk_destroy(self, objects):
+        with transaction.atomic():
+            for obj in objects:
+                if hasattr(obj, 'snapshot'):
+                    obj.snapshot()
+                self.perform_destroy(obj)
+
+
+class ObjectValidationMixin:
+
+    def _validate_objects(self, instance):
+        """
+        Check that the provided instance or list of instances are matched by the current queryset. This confirms that
+        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
+        """
+        if type(instance) is list:
+            # Check that all instances are still included in the view's queryset
+            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
+            if conforming_count != len(instance):
+                raise ObjectDoesNotExist
+        else:
+            # Check that the instance is matched by the view's queryset
+            self.queryset.get(pk=instance.pk)

+ 5 - 5
netbox/users/api/views.py

@@ -9,7 +9,7 @@ from rest_framework.status import HTTP_201_CREATED
 from rest_framework.views import APIView
 from rest_framework.views import APIView
 from rest_framework.viewsets import ViewSet
 from rest_framework.viewsets import ViewSet
 
 
-from netbox.api.views import ModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
 from users import filtersets
 from users import filtersets
 from users.models import ObjectPermission, Token, UserConfig
 from users.models import ObjectPermission, Token, UserConfig
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
@@ -29,13 +29,13 @@ class UsersRootView(APIRootView):
 # Users and groups
 # Users and groups
 #
 #
 
 
-class UserViewSet(ModelViewSet):
+class UserViewSet(NetBoxModelViewSet):
     queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
     queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
     serializer_class = serializers.UserSerializer
     serializer_class = serializers.UserSerializer
     filterset_class = filtersets.UserFilterSet
     filterset_class = filtersets.UserFilterSet
 
 
 
 
-class GroupViewSet(ModelViewSet):
+class GroupViewSet(NetBoxModelViewSet):
     queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
     queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
     serializer_class = serializers.GroupSerializer
     serializer_class = serializers.GroupSerializer
     filterset_class = filtersets.GroupFilterSet
     filterset_class = filtersets.GroupFilterSet
@@ -45,7 +45,7 @@ class GroupViewSet(ModelViewSet):
 # REST API tokens
 # REST API tokens
 #
 #
 
 
-class TokenViewSet(ModelViewSet):
+class TokenViewSet(NetBoxModelViewSet):
     queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
     queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
     serializer_class = serializers.TokenSerializer
     serializer_class = serializers.TokenSerializer
     filterset_class = filtersets.TokenFilterSet
     filterset_class = filtersets.TokenFilterSet
@@ -94,7 +94,7 @@ class TokenProvisionView(APIView):
 # ObjectPermissions
 # ObjectPermissions
 #
 #
 
 
-class ObjectPermissionViewSet(ModelViewSet):
+class ObjectPermissionViewSet(NetBoxModelViewSet):
     queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
     queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
     serializer_class = serializers.ObjectPermissionSerializer
     serializer_class = serializers.ObjectPermissionSerializer
     filterset_class = filtersets.ObjectPermissionFilterSet
     filterset_class = filtersets.ObjectPermissionFilterSet

+ 2 - 2
netbox/virtualization/api/views.py

@@ -1,7 +1,7 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from dcim.models import Device
 from dcim.models import Device
-from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, ModelViewSet
+from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, NetBoxModelViewSet
 from utilities.utils import count_related
 from utilities.utils import count_related
 from virtualization import filtersets
 from virtualization import filtersets
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -78,7 +78,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
         return serializers.VirtualMachineWithConfigContextSerializer
         return serializers.VirtualMachineWithConfigContextSerializer
 
 
 
 
-class VMInterfaceViewSet(ModelViewSet):
+class VMInterfaceViewSet(NetBoxModelViewSet):
     queryset = VMInterface.objects.prefetch_related(
     queryset = VMInterface.objects.prefetch_related(
         'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
         'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
         'fhrp_group_assignments',
         'fhrp_group_assignments',