mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge pull request #1930 from davcamer/drf-yasg
Use drf_yasg to generate swagger
This commit is contained in:
commit
05b5609d86
@ -6,6 +6,9 @@ from django.conf import settings
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.openapi import Parameter
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -418,14 +421,20 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
* `peer-interface`: The name of the peer interface
|
* `peer-interface`: The name of the peer interface
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
_device_param = Parameter('peer-device', 'query',
|
||||||
|
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
|
||||||
|
_interface_param = Parameter('peer-interface', 'query',
|
||||||
|
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Connected Device Locator"
|
return "Connected Device Locator"
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
peer_device_name = request.query_params.get('peer-device')
|
peer_device_name = request.query_params.get(self._device_param.name)
|
||||||
peer_interface_name = request.query_params.get('peer-interface')
|
peer_interface_name = request.query_params.get(self._interface_param.name)
|
||||||
if not peer_device_name or not peer_interface_name:
|
if not peer_device_name or not peer_interface_name:
|
||||||
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
||||||
|
|
||||||
|
@ -133,7 +133,6 @@ INSTALLED_APPS = (
|
|||||||
'django_tables2',
|
'django_tables2',
|
||||||
'mptt',
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
|
||||||
'timezone_field',
|
'timezone_field',
|
||||||
'circuits',
|
'circuits',
|
||||||
'dcim',
|
'dcim',
|
||||||
@ -144,6 +143,7 @@ INSTALLED_APPS = (
|
|||||||
'users',
|
'users',
|
||||||
'utilities',
|
'utilities',
|
||||||
'virtualization',
|
'virtualization',
|
||||||
|
'drf_yasg',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
@ -246,6 +246,32 @@ REST_FRAMEWORK = {
|
|||||||
'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
|
'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# drf_yasg settings for Swagger
|
||||||
|
SWAGGER_SETTINGS = {
|
||||||
|
'DEFAULT_FIELD_INSPECTORS': [
|
||||||
|
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||||
|
'utilities.custom_inspectors.CustomChoiceFieldInspector',
|
||||||
|
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
||||||
|
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
||||||
|
'drf_yasg.inspectors.RelatedFieldInspector',
|
||||||
|
'drf_yasg.inspectors.ChoiceFieldInspector',
|
||||||
|
'drf_yasg.inspectors.FileFieldInspector',
|
||||||
|
'drf_yasg.inspectors.DictFieldInspector',
|
||||||
|
'drf_yasg.inspectors.SimpleFieldInspector',
|
||||||
|
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
||||||
|
],
|
||||||
|
'DEFAULT_FILTER_INSPECTORS': [
|
||||||
|
'utilities.custom_inspectors.IdInFilterInspector',
|
||||||
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
|
],
|
||||||
|
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||||
|
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||||
|
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||||
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Django debug toolbar
|
# Django debug toolbar
|
||||||
INTERNAL_IPS = (
|
INTERNAL_IPS = (
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
|
@ -4,12 +4,24 @@ from django.conf import settings
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
from rest_framework_swagger.views import get_swagger_view
|
from drf_yasg.views import get_schema_view
|
||||||
|
from drf_yasg import openapi
|
||||||
|
|
||||||
from netbox.views import APIRootView, HomeView, SearchView
|
from netbox.views import APIRootView, HomeView, SearchView
|
||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
|
|
||||||
swagger_view = get_swagger_view(title='NetBox API')
|
schema_view = get_schema_view(
|
||||||
|
openapi.Info(
|
||||||
|
title="NetBox API",
|
||||||
|
default_version='v2',
|
||||||
|
description="API to access NetBox",
|
||||||
|
terms_of_service="https://github.com/digitalocean/netbox",
|
||||||
|
contact=openapi.Contact(email="netbox@digitalocean.com"),
|
||||||
|
license=openapi.License(name="Apache v2 License"),
|
||||||
|
),
|
||||||
|
validators=['flex', 'ssv'],
|
||||||
|
public=True,
|
||||||
|
)
|
||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
@ -40,7 +52,9 @@ _patterns = [
|
|||||||
url(r'^api/secrets/', include('secrets.api.urls')),
|
url(r'^api/secrets/', include('secrets.api.urls')),
|
||||||
url(r'^api/tenancy/', include('tenancy.api.urls')),
|
url(r'^api/tenancy/', include('tenancy.api.urls')),
|
||||||
url(r'^api/virtualization/', include('virtualization.api.urls')),
|
url(r'^api/virtualization/', include('virtualization.api.urls')),
|
||||||
url(r'^api/docs/', swagger_view, name='api_docs'),
|
url(r'^api/docs/$', schema_view.with_ui('swagger', cache_timeout=None), name='api_docs'),
|
||||||
|
url(r'^api/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'),
|
||||||
|
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'),
|
||||||
|
|
||||||
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
# Serving static media in Django to pipe it through LoginRequiredMiddleware
|
||||||
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||||
|
76
netbox/utilities/custom_inspectors.py
Normal file
76
netbox/utilities/custom_inspectors.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector
|
||||||
|
from rest_framework.fields import ChoiceField
|
||||||
|
|
||||||
|
from extras.api.customfields import CustomFieldsSerializer
|
||||||
|
from utilities.api import ChoiceFieldSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CustomChoiceFieldInspector(FieldInspector):
|
||||||
|
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||||
|
# this returns a callable which extracts title, description and other stuff
|
||||||
|
# https://drf-yasg.readthedocs.io/en/stable/_modules/drf_yasg/inspectors/base.html#FieldInspector._get_partial_types
|
||||||
|
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||||
|
|
||||||
|
if isinstance(field, ChoiceFieldSerializer):
|
||||||
|
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
|
||||||
|
|
||||||
|
choices = list(field._choices.keys())
|
||||||
|
if set([None] + choices) == {None, True, False}:
|
||||||
|
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
|
||||||
|
# differentiated since they each have subtly different values in their choice keys.
|
||||||
|
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
|
||||||
|
# - face is an integer set {0, 1} which is easily confused with {False, True}
|
||||||
|
schema_type = openapi.TYPE_INTEGER
|
||||||
|
if all(type(x) == bool for x in [c for c in choices if c is not None]):
|
||||||
|
schema_type = openapi.TYPE_BOOLEAN
|
||||||
|
value_schema = openapi.Schema(type=schema_type)
|
||||||
|
value_schema['x-nullable'] = True
|
||||||
|
|
||||||
|
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
|
||||||
|
"label": openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
"value": value_schema
|
||||||
|
})
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
elif isinstance(field, CustomFieldsSerializer):
|
||||||
|
schema = SwaggerType(type=openapi.TYPE_OBJECT)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
return NotHandled
|
||||||
|
|
||||||
|
|
||||||
|
class NullableBooleanFieldInspector(FieldInspector):
|
||||||
|
def process_result(self, result, method_name, obj, **kwargs):
|
||||||
|
|
||||||
|
if isinstance(result, openapi.Schema) and isinstance(obj, ChoiceField) and result.type == 'boolean':
|
||||||
|
keys = obj.choices.keys()
|
||||||
|
if set(keys) == {None, True, False}:
|
||||||
|
result['x-nullable'] = True
|
||||||
|
result.type = 'boolean'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class IdInFilterInspector(FilterInspector):
|
||||||
|
def process_result(self, result, method_name, obj, **kwargs):
|
||||||
|
if isinstance(result, list):
|
||||||
|
params = [p for p in result if isinstance(p, openapi.Parameter) and p.name == 'id__in']
|
||||||
|
for p in params:
|
||||||
|
p.type = 'string'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class NullablePaginatorInspector(PaginatorInspector):
|
||||||
|
def process_result(self, result, method_name, obj, **kwargs):
|
||||||
|
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
|
||||||
|
next = result.properties['next']
|
||||||
|
if isinstance(next, openapi.Schema):
|
||||||
|
next['x-nullable'] = True
|
||||||
|
previous = result.properties['previous']
|
||||||
|
if isinstance(previous, openapi.Schema):
|
||||||
|
previous['x-nullable'] = True
|
||||||
|
|
||||||
|
return result
|
@ -1,2 +1,3 @@
|
|||||||
|
django-rest-swagger
|
||||||
psycopg2
|
psycopg2
|
||||||
pycrypto
|
pycrypto
|
||||||
|
@ -3,10 +3,10 @@ django-cors-headers>=2.1.0
|
|||||||
django-debug-toolbar>=1.9.0
|
django-debug-toolbar>=1.9.0
|
||||||
django-filter>=1.1.0
|
django-filter>=1.1.0
|
||||||
django-mptt>=0.9.0
|
django-mptt>=0.9.0
|
||||||
django-rest-swagger>=2.1.0
|
|
||||||
django-tables2>=1.19.0
|
django-tables2>=1.19.0
|
||||||
django-timezone-field>=2.0
|
django-timezone-field>=2.0
|
||||||
djangorestframework>=3.7.7
|
djangorestframework>=3.7.7
|
||||||
|
drf-yasg>=1.4.4
|
||||||
graphviz>=0.8.2
|
graphviz>=0.8.2
|
||||||
Markdown>=2.6.11
|
Markdown>=2.6.11
|
||||||
natsort>=5.2.0
|
natsort>=5.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user