Przeglądaj źródła

Merge pull request #8833 from netbox-community/8823-api-serializers

Closes #8823: Add plugin support for REST API components
Jeremy Stretch 4 lat temu
rodzic
commit
f559ceeb7f

+ 27 - 10
docs/plugins/development/rest-api.md

@@ -6,25 +6,37 @@ Generally speaking, there aren't many NetBox-specific components to implementing
 
 
 ## Serializers
 ## Serializers
 
 
-First, create a serializer for the plugin model, in `api/serializers.py`. Specify its model class and the fields to include within the serializer's `Meta` class.
+Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
+
+### Example
+
+To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
 
 
 ```python
 ```python
-from rest_framework.serializers import ModelSerializer
+# api/serializers.py
+from netbox.api.serializers import NetBoxModelSerializer
 from my_plugin.models import MyModel
 from my_plugin.models import MyModel
 
 
-class MyModelSerializer(ModelSerializer):
+class MyModelSerializer(NetBoxModelSerializer):
 
 
     class Meta:
     class Meta:
         model = MyModel
         model = MyModel
         fields = ('id', 'foo', 'bar')
         fields = ('id', 'foo', 'bar')
 ```
 ```
 
 
-## Views
+## Viewsets
+
+Just as in the user interface, a REST API view handles the business logic of displaying and interacting with NetBox objects. NetBox provides the `NetBoxModelViewSet` class, which extends DRF's built-in `ModelViewSet` to handle bulk operations and object validation.
+
+Unlike the user interface, typically only a single view set is required per model: This view set handles all request types (`GET`, `POST`, `DELETE`, etc.).
 
 
-Next, create a generic API view set that allows basic CRUD (create, read, update, and delete) operations for objects. This is defined in `api/views.py`. Specify the `queryset` and `serializer_class` attributes under the view set.
+### Example
+
+To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/views.py`, and define the `queryset` and `serializer_class` attributes.
 
 
 ```python
 ```python
-from rest_framework.viewsets import ModelViewSet
+# api/views.py
+from netbox.api.viewsets import ModelViewSet
 from my_plugin.models import MyModel
 from my_plugin.models import MyModel
 from .serializers import MyModelSerializer
 from .serializers import MyModelSerializer
 
 
@@ -33,11 +45,16 @@ class MyModelViewSet(ModelViewSet):
     serializer_class = MyModelSerializer
     serializer_class = MyModelSerializer
 ```
 ```
 
 
-## URLs
+## Routers
+
+Routers map URLs to REST API views (endpoints). NetBox does not provide any custom components for this; the [`DefaultRouter`](https://www.django-rest-framework.org/api-guide/routers/#defaultrouter) class provided by DRF should suffice for most use cases.
+
+Routers should be exposed in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
 
 
-Finally, we'll register a URL for our endpoint in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
+### Example
 
 
 ```python
 ```python
+# api/urls.py
 from rest_framework import routers
 from rest_framework import routers
 from .views import MyModelViewSet
 from .views import MyModelViewSet
 
 
@@ -46,7 +63,7 @@ router.register('my-model', MyModelViewSet)
 urlpatterns = router.urls
 urlpatterns = router.urls
 ```
 ```
 
 
-With these three components in place, we can request `/api/plugins/my-plugin/my-model/` to retrieve a list of all MyModel instances.
+This will make the plugin's view accessible at `/api/plugins/my-plugin/my-model/`.
 
 
 !!! warning
 !!! warning
-    This example is provided as a minimal reference implementation only. It does not address authentication, performance, or myriad other concerns that plugin authors may need to address.
+    The examples provided here are intended to serve as a minimal reference implementation only. This documentation does not address authentication, performance, or myriad other concerns that plugin authors may need to address.

+ 5 - 5
netbox/circuits/api/serializers.py

@@ -5,7 +5,7 @@ from circuits.models import *
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.serializers import LinkTerminationSerializer
 from dcim.api.serializers import LinkTerminationSerializer
 from netbox.api import ChoiceField
 from netbox.api import ChoiceField
-from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
+from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from .nested_serializers import *
 from .nested_serializers import *
 
 
@@ -14,7 +14,7 @@ from .nested_serializers import *
 # Providers
 # Providers
 #
 #
 
 
-class ProviderSerializer(PrimaryModelSerializer):
+class ProviderSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     circuit_count = serializers.IntegerField(read_only=True)
     circuit_count = serializers.IntegerField(read_only=True)
 
 
@@ -30,7 +30,7 @@ class ProviderSerializer(PrimaryModelSerializer):
 # Provider networks
 # Provider networks
 #
 #
 
 
-class ProviderNetworkSerializer(PrimaryModelSerializer):
+class ProviderNetworkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
 
 
@@ -46,7 +46,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
 # Circuits
 # Circuits
 #
 #
 
 
-class CircuitTypeSerializer(PrimaryModelSerializer):
+class CircuitTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
     circuit_count = serializers.IntegerField(read_only=True)
     circuit_count = serializers.IntegerField(read_only=True)
 
 
@@ -70,7 +70,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
         ]
         ]
 
 
 
 
-class CircuitSerializer(PrimaryModelSerializer):
+class CircuitSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)

+ 6 - 7
netbox/circuits/api/views.py

@@ -3,8 +3,7 @@ from rest_framework.routers import APIRootView
 from circuits import filtersets
 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 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
 
 
@@ -21,7 +20,7 @@ class CircuitsRootView(APIRootView):
 # Providers
 # Providers
 #
 #
 
 
-class ProviderViewSet(CustomFieldModelViewSet):
+class ProviderViewSet(NetBoxModelViewSet):
     queryset = Provider.objects.prefetch_related('tags').annotate(
     queryset = Provider.objects.prefetch_related('tags').annotate(
         circuit_count=count_related(Circuit, 'provider')
         circuit_count=count_related(Circuit, 'provider')
     )
     )
@@ -33,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
 #  Circuit Types
 #  Circuit Types
 #
 #
 
 
-class CircuitTypeViewSet(CustomFieldModelViewSet):
+class CircuitTypeViewSet(NetBoxModelViewSet):
     queryset = CircuitType.objects.prefetch_related('tags').annotate(
     queryset = CircuitType.objects.prefetch_related('tags').annotate(
         circuit_count=count_related(Circuit, 'type')
         circuit_count=count_related(Circuit, 'type')
     )
     )
@@ -45,7 +44,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
 # Circuits
 # Circuits
 #
 #
 
 
-class CircuitViewSet(CustomFieldModelViewSet):
+class CircuitViewSet(NetBoxModelViewSet):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
         'type', 'tenant', 'provider', 'termination_a', 'termination_z'
         'type', 'tenant', 'provider', 'termination_a', 'termination_z'
     ).prefetch_related('tags')
     ).prefetch_related('tags')
@@ -57,7 +56,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'
     )
     )
@@ -70,7 +69,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
 # Provider networks
 # Provider networks
 #
 #
 
 
-class ProviderNetworkViewSet(CustomFieldModelViewSet):
+class ProviderNetworkViewSet(NetBoxModelViewSet):
     queryset = ProviderNetwork.objects.prefetch_related('tags')
     queryset = ProviderNetwork.objects.prefetch_related('tags')
     serializer_class = serializers.ProviderNetworkSerializer
     serializer_class = serializers.ProviderNetworkSerializer
     filterset_class = filtersets.ProviderNetworkFilterSet
     filterset_class = filtersets.ProviderNetworkFilterSet

+ 27 - 27
netbox/dcim/api/serializers.py

@@ -12,7 +12,7 @@ from ipam.api.nested_serializers import (
 from ipam.models import ASN, VLAN
 from ipam.models import ASN, VLAN
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import (
 from netbox.api.serializers import (
-    NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
+    NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
 )
 )
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -109,7 +109,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class SiteSerializer(PrimaryModelSerializer):
+class SiteSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     region = NestedRegionSerializer(required=False, allow_null=True)
     region = NestedRegionSerializer(required=False, allow_null=True)
@@ -161,7 +161,7 @@ class LocationSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class RackRoleSerializer(PrimaryModelSerializer):
+class RackRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
     rack_count = serializers.IntegerField(read_only=True)
     rack_count = serializers.IntegerField(read_only=True)
 
 
@@ -173,7 +173,7 @@ class RackRoleSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class RackSerializer(PrimaryModelSerializer):
+class RackSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     location = NestedLocationSerializer(required=False, allow_null=True, default=None)
     location = NestedLocationSerializer(required=False, allow_null=True, default=None)
@@ -212,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer):
         return obj['name']
         return obj['name']
 
 
 
 
-class RackReservationSerializer(PrimaryModelSerializer):
+class RackReservationSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
     rack = NestedRackSerializer()
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
     user = NestedUserSerializer()
@@ -266,7 +266,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
 # Device/module types
 # Device/module types
 #
 #
 
 
-class ManufacturerSerializer(PrimaryModelSerializer):
+class ManufacturerSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
     devicetype_count = serializers.IntegerField(read_only=True)
     devicetype_count = serializers.IntegerField(read_only=True)
     inventoryitem_count = serializers.IntegerField(read_only=True)
     inventoryitem_count = serializers.IntegerField(read_only=True)
@@ -280,7 +280,7 @@ class ManufacturerSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class DeviceTypeSerializer(PrimaryModelSerializer):
+class DeviceTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     manufacturer = NestedManufacturerSerializer()
     manufacturer = NestedManufacturerSerializer()
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@@ -296,7 +296,7 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ModuleTypeSerializer(PrimaryModelSerializer):
+class ModuleTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
     manufacturer = NestedManufacturerSerializer()
     manufacturer = NestedManufacturerSerializer()
     # module_count = serializers.IntegerField(read_only=True)
     # module_count = serializers.IntegerField(read_only=True)
@@ -487,7 +487,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
 # Devices
 # Devices
 #
 #
 
 
-class DeviceRoleSerializer(PrimaryModelSerializer):
+class DeviceRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
     virtualmachine_count = serializers.IntegerField(read_only=True)
     virtualmachine_count = serializers.IntegerField(read_only=True)
@@ -500,7 +500,7 @@ class DeviceRoleSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class PlatformSerializer(PrimaryModelSerializer):
+class PlatformSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
@@ -514,7 +514,7 @@ class PlatformSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class DeviceSerializer(PrimaryModelSerializer):
+class DeviceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
     device_role = NestedDeviceRoleSerializer()
@@ -556,7 +556,7 @@ class DeviceSerializer(PrimaryModelSerializer):
         return data
         return data
 
 
 
 
-class ModuleSerializer(PrimaryModelSerializer):
+class ModuleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module_bay = NestedModuleBaySerializer()
     module_bay = NestedModuleBaySerializer()
@@ -594,7 +594,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
 # Device components
 # Device components
 #
 #
 
 
-class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -622,7 +622,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
         ]
         ]
 
 
 
 
-class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -650,7 +650,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
         ]
         ]
 
 
 
 
-class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -685,7 +685,7 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
         ]
         ]
 
 
 
 
-class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -709,7 +709,7 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
         ]
         ]
 
 
 
 
-class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -768,7 +768,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
         return super().validate(data)
         return super().validate(data)
 
 
 
 
-class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
+class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -798,7 +798,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'display', 'name', 'label']
         fields = ['id', 'url', 'display', 'name', 'label']
 
 
 
 
-class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
+class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     module = ComponentNestedModuleSerializer(
     module = ComponentNestedModuleSerializer(
@@ -818,7 +818,7 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
         ]
         ]
 
 
 
 
-class ModuleBaySerializer(PrimaryModelSerializer):
+class ModuleBaySerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     # installed_module = NestedModuleSerializer(required=False, allow_null=True)
     # installed_module = NestedModuleSerializer(required=False, allow_null=True)
@@ -831,7 +831,7 @@ class ModuleBaySerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class DeviceBaySerializer(PrimaryModelSerializer):
+class DeviceBaySerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     installed_device = NestedDeviceSerializer(required=False, allow_null=True)
     installed_device = NestedDeviceSerializer(required=False, allow_null=True)
@@ -844,7 +844,7 @@ class DeviceBaySerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class InventoryItemSerializer(PrimaryModelSerializer):
+class InventoryItemSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
@@ -879,7 +879,7 @@ class InventoryItemSerializer(PrimaryModelSerializer):
 # Device component roles
 # Device component roles
 #
 #
 
 
-class InventoryItemRoleSerializer(PrimaryModelSerializer):
+class InventoryItemRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
     inventoryitem_count = serializers.IntegerField(read_only=True)
     inventoryitem_count = serializers.IntegerField(read_only=True)
 
 
@@ -895,7 +895,7 @@ class InventoryItemRoleSerializer(PrimaryModelSerializer):
 # Cables
 # Cables
 #
 #
 
 
-class CableSerializer(PrimaryModelSerializer):
+class CableSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
     termination_a_type = ContentTypeField(
     termination_a_type = ContentTypeField(
         queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
         queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@@ -1001,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisSerializer(PrimaryModelSerializer):
+class VirtualChassisSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     master = NestedDeviceSerializer(required=False)
     master = NestedDeviceSerializer(required=False)
     member_count = serializers.IntegerField(read_only=True)
     member_count = serializers.IntegerField(read_only=True)
@@ -1018,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
 # Power panels
 # Power panels
 #
 #
 
 
-class PowerPanelSerializer(PrimaryModelSerializer):
+class PowerPanelSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     location = NestedLocationSerializer(
     location = NestedLocationSerializer(
@@ -1036,7 +1036,7 @@ class PowerPanelSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
+class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
     power_panel = NestedPowerPanelSerializer()
     power_panel = NestedPowerPanelSerializer()
     rack = NestedRackSerializer(
     rack = NestedRackSerializer(

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

@@ -14,12 +14,12 @@ from rest_framework.viewsets import ViewSet
 from circuits.models import Circuit
 from circuits.models import Circuit
 from dcim import filtersets
 from dcim import filtersets
 from dcim.models import *
 from dcim.models import *
-from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
+from extras.api.views import ConfigContextQuerySetMixin
 from ipam.models import Prefix, VLAN
 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
@@ -103,7 +103,7 @@ class PassThroughPortMixin(object):
 # Regions
 # Regions
 #
 #
 
 
-class RegionViewSet(CustomFieldModelViewSet):
+class RegionViewSet(NetBoxModelViewSet):
     queryset = Region.objects.add_related_count(
     queryset = Region.objects.add_related_count(
         Region.objects.all(),
         Region.objects.all(),
         Site,
         Site,
@@ -119,7 +119,7 @@ class RegionViewSet(CustomFieldModelViewSet):
 # Site groups
 # Site groups
 #
 #
 
 
-class SiteGroupViewSet(CustomFieldModelViewSet):
+class SiteGroupViewSet(NetBoxModelViewSet):
     queryset = SiteGroup.objects.add_related_count(
     queryset = SiteGroup.objects.add_related_count(
         SiteGroup.objects.all(),
         SiteGroup.objects.all(),
         Site,
         Site,
@@ -135,7 +135,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
 # Sites
 # Sites
 #
 #
 
 
-class SiteViewSet(CustomFieldModelViewSet):
+class SiteViewSet(NetBoxModelViewSet):
     queryset = Site.objects.prefetch_related(
     queryset = Site.objects.prefetch_related(
         'region', 'tenant', 'asns', 'tags'
         'region', 'tenant', 'asns', 'tags'
     ).annotate(
     ).annotate(
@@ -154,7 +154,7 @@ class SiteViewSet(CustomFieldModelViewSet):
 # Locations
 # Locations
 #
 #
 
 
-class LocationViewSet(CustomFieldModelViewSet):
+class LocationViewSet(NetBoxModelViewSet):
     queryset = Location.objects.add_related_count(
     queryset = Location.objects.add_related_count(
         Location.objects.add_related_count(
         Location.objects.add_related_count(
             Location.objects.all(),
             Location.objects.all(),
@@ -176,7 +176,7 @@ class LocationViewSet(CustomFieldModelViewSet):
 # Rack roles
 # Rack roles
 #
 #
 
 
-class RackRoleViewSet(CustomFieldModelViewSet):
+class RackRoleViewSet(NetBoxModelViewSet):
     queryset = RackRole.objects.prefetch_related('tags').annotate(
     queryset = RackRole.objects.prefetch_related('tags').annotate(
         rack_count=count_related(Rack, 'role')
         rack_count=count_related(Rack, 'role')
     )
     )
@@ -188,7 +188,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
 # Racks
 # Racks
 #
 #
 
 
-class RackViewSet(CustomFieldModelViewSet):
+class RackViewSet(NetBoxModelViewSet):
     queryset = Rack.objects.prefetch_related(
     queryset = Rack.objects.prefetch_related(
         'site', 'location', 'role', 'tenant', 'tags'
         'site', 'location', 'role', 'tenant', 'tags'
     ).annotate(
     ).annotate(
@@ -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
@@ -260,7 +260,7 @@ class RackReservationViewSet(ModelViewSet):
 # Manufacturers
 # Manufacturers
 #
 #
 
 
-class ManufacturerViewSet(CustomFieldModelViewSet):
+class ManufacturerViewSet(NetBoxModelViewSet):
     queryset = Manufacturer.objects.prefetch_related('tags').annotate(
     queryset = Manufacturer.objects.prefetch_related('tags').annotate(
         devicetype_count=count_related(DeviceType, 'manufacturer'),
         devicetype_count=count_related(DeviceType, 'manufacturer'),
         inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
         inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
@@ -274,7 +274,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
 # Device/module types
 # Device/module types
 #
 #
 
 
-class DeviceTypeViewSet(CustomFieldModelViewSet):
+class DeviceTypeViewSet(NetBoxModelViewSet):
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
         device_count=count_related(Device, 'device_type')
         device_count=count_related(Device, 'device_type')
     )
     )
@@ -283,7 +283,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
     brief_prefetch_fields = ['manufacturer']
     brief_prefetch_fields = ['manufacturer']
 
 
 
 
-class ModuleTypeViewSet(CustomFieldModelViewSet):
+class ModuleTypeViewSet(NetBoxModelViewSet):
     queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
     queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
         # module_count=count_related(Module, 'module_type')
         # module_count=count_related(Module, 'module_type')
     )
     )
@@ -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
@@ -360,7 +360,7 @@ class InventoryItemTemplateViewSet(ModelViewSet):
 # Device roles
 # Device roles
 #
 #
 
 
-class DeviceRoleViewSet(CustomFieldModelViewSet):
+class DeviceRoleViewSet(NetBoxModelViewSet):
     queryset = DeviceRole.objects.prefetch_related('tags').annotate(
     queryset = DeviceRole.objects.prefetch_related('tags').annotate(
         device_count=count_related(Device, 'device_role'),
         device_count=count_related(Device, 'device_role'),
         virtualmachine_count=count_related(VirtualMachine, 'role')
         virtualmachine_count=count_related(VirtualMachine, 'role')
@@ -373,7 +373,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
 # Platforms
 # Platforms
 #
 #
 
 
-class PlatformViewSet(CustomFieldModelViewSet):
+class PlatformViewSet(NetBoxModelViewSet):
     queryset = Platform.objects.prefetch_related('tags').annotate(
     queryset = Platform.objects.prefetch_related('tags').annotate(
         device_count=count_related(Device, 'platform'),
         device_count=count_related(Device, 'platform'),
         virtualmachine_count=count_related(VirtualMachine, 'platform')
         virtualmachine_count=count_related(VirtualMachine, 'platform')
@@ -386,7 +386,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
 # Devices/modules
 # Devices/modules
 #
 #
 
 
-class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
+class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
     queryset = Device.objects.prefetch_related(
     queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
         'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
         'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
@@ -532,7 +532,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
         return Response(response)
         return Response(response)
 
 
 
 
-class ModuleViewSet(CustomFieldModelViewSet):
+class ModuleViewSet(NetBoxModelViewSet):
     queryset = Module.objects.prefetch_related(
     queryset = Module.objects.prefetch_related(
         'device', 'module_bay', 'module_type__manufacturer', 'tags',
         'device', 'module_bay', 'module_type__manufacturer', 'tags',
     )
     )
@@ -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
@@ -633,7 +633,7 @@ class InventoryItemViewSet(ModelViewSet):
 # Device component roles
 # Device component roles
 #
 #
 
 
-class InventoryItemRoleViewSet(CustomFieldModelViewSet):
+class InventoryItemRoleViewSet(NetBoxModelViewSet):
     queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
     queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
         inventoryitem_count=count_related(InventoryItem, 'role')
         inventoryitem_count=count_related(InventoryItem, 'role')
     )
     )
@@ -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(
@@ -685,7 +685,7 @@ class PowerPanelViewSet(ModelViewSet):
 # Power feeds
 # Power feeds
 #
 #
 
 
-class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
+class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
     queryset = PowerFeed.objects.prefetch_related(
     queryset = PowerFeed.objects.prefetch_related(
         'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
         'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
     )
     )

+ 9 - 27
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,36 +69,18 @@ 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):
-    """
-    Include the applicable set of CustomFields in the ModelViewSet context.
-    """
-
-    def get_serializer_context(self):
-
-        # Gather all custom fields for the model
-        content_type = ContentType.objects.get_for_model(self.queryset.model)
-        custom_fields = content_type.custom_fields.all()
-
-        context = super().get_serializer_context()
-        context.update({
-            'custom_fields': custom_fields,
-        })
-        return context
-
-
 #
 #
 # 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 +91,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 +102,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 +114,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 +125,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 +136,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',
     )
     )

+ 17 - 17
netbox/ipam/api/serializers.py

@@ -9,7 +9,7 @@ from ipam.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.models import *
 from ipam.models import *
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
-from netbox.api.serializers import PrimaryModelSerializer
+from netbox.api.serializers import NetBoxModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
 from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
@@ -20,7 +20,7 @@ from .nested_serializers import *
 # ASNs
 # ASNs
 #
 #
 
 
-class ASNSerializer(PrimaryModelSerializer):
+class ASNSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     site_count = serializers.IntegerField(read_only=True)
     site_count = serializers.IntegerField(read_only=True)
@@ -37,7 +37,7 @@ class ASNSerializer(PrimaryModelSerializer):
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFSerializer(PrimaryModelSerializer):
+class VRFSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     import_targets = SerializedPKRelatedField(
     import_targets = SerializedPKRelatedField(
@@ -67,7 +67,7 @@ class VRFSerializer(PrimaryModelSerializer):
 # Route targets
 # Route targets
 #
 #
 
 
-class RouteTargetSerializer(PrimaryModelSerializer):
+class RouteTargetSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
 
 
@@ -82,7 +82,7 @@ class RouteTargetSerializer(PrimaryModelSerializer):
 # RIRs/aggregates
 # RIRs/aggregates
 #
 #
 
 
-class RIRSerializer(PrimaryModelSerializer):
+class RIRSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
     aggregate_count = serializers.IntegerField(read_only=True)
     aggregate_count = serializers.IntegerField(read_only=True)
 
 
@@ -94,7 +94,7 @@ class RIRSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class AggregateSerializer(PrimaryModelSerializer):
+class AggregateSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     rir = NestedRIRSerializer()
     rir = NestedRIRSerializer()
@@ -113,7 +113,7 @@ class AggregateSerializer(PrimaryModelSerializer):
 # FHRP Groups
 # FHRP Groups
 #
 #
 
 
-class FHRPGroupSerializer(PrimaryModelSerializer):
+class FHRPGroupSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
     ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
     ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
 
 
@@ -125,7 +125,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
+class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
     group = NestedFHRPGroupSerializer()
     group = NestedFHRPGroupSerializer()
     interface_type = ContentTypeField(
     interface_type = ContentTypeField(
@@ -153,7 +153,7 @@ class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
 # VLANs
 # VLANs
 #
 #
 
 
-class RoleSerializer(PrimaryModelSerializer):
+class RoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
     prefix_count = serializers.IntegerField(read_only=True)
     prefix_count = serializers.IntegerField(read_only=True)
     vlan_count = serializers.IntegerField(read_only=True)
     vlan_count = serializers.IntegerField(read_only=True)
@@ -166,7 +166,7 @@ class RoleSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class VLANGroupSerializer(PrimaryModelSerializer):
+class VLANGroupSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
     scope_type = ContentTypeField(
     scope_type = ContentTypeField(
         queryset=ContentType.objects.filter(
         queryset=ContentType.objects.filter(
@@ -196,7 +196,7 @@ class VLANGroupSerializer(PrimaryModelSerializer):
         return serializer(obj.scope, context=context).data
         return serializer(obj.scope, context=context).data
 
 
 
 
-class VLANSerializer(PrimaryModelSerializer):
+class VLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
     group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
@@ -230,7 +230,7 @@ class AvailableVLANSerializer(serializers.Serializer):
         ])
         ])
 
 
 
 
-class CreateAvailableVLANSerializer(PrimaryModelSerializer):
+class CreateAvailableVLANSerializer(NetBoxModelSerializer):
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
@@ -251,7 +251,7 @@ class CreateAvailableVLANSerializer(PrimaryModelSerializer):
 # Prefixes
 # Prefixes
 #
 #
 
 
-class PrefixSerializer(PrimaryModelSerializer):
+class PrefixSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
@@ -323,7 +323,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
 # IP ranges
 # IP ranges
 #
 #
 
 
-class IPRangeSerializer(PrimaryModelSerializer):
+class IPRangeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
@@ -345,7 +345,7 @@ class IPRangeSerializer(PrimaryModelSerializer):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressSerializer(PrimaryModelSerializer):
+class IPAddressSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
@@ -403,7 +403,7 @@ class AvailableIPSerializer(serializers.Serializer):
 # Services
 # Services
 #
 #
 
 
-class ServiceTemplateSerializer(PrimaryModelSerializer):
+class ServiceTemplateSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
 
 
@@ -415,7 +415,7 @@ class ServiceTemplateSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ServiceSerializer(PrimaryModelSerializer):
+class ServiceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
     device = NestedDeviceSerializer(required=False, allow_null=True)
     device = NestedDeviceSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)

+ 18 - 19
netbox/ipam/api/views.py

@@ -1,19 +1,18 @@
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db import transaction
 from django.db import transaction
-from django_pglocks import advisory_lock
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
+from django_pglocks import advisory_lock
 from drf_yasg.utils import swagger_auto_schema
 from drf_yasg.utils import swagger_auto_schema
 from rest_framework import status
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 from rest_framework.views import APIView
 from rest_framework.views import APIView
 
 
-
 from dcim.models import Site
 from dcim.models import Site
-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 import NetBoxModelViewSet
+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
@@ -32,13 +31,13 @@ class IPAMRootView(APIRootView):
 # Viewsets
 # Viewsets
 #
 #
 
 
-class ASNViewSet(CustomFieldModelViewSet):
+class ASNViewSet(NetBoxModelViewSet):
     queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
     queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
     serializer_class = serializers.ASNSerializer
     serializer_class = serializers.ASNSerializer
     filterset_class = filtersets.ASNFilterSet
     filterset_class = filtersets.ASNFilterSet
 
 
 
 
-class VRFViewSet(CustomFieldModelViewSet):
+class VRFViewSet(NetBoxModelViewSet):
     queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
     queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
         'import_targets', 'export_targets', 'tags'
         'import_targets', 'export_targets', 'tags'
     ).annotate(
     ).annotate(
@@ -49,13 +48,13 @@ class VRFViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VRFFilterSet
     filterset_class = filtersets.VRFFilterSet
 
 
 
 
-class RouteTargetViewSet(CustomFieldModelViewSet):
+class RouteTargetViewSet(NetBoxModelViewSet):
     queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
     queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
     serializer_class = serializers.RouteTargetSerializer
     serializer_class = serializers.RouteTargetSerializer
     filterset_class = filtersets.RouteTargetFilterSet
     filterset_class = filtersets.RouteTargetFilterSet
 
 
 
 
-class RIRViewSet(CustomFieldModelViewSet):
+class RIRViewSet(NetBoxModelViewSet):
     queryset = RIR.objects.annotate(
     queryset = RIR.objects.annotate(
         aggregate_count=count_related(Aggregate, 'rir')
         aggregate_count=count_related(Aggregate, 'rir')
     ).prefetch_related('tags')
     ).prefetch_related('tags')
@@ -63,13 +62,13 @@ class RIRViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.RIRFilterSet
     filterset_class = filtersets.RIRFilterSet
 
 
 
 
-class AggregateViewSet(CustomFieldModelViewSet):
+class AggregateViewSet(NetBoxModelViewSet):
     queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
     queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
     serializer_class = serializers.AggregateSerializer
     serializer_class = serializers.AggregateSerializer
     filterset_class = filtersets.AggregateFilterSet
     filterset_class = filtersets.AggregateFilterSet
 
 
 
 
-class RoleViewSet(CustomFieldModelViewSet):
+class RoleViewSet(NetBoxModelViewSet):
     queryset = Role.objects.annotate(
     queryset = Role.objects.annotate(
         prefix_count=count_related(Prefix, 'role'),
         prefix_count=count_related(Prefix, 'role'),
         vlan_count=count_related(VLAN, 'role')
         vlan_count=count_related(VLAN, 'role')
@@ -78,7 +77,7 @@ class RoleViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.RoleFilterSet
     filterset_class = filtersets.RoleFilterSet
 
 
 
 
-class PrefixViewSet(CustomFieldModelViewSet):
+class PrefixViewSet(NetBoxModelViewSet):
     queryset = Prefix.objects.prefetch_related(
     queryset = Prefix.objects.prefetch_related(
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
     )
     )
@@ -93,7 +92,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
         return super().get_serializer_class()
         return super().get_serializer_class()
 
 
 
 
-class IPRangeViewSet(CustomFieldModelViewSet):
+class IPRangeViewSet(NetBoxModelViewSet):
     queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
     queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
     serializer_class = serializers.IPRangeSerializer
     serializer_class = serializers.IPRangeSerializer
     filterset_class = filtersets.IPRangeFilterSet
     filterset_class = filtersets.IPRangeFilterSet
@@ -101,7 +100,7 @@ class IPRangeViewSet(CustomFieldModelViewSet):
     parent_model = IPRange  # AvailableIPsMixin
     parent_model = IPRange  # AvailableIPsMixin
 
 
 
 
-class IPAddressViewSet(CustomFieldModelViewSet):
+class IPAddressViewSet(NetBoxModelViewSet):
     queryset = IPAddress.objects.prefetch_related(
     queryset = IPAddress.objects.prefetch_related(
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
     )
     )
@@ -109,20 +108,20 @@ class IPAddressViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.IPAddressFilterSet
     filterset_class = filtersets.IPAddressFilterSet
 
 
 
 
-class FHRPGroupViewSet(CustomFieldModelViewSet):
+class FHRPGroupViewSet(NetBoxModelViewSet):
     queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
     queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
     serializer_class = serializers.FHRPGroupSerializer
     serializer_class = serializers.FHRPGroupSerializer
     filterset_class = filtersets.FHRPGroupFilterSet
     filterset_class = filtersets.FHRPGroupFilterSet
     brief_prefetch_fields = ('ip_addresses',)
     brief_prefetch_fields = ('ip_addresses',)
 
 
 
 
-class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
+class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
     queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
     queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
     serializer_class = serializers.FHRPGroupAssignmentSerializer
     serializer_class = serializers.FHRPGroupAssignmentSerializer
     filterset_class = filtersets.FHRPGroupAssignmentFilterSet
     filterset_class = filtersets.FHRPGroupAssignmentFilterSet
 
 
 
 
-class VLANGroupViewSet(CustomFieldModelViewSet):
+class VLANGroupViewSet(NetBoxModelViewSet):
     queryset = VLANGroup.objects.annotate(
     queryset = VLANGroup.objects.annotate(
         vlan_count=count_related(VLAN, 'group')
         vlan_count=count_related(VLAN, 'group')
     ).prefetch_related('tags')
     ).prefetch_related('tags')
@@ -130,7 +129,7 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VLANGroupFilterSet
     filterset_class = filtersets.VLANGroupFilterSet
 
 
 
 
-class VLANViewSet(CustomFieldModelViewSet):
+class VLANViewSet(NetBoxModelViewSet):
     queryset = VLAN.objects.prefetch_related(
     queryset = VLAN.objects.prefetch_related(
         'site', 'group', 'tenant', 'role', 'tags'
         'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
     ).annotate(
@@ -140,13 +139,13 @@ class VLANViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.VLANFilterSet
     filterset_class = filtersets.VLANFilterSet
 
 
 
 
-class ServiceTemplateViewSet(CustomFieldModelViewSet):
+class ServiceTemplateViewSet(NetBoxModelViewSet):
     queryset = ServiceTemplate.objects.prefetch_related('tags')
     queryset = ServiceTemplate.objects.prefetch_related('tags')
     serializer_class = serializers.ServiceTemplateSerializer
     serializer_class = serializers.ServiceTemplateSerializer
     filterset_class = filtersets.ServiceTemplateFilterSet
     filterset_class = filtersets.ServiceTemplateFilterSet
 
 
 
 
-class ServiceViewSet(CustomFieldModelViewSet):
+class ServiceViewSet(NetBoxModelViewSet):
     queryset = Service.objects.prefetch_related(
     queryset = Service.objects.prefetch_related(
         'device', 'virtual_machine', 'tags', 'ipaddresses'
         'device', 'virtual_machine', 'tags', 'ipaddresses'
     )
     )

+ 0 - 193
netbox/netbox/api/serializers.py

@@ -1,193 +0,0 @@
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
-from django.db.models import ManyToManyField
-from rest_framework import serializers
-from rest_framework.exceptions import ValidationError
-from rest_framework.fields import CreateOnlyDefault
-
-from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
-from extras.models import CustomField, Tag
-from utilities.utils import dict_to_filter_params
-
-
-class BaseModelSerializer(serializers.ModelSerializer):
-    display = serializers.SerializerMethodField(read_only=True)
-
-    def get_display(self, obj):
-        return str(obj)
-
-
-class ValidatedModelSerializer(BaseModelSerializer):
-    """
-    Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
-    validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
-    """
-    def validate(self, data):
-
-        # Remove custom fields data and tags (if any) prior to model validation
-        attrs = data.copy()
-        attrs.pop('custom_fields', None)
-        attrs.pop('tags', None)
-
-        # Skip ManyToManyFields
-        for field in self.Meta.model._meta.get_fields():
-            if isinstance(field, ManyToManyField):
-                attrs.pop(field.name, None)
-
-        # Run clean() on an instance of the model
-        if self.instance is None:
-            instance = self.Meta.model(**attrs)
-        else:
-            instance = self.instance
-            for k, v in attrs.items():
-                setattr(instance, k, v)
-        instance.full_clean()
-
-        return data
-
-
-class CustomFieldModelSerializer(ValidatedModelSerializer):
-    """
-    Extends ModelSerializer to render any CustomFields and their values associated with an object.
-    """
-    custom_fields = CustomFieldsDataField(
-        source='custom_field_data',
-        default=CreateOnlyDefault(CustomFieldDefaultValues())
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        if self.instance is not None:
-
-            # Retrieve the set of CustomFields which apply to this type of object
-            content_type = ContentType.objects.get_for_model(self.Meta.model)
-            fields = CustomField.objects.filter(content_types=content_type)
-
-            # Populate CustomFieldValues for each instance from database
-            if type(self.instance) in (list, tuple):
-                for obj in self.instance:
-                    self._populate_custom_fields(obj, fields)
-            else:
-                self._populate_custom_fields(self.instance, fields)
-
-    def _populate_custom_fields(self, instance, custom_fields):
-        instance.custom_fields = {}
-        for field in custom_fields:
-            instance.custom_fields[field.name] = instance.cf.get(field.name)
-
-
-#
-# Nested serializers
-#
-
-class WritableNestedSerializer(BaseModelSerializer):
-    """
-    Returns a nested representation of an object on read, but accepts only a primary key on write.
-    """
-    def to_internal_value(self, data):
-
-        if data is None:
-            return None
-
-        # Dictionary of related object attributes
-        if isinstance(data, dict):
-            params = dict_to_filter_params(data)
-            queryset = self.Meta.model.objects
-            try:
-                return queryset.get(**params)
-            except ObjectDoesNotExist:
-                raise ValidationError(
-                    "Related object not found using the provided attributes: {}".format(params)
-                )
-            except MultipleObjectsReturned:
-                raise ValidationError(
-                    "Multiple objects match the provided attributes: {}".format(params)
-                )
-            except FieldError as e:
-                raise ValidationError(e)
-
-        # Integer PK of related object
-        if isinstance(data, int):
-            pk = data
-        else:
-            try:
-                # PK might have been mistakenly passed as a string
-                pk = int(data)
-            except (TypeError, ValueError):
-                raise ValidationError(
-                    "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
-                    "unrecognized value: {}".format(data)
-                )
-
-        # Look up object by PK
-        queryset = self.Meta.model.objects
-        try:
-            return queryset.get(pk=int(data))
-        except ObjectDoesNotExist:
-            raise ValidationError(
-                "Related object not found using the provided numeric ID: {}".format(pk)
-            )
-
-
-#
-# Nested tags serialization
-#
-
-# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
-class NestedTagSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
-
-    class Meta:
-        model = Tag
-        fields = ['id', 'url', 'display', 'name', 'slug', 'color']
-
-
-#
-# Base model serializers
-#
-
-class PrimaryModelSerializer(CustomFieldModelSerializer):
-    """
-    Adds support for custom fields and tags.
-    """
-    tags = NestedTagSerializer(many=True, required=False)
-
-    def create(self, validated_data):
-        tags = validated_data.pop('tags', None)
-        instance = super().create(validated_data)
-
-        if tags is not None:
-            return self._save_tags(instance, tags)
-        return instance
-
-    def update(self, instance, validated_data):
-        tags = validated_data.pop('tags', None)
-
-        # Cache tags on instance for change logging
-        instance._tags = tags or []
-
-        instance = super().update(instance, validated_data)
-
-        if tags is not None:
-            return self._save_tags(instance, tags)
-        return instance
-
-    def _save_tags(self, instance, tags):
-        if tags:
-            instance.tags.set([t.name for t in tags])
-        else:
-            instance.tags.clear()
-
-        return instance
-
-
-class NestedGroupModelSerializer(PrimaryModelSerializer):
-    """
-    Extends PrimaryModelSerializer to include MPTT support.
-    """
-    _depth = serializers.IntegerField(source='level', read_only=True)
-
-
-class BulkOperationSerializer(serializers.Serializer):
-    id = serializers.IntegerField()

+ 27 - 0
netbox/netbox/api/serializers/__init__.py

@@ -0,0 +1,27 @@
+from rest_framework import serializers
+
+from .base import *
+from .features import *
+from .nested import *
+
+
+#
+# Base model serializers
+#
+
+class NetBoxModelSerializer(TaggableModelSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
+    """
+    Adds support for custom fields and tags.
+    """
+    pass
+
+
+class NestedGroupModelSerializer(NetBoxModelSerializer):
+    """
+    Extends PrimaryModelSerializer to include MPTT support.
+    """
+    _depth = serializers.IntegerField(source='level', read_only=True)
+
+
+class BulkOperationSerializer(serializers.Serializer):
+    id = serializers.IntegerField()

+ 43 - 0
netbox/netbox/api/serializers/base.py

@@ -0,0 +1,43 @@
+from django.db.models import ManyToManyField
+from rest_framework import serializers
+
+__all__ = (
+    'BaseModelSerializer',
+    'ValidatedModelSerializer',
+)
+
+
+class BaseModelSerializer(serializers.ModelSerializer):
+    display = serializers.SerializerMethodField(read_only=True)
+
+    def get_display(self, obj):
+        return str(obj)
+
+
+class ValidatedModelSerializer(BaseModelSerializer):
+    """
+    Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
+    validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
+    """
+    def validate(self, data):
+
+        # Remove custom fields data and tags (if any) prior to model validation
+        attrs = data.copy()
+        attrs.pop('custom_fields', None)
+        attrs.pop('tags', None)
+
+        # Skip ManyToManyFields
+        for field in self.Meta.model._meta.get_fields():
+            if isinstance(field, ManyToManyField):
+                attrs.pop(field.name, None)
+
+        # Run clean() on an instance of the model
+        if self.instance is None:
+            instance = self.Meta.model(**attrs)
+        else:
+            instance = self.instance
+            for k, v in attrs.items():
+                setattr(instance, k, v)
+        instance.full_clean()
+
+        return data

+ 80 - 0
netbox/netbox/api/serializers/features.py

@@ -0,0 +1,80 @@
+from django.contrib.contenttypes.models import ContentType
+from rest_framework import serializers
+from rest_framework.fields import CreateOnlyDefault
+
+from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
+from extras.models import CustomField
+from .nested import NestedTagSerializer
+
+__all__ = (
+    'CustomFieldModelSerializer',
+    'TaggableModelSerializer',
+)
+
+
+class CustomFieldModelSerializer(serializers.Serializer):
+    """
+    Introduces support for custom field assignment. Adds `custom_fields` serialization and ensures
+    that custom field data is populated upon initialization.
+    """
+    custom_fields = CustomFieldsDataField(
+        source='custom_field_data',
+        default=CreateOnlyDefault(CustomFieldDefaultValues())
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if self.instance is not None:
+
+            # Retrieve the set of CustomFields which apply to this type of object
+            content_type = ContentType.objects.get_for_model(self.Meta.model)
+            fields = CustomField.objects.filter(content_types=content_type)
+
+            # Populate custom field values for each instance from database
+            if type(self.instance) in (list, tuple):
+                for obj in self.instance:
+                    self._populate_custom_fields(obj, fields)
+            else:
+                self._populate_custom_fields(self.instance, fields)
+
+    def _populate_custom_fields(self, instance, custom_fields):
+        instance.custom_fields = {}
+        for field in custom_fields:
+            instance.custom_fields[field.name] = instance.cf.get(field.name)
+
+
+class TaggableModelSerializer(serializers.Serializer):
+    """
+    Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment
+    on create() and update().
+    """
+    tags = NestedTagSerializer(many=True, required=False)
+
+    def create(self, validated_data):
+        tags = validated_data.pop('tags', None)
+        instance = super().create(validated_data)
+
+        if tags is not None:
+            return self._save_tags(instance, tags)
+        return instance
+
+    def update(self, instance, validated_data):
+        tags = validated_data.pop('tags', None)
+
+        # Cache tags on instance for change logging
+        instance._tags = tags or []
+
+        instance = super().update(instance, validated_data)
+
+        if tags is not None:
+            return self._save_tags(instance, tags)
+        return instance
+
+    def _save_tags(self, instance, tags):
+        if tags:
+            instance.tags.set([t.name for t in tags])
+        else:
+            instance.tags.clear()
+
+        return instance

+ 62 - 0
netbox/netbox/api/serializers/nested.py

@@ -0,0 +1,62 @@
+from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
+from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
+
+from extras.models import Tag
+from utilities.utils import dict_to_filter_params
+from .base import BaseModelSerializer
+
+__all__ = (
+    'NestedTagSerializer',
+    'WritableNestedSerializer',
+)
+
+
+class WritableNestedSerializer(BaseModelSerializer):
+    """
+    Represents an object related through a ForeignKey field. On write, it accepts a primary key (PK) value or a
+    dictionary of attributes which can be used to uniquely identify the related object. This class should be
+    subclassed to return a full representation of the related object on read.
+    """
+    def to_internal_value(self, data):
+
+        if data is None:
+            return None
+
+        # Dictionary of related object attributes
+        if isinstance(data, dict):
+            params = dict_to_filter_params(data)
+            queryset = self.Meta.model.objects
+            try:
+                return queryset.get(**params)
+            except ObjectDoesNotExist:
+                raise ValidationError(f"Related object not found using the provided attributes: {params}")
+            except MultipleObjectsReturned:
+                raise ValidationError(f"Multiple objects match the provided attributes: {params}")
+            except FieldError as e:
+                raise ValidationError(e)
+
+        # Integer PK of related object
+        try:
+            # Cast as integer in case a PK was mistakenly sent as a string
+            pk = int(data)
+        except (TypeError, ValueError):
+            raise ValidationError(
+                f"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
+                f"unrecognized value: {data}"
+            )
+
+        # Look up object by PK
+        try:
+            return self.Meta.model.objects.get(pk=pk)
+        except ObjectDoesNotExist:
+            raise ValidationError(f"Related object not found using the provided numeric ID: {pk}")
+
+
+# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
+class NestedTagSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
+
+    class Meta:
+        model = Tag
+        fields = ['id', 'url', 'display', 'name', 'slug', 'color']

+ 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):
     """
     """

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

@@ -0,0 +1,182 @@
+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_serializer_context(self):
+        """
+        For models which support custom fields, populate the `custom_fields` context.
+        """
+        context = super().get_serializer_context()
+
+        if hasattr(self.queryset.model, 'custom_fields'):
+            content_type = ContentType.objects.get_for_model(self.queryset.model)
+            context.update({
+                'custom_fields': content_type.custom_fields.all(),
+            })
+
+        return context
+
+    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/tenancy/api/serializers.py

@@ -3,7 +3,7 @@ from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from netbox.api import ChoiceField, ContentTypeField
 from netbox.api import ChoiceField, ContentTypeField
-from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
+from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from tenancy.choices import ContactPriorityChoices
 from tenancy.choices import ContactPriorityChoices
 from tenancy.models import *
 from tenancy.models import *
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
@@ -27,7 +27,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class TenantSerializer(PrimaryModelSerializer):
+class TenantSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
     circuit_count = serializers.IntegerField(read_only=True)
     circuit_count = serializers.IntegerField(read_only=True)
@@ -67,7 +67,7 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class ContactRoleSerializer(PrimaryModelSerializer):
+class ContactRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
 
 
     class Meta:
     class Meta:
@@ -77,7 +77,7 @@ class ContactRoleSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ContactSerializer(PrimaryModelSerializer):
+class ContactSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
     group = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
     group = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
 
 
@@ -89,7 +89,7 @@ class ContactSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ContactAssignmentSerializer(PrimaryModelSerializer):
+class ContactAssignmentSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
     content_type = ContentTypeField(
     content_type = ContentTypeField(
         queryset=ContentType.objects.all()
         queryset=ContentType.objects.all()

+ 8 - 8
netbox/tenancy/api/views.py

@@ -1,9 +1,9 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
-from dcim.models import Device, Rack, Site, Cable
-from extras.api.views import CustomFieldModelViewSet
+from dcim.models import Device, Rack, Site
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from ipam.models import IPAddress, Prefix, VLAN, VRF
+from netbox.api.viewsets import NetBoxModelViewSet
 from tenancy import filtersets
 from tenancy import filtersets
 from tenancy.models import *
 from tenancy.models import *
 from utilities.utils import count_related
 from utilities.utils import count_related
@@ -23,7 +23,7 @@ class TenancyRootView(APIRootView):
 # Tenants
 # Tenants
 #
 #
 
 
-class TenantGroupViewSet(CustomFieldModelViewSet):
+class TenantGroupViewSet(NetBoxModelViewSet):
     queryset = TenantGroup.objects.add_related_count(
     queryset = TenantGroup.objects.add_related_count(
         TenantGroup.objects.all(),
         TenantGroup.objects.all(),
         Tenant,
         Tenant,
@@ -35,7 +35,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.TenantGroupFilterSet
     filterset_class = filtersets.TenantGroupFilterSet
 
 
 
 
-class TenantViewSet(CustomFieldModelViewSet):
+class TenantViewSet(NetBoxModelViewSet):
     queryset = Tenant.objects.prefetch_related(
     queryset = Tenant.objects.prefetch_related(
         'group', 'tags'
         'group', 'tags'
     ).annotate(
     ).annotate(
@@ -58,7 +58,7 @@ class TenantViewSet(CustomFieldModelViewSet):
 # Contacts
 # Contacts
 #
 #
 
 
-class ContactGroupViewSet(CustomFieldModelViewSet):
+class ContactGroupViewSet(NetBoxModelViewSet):
     queryset = ContactGroup.objects.add_related_count(
     queryset = ContactGroup.objects.add_related_count(
         ContactGroup.objects.all(),
         ContactGroup.objects.all(),
         Contact,
         Contact,
@@ -70,19 +70,19 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.ContactGroupFilterSet
     filterset_class = filtersets.ContactGroupFilterSet
 
 
 
 
-class ContactRoleViewSet(CustomFieldModelViewSet):
+class ContactRoleViewSet(NetBoxModelViewSet):
     queryset = ContactRole.objects.prefetch_related('tags')
     queryset = ContactRole.objects.prefetch_related('tags')
     serializer_class = serializers.ContactRoleSerializer
     serializer_class = serializers.ContactRoleSerializer
     filterset_class = filtersets.ContactRoleFilterSet
     filterset_class = filtersets.ContactRoleFilterSet
 
 
 
 
-class ContactViewSet(CustomFieldModelViewSet):
+class ContactViewSet(NetBoxModelViewSet):
     queryset = Contact.objects.prefetch_related('group', 'tags')
     queryset = Contact.objects.prefetch_related('group', 'tags')
     serializer_class = serializers.ContactSerializer
     serializer_class = serializers.ContactSerializer
     filterset_class = filtersets.ContactFilterSet
     filterset_class = filtersets.ContactFilterSet
 
 
 
 
-class ContactAssignmentViewSet(CustomFieldModelViewSet):
+class ContactAssignmentViewSet(NetBoxModelViewSet):
     queryset = ContactAssignment.objects.prefetch_related('object', 'contact', 'role')
     queryset = ContactAssignment.objects.prefetch_related('object', 'contact', 'role')
     serializer_class = serializers.ContactAssignmentSerializer
     serializer_class = serializers.ContactAssignmentSerializer
     filterset_class = filtersets.ContactAssignmentFilterSet
     filterset_class = filtersets.ContactAssignmentFilterSet

+ 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

+ 6 - 6
netbox/virtualization/api/serializers.py

@@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.api import ChoiceField, SerializedPKRelatedField
 from netbox.api import ChoiceField, SerializedPKRelatedField
-from netbox.api.serializers import PrimaryModelSerializer
+from netbox.api.serializers import NetBoxModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from virtualization.choices import *
 from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -17,7 +17,7 @@ from .nested_serializers import *
 # Clusters
 # Clusters
 #
 #
 
 
-class ClusterTypeSerializer(PrimaryModelSerializer):
+class ClusterTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
     cluster_count = serializers.IntegerField(read_only=True)
     cluster_count = serializers.IntegerField(read_only=True)
 
 
@@ -29,7 +29,7 @@ class ClusterTypeSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ClusterGroupSerializer(PrimaryModelSerializer):
+class ClusterGroupSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
     cluster_count = serializers.IntegerField(read_only=True)
     cluster_count = serializers.IntegerField(read_only=True)
 
 
@@ -41,7 +41,7 @@ class ClusterGroupSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class ClusterSerializer(PrimaryModelSerializer):
+class ClusterSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     type = NestedClusterTypeSerializer()
     type = NestedClusterTypeSerializer()
     group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
     group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
@@ -62,7 +62,7 @@ class ClusterSerializer(PrimaryModelSerializer):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-class VirtualMachineSerializer(PrimaryModelSerializer):
+class VirtualMachineSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     site = NestedSiteSerializer(read_only=True)
     site = NestedSiteSerializer(read_only=True)
@@ -103,7 +103,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceSerializer(PrimaryModelSerializer):
+class VMInterfaceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
     virtual_machine = NestedVirtualMachineSerializer()
     virtual_machine = NestedVirtualMachineSerializer()
     parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
     parent = NestedVMInterfaceSerializer(required=False, allow_null=True)

+ 7 - 6
netbox/virtualization/api/views.py

@@ -1,7 +1,8 @@
 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
+from netbox.api.viewsets import 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
@@ -20,7 +21,7 @@ class VirtualizationRootView(APIRootView):
 # Clusters
 # Clusters
 #
 #
 
 
-class ClusterTypeViewSet(CustomFieldModelViewSet):
+class ClusterTypeViewSet(NetBoxModelViewSet):
     queryset = ClusterType.objects.annotate(
     queryset = ClusterType.objects.annotate(
         cluster_count=count_related(Cluster, 'type')
         cluster_count=count_related(Cluster, 'type')
     ).prefetch_related('tags')
     ).prefetch_related('tags')
@@ -28,7 +29,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.ClusterTypeFilterSet
     filterset_class = filtersets.ClusterTypeFilterSet
 
 
 
 
-class ClusterGroupViewSet(CustomFieldModelViewSet):
+class ClusterGroupViewSet(NetBoxModelViewSet):
     queryset = ClusterGroup.objects.annotate(
     queryset = ClusterGroup.objects.annotate(
         cluster_count=count_related(Cluster, 'group')
         cluster_count=count_related(Cluster, 'group')
     ).prefetch_related('tags')
     ).prefetch_related('tags')
@@ -36,7 +37,7 @@ class ClusterGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.ClusterGroupFilterSet
     filterset_class = filtersets.ClusterGroupFilterSet
 
 
 
 
-class ClusterViewSet(CustomFieldModelViewSet):
+class ClusterViewSet(NetBoxModelViewSet):
     queryset = Cluster.objects.prefetch_related(
     queryset = Cluster.objects.prefetch_related(
         'type', 'group', 'tenant', 'site', 'tags'
         'type', 'group', 'tenant', 'site', 'tags'
     ).annotate(
     ).annotate(
@@ -51,7 +52,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
+class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
     queryset = VirtualMachine.objects.prefetch_related(
     queryset = VirtualMachine.objects.prefetch_related(
         'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
         'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
     )
     )
@@ -78,7 +79,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',

+ 3 - 3
netbox/wireless/api/serializers.py

@@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices
 from dcim.api.serializers import NestedInterfaceSerializer
 from dcim.api.serializers import NestedInterfaceSerializer
 from ipam.api.serializers import NestedVLANSerializer
 from ipam.api.serializers import NestedVLANSerializer
 from netbox.api import ChoiceField
 from netbox.api import ChoiceField
-from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
+from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from wireless.choices import *
 from wireless.choices import *
 from wireless.models import *
 from wireless.models import *
 from .nested_serializers import *
 from .nested_serializers import *
@@ -29,7 +29,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class WirelessLANSerializer(PrimaryModelSerializer):
+class WirelessLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
     group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
     group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
     vlan = NestedVLANSerializer(required=False, allow_null=True)
@@ -44,7 +44,7 @@ class WirelessLANSerializer(PrimaryModelSerializer):
         ]
         ]
 
 
 
 
-class WirelessLinkSerializer(PrimaryModelSerializer):
+class WirelessLinkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
     status = ChoiceField(choices=LinkStatusChoices, required=False)
     status = ChoiceField(choices=LinkStatusChoices, required=False)
     interface_a = NestedInterfaceSerializer()
     interface_a = NestedInterfaceSerializer()

+ 4 - 4
netbox/wireless/api/views.py

@@ -1,6 +1,6 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
-from extras.api.views import CustomFieldModelViewSet
+from netbox.api.viewsets import NetBoxModelViewSet
 from wireless import filtersets
 from wireless import filtersets
 from wireless.models import *
 from wireless.models import *
 from . import serializers
 from . import serializers
@@ -14,7 +14,7 @@ class WirelessRootView(APIRootView):
         return 'Wireless'
         return 'Wireless'
 
 
 
 
-class WirelessLANGroupViewSet(CustomFieldModelViewSet):
+class WirelessLANGroupViewSet(NetBoxModelViewSet):
     queryset = WirelessLANGroup.objects.add_related_count(
     queryset = WirelessLANGroup.objects.add_related_count(
         WirelessLANGroup.objects.all(),
         WirelessLANGroup.objects.all(),
         WirelessLAN,
         WirelessLAN,
@@ -26,13 +26,13 @@ class WirelessLANGroupViewSet(CustomFieldModelViewSet):
     filterset_class = filtersets.WirelessLANGroupFilterSet
     filterset_class = filtersets.WirelessLANGroupFilterSet
 
 
 
 
-class WirelessLANViewSet(CustomFieldModelViewSet):
+class WirelessLANViewSet(NetBoxModelViewSet):
     queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
     queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
     serializer_class = serializers.WirelessLANSerializer
     serializer_class = serializers.WirelessLANSerializer
     filterset_class = filtersets.WirelessLANFilterSet
     filterset_class = filtersets.WirelessLANFilterSet
 
 
 
 
-class WirelessLinkViewSet(CustomFieldModelViewSet):
+class WirelessLinkViewSet(NetBoxModelViewSet):
     queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
     queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
     serializer_class = serializers.WirelessLinkSerializer
     serializer_class = serializers.WirelessLinkSerializer
     filterset_class = filtersets.WirelessLinkFilterSet
     filterset_class = filtersets.WirelessLinkFilterSet