Skip to content
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ You can find detailed information about the package's settings at [the docs](htt

REST_FRAMEWORK_DOCS = {
'HIDE_DOCS': True # Default: False
'MODULE_ROUTERS': {},
'DEFAULT_MODULE_ROUTER': 'router',
'DEFAULT_ROUTER': None,
}


Expand Down
12 changes: 11 additions & 1 deletion demo/project/accounts/urls.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from django.conf.urls import url
from django.conf.urls import url, include
from rest_framework import routers
from project.accounts import views


router = routers.DefaultRouter()
router.register(
r'user_viewset',
views.UserModelViewSet,
)


urlpatterns = [
url(r'^test/$', views.TestView.as_view(), name="test-view"),

url(r'^login/$', views.LoginView.as_view(), name="login"),
url(r'^register/$', views.UserRegistrationView.as_view(), name="register"),
url(r'^register/$', views.UserRegistrationView.as_view(), name="register"),
url(r'^reset-password/$', view=views.PasswordResetView.as_view(), name="reset-password"),
url(r'^reset-password/confirm/$', views.PasswordResetConfirmView.as_view(), name="reset-password-confirm"),

url(r'^user/profile/$', views.UserProfileView.as_view(), name="profile"),

url(r'^viewset_test/', include(router.urls), name="user_viewset"),
]
6 changes: 6 additions & 0 deletions demo/project/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from project.accounts.models import User
from project.accounts.serializers import (
UserRegistrationSerializer, UserProfileSerializer, ResetPasswordSerializer
Expand Down Expand Up @@ -81,3 +82,8 @@ def post(self, request, *args, **kwargs):
if not serializer.is_valid():
return Response({'errors': serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
return Response({"msg": "Password updated successfully."}, status=status.HTTP_200_OK)


class UserModelViewSet(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserProfileSerializer
13 changes: 11 additions & 2 deletions demo/project/organisations/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
from django.conf.urls import url
from django.conf.urls import url, include
from rest_framework import routers
from project.organisations import views


router = routers.DefaultRouter()
router.register(
r'organisation_viewset',
views.OrganisationViewSet,
)


urlpatterns = [

url(r'^create/$', view=views.CreateOrganisationView.as_view(), name="create"),
url(r'^(?P<slug>[\w-]+)/$', view=views.RetrieveOrganisationView.as_view(), name="organisation"),
url(r'^(?P<slug>[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"),
url(r'^(?P<slug>[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave")
url(r'^(?P<slug>[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave"),

url(r'^', include(router.urls), name="organisation_viewset"),
]
9 changes: 8 additions & 1 deletion demo/project/organisations/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from project.organisations.models import Organisation, Membership
from project.organisations.serializers import (
CreateOrganisationSerializer, OrganisationMembersSerializer, RetrieveOrganisationSerializer
CreateOrganisationSerializer, OrganisationMembersSerializer,
RetrieveOrganisationSerializer, OrganisationDetailSerializer
)


Expand Down Expand Up @@ -34,3 +36,8 @@ def delete(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)


class OrganisationViewSet(ModelViewSet):
queryset = Organisation.objects.all()
serializer_class = OrganisationDetailSerializer
8 changes: 8 additions & 0 deletions demo/project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@
)
}

REST_FRAMEWORK_DOCS = {
'HIDE_DOCS': False,
'MODULE_ROUTERS': {
'project.accounts.urls': 'router',
},
'DEFAULT_MODULE_ROUTER': 'router',
}

# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/

Expand Down
4 changes: 4 additions & 0 deletions demo/project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
from django.conf.urls import include, url
from django.contrib import admin


urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^docs/', include('rest_framework_docs.urls')),

# API
url(r'^accounts/', view=include('project.accounts.urls', namespace='accounts')),


url(r'^organisations/', view=include('project.organisations.urls', namespace='organisations')),

]
35 changes: 30 additions & 5 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ source_filename: settings
To set DRF docs' settings just include the dictionary below in Django's `settings.py` file.

REST_FRAMEWORK_DOCS = {
'HIDE_DOCS': True
'HIDE_DOCS': True,
'MODULE_ROUTERS': {
'project.accounts.urls': 'accounts_router',
},
'DEFAULT_MODULE_ROUTER': 'router',
'DEFAULT_ROUTER': 'project.urls.default_router',
}


Expand All @@ -21,9 +26,29 @@ You can use hidden to prevent your docs from showing up in different environment

Then set the value of the environment variable `HIDE_DRFDOCS` for each environment (ie. Use `.env` files)

##### MODULE_ROUTERS
Use this setting to manually bind url modules to a router instance. The router must be defined in the module, or imported in the module.
For instance, if the router of an app called 'gifts' is 'gifts_router', and the router of another app called 'scuba_diving' is 'scuba_diving_router', the MODULE_ROUTERS setting should look like:

'MODULE_ROUTERS': {
'gifts.urls': 'gift_router',
'scuba_diving.urls': 'scuba_diving_router'
}

If there is no entry for a given module, if this setting is not set, or if it is set to None, the value of the DEFAULT_MODULE_ROUTER setting is used.

##### DEFAULT_MODULE_ROUTER
When set, the value of this setting will be used to find a router for a urls module. If there is no router having the DEFAULT_MODULE_ROUTER name, the setting is ignored and the value of DEFAULT_ROUTER is used.

##### DEFAULT_ROUTER
When defined, this setting must describe a python dotted path leading to the router that should be used when MODULE_ROUTERS and DEFAULT_MODULE_ROUTER are not set.
This parameter is useful when there is only one router for your whole API.

### List of Settings

| Setting | Type | Options | Default |
|---------|---------|-----------------|---------|
|HIDE_DOCS| Boolean | `True`, `False` | `False` |
| | | | |
| Setting | Type | Options | Default |
|---------------------|-----------------------------------------------------------|-----------------|---------|
|HIDE_DOCS | Boolean | `True`, `False` | `False` |
|MODULE_ROUTERS | dict of python dotted paths -> router instance name | | `None` |
|DEFAULT_MODULE_ROUTER| str representing a default router instance name | | `None` |
|DEFAULT_ROUTER | str representing a python dotted path to a router instance| | `None` |
130 changes: 113 additions & 17 deletions rest_framework_docs/api_docs.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
from importlib import import_module
from types import ModuleType
from django.conf import settings
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
from django.utils.module_loading import import_string
from rest_framework.views import APIView
from rest_framework_docs.api_endpoint import ApiEndpoint
from rest_framework.routers import BaseRouter
from rest_framework_docs.api_endpoint import ApiNode, ApiEndpoint
from rest_framework_docs.settings import DRFSettings


drf_settings = DRFSettings().settings


class ApiDocumentation(object):

def __init__(self, drf_router=None):
self.endpoints = []
self.drf_router = drf_router

try:
root_urlconf = import_string(settings.ROOT_URLCONF)
except ImportError:
Expand All @@ -21,26 +28,115 @@ def __init__(self, drf_router=None):
else:
self.get_all_view_names(root_urlconf.urlpatterns)

def get_all_view_names(self, urlpatterns, parent_pattern=None):
def get_all_view_names(self, urlpatterns, parent_api_node=None):
for pattern in urlpatterns:
if isinstance(pattern, RegexURLResolver):
parent_pattern = None if pattern._regex == "^" else pattern
self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=parent_pattern)
elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern):
api_endpoint = ApiEndpoint(pattern, parent_pattern, self.drf_router)
# Try to get router from settings, if no router is found,
# Use the instance drf_router property.
router = get_router(pattern)
if router is None:
parent_router = None
if parent_api_node is not None:
parent_router = parent_api_node.drf_router
if parent_router is not None:
router = parent_router
else:
router = self.drf_router
if pattern._regex == "^":
parent = parent_api_node
else:
parent = ApiNode(
pattern,
parent_node=parent_api_node,
drf_router=router
)
self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_api_node=parent)
elif isinstance(pattern, RegexURLPattern) and _is_drf_view(pattern) and not _is_format_endpoint(pattern):
router = self.drf_router
if parent_api_node is not None:
if parent_api_node.drf_router is not None:
router = parent_api_node.drf_router
api_endpoint = ApiEndpoint(pattern, parent_api_node, router)
self.endpoints.append(api_endpoint)

def _is_drf_view(self, pattern):
"""
Should check whether a pattern inherits from DRF's APIView
"""
return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView)
def get_endpoints(self):
return self.endpoints

def _is_format_endpoint(self, pattern):
"""
Exclude endpoints with a "format" parameter

def _is_drf_view(pattern):
"""
Should check whether a pattern inherits from DRF's APIView
"""
return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls,
APIView)


def _is_format_endpoint(pattern):
"""
Exclude endpoints with a "format" parameter
"""
return '?P<format>' in pattern._regex


def get_router(pattern):
urlconf = pattern.urlconf_name
router = None
if isinstance(urlconf, ModuleType):
# First: try MODULE_ROUTERS setting - Don't ignore errors
router = get_module_router(urlconf)
if router is not None:
return router
# Second: try DEFAULT_MODULE_ROUTER setting - Ignore errors
try:
router = get_default_module_router(urlconf)
if router is not None:
return router
except:
pass
# Third: try DEFAULT_ROUTER setting - Don't ignore errors
router = get_default_router()
if router is not None:
return router
return router


def get_module_router(module):
routers = drf_settings['MODULE_ROUTERS']
if routers is None:
return None
if module.__name__ in routers:
router_name = routers[module.__name__]
router = getattr(module, router_name)
assert isinstance(router, BaseRouter), \
"""
drfdocs 'ROUTERS' setting does not correspond to
a router instance for module {}.
""".format(module.__name__)
return router
return None


def get_default_module_router(module):
default_module_router = drf_settings['DEFAULT_MODULE_ROUTER']
if default_module_router is None:
return None
router = getattr(module, default_module_router)
assert isinstance(router, BaseRouter), \
"""
return '?P<format>' in pattern._regex
drfdocs 'DEFAULT_MODULE_ROUTER' setting does not correspond to
a router instance for module {}.
""".format(module.__name__)
return router

def get_endpoints(self):
return self.endpoints

def get_default_router():
default_router_path = drf_settings['DEFAULT_ROUTER']
if default_router_path is None:
return None
router = import_string(default_router_path)
assert isinstance(router, BaseRouter), \
"""
drfdocs 'DEFAULT_ROUTER_NAME' setting does not correspond to
a router instance {}.
""".format(router.__name__)
return router
Loading