diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py
index 03f374b..2558ae9 100644
--- a/rest_framework_docs/api_docs.py
+++ b/rest_framework_docs/api_docs.py
@@ -1,3 +1,4 @@
+from operator import attrgetter
from django.conf import settings
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
from rest_framework.views import APIView
@@ -6,25 +7,34 @@
class ApiDocumentation(object):
- def __init__(self):
+ def __init__(self, filter_app=None):
+ """
+ :param filter_app: namespace or app_name
+ """
self.endpoints = []
root_urlconf = __import__(settings.ROOT_URLCONF)
if hasattr(root_urlconf, 'urls'):
- self.get_all_view_names(root_urlconf.urls.urlpatterns)
+ self.get_all_view_names(root_urlconf.urls.urlpatterns, filter_app=filter_app)
else:
- self.get_all_view_names(root_urlconf.urlpatterns)
+ self.get_all_view_names(root_urlconf.urlpatterns, filter_app=filter_app)
- def get_all_view_names(self, urlpatterns, parent_pattern=None):
+ def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_app=None):
for pattern in urlpatterns:
- if isinstance(pattern, RegexURLResolver):
- self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern)
+ if isinstance(pattern, RegexURLResolver) and (not filter_app or filter_app in [pattern.app_name, pattern.namespace]):
+ self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern, filter_app=filter_app)
elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern):
- api_endpoint = ApiEndpoint(pattern, parent_pattern)
- self.endpoints.append(api_endpoint)
+ if not filter_app or (parent_pattern and filter_app in [parent_pattern.app_name, parent_pattern.namespace]):
+ api_endpoint = ApiEndpoint(pattern, parent_pattern)
+ self.endpoints.append(api_endpoint)
def _is_drf_view(self, pattern):
- # Should check whether a pattern inherits from DRF's APIView
+ """
+ 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
+ """
+ Returns the endpoints sorted by the app name
+ """
+ return sorted(self.endpoints, key=attrgetter('name'))
diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py
index 26cb0f6..9e90d49 100644
--- a/rest_framework_docs/api_endpoint.py
+++ b/rest_framework_docs/api_endpoint.py
@@ -1,6 +1,7 @@
import json
import inspect
from django.contrib.admindocs.views import simplify_regex
+from rest_framework.viewsets import ModelViewSet
class ApiEndpoint(object):
@@ -8,12 +9,21 @@ class ApiEndpoint(object):
def __init__(self, pattern, parent_pattern=None):
self.pattern = pattern
self.callback = pattern.callback
- # self.name = pattern.name
self.docstring = self.__get_docstring__()
- self.name_parent = simplify_regex(parent_pattern.regex.pattern).replace('/', '') if parent_pattern else None
+
+ if parent_pattern:
+ self.name_parent = parent_pattern.namespace or parent_pattern.app_name or \
+ simplify_regex(parent_pattern.regex.pattern).replace('/', '-')
+ self.name = self.name_parent
+ if hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, ModelViewSet):
+ self.name = '%s (REST)' % self.name_parent
+ else:
+ self.name_parent = ''
+ self.name = ''
+
+ self.labels = dict(parent=self.name_parent, name=self.name)
self.path = self.__get_path__(parent_pattern)
self.allowed_methods = self.__get_allowed_methods__()
- # self.view_name = pattern.callback.__name__
self.errors = None
self.fields = self.__get_serializer_fields__()
self.fields_json = self.__get_serializer_fields_json__()
@@ -21,7 +31,7 @@ def __init__(self, pattern, parent_pattern=None):
def __get_path__(self, parent_pattern):
if parent_pattern:
- return "/{0}{1}".format(self.name_parent, simplify_regex(self.pattern.regex.pattern))
+ return simplify_regex(parent_pattern.regex.pattern + self.pattern.regex.pattern)
return simplify_regex(self.pattern.regex.pattern)
def __get_allowed_methods__(self):
diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html
index 76d783d..597ca1f 100644
--- a/rest_framework_docs/templates/rest_framework_docs/home.html
+++ b/rest_framework_docs/templates/rest_framework_docs/home.html
@@ -1,86 +1,91 @@
{% extends "rest_framework_docs/docs.html" %}
{% block apps_menu %}
-{% regroup endpoints by name_parent as endpoints_grouped %}
-
- Jump To
-
-
+ {% regroup endpoints by labels as endpoints_grouped %}
+
+ {% if endpoints_grouped|length > 1 %}
+
+ Jump To
+
+
+ {% endif %}
{% endblock %}
{% block content %}
-
- {% regroup endpoints by name_parent as endpoints_grouped %}
+ {% regroup endpoints by labels as endpoints_grouped %}
{% if endpoints_grouped %}
- {% for group in endpoints_grouped %}
-
- {{group.grouper}}
-
-
-
- {% for endpoint in group.list %}
-
-
-
-
-
-
-
- {{ endpoint.path }}
-
+ {% for group in endpoints_grouped %}
+
+ {% if group.grouper.parent %}
+ {{ group.grouper.name }}
+ {% endif %}
+
+
+
+
+ {% for endpoint in group.list %}
+
+
+
+
+
+
+
+ {{ endpoint.path }}
+
+
+
+
+
+ {% for method in endpoint.allowed_methods %}
+ - {{ method }}
+ {% endfor %}
+ -
+
+
+
+
-
-
- {% for method in endpoint.allowed_methods %}
- - {{ method }}
+
+
+ {% if endpoint.docstring %}
+
{{ endpoint.docstring }}
+ {% endif %}
+
+ {% if endpoint.errors %}
+
Oops! There was something wrong with {{ endpoint.errors }}. Please check your code.
+ {% endif %}
+
+ {% if endpoint.fields %}
+
Fields:
+
+ {% for field in endpoint.fields %}
+ - {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %}
{% endfor %}
- -
-
-
+
+ {% elif not endpoint.errors %}
+
No fields.
+ {% endif %}
+ {% endfor %}
-
-
- {% if endpoint.docstring %}
-
{{ endpoint.docstring }}
- {% endif %}
-
- {% if endpoint.errors %}
-
Oops! There was something wrong with {{ endpoint.errors }}. Please check your code.
- {% endif %}
-
- {% if endpoint.fields %}
-
Fields:
-
- {% for field in endpoint.fields %}
- - {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %}
- {% endfor %}
-
- {% elif not endpoint.errors %}
-
No fields.
- {% endif %}
-
-
- {% endfor %}
-
-
- {% endfor %}
+ {% endfor %}
{% elif not query %}
There are currently no api endpoints to document.
{% else %}
diff --git a/rest_framework_docs/urls.py b/rest_framework_docs/urls.py
index beb1588..44b043a 100644
--- a/rest_framework_docs/urls.py
+++ b/rest_framework_docs/urls.py
@@ -4,4 +4,7 @@
urlpatterns = [
# Url to view the API Docs
url(r'^$', DRFDocsView.as_view(), name='drfdocs'),
+
+ # Url to view the API Docs with a specific namespace or app_name
+ url(r'^(?P
[\w-]+)/$', DRFDocsView.as_view(), name='drfdocs-filter'),
]
diff --git a/rest_framework_docs/views.py b/rest_framework_docs/views.py
index 04074cc..98daf93 100644
--- a/rest_framework_docs/views.py
+++ b/rest_framework_docs/views.py
@@ -10,17 +10,22 @@ class DRFDocsView(TemplateView):
def get_context_data(self, **kwargs):
settings = DRFSettings().settings
+ search_query = self.request.GET.get("search", "")
+ filter_app = self.kwargs.get("filter_app", None)
+
if settings["HIDE_DOCS"]:
raise Http404("Django Rest Framework Docs are hidden. Check your settings.")
- context = super(DRFDocsView, self).get_context_data(**kwargs)
- docs = ApiDocumentation()
+ docs = ApiDocumentation(filter_app=filter_app)
endpoints = docs.get_endpoints()
- query = self.request.GET.get("search", "")
- if query and endpoints:
- endpoints = [endpoint for endpoint in endpoints if query in endpoint.path]
+ if filter_app and not endpoints:
+ raise Http404("The are no endpoints for \"%s\"." % filter_app)
- context['query'] = query
+ if search_query and endpoints:
+ endpoints = [endpoint for endpoint in endpoints if search_query in endpoint.path]
+
+ context = super(DRFDocsView, self).get_context_data(**kwargs)
+ context['query'] = search_query
context['endpoints'] = endpoints
return context
diff --git a/runtests.py b/runtests.py
index c388477..4b06497 100644
--- a/runtests.py
+++ b/runtests.py
@@ -53,6 +53,7 @@ def run_tests_coverage():
cov.report()
cov.html_report(directory='covhtml')
+
exit_on_failure(flake8_main(FLAKE8_ARGS))
exit_on_failure(run_tests_eslint())
exit_on_failure(run_tests_coverage())
diff --git a/tests/tests.py b/tests/tests.py
index afb58d0..8e2825c 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -19,7 +19,7 @@ def test_settings_module(self):
self.assertEqual(settings.get_setting("HIDE_DOCS"), False)
self.assertEqual(settings.get_setting("TEST"), None)
- def test_index_view_with_endpoints(self):
+ def test_docs_home_view_with_endpoints(self):
"""
Should load the drf focs view with all the endpoints.
NOTE: Views that do **not** inherit from DRF's "APIView" are not included.
@@ -30,16 +30,16 @@ def test_index_view_with_endpoints(self):
self.assertEqual(len(response.context["endpoints"]), 10)
# Test the login view
- self.assertEqual(response.context["endpoints"][0].name_parent, "accounts")
- self.assertEqual(response.context["endpoints"][0].allowed_methods, ['POST', 'OPTIONS'])
- self.assertEqual(response.context["endpoints"][0].path, "/accounts/login/")
- self.assertEqual(response.context["endpoints"][0].docstring, "A view that allows users to login providing their username and password.")
- self.assertEqual(len(response.context["endpoints"][0].fields), 2)
- self.assertEqual(response.context["endpoints"][0].fields[0]["type"], "CharField")
- self.assertTrue(response.context["endpoints"][0].fields[0]["required"])
+ self.assertEqual(response.context["endpoints"][1].name_parent, "accounts")
+ self.assertEqual(response.context["endpoints"][1].allowed_methods, ['POST', 'OPTIONS'])
+ self.assertEqual(response.context["endpoints"][1].path, "/accounts/login/")
+ self.assertEqual(response.context["endpoints"][1].docstring, "A view that allows users to login providing their username and password.")
+ self.assertEqual(len(response.context["endpoints"][1].fields), 2)
+ self.assertEqual(response.context["endpoints"][1].fields[0]["type"], "CharField")
+ self.assertTrue(response.context["endpoints"][1].fields[0]["required"])
# The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error.
- self.assertEqual(str(response.context["endpoints"][8].errors), "'test_value'")
+ self.assertEqual(str(response.context["endpoints"][9].errors), "'test_value'")
def test_index_search_with_endpoints(self):
response = self.client.get("%s?search=reset-password" % reverse("drfdocs"))
@@ -59,3 +59,68 @@ def test_index_view_docs_hidden(self):
self.assertEqual(response.status_code, 404)
self.assertEqual(response.reason_phrase.upper(), "NOT FOUND")
+
+ def test_index_view_with_existent_namespace(self):
+ """
+ Should load the drf docs view with all the endpoints contained in the specified namespace.
+ NOTE: Views that do **not** inherit from DRF's "APIView" are not included.
+ """
+ # Test 'accounts' namespace
+ response = self.client.get(reverse('drfdocs-filter', args=['accounts']))
+ # response = self.client.get(reverse('drfdocs-filter', kwargs={'parent_app': 'accountss'}))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 5)
+
+ # Test the login view
+ self.assertEqual(response.context["endpoints"][0].name_parent, "accounts")
+ self.assertEqual(response.context["endpoints"][0].allowed_methods, ['POST', 'OPTIONS'])
+ self.assertEqual(response.context["endpoints"][0].path, "/accounts/login/")
+
+ # Test 'organisations' namespace
+ response = self.client.get(reverse('drfdocs-filter', args=['organisations']))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 3)
+
+ # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error.
+ self.assertEqual(str(response.context["endpoints"][2].errors), "'test_value'")
+
+ # Test 'members' namespace
+ response = self.client.get(reverse('drfdocs-filter', args=['members']))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 1)
+
+ def test_index_search_with_existent_namespace(self):
+ response = self.client.get("%s?search=reset-password" % reverse('drfdocs-filter', args=['accounts']))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 2)
+ self.assertEqual(response.context["endpoints"][1].path, "/accounts/reset-password/confirm/")
+ self.assertEqual(len(response.context["endpoints"][1].fields), 3)
+
+ def test_index_view_with_existent_app_name(self):
+ """
+ Should load the drf docs view with all the endpoints contained in the specified app_name.
+ NOTE: Views that do **not** inherit from DRF's "APIView" are not included.
+ """
+ # Test 'organisations_app' app_name
+ response = self.client.get(reverse('drfdocs-filter', args=['organisations_app']))
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 4)
+ parents_name = [e.name_parent for e in response.context["endpoints"]]
+ self.assertEquals(parents_name.count('organisations'), 3)
+ self.assertEquals(parents_name.count('members'), 1)
+
+ def test_index_search_with_existent_app_name(self):
+ response = self.client.get("%s?search=create" % reverse('drfdocs-filter', args=['organisations_app']))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.context["endpoints"]), 1)
+ self.assertEqual(response.context["endpoints"][0].path, "/organisations/create/")
+ self.assertEqual(len(response.context["endpoints"][0].fields), 2)
+
+ def test_index_view_with_non_existent_namespace_or_app_name(self):
+ """
+ Should raise a 404 if there is no such app name like the param.
+ """
+ response = self.client.get(reverse('drfdocs-filter', args=['non-existent-ns-or-app-name']))
+ self.assertEqual(response.status_code, 404)
diff --git a/tests/urls.py b/tests/urls.py
index b226620..24b7282 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import, division, print_function
+import django
from django.conf.urls import include, url
from django.contrib import admin
from tests import views
@@ -17,19 +18,36 @@
organisations_urls = [
url(r'^create/$', view=views.CreateOrganisationView.as_view(), name="create"),
- url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"),
url(r'^(?P[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave"),
url(r'^(?P[\w-]+)/errored/$', view=views.OrganisationErroredView.as_view(), name="errored")
]
+members_urls = [
+ url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"),
+]
+
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^docs/', include('rest_framework_docs.urls')),
# API
url(r'^accounts/', view=include(accounts_urls, namespace='accounts')),
- url(r'^organisations/', view=include(organisations_urls, namespace='organisations')),
-
# Endpoints without parents/namespaces
url(r'^another-login/$', views.LoginView.as_view(), name="login"),
]
+
+# Django 1.9 Support for the app_name argument is deprecated
+# https://docs.djangoproject.com/en/1.9/ref/urls/#include
+django_version = django.VERSION
+if django.VERSION[:2] >= (1, 9, ):
+ organisations_urls = (organisations_urls, 'organisations_app', )
+ members_urls = (members_urls, 'organisations_app', )
+ urlpatterns.extend([
+ url(r'^organisations/', view=include(organisations_urls, namespace='organisations')),
+ url(r'^members/', view=include(members_urls, namespace='members')),
+ ])
+else:
+ urlpatterns.extend([
+ url(r'^organisations/', view=include(organisations_urls, namespace='organisations', app_name='organisations_app')),
+ url(r'^members/', view=include(members_urls, namespace='members', app_name='organisations_app')),
+ ])