Просмотр исходного кода

Closes #4997: Introduce OrderedDefaultRouter; move root API views to views.py

Jeremy Stretch 5 лет назад
Родитель
Сommit
fd139a77f5

+ 1 - 0
docs/release-notes/version-2.9.md

@@ -14,6 +14,7 @@
 * [#4982](https://github.com/netbox-community/netbox/issues/4982) - Extended ObjectVar to allow filtering API query
 * [#4994](https://github.com/netbox-community/netbox/issues/4994) - Add `cable` attribute to PowerFeed API serializer
 * [#4996](https://github.com/netbox-community/netbox/issues/4996) - Add "connect" buttons to individual device component views
+* [#4997](https://github.com/netbox-community/netbox/issues/4997) - The browsable API now lists available endpoints alphabetically
 
 ### Bug Fixes
 

+ 3 - 12
netbox/circuits/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class CircuitsRootView(routers.APIRootView):
-    """
-    Circuits API root view
-    """
-    def get_view_name(self):
-        return 'Circuits'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = CircuitsRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.CircuitsRootView
 
 # Providers
 router.register('providers', views.ProviderViewSet)

+ 9 - 0
netbox/circuits/api/views.py

@@ -2,6 +2,7 @@ from django.db.models import Count, Prefetch
 from django.shortcuts import get_object_or_404
 from rest_framework.decorators import action
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 
 from circuits import filters
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
@@ -12,6 +13,14 @@ from utilities.api import ModelViewSet
 from . import serializers
 
 
+class CircuitsRootView(APIRootView):
+    """
+    Circuits API root view
+    """
+    def get_view_name(self):
+        return 'Circuits'
+
+
 #
 # Providers
 #

+ 3 - 12
netbox/dcim/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class DCIMRootView(routers.APIRootView):
-    """
-    DCIM API root view
-    """
-    def get_view_name(self):
-        return 'DCIM'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = DCIMRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.DCIMRootView
 
 # Sites
 router.register('regions', views.RegionViewSet)

+ 9 - 0
netbox/dcim/api/views.py

@@ -11,6 +11,7 @@ from drf_yasg.utils import swagger_auto_schema
 from rest_framework.decorators import action
 from rest_framework.mixins import ListModelMixin
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 from rest_framework.viewsets import GenericViewSet, ViewSet
 
 from circuits.models import Circuit
@@ -36,6 +37,14 @@ from . import serializers
 from .exceptions import MissingFilterException
 
 
+class DCIMRootView(APIRootView):
+    """
+    DCIM API root view
+    """
+    def get_view_name(self):
+        return 'DCIM'
+
+
 # Mixins
 
 class CableTraceMixin(object):

+ 3 - 12
netbox/extras/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class ExtrasRootView(routers.APIRootView):
-    """
-    Extras API root view
-    """
-    def get_view_name(self):
-        return 'Extras'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = ExtrasRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.ExtrasRootView
 
 # Custom field choices
 router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')

+ 9 - 0
netbox/extras/api/views.py

@@ -8,6 +8,7 @@ from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 from rq import Worker
 
@@ -25,6 +26,14 @@ from utilities.utils import copy_safe_request
 from . import serializers
 
 
+class ExtrasRootView(APIRootView):
+    """
+    Extras API root view
+    """
+    def get_view_name(self):
+        return 'Extras'
+
+
 #
 # Custom field choices
 #

+ 3 - 12
netbox/ipam/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class IPAMRootView(routers.APIRootView):
-    """
-    IPAM API root view
-    """
-    def get_view_name(self):
-        return 'IPAM'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = IPAMRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.IPAMRootView
 
 # VRFs
 router.register('vrfs', views.VRFViewSet)

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

@@ -1,11 +1,12 @@
 from django.conf import settings
-from django.db.models import Count, Prefetch
+from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from django_pglocks import advisory_lock
 from drf_yasg.utils import swagger_auto_schema
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 
 from extras.api.views import CustomFieldModelViewSet
 from ipam import filters
@@ -16,6 +17,14 @@ from utilities.utils import get_subquery
 from . import serializers
 
 
+class IPAMRootView(APIRootView):
+    """
+    IPAM API root view
+    """
+    def get_view_name(self):
+        return 'IPAM'
+
+
 #
 # VRFs
 #

+ 3 - 12
netbox/secrets/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class SecretsRootView(routers.APIRootView):
-    """
-    Secrets API root view
-    """
-    def get_view_name(self):
-        return 'Secrets'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = SecretsRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.SecretsRootView
 
 # Secrets
 router.register('secret-roles', views.SecretRoleViewSet)

+ 9 - 0
netbox/secrets/api/views.py

@@ -6,6 +6,7 @@ from django.http import HttpResponseBadRequest
 from rest_framework.exceptions import ValidationError
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 from rest_framework.viewsets import ViewSet
 
 from secrets import filters
@@ -20,6 +21,14 @@ ERR_PRIVKEY_MISSING = "Private key was not provided."
 ERR_PRIVKEY_INVALID = "Invalid private key."
 
 
+class SecretsRootView(APIRootView):
+    """
+    Secrets API root view
+    """
+    def get_view_name(self):
+        return 'Secrets'
+
+
 #
 # Secret Roles
 #

+ 3 - 12
netbox/tenancy/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class TenancyRootView(routers.APIRootView):
-    """
-    Tenancy API root view
-    """
-    def get_view_name(self):
-        return 'Tenancy'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = TenancyRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.TenancyRootView
 
 # Tenants
 router.register('tenant-groups', views.TenantGroupViewSet)

+ 10 - 0
netbox/tenancy/api/views.py

@@ -1,3 +1,5 @@
+from rest_framework.routers import APIRootView
+
 from circuits.models import Circuit
 from dcim.models import Device, Rack, Site
 from extras.api.views import CustomFieldModelViewSet
@@ -10,6 +12,14 @@ from virtualization.models import VirtualMachine
 from . import serializers
 
 
+class TenancyRootView(APIRootView):
+    """
+    Tenancy API root view
+    """
+    def get_view_name(self):
+        return 'Tenancy'
+
+
 #
 # Tenant Groups
 #

+ 3 - 12
netbox/users/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class UsersRootView(routers.APIRootView):
-    """
-    Users API root view
-    """
-    def get_view_name(self):
-        return 'Users'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = UsersRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.UsersRootView
 
 # Users and groups
 router.register('users', views.UserViewSet)

+ 9 - 0
netbox/users/api/views.py

@@ -1,5 +1,6 @@
 from django.contrib.auth.models import Group, User
 from django.db.models import Count
+from rest_framework.routers import APIRootView
 
 from users import filters
 from users.models import ObjectPermission
@@ -8,6 +9,14 @@ from utilities.querysets import RestrictedQuerySet
 from . import serializers
 
 
+class UsersRootView(APIRootView):
+    """
+    Users API root view
+    """
+    def get_view_name(self):
+        return 'Users'
+
+
 #
 # Users and groups
 #

+ 19 - 0
netbox/utilities/api.py

@@ -13,6 +13,7 @@ from rest_framework.exceptions import APIException, ValidationError
 from rest_framework.permissions import BasePermission
 from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
 from rest_framework.response import Response
+from rest_framework.routers import DefaultRouter
 from rest_framework.viewsets import ModelViewSet as _ModelViewSet
 
 from .utils import dict_to_filter_params, dynamic_import
@@ -399,3 +400,21 @@ class ModelViewSet(_ModelViewSet):
         logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
 
         return super().perform_destroy(instance)
+
+
+#
+# Routers
+#
+
+class OrderedDefaultRouter(DefaultRouter):
+
+    def get_api_root_view(self, api_urls=None):
+        """
+        Wrap DRF's DefaultRouter to return an alphabetized list of endpoints.
+        """
+        api_root_dict = OrderedDict()
+        list_name = self.routes[0].name
+        for prefix, viewset, basename in sorted(self.registry, key=lambda x: x[0]):
+            api_root_dict[prefix] = list_name.format(basename=basename)
+
+        return self.APIRootView.as_view(api_root_dict=api_root_dict)

+ 3 - 12
netbox/virtualization/api/urls.py

@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from utilities.api import OrderedDefaultRouter
 from . import views
 
 
-class VirtualizationRootView(routers.APIRootView):
-    """
-    Virtualization API root view
-    """
-    def get_view_name(self):
-        return 'Virtualization'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = VirtualizationRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.VirtualizationRootView
 
 # Clusters
 router.register('cluster-types', views.ClusterTypeViewSet)

+ 9 - 0
netbox/virtualization/api/views.py

@@ -2,6 +2,7 @@ from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from rest_framework.decorators import action
 from rest_framework.response import Response
+from rest_framework.routers import APIRootView
 
 from dcim.models import Device
 from extras.api.serializers import RenderedGraphSerializer
@@ -14,6 +15,14 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
 from . import serializers
 
 
+class VirtualizationRootView(APIRootView):
+    """
+    Virtualization API root view
+    """
+    def get_view_name(self):
+        return 'Virtualization'
+
+
 #
 # Clusters
 #