From d036de034e9ace7e66a4fb76f678faa5c7fd3019 Mon Sep 17 00:00:00 2001 From: Maxence Date: Tue, 22 Dec 2015 17:24:50 +0100 Subject: [PATCH 01/11] (Not tested yet) Added the possibility to filter by application name in the url + modifed the name_parent's value of the ApiEndpoint --- rest_framework_docs/api_docs.py | 20 ++++++++++--------- rest_framework_docs/api_endpoint.py | 12 ++++++++--- .../templates/rest_framework_docs/base.html | 12 +++++------ rest_framework_docs/urls.py | 1 + rest_framework_docs/views.py | 4 ++-- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index 6e9028a..56382c1 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,27 +7,28 @@ class ApiDocumentation(object): - def __init__(self): + def __init__(self, app_name=None): 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, app_name=app_name) else: - self.get_all_view_names(root_urlconf.urlpatterns) + self.get_all_view_names(root_urlconf.urlpatterns, app_name=app_name) - def get_all_view_names(self, urlpatterns, parent_pattern=None): + def get_all_view_names(self, urlpatterns, parent_pattern=None, app_name=None): for pattern in urlpatterns: - if isinstance(pattern, RegexURLResolver): + if isinstance(pattern, RegexURLResolver) and (not app_name or app_name == pattern.app_name): self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern) elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern): - api_endpoint = ApiEndpoint(pattern, parent_pattern) - self.endpoints.append(api_endpoint) + if not app_name or getattr(parent_pattern, 'app_name', None) == app_name: + 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 - if (hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView)): + if hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView): return True return False def get_endpoints(self): - return self.endpoints + return sorted(self.endpoints, key=attrgetter('name_parent')) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 60cc763..3e0ceba 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -1,5 +1,6 @@ import inspect from django.contrib.admindocs.views import simplify_regex +from rest_framework.viewsets import ModelViewSet class ApiEndpoint(object): @@ -9,16 +10,21 @@ def __init__(self, pattern, parent_pattern=None): 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('/', '-') + if hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, ModelViewSet): + self.name_parent = '{} (REST)'.format(self.name_parent) + else: + self.name_parent = '' 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__() 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 71a7f61..c9a8b9b 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/urls.py b/rest_framework_docs/urls.py index beb1588..3e7d7f5 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -4,4 +4,5 @@ urlpatterns = [ # Url to view the API Docs url(r'^$', DRFDocsView.as_view(), name='drfdocs'), + url(r'^(?P\w+)/$', DRFDocsView.as_view(), name='drfdocs-ns'), ] diff --git a/rest_framework_docs/views.py b/rest_framework_docs/views.py index 3d8805a..ef99d91 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, app_name=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(app_name=app_name) endpoints = docs.get_endpoints() query = self.request.GET.get("search", "") From deb94461af2d2030d8d5bb56c9faa0637d7d4e75 Mon Sep 17 00:00:00 2001 From: Maxence Date: Mon, 4 Jan 2016 17:01:00 +0100 Subject: [PATCH 02/11] Add app_name in urls + tests --- demo/project/urls.py | 5 +-- rest_framework_docs/api_endpoint.py | 3 +- tests/tests.py | 55 ++++++++++++++++++++++++----- tests/urls.py | 4 +-- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/demo/project/urls.py b/demo/project/urls.py index d8e049f..bf25fbd 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -21,6 +21,7 @@ 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')), + url(r'^accounts/', view=include('project.accounts.urls', namespace='accounts', app_name='accounts')), + url(r'^organisations/', view=include('project.organisations.urls', namespace='organisations', + app_name='organisations')), ] diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 7cd51a9..07a7737 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -9,13 +9,12 @@ 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__() if parent_pattern: self.name_parent = parent_pattern.namespace or parent_pattern.app_name or \ simplify_regex(parent_pattern.regex.pattern).replace('/', '-') if hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, ModelViewSet): - self.name_parent = '{} (REST)'.format(self.name_parent) + self.name_parent = '%s (REST)' % self.name_parent else: self.name_parent = '' self.path = self.__get_path__(parent_pattern) diff --git a/tests/tests.py b/tests/tests.py index e9ba17a..bd644e9 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,42 @@ 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_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 'accounts' app_name + response = self.client.get(reverse('drfdocs-ns', 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' app_name + response = self.client.get(reverse('drfdocs-ns', args=['organisations'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context["endpoints"]), 4) + + # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error. + self.assertEqual(str(response.context["endpoints"][3].errors), "'test_value'") + + def test_index_search_with_existent_app_name(self): + response = self.client.get("%s?search=reset-password" % reverse('drfdocs-ns', 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_non_existent_app_name(self): + """ + Should load the drf docs view with no endpoint. + """ + response = self.client.get(reverse('drfdocs-ns', args=['non_existent_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..1efb9f1 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -27,8 +27,8 @@ 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')), + url(r'^accounts/', view=include(accounts_urls, namespace='accounts', app_name='accounts')), + url(r'^organisations/', view=include(organisations_urls, namespace='organisations', app_name='organisations')), # Endpoints without parents/namespaces url(r'^another-login/$', views.LoginView.as_view(), name="login"), From e2744a0f65fa0ee24e2e60a2de87ade592159a65 Mon Sep 17 00:00:00 2001 From: Maxence Date: Tue, 5 Jan 2016 15:25:40 +0100 Subject: [PATCH 03/11] - Show the "Jump to" dropdown only if there is more than 1 value - Add link to the ''/docs/[filter_name]'' for each group.grouper (name_parent) - Parameter in the 'docs/filter_name' now works with app_name or namespace - WARNING: Modify the urlpatterns for django version >= 1.9 (see deprecated use of app_name : https://docs.djangoproject.com/en/1.9/ref/urls/#include) --- demo/project/organisations/urls.py | 15 ++++-- demo/project/urls.py | 20 ++++++-- rest_framework_docs/api_docs.py | 17 ++++--- .../templates/rest_framework_docs/home.html | 27 +++++----- rest_framework_docs/urls.py | 3 +- rest_framework_docs/views.py | 4 +- tests/tests.py | 50 ++++++++++++++----- tests/urls.py | 26 ++++++++-- 8 files changed, 118 insertions(+), 44 deletions(-) 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/urls.py b/demo/project/urls.py index bf25fbd..8cae347 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -13,15 +13,29 @@ 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)), url(r'^docs/', include('rest_framework_docs.urls')), # API - url(r'^accounts/', view=include('project.accounts.urls', namespace='accounts', app_name='accounts')), - url(r'^organisations/', view=include('project.organisations.urls', namespace='organisations', - app_name='organisations')), + url(r'^accounts/', view=include('project.accounts.urls', namespace='accounts')), ] + +# 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 f7c342d..2baf533 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -7,20 +7,23 @@ class ApiDocumentation(object): - def __init__(self, app_name=None): + 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, app_name=app_name) + self.get_all_view_names(root_urlconf.urls.urlpatterns, filter_param=filter_param) else: - self.get_all_view_names(root_urlconf.urlpatterns, app_name=app_name) + self.get_all_view_names(root_urlconf.urlpatterns, filter_param=filter_param) - def get_all_view_names(self, urlpatterns, parent_pattern=None, app_name=None): + def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_param=None): for pattern in urlpatterns: - if isinstance(pattern, RegexURLResolver) and (not app_name or app_name == pattern.app_name): - 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): - if not app_name or getattr(parent_pattern, 'app_name', None) == app_name: + 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) diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index 76d783d..f22973a 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -2,25 +2,28 @@ {% block apps_menu %} {% regroup endpoints by name_parent as endpoints_grouped %} - +{% if endpoints_grouped|length > 1 %} + +{% endif %} {% endblock %} {% block content %} - {% regroup endpoints by name_parent as endpoints_grouped %} - {% if endpoints_grouped %} {% for group in endpoints_grouped %} - -

{{group.grouper}}

+

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

diff --git a/rest_framework_docs/urls.py b/rest_framework_docs/urls.py index 3e7d7f5..795bedb 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -4,5 +4,6 @@ urlpatterns = [ # Url to view the API Docs url(r'^$', DRFDocsView.as_view(), name='drfdocs'), - url(r'^(?P\w+)/$', DRFDocsView.as_view(), name='drfdocs-ns'), + # 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 ef99d91..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, app_name=None, **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(app_name=app_name) + docs = ApiDocumentation(filter_param=filter_param) endpoints = docs.get_endpoints() query = self.request.GET.get("search", "") diff --git a/tests/tests.py b/tests/tests.py index bd644e9..b84151b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -60,13 +60,13 @@ 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_app_name(self): + def test_index_view_with_existent_namespace(self): """ - Should load the drf docs view with all the endpoints contained in the specified app_name. + 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' app_name - response = self.client.get(reverse('drfdocs-ns', args=['accounts'])) + # 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) @@ -75,26 +75,52 @@ def test_index_view_with_existent_app_name(self): self.assertEqual(response.context["endpoints"][0].allowed_methods, ['POST', 'OPTIONS']) self.assertEqual(response.context["endpoints"][0].path, "/accounts/login/") - # Test 'organisations' app_name - response = self.client.get(reverse('drfdocs-ns', args=['organisations'])) + # Test 'organisations' namespace + response = self.client.get(reverse('drfdocs-filter', args=['organisations'])) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context["endpoints"]), 4) + self.assertEqual(len(response.context["endpoints"]), 3) # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error. - self.assertEqual(str(response.context["endpoints"][3].errors), "'test_value'") + self.assertEqual(str(response.context["endpoints"][2].errors), "'test_value'") - def test_index_search_with_existent_app_name(self): - response = self.client.get("%s?search=reset-password" % reverse('drfdocs-ns', args=['accounts'])) + # 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_non_existent_app_name(self): + 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-ns', args=['non_existent_app_name'])) + 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 1efb9f1..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', app_name='accounts')), - url(r'^organisations/', view=include(organisations_urls, namespace='organisations', app_name='organisations')), - + url(r'^accounts/', view=include(accounts_urls, namespace='accounts')), # 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')), + ]) From b774268b97daa7c28d7a5e59a9a8d19ecc306580 Mon Sep 17 00:00:00 2001 From: Maxence Date: Tue, 5 Jan 2016 16:08:08 +0100 Subject: [PATCH 04/11] Add '-' to the url patten --- demo/project/settings.py | 1 - rest_framework_docs/urls.py | 2 +- runtests.py | 1 + tests/tests.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) 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/rest_framework_docs/urls.py b/rest_framework_docs/urls.py index 795bedb..5bcfc8b 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -5,5 +5,5 @@ # 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'), + url(r'^(?P[\w-]+)/$', DRFDocsView.as_view(), name='drfdocs-filter'), ] 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 b84151b..7b63b34 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -121,6 +121,6 @@ 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'])) + 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) From e866a414b4fd1e2bb3eb6157fc0a15e3336cf9ae Mon Sep 17 00:00:00 2001 From: Maxence Date: Tue, 5 Jan 2016 17:26:40 +0100 Subject: [PATCH 05/11] Use the endpoint's namespace into the group url --- rest_framework_docs/api_docs.py | 2 +- rest_framework_docs/api_endpoint.py | 6 +++++- .../templates/rest_framework_docs/home.html | 12 ++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index 2baf533..3673228 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -32,4 +32,4 @@ def _is_drf_view(self, pattern): return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView) def get_endpoints(self): - return sorted(self.endpoints, key=attrgetter('name_parent')) + return sorted(self.endpoints, key=attrgetter('name')) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 07a7737..42af2b3 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -13,10 +13,14 @@ def __init__(self, pattern, parent_pattern=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_parent = '%s (REST)' % self.name_parent + 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.errors = None diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index f22973a..e63d4ac 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -1,13 +1,13 @@ {% 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 %} @@ -16,12 +16,12 @@ {% 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 %} -

- {% if group.grouper %} - {{group.grouper}} +

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

From b7ae77208717225ac8a1637fac5e3257622ef565 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Tue, 5 Jan 2016 21:58:11 +0000 Subject: [PATCH 06/11] Put jumbotron back --- .../templates/rest_framework_docs/base.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework_docs/templates/rest_framework_docs/base.html b/rest_framework_docs/templates/rest_framework_docs/base.html index 3d642d0..7e8b757 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 %} From c3e14a6ad81c68c3cedfee177a565f6ee1fb5bdd Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 6 Jan 2016 22:07:01 +0000 Subject: [PATCH 07/11] Revert `demo` app --- demo/project/organisations/urls.py | 15 +++------------ demo/project/settings.py | 4 +--- demo/project/urls.py | 18 +----------------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/demo/project/organisations/urls.py b/demo/project/organisations/urls.py index ac21289..423e04d 100644 --- a/demo/project/organisations/urls.py +++ b/demo/project/organisations/urls.py @@ -1,20 +1,11 @@ -import django from django.conf.urls import url from project.organisations import views -organisations_urlpatterns = [ +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 0c33d3d..24ebdbe 100644 --- a/demo/project/settings.py +++ b/demo/project/settings.py @@ -1,11 +1,8 @@ """ Django settings for demo project. - Generated by 'django-admin startproject' using Django 1.8.7. - For more information on this file, see https://docs.djangoproject.com/en/1.8/topics/settings/ - For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ @@ -43,6 +40,7 @@ 'project.accounts', 'project.organisations', + ) MIDDLEWARE_CLASSES = ( diff --git a/demo/project/urls.py b/demo/project/urls.py index 8cae347..be4a8b9 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -1,5 +1,4 @@ """demo URL Configuration - The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.8/topics/http/urls/ Examples: @@ -13,10 +12,8 @@ 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)), @@ -24,18 +21,5 @@ # 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')), - ]) From 94ed9040ab8b82703b174e595462a6428190fc3e Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 6 Jan 2016 22:08:25 +0000 Subject: [PATCH 08/11] Breaklines --- demo/project/settings.py | 3 +++ demo/project/urls.py | 1 + 2 files changed, 4 insertions(+) diff --git a/demo/project/settings.py b/demo/project/settings.py index 24ebdbe..5e06207 100644 --- a/demo/project/settings.py +++ b/demo/project/settings.py @@ -1,8 +1,11 @@ """ Django settings for demo project. + Generated by 'django-admin startproject' using Django 1.8.7. + For more information on this file, see https://docs.djangoproject.com/en/1.8/topics/settings/ + For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ diff --git a/demo/project/urls.py b/demo/project/urls.py index be4a8b9..d8e049f 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -1,4 +1,5 @@ """demo URL Configuration + The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.8/topics/http/urls/ Examples: From 18205bf288e89ba3136d75ea8b284e1fd97f78ea Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Wed, 6 Jan 2016 22:36:50 +0000 Subject: [PATCH 09/11] Refactor docs view --- rest_framework_docs/views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rest_framework_docs/views.py b/rest_framework_docs/views.py index 9d79457..cf98cb5 100644 --- a/rest_framework_docs/views.py +++ b/rest_framework_docs/views.py @@ -8,19 +8,24 @@ class DRFDocsView(TemplateView): template_name = "rest_framework_docs/home.html" - def get_context_data(self, filter_param=None, **kwargs): + def get_context_data(self, **kwargs): settings = DRFSettings().settings + search_query = self.request.GET.get("search", "") + filter_param = self.kwargs.get("filter_param", 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(filter_param=filter_param) 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_param and not endpoints: + raise Http404("The are no endpoints for \"%s\"." % filter_param) - 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 From 4c323d22b3edd20fc023e35ba9035dc5a33a2047 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Sat, 23 Jan 2016 20:42:48 +0000 Subject: [PATCH 10/11] Rename URL parameter --- rest_framework_docs/api_docs.py | 23 ++++++++++++++--------- rest_framework_docs/api_endpoint.py | 3 ++- rest_framework_docs/urls.py | 3 ++- rest_framework_docs/views.py | 8 ++++---- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index 3673228..c00804f 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -7,28 +7,33 @@ class ApiDocumentation(object): - def __init__(self, filter_param=None): + def __init__(self, parent_app=None): """ - :param filter_param: namespace or app_name + parent_app: namespace or app_name """ + + self.parent_app = parent_app self.endpoints = [] root_urlconf = __import__(settings.ROOT_URLCONF) if hasattr(root_urlconf, 'urls'): - self.get_all_view_names(root_urlconf.urls.urlpatterns, filter_param=filter_param) + self.get_all_view_names(root_urlconf.urls.urlpatterns) else: - self.get_all_view_names(root_urlconf.urlpatterns, filter_param=filter_param) + self.get_all_view_names(root_urlconf.urlpatterns) - def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_param=None): + def get_all_view_names(self, urlpatterns, parent_pattern=None): for pattern in urlpatterns: - 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) + if isinstance(pattern, RegexURLResolver) and (self.parent_app in [pattern.app_name, pattern.namespace]): + self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern) + elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern): - if not filter_param or (parent_pattern and filter_param in [parent_pattern.app_name, parent_pattern.namespace]): + if not self.parent_app or (parent_pattern and self.parent_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): diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 42af2b3..9e90d49 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -10,6 +10,7 @@ def __init__(self, pattern, parent_pattern=None): self.pattern = pattern self.callback = pattern.callback self.docstring = self.__get_docstring__() + if parent_pattern: self.name_parent = parent_pattern.namespace or parent_pattern.app_name or \ simplify_regex(parent_pattern.regex.pattern).replace('/', '-') @@ -19,7 +20,7 @@ def __init__(self, pattern, parent_pattern=None): 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__() diff --git a/rest_framework_docs/urls.py b/rest_framework_docs/urls.py index 5bcfc8b..9a0249b 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -4,6 +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'), + 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 cf98cb5..ddb5921 100644 --- a/rest_framework_docs/views.py +++ b/rest_framework_docs/views.py @@ -11,16 +11,16 @@ class DRFDocsView(TemplateView): def get_context_data(self, **kwargs): settings = DRFSettings().settings search_query = self.request.GET.get("search", "") - filter_param = self.kwargs.get("filter_param", None) + parent_app = self.kwargs.get("parent_app", None) if settings["HIDE_DOCS"]: raise Http404("Django Rest Framework Docs are hidden. Check your settings.") - docs = ApiDocumentation(filter_param=filter_param) + docs = ApiDocumentation(parent_app=parent_app) endpoints = docs.get_endpoints() - if filter_param and not endpoints: - raise Http404("The are no endpoints for \"%s\"." % filter_param) + if parent_app and not endpoints: + raise Http404("The are no endpoints for \"%s\"." % parent_app) if search_query and endpoints: endpoints = [endpoint for endpoint in endpoints if search_query in endpoint.path] From 4d3513d8e4bed6f1cb841119291f907368541bfa Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Sat, 23 Jan 2016 21:02:24 +0000 Subject: [PATCH 11/11] Fix tests --- rest_framework_docs/api_docs.py | 22 +-- .../templates/rest_framework_docs/home.html | 138 +++++++++--------- rest_framework_docs/urls.py | 2 +- rest_framework_docs/views.py | 8 +- tests/tests.py | 8 +- 5 files changed, 90 insertions(+), 88 deletions(-) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index c00804f..2558ae9 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -7,26 +7,23 @@ class ApiDocumentation(object): - def __init__(self, parent_app=None): + def __init__(self, filter_app=None): """ - parent_app: namespace or app_name + :param filter_app: namespace or app_name """ - - self.parent_app = parent_app 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) and (self.parent_app in [pattern.app_name, pattern.namespace]): - 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): - if not self.parent_app or (parent_pattern and self.parent_app in [parent_pattern.app_name, parent_pattern.namespace]): + 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) @@ -37,4 +34,7 @@ def _is_drf_view(self, pattern): return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView) def get_endpoints(self): + """ + Returns the endpoints sorted by the app name + """ return sorted(self.endpoints, key=attrgetter('name')) diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index e63d4ac..597ca1f 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -1,89 +1,91 @@ {% extends "rest_framework_docs/docs.html" %} {% block apps_menu %} -{% regroup endpoints by labels as endpoints_grouped %} -{% if endpoints_grouped|length > 1 %} - -{% endif %} + {% regroup endpoints by labels as endpoints_grouped %} + + {% if endpoints_grouped|length > 1 %} + + {% endif %} {% endblock %} {% block content %} {% regroup endpoints by labels as endpoints_grouped %} - {% if endpoints_grouped %} - {% for group in endpoints_grouped %} -

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

-
- - {% for endpoint in group.list %} - -
- -