Browse Source

Initial work on API v2.0

Jeremy Stretch 8 years ago
parent
commit
062a5bfe8d

+ 6 - 6
netbox/circuits/api/urls.py

@@ -9,17 +9,17 @@ from .views import *
 urlpatterns = [
 
     # Providers
-    url(r'^providers/$', ProviderListView.as_view(), name='provider_list'),
-    url(r'^providers/(?P<pk>\d+)/$', ProviderDetailView.as_view(), name='provider_detail'),
+    url(r'^providers/$', ProviderViewSet.as_view({'get': 'list'}), name='provider_list'),
+    url(r'^providers/(?P<pk>\d+)/$', ProviderViewSet.as_view({'get': 'retrieve'}), name='provider_detail'),
     url(r'^providers/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER},
         name='provider_graphs'),
 
     # Circuit types
-    url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'),
-    url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'),
+    url(r'^circuit-types/$', CircuitTypeViewSet.as_view({'get': 'list'}), name='circuittype_list'),
+    url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeViewSet.as_view({'get': 'retrieve'}), name='circuittype_detail'),
 
     # Circuits
-    url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'),
-    url(r'^circuits/(?P<pk>\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'),
+    url(r'^circuits/$', CircuitViewSet.as_view({'get': 'list'}), name='circuit_list'),
+    url(r'^circuits/(?P<pk>\d+)/$', CircuitViewSet.as_view({'get': 'retrieve'}), name='circuit_detail'),
 
 ]

+ 20 - 34
netbox/circuits/api/views.py

@@ -1,58 +1,44 @@
-from rest_framework import generics
+from rest_framework.viewsets import ModelViewSet
 
 from circuits.models import Provider, CircuitType, Circuit
 from circuits.filters import CircuitFilter
 
-from extras.api.views import CustomFieldModelAPIView
+from extras.api.views import CustomFieldModelViewSet
 from . import serializers
 
 
-class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
-    """
-    List all providers
-    """
-    queryset = Provider.objects.prefetch_related('custom_field_values__field')
-    serializer_class = serializers.ProviderSerializer
-
+#
+# Providers
+#
 
-class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
+class ProviderViewSet(CustomFieldModelViewSet):
     """
-    Retrieve a single provider
+    List and retrieve circuit providers
     """
-    queryset = Provider.objects.prefetch_related('custom_field_values__field')
+    queryset = Provider.objects.all()
     serializer_class = serializers.ProviderSerializer
 
 
-class CircuitTypeListView(generics.ListAPIView):
-    """
-    List all circuit types
-    """
-    queryset = CircuitType.objects.all()
-    serializer_class = serializers.CircuitTypeSerializer
+#
+#  Circuit Types
+#
 
-
-class CircuitTypeDetailView(generics.RetrieveAPIView):
+class CircuitTypeViewSet(ModelViewSet):
     """
-    Retrieve a single circuit type
+    List and retrieve circuit types
     """
     queryset = CircuitType.objects.all()
     serializer_class = serializers.CircuitTypeSerializer
 
 
-class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
-    """
-    List circuits (filterable)
-    """
-    queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
-        .prefetch_related('custom_field_values__field')
-    serializer_class = serializers.CircuitSerializer
-    filter_class = CircuitFilter
-
+#
+# Circuits
+#
 
-class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
+class CircuitViewSet(CustomFieldModelViewSet):
     """
-    Retrieve a single circuit
+    List and retrieve circuits
     """
-    queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
-        .prefetch_related('custom_field_values__field')
+    queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
     serializer_class = serializers.CircuitSerializer
+    filter_class = CircuitFilter

+ 25 - 27
netbox/dcim/api/urls.py

@@ -9,52 +9,50 @@ from .views import *
 urlpatterns = [
 
     # Sites
-    url(r'^sites/$', SiteListView.as_view(), name='site_list'),
-    url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
+    url(r'^sites/$', SiteViewSet.as_view({'get': 'list'}), name='site_list'),
+    url(r'^sites/(?P<pk>\d+)/$', SiteViewSet.as_view({'get': 'retrieve'}), name='site_detail'),
     url(r'^sites/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'),
-    url(r'^sites/(?P<site>\d+)/racks/$', RackListView.as_view(), name='site_racks'),
 
     # Rack groups
-    url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
-    url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
+    url(r'^rack-groups/$', RackGroupViewSet.as_view({'get': 'list'}), name='rackgroup_list'),
+    url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupViewSet.as_view({'get': 'retrieve'}), name='rackgroup_detail'),
 
     # Rack roles
-    url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
-    url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
+    url(r'^rack-roles/$', RackRoleViewSet.as_view({'get': 'list'}), name='rackrole_list'),
+    url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleViewSet.as_view({'get': 'retrieve'}), name='rackrole_detail'),
 
     # Racks
-    url(r'^racks/$', RackListView.as_view(), name='rack_list'),
-    url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
+    url(r'^racks/$', RackViewSet.as_view({'get': 'list'}), name='rack_list'),
+    url(r'^racks/(?P<pk>\d+)/$', RackViewSet.as_view({'get': 'retrieve'}), name='rack_detail'),
     url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
 
     # Manufacturers
-    url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
-    url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
+    url(r'^manufacturers/$', ManufacturerViewSet.as_view({'get': 'list'}), name='manufacturer_list'),
+    url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerViewSet.as_view({'get': 'retrieve'}), name='manufacturer_detail'),
 
     # Device types
-    url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'),
-    url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'),
+    url(r'^device-types/$', DeviceTypeViewSet.as_view({'get': 'list'}), name='devicetype_list'),
+    url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeViewSet.as_view({'get': 'retrieve'}), name='devicetype_detail'),
 
     # Device roles
-    url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'),
-    url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'),
+    url(r'^device-roles/$', DeviceRoleViewSet.as_view({'get': 'list'}), name='devicerole_list'),
+    url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleViewSet.as_view({'get': 'retrieve'}), name='devicerole_detail'),
 
     # Platforms
-    url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'),
-    url(r'^platforms/(?P<pk>\d+)/$', PlatformDetailView.as_view(), name='platform_detail'),
+    url(r'^platforms/$', PlatformViewSet.as_view({'get': 'list'}), name='platform_list'),
+    url(r'^platforms/(?P<pk>\d+)/$', PlatformViewSet.as_view({'get': 'retrieve'}), name='platform_detail'),
 
     # Devices
-    url(r'^devices/$', DeviceListView.as_view(), name='device_list'),
-    url(r'^devices/(?P<pk>\d+)/$', DeviceDetailView.as_view(), name='device_detail'),
+    url(r'^devices/$', DeviceViewSet.as_view({'get': 'list'}), name='device_list'),
+    url(r'^devices/(?P<pk>\d+)/$', DeviceViewSet.as_view({'get': 'retrieve'}), name='device_detail'),
     url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'),
-    url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'),
-    url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(),
-        name='device_consoleserverports'),
-    url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
-    url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
-    url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
-    url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
-    url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
+    url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'),
+    url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'),
+    url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortViewSet.as_view({'get': 'list'}), name='device_powerports'),
+    url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletViewSet.as_view({'get': 'list'}), name='device_poweroutlets'),
+    url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'),
+    url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'),
+    url(r'^devices/(?P<pk>\d+)/modules/$', ModuleViewSet.as_view({'get': 'list'}), name='device_modules'),
 
     # Console ports
     url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),

+ 64 - 129
netbox/dcim/api/views.py

@@ -3,10 +3,10 @@ from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
 from rest_framework.response import Response
 from rest_framework.settings import api_settings
 from rest_framework.views import APIView
+from rest_framework.viewsets import ModelViewSet
 
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Count
 from django.http import Http404
 from django.shortcuts import get_object_or_404
 
@@ -15,7 +15,7 @@ from dcim.models import (
     InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
 )
 from dcim import filters
-from extras.api.views import CustomFieldModelAPIView
+from extras.api.views import CustomFieldModelViewSet
 from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
 from utilities.api import ServiceUnavailable
 from .exceptions import MissingFilterException
@@ -26,19 +26,11 @@ from . import serializers
 # Sites
 #
 
-class SiteListView(CustomFieldModelAPIView, generics.ListAPIView):
+class SiteViewSet(CustomFieldModelViewSet):
     """
-    List all sites
+    List and retrieve sites
     """
-    queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.SiteSerializer
-
-
-class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single site
-    """
-    queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field')
+    queryset = Site.objects.select_related('tenant')
     serializer_class = serializers.SiteSerializer
 
 
@@ -46,38 +38,22 @@ class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
 # Rack groups
 #
 
-class RackGroupListView(generics.ListAPIView):
+class RackGroupViewSet(ModelViewSet):
     """
-    List all rack groups
+    List and retrieve rack groups
     """
     queryset = RackGroup.objects.select_related('site')
     serializer_class = serializers.RackGroupSerializer
     filter_class = filters.RackGroupFilter
 
 
-class RackGroupDetailView(generics.RetrieveAPIView):
-    """
-    Retrieve a single rack group
-    """
-    queryset = RackGroup.objects.select_related('site')
-    serializer_class = serializers.RackGroupSerializer
-
-
 #
 # Rack roles
 #
 
-class RackRoleListView(generics.ListAPIView):
-    """
-    List all rack roles
-    """
-    queryset = RackRole.objects.all()
-    serializer_class = serializers.RackRoleSerializer
-
-
-class RackRoleDetailView(generics.RetrieveAPIView):
+class RackRoleViewSet(ModelViewSet):
     """
-    Retrieve a single rack role
+    List and retrieve rack roles
     """
     queryset = RackRole.objects.all()
     serializer_class = serializers.RackRoleSerializer
@@ -87,28 +63,18 @@ class RackRoleDetailView(generics.RetrieveAPIView):
 # Racks
 #
 
-class RackListView(CustomFieldModelAPIView, generics.ListAPIView):
+class RackViewSet(CustomFieldModelViewSet):
     """
-    List racks (filterable)
+    List and retrieve racks
     """
-    queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
-        .prefetch_related('custom_field_values__field')
-    serializer_class = serializers.RackSerializer
+    queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
     filter_class = filters.RackFilter
 
+    def get_serializer_class(self):
+        if self.action == 'retrieve':
+            return serializers.RackDetailSerializer
+        return serializers.RackSerializer
 
-class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single rack
-    """
-    queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\
-        .prefetch_related('custom_field_values__field')
-    serializer_class = serializers.RackDetailSerializer
-
-
-#
-# Rack units
-#
 
 class RackUnitListView(APIView):
     """
@@ -139,17 +105,9 @@ class RackUnitListView(APIView):
 # Manufacturers
 #
 
-class ManufacturerListView(generics.ListAPIView):
-    """
-    List all hardware manufacturers
-    """
-    queryset = Manufacturer.objects.all()
-    serializer_class = serializers.ManufacturerSerializer
-
-
-class ManufacturerDetailView(generics.RetrieveAPIView):
+class ManufacturerViewSet(ModelViewSet):
     """
-    Retrieve a single hardware manufacturers
+    List and retrieve manufacturers
     """
     queryset = Manufacturer.objects.all()
     serializer_class = serializers.ManufacturerSerializer
@@ -159,38 +117,26 @@ class ManufacturerDetailView(generics.RetrieveAPIView):
 # Device Types
 #
 
-class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView):
+class DeviceTypeViewSet(CustomFieldModelViewSet):
     """
-    List device types (filterable)
+    List and retrieve device types
     """
-    queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.DeviceTypeSerializer
+    queryset = DeviceType.objects.select_related('manufacturer')
     filter_class = filters.DeviceTypeFilter
 
-
-class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single device type
-    """
-    queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.DeviceTypeDetailSerializer
+    def get_serializer_class(self):
+        if self.action == 'retrieve':
+            return serializers.DeviceTypeDetailSerializer
+        return serializers.DeviceTypeSerializer
 
 
 #
 # Device roles
 #
 
-class DeviceRoleListView(generics.ListAPIView):
-    """
-    List all device roles
-    """
-    queryset = DeviceRole.objects.all()
-    serializer_class = serializers.DeviceRoleSerializer
-
-
-class DeviceRoleDetailView(generics.RetrieveAPIView):
+class DeviceRoleViewSet(ModelViewSet):
     """
-    Retrieve a single device role
+    List and retrieve device roles
     """
     queryset = DeviceRole.objects.all()
     serializer_class = serializers.DeviceRoleSerializer
@@ -200,17 +146,9 @@ class DeviceRoleDetailView(generics.RetrieveAPIView):
 # Platforms
 #
 
-class PlatformListView(generics.ListAPIView):
-    """
-    List all platforms
-    """
-    queryset = Platform.objects.all()
-    serializer_class = serializers.PlatformSerializer
-
-
-class PlatformDetailView(generics.RetrieveAPIView):
+class PlatformViewSet(ModelViewSet):
     """
-    Retrieve a single platform
+    List and retrieve platforms
     """
     queryset = Platform.objects.all()
     serializer_class = serializers.PlatformSerializer
@@ -220,40 +158,31 @@ class PlatformDetailView(generics.RetrieveAPIView):
 # Devices
 #
 
-class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView):
+class DeviceViewSet(CustomFieldModelViewSet):
     """
-    List devices (filterable)
+    List and retrieve devices
     """
-    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
-                                             'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
-                                                                                          'primary_ip6__nat_outside',
-                                                                                          'custom_field_values__field')
+    queryset = Device.objects.select_related(
+        'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay',
+    ).prefetch_related(
+        'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
+    )
     serializer_class = serializers.DeviceSerializer
     filter_class = filters.DeviceFilter
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
 
 
-class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single device
-    """
-    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
-                                             'rack__site', 'parent_bay').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.DeviceSerializer
-
-
 #
 # Console ports
 #
 
-class ConsolePortListView(generics.ListAPIView):
+class ConsolePortViewSet(ModelViewSet):
     """
-    List console ports (by device)
+    List and retrieve console ports (by device)
     """
     serializer_class = serializers.ConsolePortSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return ConsolePort.objects.filter(device=device).select_related('cs_port')
 
@@ -268,14 +197,13 @@ class ConsolePortView(generics.RetrieveUpdateDestroyAPIView):
 # Console server ports
 #
 
-class ConsoleServerPortListView(generics.ListAPIView):
+class ConsoleServerPortViewSet(ModelViewSet):
     """
-    List console server ports (by device)
+    List and retrieve console server ports (by device)
     """
     serializer_class = serializers.ConsoleServerPortSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
 
@@ -284,14 +212,13 @@ class ConsoleServerPortListView(generics.ListAPIView):
 # Power ports
 #
 
-class PowerPortListView(generics.ListAPIView):
+class PowerPortViewSet(ModelViewSet):
     """
-    List power ports (by device)
+    List and retrieve power ports (by device)
     """
     serializer_class = serializers.PowerPortSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return PowerPort.objects.filter(device=device).select_related('power_outlet')
 
@@ -306,14 +233,13 @@ class PowerPortView(generics.RetrieveUpdateDestroyAPIView):
 # Power outlets
 #
 
-class PowerOutletListView(generics.ListAPIView):
+class PowerOutletViewSet(ModelViewSet):
     """
-    List power outlets (by device)
+    List and retrieve power outlets (by device)
     """
     serializer_class = serializers.PowerOutletSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return PowerOutlet.objects.filter(device=device).select_related('connected_port')
 
@@ -322,9 +248,9 @@ class PowerOutletListView(generics.ListAPIView):
 # Interfaces
 #
 
-class InterfaceListView(generics.ListAPIView):
+class InterfaceViewSet(ModelViewSet):
     """
-    List interfaces (by device)
+    List and retrieve interfaces (by device)
     """
     serializer_class = serializers.InterfaceSerializer
     filter_class = filters.InterfaceFilter
@@ -372,14 +298,13 @@ class InterfaceConnectionListView(generics.ListAPIView):
 # Device bays
 #
 
-class DeviceBayListView(generics.ListAPIView):
+class DeviceBayViewSet(ModelViewSet):
     """
-    List device bays (by device)
+    List and retrieve device bays (by device)
     """
     serializer_class = serializers.DeviceBayNestedSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return DeviceBay.objects.filter(device=device).select_related('installed_device')
 
@@ -388,14 +313,13 @@ class DeviceBayListView(generics.ListAPIView):
 # Modules
 #
 
-class ModuleListView(generics.ListAPIView):
+class ModuleViewSet(ModelViewSet):
     """
-    List device modules (by device)
+    List and retrieve modules (by device)
     """
     serializer_class = serializers.ModuleSerializer
 
     def get_queryset(self):
-
         device = get_object_or_404(Device, pk=self.kwargs['pk'])
         return Module.objects.filter(device=device).select_related('device', 'manufacturer')
 
@@ -442,8 +366,19 @@ class RelatedConnectionsView(APIView):
         super(RelatedConnectionsView, self).__init__()
 
         # Custom fields
-        self.content_type = ContentType.objects.get_for_model(Device)
-        self.custom_fields = self.content_type.custom_fields.prefetch_related('choices')
+        content_type = ContentType.objects.get_for_model(Device)
+        custom_fields = content_type.custom_fields.prefetch_related('choices')
+
+        # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.
+        custom_field_choices = {}
+        for field in custom_fields:
+            for cfc in field.choices.all():
+                custom_field_choices[cfc.id] = cfc.value
+
+        self.context = {
+            'custom_fields': custom_fields,
+            'custom_field_choices': custom_field_choices,
+        }
 
     def get(self, request):
 
@@ -469,7 +404,7 @@ class RelatedConnectionsView(APIView):
 
         # Initialize response skeleton
         response = {
-            'device': serializers.DeviceSerializer(device, context={'view': self}).data,
+            'device': serializers.DeviceSerializer(device, context=self.context).data,
             'console-ports': [],
             'power-ports': [],
             'interfaces': [],

+ 1 - 15
netbox/dcim/tests/test_apis.py

@@ -82,21 +82,6 @@ class SiteTest(APITestCase):
             sorted(self.standard_fields),
         )
 
-    def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)):
-        response = self.client.get(endpoint)
-        content = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        for i in json.loads(response.content.decode('utf-8')):
-            self.assertEqual(
-                sorted(i.keys()),
-                sorted(self.rack_fields),
-            )
-            # Check Nested Serializer.
-            self.assertEqual(
-                sorted(i.get('site').keys()),
-                sorted(self.nested_fields),
-            )
-
     def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)):
         response = self.client.get(endpoint)
         content = json.loads(response.content.decode('utf-8'))
@@ -239,6 +224,7 @@ class DeviceTypeTest(APITestCase):
         'subdevice_role',
         'comments',
         'custom_fields',
+        'instance_count',
     ]
 
     nested_fields = [

+ 2 - 0
netbox/extras/api/renderers.py

@@ -27,6 +27,8 @@ class BINDZoneRenderer(renderers.BaseRenderer):
 
     def render(self, data, media_type=None, renderer_context=None):
         records = []
+        if not isinstance(data, (list, tuple)):
+            data = (data,)
         for record in data:
             if record.get('name') and record.get('primary_ip'):
                 try:

+ 4 - 6
netbox/extras/api/serializers.py

@@ -12,22 +12,20 @@ class CustomFieldSerializer(serializers.Serializer):
     def get_custom_fields(self, obj):
 
         # Gather all CustomFields applicable to this object
-        fields = {cf.name: None for cf in self.context['view'].custom_fields}
+        fields = {cf.name: None for cf in self.context['custom_fields']}
+        custom_field_choices = self.context['custom_field_choices']
 
         # Attach any defined CustomFieldValues to their respective CustomFields
         for cfv in obj.custom_field_values.all():
 
             # Attempt to suppress database lookups for CustomFieldChoices by using the cached choice set from the view
             # context.
-            if cfv.field.type == CF_TYPE_SELECT and hasattr(self, 'custom_field_choices'):
+            if cfv.field.type == CF_TYPE_SELECT:
                 cfc = {
                     'id': int(cfv.serialized_value),
-                    'value': self.context['view'].custom_field_choices[int(cfv.serialized_value)]
+                    'value': custom_field_choices[int(cfv.serialized_value)]
                 }
                 fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfc).data
-            # Fall back to hitting the database in case we're in a view that doesn't inherit CustomFieldModelAPIView.
-            elif cfv.field.type == CF_TYPE_SELECT:
-                fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfv.value).data
             else:
                 fields[cfv.field.name] = cfv.value
 

+ 19 - 8
netbox/extras/api/views.py

@@ -1,6 +1,7 @@
 import graphviz
 from rest_framework import generics
 from rest_framework.views import APIView
+from rest_framework.viewsets import ModelViewSet
 
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
@@ -14,22 +15,32 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_P
 from .serializers import GraphSerializer
 
 
-class CustomFieldModelAPIView(object):
+class CustomFieldModelViewSet(ModelViewSet):
     """
-    Include the applicable set of CustomField in the view context.
+    Include the applicable set of CustomField in the ModelViewSet context.
     """
 
-    def __init__(self):
-        super(CustomFieldModelAPIView, self).__init__()
-        self.content_type = ContentType.objects.get_for_model(self.queryset.model)
-        self.custom_fields = self.content_type.custom_fields.prefetch_related('choices')
+    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.prefetch_related('choices')
 
         # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object.
         custom_field_choices = {}
-        for field in self.custom_fields:
+        for field in custom_fields:
             for cfc in field.choices.all():
                 custom_field_choices[cfc.id] = cfc.value
-        self.custom_field_choices = custom_field_choices
+        custom_field_choices = custom_field_choices
+
+        return {
+            'custom_fields': custom_fields,
+            'custom_field_choices': custom_field_choices,
+        }
+
+    def get_queryset(self):
+        # Prefetch custom field values
+        return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
 
 
 class GraphListView(generics.ListAPIView):

+ 18 - 18
netbox/ipam/api/urls.py

@@ -6,39 +6,39 @@ from .views import *
 urlpatterns = [
 
     # VRFs
-    url(r'^vrfs/$', VRFListView.as_view(), name='vrf_list'),
-    url(r'^vrfs/(?P<pk>\d+)/$', VRFDetailView.as_view(), name='vrf_detail'),
+    url(r'^vrfs/$', VRFViewSet.as_view({'get': 'list'}), name='vrf_list'),
+    url(r'^vrfs/(?P<pk>\d+)/$', VRFViewSet.as_view({'get': 'retrieve'}), name='vrf_detail'),
 
     # Roles
-    url(r'^roles/$', RoleListView.as_view(), name='role_list'),
-    url(r'^roles/(?P<pk>\d+)/$', RoleDetailView.as_view(), name='role_detail'),
+    url(r'^roles/$', RoleViewSet.as_view({'get': 'list'}), name='role_list'),
+    url(r'^roles/(?P<pk>\d+)/$', RoleViewSet.as_view({'get': 'retrieve'}), name='role_detail'),
 
     # RIRs
-    url(r'^rirs/$', RIRListView.as_view(), name='rir_list'),
-    url(r'^rirs/(?P<pk>\d+)/$', RIRDetailView.as_view(), name='rir_detail'),
+    url(r'^rirs/$', RIRViewSet.as_view({'get': 'list'}), name='rir_list'),
+    url(r'^rirs/(?P<pk>\d+)/$', RIRViewSet.as_view({'get': 'retrieve'}), name='rir_detail'),
 
     # Aggregates
-    url(r'^aggregates/$', AggregateListView.as_view(), name='aggregate_list'),
-    url(r'^aggregates/(?P<pk>\d+)/$', AggregateDetailView.as_view(), name='aggregate_detail'),
+    url(r'^aggregates/$', AggregateViewSet.as_view({'get': 'list'}), name='aggregate_list'),
+    url(r'^aggregates/(?P<pk>\d+)/$', AggregateViewSet.as_view({'get': 'retrieve'}), name='aggregate_detail'),
 
     # Prefixes
-    url(r'^prefixes/$', PrefixListView.as_view(), name='prefix_list'),
-    url(r'^prefixes/(?P<pk>\d+)/$', PrefixDetailView.as_view(), name='prefix_detail'),
+    url(r'^prefixes/$', PrefixViewSet.as_view({'get': 'list'}), name='prefix_list'),
+    url(r'^prefixes/(?P<pk>\d+)/$', PrefixViewSet.as_view({'get': 'retrieve'}), name='prefix_detail'),
 
     # IP addresses
-    url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
-    url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
+    url(r'^ip-addresses/$', IPAddressViewSet.as_view({'get': 'list'}), name='ipaddress_list'),
+    url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressViewSet.as_view({'get': 'retrieve'}), name='ipaddress_detail'),
 
     # VLAN groups
-    url(r'^vlan-groups/$', VLANGroupListView.as_view(), name='vlangroup_list'),
-    url(r'^vlan-groups/(?P<pk>\d+)/$', VLANGroupDetailView.as_view(), name='vlangroup_detail'),
+    url(r'^vlan-groups/$', VLANGroupViewSet.as_view({'get': 'list'}), name='vlangroup_list'),
+    url(r'^vlan-groups/(?P<pk>\d+)/$', VLANGroupViewSet.as_view({'get': 'retrieve'}), name='vlangroup_detail'),
 
     # VLANs
-    url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
-    url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
+    url(r'^vlans/$', VLANViewSet.as_view({'get': 'list'}), name='vlan_list'),
+    url(r'^vlans/(?P<pk>\d+)/$', VLANViewSet.as_view({'get': 'retrieve'}), name='vlan_detail'),
 
     # Services
-    url(r'^services/$', ServiceListView.as_view(), name='service_list'),
-    url(r'^services/(?P<pk>\d+)/$', ServiceDetailView.as_view(), name='service_detail'),
+    url(r'^services/$', ServiceViewSet.as_view({'get': 'list'}), name='service_list'),
+    url(r'^services/(?P<pk>\d+)/$', ServiceViewSet.as_view({'get': 'retrieve'}), name='service_detail'),
 
 ]

+ 25 - 103
netbox/ipam/api/views.py

@@ -1,9 +1,9 @@
-from rest_framework import generics
+from rest_framework.viewsets import ModelViewSet
 
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam import filters
 
-from extras.api.views import CustomFieldModelAPIView
+from extras.api.views import CustomFieldModelViewSet
 from . import serializers
 
 
@@ -11,38 +11,22 @@ from . import serializers
 # VRFs
 #
 
-class VRFListView(CustomFieldModelAPIView, generics.ListAPIView):
+class VRFViewSet(CustomFieldModelViewSet):
     """
-    List all VRFs
+    List and retrieve VRFs
     """
-    queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values__field')
+    queryset = VRF.objects.select_related('tenant')
     serializer_class = serializers.VRFSerializer
     filter_class = filters.VRFFilter
 
 
-class VRFDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single VRF
-    """
-    queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.VRFSerializer
-
-
 #
 # Roles
 #
 
-class RoleListView(generics.ListAPIView):
+class RoleViewSet(ModelViewSet):
     """
-    List all roles
-    """
-    queryset = Role.objects.all()
-    serializer_class = serializers.RoleSerializer
-
-
-class RoleDetailView(generics.RetrieveAPIView):
-    """
-    Retrieve a single role
+    List and retrieve prefix/VLAN roles
     """
     queryset = Role.objects.all()
     serializer_class = serializers.RoleSerializer
@@ -52,17 +36,9 @@ class RoleDetailView(generics.RetrieveAPIView):
 # RIRs
 #
 
-class RIRListView(generics.ListAPIView):
+class RIRViewSet(ModelViewSet):
     """
-    List all RIRs
-    """
-    queryset = RIR.objects.all()
-    serializer_class = serializers.RIRSerializer
-
-
-class RIRDetailView(generics.RetrieveAPIView):
-    """
-    Retrieve a single RIR
+    List and retrieve RIRs
     """
     queryset = RIR.objects.all()
     serializer_class = serializers.RIRSerializer
@@ -72,129 +48,75 @@ class RIRDetailView(generics.RetrieveAPIView):
 # Aggregates
 #
 
-class AggregateListView(CustomFieldModelAPIView, generics.ListAPIView):
+class AggregateViewSet(CustomFieldModelViewSet):
     """
-    List aggregates (filterable)
+    List and retrieve aggregates
     """
-    queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values__field')
+    queryset = Aggregate.objects.select_related('rir')
     serializer_class = serializers.AggregateSerializer
     filter_class = filters.AggregateFilter
 
 
-class AggregateDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single aggregate
-    """
-    queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.AggregateSerializer
-
-
 #
 # Prefixes
 #
 
-class PrefixListView(CustomFieldModelAPIView, generics.ListAPIView):
+class PrefixViewSet(CustomFieldModelViewSet):
     """
-    List prefixes (filterable)
+    List and retrieve prefixes
     """
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\
-        .prefetch_related('custom_field_values__field')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     serializer_class = serializers.PrefixSerializer
     filter_class = filters.PrefixFilter
 
 
-class PrefixDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single prefix
-    """
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\
-        .prefetch_related('custom_field_values__field')
-    serializer_class = serializers.PrefixSerializer
-
-
 #
 # IP addresses
 #
 
-class IPAddressListView(CustomFieldModelAPIView, generics.ListAPIView):
+class IPAddressViewSet(CustomFieldModelViewSet):
     """
-    List IP addresses (filterable)
+    List and retrieve IP addresses
     """
-    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
-        .prefetch_related('nat_outside', 'custom_field_values__field')
+    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
     serializer_class = serializers.IPAddressSerializer
     filter_class = filters.IPAddressFilter
 
 
-class IPAddressDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single IP address
-    """
-    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
-        .prefetch_related('nat_outside', 'custom_field_values__field')
-    serializer_class = serializers.IPAddressSerializer
-
-
 #
 # VLAN groups
 #
 
-class VLANGroupListView(generics.ListAPIView):
+class VLANGroupViewSet(ModelViewSet):
     """
-    List all VLAN groups
+    List and retrieve VLAN groups
     """
     queryset = VLANGroup.objects.select_related('site')
     serializer_class = serializers.VLANGroupSerializer
     filter_class = filters.VLANGroupFilter
 
 
-class VLANGroupDetailView(generics.RetrieveAPIView):
-    """
-    Retrieve a single VLAN group
-    """
-    queryset = VLANGroup.objects.select_related('site')
-    serializer_class = serializers.VLANGroupSerializer
-
-
 #
 # VLANs
 #
 
-class VLANListView(CustomFieldModelAPIView, generics.ListAPIView):
+class VLANViewSet(CustomFieldModelViewSet):
     """
-    List VLANs (filterable)
+    List and retrieve VLANs
     """
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
-        .prefetch_related('custom_field_values__field')
+    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     serializer_class = serializers.VLANSerializer
     filter_class = filters.VLANFilter
 
 
-class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
-    """
-    Retrieve a single VLAN
-    """
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
-        .prefetch_related('custom_field_values__field')
-    serializer_class = serializers.VLANSerializer
-
-
 #
 # Services
 #
 
-class ServiceListView(generics.ListAPIView):
+class ServiceViewSet(ModelViewSet):
     """
-    List services (filterable)
+    List and retrieve services
     """
     queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
     serializer_class = serializers.ServiceSerializer
     filter_class = filters.ServiceFilter
-
-
-class ServiceDetailView(generics.RetrieveAPIView):
-    """
-    Retrieve a single service
-    """
-    queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
-    serializer_class = serializers.ServiceSerializer

+ 4 - 4
netbox/secrets/api/urls.py

@@ -5,14 +5,14 @@ from .views import *
 
 urlpatterns = [
 
+    # Secret roles
+    url(r'^secret-roles/$', SecretRoleViewSet.as_view({'get': 'list'}), name='secretrole_list'),
+    url(r'^secret-roles/(?P<pk>\d+)/$', SecretRoleViewSet.as_view({'get': 'retrieve'}), name='secretrole_detail'),
+
     # Secrets
     url(r'^secrets/$', SecretListView.as_view(), name='secret_list'),
     url(r'^secrets/(?P<pk>\d+)/$', SecretDetailView.as_view(), name='secret_detail'),
 
-    # Secret roles
-    url(r'^secret-roles/$', SecretRoleListView.as_view(), name='secretrole_list'),
-    url(r'^secret-roles/(?P<pk>\d+)/$', SecretRoleDetailView.as_view(), name='secretrole_detail'),
-
     # Miscellaneous
     url(r'^generate-keys/$', RSAKeyGeneratorView.as_view(), name='generate_keys'),
 

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

@@ -9,6 +9,7 @@ from rest_framework.permissions import IsAuthenticated
 from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
 from rest_framework.views import APIView
+from rest_framework.viewsets import ModelViewSet
 
 from extras.api.renderers import FormlessBrowsableAPIRenderer, FreeRADIUSClientsRenderer
 from secrets.filters import SecretFilter
@@ -22,24 +23,23 @@ ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
 ERR_PRIVKEY_INVALID = "Invalid private key."
 
 
-class SecretRoleListView(generics.ListAPIView):
-    """
-    List all secret roles
-    """
-    queryset = SecretRole.objects.all()
-    serializer_class = serializers.SecretRoleSerializer
-    permission_classes = [IsAuthenticated]
-
+#
+# Secret Roles
+#
 
-class SecretRoleDetailView(generics.RetrieveAPIView):
+class SecretRoleViewSet(ModelViewSet):
     """
-    Retrieve a single secret role
+    List and retrieve secret roles
     """
     queryset = SecretRole.objects.all()
     serializer_class = serializers.SecretRoleSerializer
     permission_classes = [IsAuthenticated]
 
 
+#
+# Secrets
+#
+
 class SecretListView(generics.GenericAPIView):
     """
     List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.

+ 4 - 4
netbox/tenancy/api/urls.py

@@ -6,11 +6,11 @@ from .views import *
 urlpatterns = [
 
     # Tenant groups
-    url(r'^tenant-groups/$', TenantGroupListView.as_view(), name='tenantgroup_list'),
-    url(r'^tenant-groups/(?P<pk>\d+)/$', TenantGroupDetailView.as_view(), name='tenantgroup_detail'),
+    url(r'^tenant-groups/$', TenantGroupViewSet.as_view({'get': 'list'}), name='tenantgroup_list'),
+    url(r'^tenant-groups/(?P<pk>\d+)/$', TenantGroupViewSet.as_view({'get': 'retrieve'}), name='tenantgroup_detail'),
 
     # Tenants
-    url(r'^tenants/$', TenantListView.as_view(), name='tenant_list'),
-    url(r'^tenants/(?P<pk>\d+)/$', TenantDetailView.as_view(), name='tenant_detail'),
+    url(r'^tenants/$', TenantViewSet.as_view({'get': 'list'}), name='tenant_list'),
+    url(r'^tenants/(?P<pk>\d+)/$', TenantViewSet.as_view({'get': 'retrieve'}), name='tenant_detail'),
 
 ]

+ 14 - 22
netbox/tenancy/api/views.py

@@ -1,40 +1,32 @@
-from rest_framework import generics
+from rest_framework.viewsets import ModelViewSet
 
 from tenancy.models import Tenant, TenantGroup
 from tenancy.filters import TenantFilter
 
-from extras.api.views import CustomFieldModelAPIView
+from extras.api.views import CustomFieldModelViewSet
 from . import serializers
 
 
-class TenantGroupListView(generics.ListAPIView):
-    """
-    List all tenant groups
-    """
-    queryset = TenantGroup.objects.all()
-    serializer_class = serializers.TenantGroupSerializer
-
+#
+# Tenant Groups
+#
 
-class TenantGroupDetailView(generics.RetrieveAPIView):
+class TenantGroupViewSet(ModelViewSet):
     """
-    Retrieve a single circuit type
+    List and retrieve tenant groups
     """
     queryset = TenantGroup.objects.all()
     serializer_class = serializers.TenantGroupSerializer
 
 
-class TenantListView(CustomFieldModelAPIView, generics.ListAPIView):
-    """
-    List tenants (filterable)
-    """
-    queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values__field')
-    serializer_class = serializers.TenantSerializer
-    filter_class = TenantFilter
+#
+# Tenants
+#
 
-
-class TenantDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
+class TenantViewSet(CustomFieldModelViewSet):
     """
-    Retrieve a single tenant
+    List and retrieve tenants
     """
-    queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values__field')
+    queryset = Tenant.objects.select_related('group')
     serializer_class = serializers.TenantSerializer
+    filter_class = TenantFilter