Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for ModelViewSet (through appropriate router attribution based on settings) - And fix incomplete API paths when recurrence is used #132

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions README.md
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
@@ -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
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
@@ -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
@@ -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
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
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
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
@@ -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