diff --git a/demo/project/organisations/urls.py b/demo/project/organisations/urls.py index 423e04d..ac21289 100644 --- a/demo/project/organisations/urls.py +++ b/demo/project/organisations/urls.py @@ -1,11 +1,20 @@ +import django from django.conf.urls import url from project.organisations import views -urlpatterns = [ - +organisations_urlpatterns = [ 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") +] +members_urlpatterns = [ + url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"), ] + +# 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_urlpatterns = (organisations_urlpatterns, 'organisations_app', ) + members_urlpatterns = (members_urlpatterns, 'organisations_app', ) diff --git a/demo/project/settings.py b/demo/project/settings.py index 5e06207..0c33d3d 100644 --- a/demo/project/settings.py +++ b/demo/project/settings.py @@ -43,7 +43,6 @@ 'project.accounts', 'project.organisations', - ) MIDDLEWARE_CLASSES = ( diff --git a/demo/project/urls.py b/demo/project/urls.py index d8e049f..8cae347 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -13,8 +13,10 @@ 1. Add an import: from blog import urls as blog_urls 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) """ +import django from django.conf.urls import include, url from django.contrib import admin +from .organisations.urls import organisations_urlpatterns, members_urlpatterns urlpatterns = [ url(r'^admin/', include(admin.site.urls)), @@ -22,5 +24,18 @@ # API url(r'^accounts/', view=include('project.accounts.urls', namespace='accounts')), - url(r'^organisations/', view=include('project.organisations.urls', namespace='organisations')), ] + +# 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, ): + urlpatterns.extend([ + url(r'^organisations/', view=include(organisations_urlpatterns, namespace='organisations')), + url(r'^members/', view=include(members_urlpatterns, namespace='members')), + ]) +else: + urlpatterns.extend([ + url(r'^organisations/', view=include(organisations_urlpatterns, namespace='organisations', app_name='organisations_app')), + url(r'^members/', view=include(members_urlpatterns, namespace='members', app_name='organisations_app')), + ]) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index 03f374b..3673228 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,29 @@ class ApiDocumentation(object): - def __init__(self): + def __init__(self, filter_param=None): + """ + :param filter_param: 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_param=filter_param) else: - self.get_all_view_names(root_urlconf.urlpatterns) + self.get_all_view_names(root_urlconf.urlpatterns, filter_param=filter_param) - def get_all_view_names(self, urlpatterns, parent_pattern=None): + def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_param=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_param or filter_param in [pattern.app_name, pattern.namespace]): + self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern, filter_param=filter_param) elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern): - api_endpoint = ApiEndpoint(pattern, parent_pattern) - self.endpoints.append(api_endpoint) + if not filter_param or (parent_pattern and filter_param 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 return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView) def get_endpoints(self): - return self.endpoints + 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..42af2b3 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,20 @@ 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 = (self.name_parent, self.name, slugify(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 +30,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/base.html b/rest_framework_docs/templates/rest_framework_docs/base.html index 7e8b757..3d642d0 100644 --- a/rest_framework_docs/templates/rest_framework_docs/base.html +++ b/rest_framework_docs/templates/rest_framework_docs/base.html @@ -54,12 +54,12 @@ - {% block jumbotron %} -
-

DRF Docs

-

Document Web APIs made with Django REST Framework.

-
- {% endblock %} + + + + + + {% block content %}{% endblock %} diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index 76d783d..e63d4ac 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -1,26 +1,29 @@ {% extends "rest_framework_docs/docs.html" %} {% block apps_menu %} -{% regroup endpoints by name_parent as endpoints_grouped %} - +{% regroup endpoints by labels as endpoints_grouped %} +{% if endpoints_grouped|length > 1 %} + +{% 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}}

+

+ {% if group.grouper.parent %} + {{ group.grouper.name }} + {% endif %} +

diff --git a/rest_framework_docs/urls.py b/rest_framework_docs/urls.py index beb1588..5bcfc8b 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -4,4 +4,6 @@ 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 3d8805a..8de0518 100644 --- a/rest_framework_docs/views.py +++ b/rest_framework_docs/views.py @@ -8,13 +8,13 @@ class DRFDocsView(TemplateView): template_name = "rest_framework_docs/home.html" - def get_context_data(self, **kwargs): + def get_context_data(self, filter_param=None, **kwargs): settings = DRFSettings().settings if settings["HIDDEN"]: raise Http404("Django Rest Framework Docs are hidden. Check your settings.") context = super(DRFDocsView, self).get_context_data(**kwargs) - docs = ApiDocumentation() + docs = ApiDocumentation(filter_param=filter_param) endpoints = docs.get_endpoints() query = self.request.GET.get("search", "") 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 e9ba17a..7b63b34 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -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'])) + 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 load the drf docs view with no endpoint. + """ + response = self.client.get(reverse('drfdocs-filter', args=['non-existent-ns-or-app-name'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context["endpoints"]), 0) 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')), + ])