From d036de034e9ace7e66a4fb76f678faa5c7fd3019 Mon Sep 17 00:00:00 2001 From: Maxence Date: Tue, 22 Dec 2015 17:24:50 +0100 Subject: [PATCH 01/21] (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/21] 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/21] - 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/21] 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/21] 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 2a56a35e9620f60fb6c631752581feeb8cc27c1d Mon Sep 17 00:00:00 2001 From: Maxence Date: Wed, 20 Jan 2016 11:24:13 +0100 Subject: [PATCH 06/21] Manage nested serializer and list fields/serializers in Endpoint fields --- demo/project/accounts/serializers.py | 10 ++++++++ demo/project/accounts/views.py | 5 ++-- rest_framework_docs/api_endpoint.py | 25 +++++++++++++++---- .../templates/rest_framework_docs/fields.html | 14 +++++++++++ .../templates/rest_framework_docs/home.html | 9 +++---- 5 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 rest_framework_docs/templates/rest_framework_docs/fields.html diff --git a/demo/project/accounts/serializers.py b/demo/project/accounts/serializers.py index e4b4cb8..067cf64 100644 --- a/demo/project/accounts/serializers.py +++ b/demo/project/accounts/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers from project.accounts.models import User +from rest_framework.authtoken.serializers import AuthTokenSerializer class UserRegistrationSerializer(serializers.ModelSerializer): @@ -30,3 +31,12 @@ class Meta: model = User fields = ('id', 'token', 'password',) extra_kwargs = {'password': {'write_only': True}} + + +class NestedSerializer(serializers.Serializer): + nb_test = serializers.IntegerField(default=0, required=False) + liste_codes = serializers.ListField(child=serializers.CharField()) + + +class CustomAuthTokenSerializer(AuthTokenSerializer): + nested = NestedSerializer(many=True) diff --git a/demo/project/accounts/views.py b/demo/project/accounts/views.py index e1bd9c0..98531ae 100644 --- a/demo/project/accounts/views.py +++ b/demo/project/accounts/views.py @@ -2,13 +2,12 @@ from django.views.generic.base import TemplateView from rest_framework import parsers, renderers, generics, status from rest_framework.authtoken.models import Token -from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from project.accounts.models import User from project.accounts.serializers import ( - UserRegistrationSerializer, UserProfileSerializer, ResetPasswordSerializer + UserRegistrationSerializer, UserProfileSerializer, ResetPasswordSerializer, CustomAuthTokenSerializer ) @@ -28,7 +27,7 @@ class LoginView(APIView): permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) renderer_classes = (renderers.JSONRenderer,) - serializer_class = AuthTokenSerializer + serializer_class = CustomAuthTokenSerializer def post(self, request): serializer = self.serializer_class(data=request.data) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 42af2b3..c60e4dd 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 import serializers from rest_framework.viewsets import ModelViewSet @@ -50,11 +51,7 @@ def __get_serializer_fields__(self): serializer = self.callback.cls.serializer_class if hasattr(serializer, 'get_fields'): try: - fields = [{ - "name": key, - "type": str(field.__class__.__name__), - "required": field.required - } for key, field in serializer().get_fields().items()] + fields = self.__get_fields__(serializer) except KeyError as e: self.errors = e fields = [] @@ -64,6 +61,24 @@ def __get_serializer_fields__(self): return fields + def __get_fields__(self, serializer): + fields = [] + for key, field in serializer().get_fields().items(): + item = dict( + name=key, + type=str(field.__class__.__name__), + required=field.required + ) + if isinstance(field, (serializers.ListSerializer, serializers.ListField)): + sub_type = field.child.__class__ + item['sub_type'] = str(sub_type.__name__) + if isinstance(sub_type(), serializers.Serializer): + item['fields'] = self.__get_fields__(sub_type) + elif isinstance(field, serializers.Serializer): + item['fields'] = self.__get_fields__(field) + fields.append(item) + return fields + def __get_serializer_fields_json__(self): # FIXME: # Return JSON or not? diff --git a/rest_framework_docs/templates/rest_framework_docs/fields.html b/rest_framework_docs/templates/rest_framework_docs/fields.html new file mode 100644 index 0000000..b3c0399 --- /dev/null +++ b/rest_framework_docs/templates/rest_framework_docs/fields.html @@ -0,0 +1,14 @@ +
    + {% for field in item.fields %} +
  • + {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %} + {% if field.sub_type %} ({{ field.sub_type }}) {% endif %} + {% if field.fields %} + {% with field as item %} + {% include 'rest_framework_docs/fields.html' %} + {% endwith %} + {% endif %} +
  • + {% endfor %} +
diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index e63d4ac..d3ec7e9 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -65,14 +65,11 @@

{% if endpoint.errors %} {% endif %} - {% if endpoint.fields %}

Fields:

-
    - {% for field in endpoint.fields %} -
  • {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %}
  • - {% endfor %} -
+ {% with endpoint as item %} + {% include 'rest_framework_docs/fields.html' %} + {% endwith %} {% elif not endpoint.errors %}

No fields.

{% endif %} From 34f9683caa9293c2d724136d1baab4150d4f66b0 Mon Sep 17 00:00:00 2001 From: Maxence Date: Wed, 20 Jan 2016 11:49:15 +0100 Subject: [PATCH 07/21] correction of recursive call --- rest_framework_docs/api_endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index c60e4dd..dfddf2f 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -75,7 +75,7 @@ def __get_fields__(self, serializer): if isinstance(sub_type(), serializers.Serializer): item['fields'] = self.__get_fields__(sub_type) elif isinstance(field, serializers.Serializer): - item['fields'] = self.__get_fields__(field) + item['fields'] = self.__get_fields__(field.__class__) fields.append(item) return fields From 5a43d4106b9c5ce026d04d33a9a41853d323d666 Mon Sep 17 00:00:00 2001 From: Maxence Date: Wed, 20 Jan 2016 17:26:16 +0100 Subject: [PATCH 08/21] Recursive include is now called with "only" Use of a dictionnary { serializer: fields } for recursive call of __get_fields__ --- rest_framework_docs/__init__.py | 2 + rest_framework_docs/api_docs.py | 5 +- rest_framework_docs/api_endpoint.py | 5 + .../templates/rest_framework_docs/fields.html | 8 +- .../templates/rest_framework_docs/home.html | 140 +++++++++--------- tests/tests.py | 4 +- 6 files changed, 87 insertions(+), 77 deletions(-) diff --git a/rest_framework_docs/__init__.py b/rest_framework_docs/__init__.py index eead319..62c78f3 100644 --- a/rest_framework_docs/__init__.py +++ b/rest_framework_docs/__init__.py @@ -1 +1,3 @@ __version__ = '0.0.5' + +SERIALIZER_FIELDS = {} diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index 3673228..4548054 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -2,6 +2,8 @@ from django.conf import settings from django.core.urlresolvers import RegexURLResolver, RegexURLPattern from rest_framework.views import APIView + +from rest_framework_docs import SERIALIZER_FIELDS from rest_framework_docs.api_endpoint import ApiEndpoint @@ -11,6 +13,7 @@ def __init__(self, filter_param=None): """ :param filter_param: namespace or app_name """ + SERIALIZER_FIELDS.clear() self.endpoints = [] root_urlconf = __import__(settings.ROOT_URLCONF) if hasattr(root_urlconf, 'urls'): @@ -32,4 +35,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')) + return sorted(self.endpoints, key=attrgetter('name', 'path')) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index dfddf2f..e9f2e10 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -3,6 +3,7 @@ from django.contrib.admindocs.views import simplify_regex from rest_framework import serializers from rest_framework.viewsets import ModelViewSet +from rest_framework_docs import SERIALIZER_FIELDS class ApiEndpoint(object): @@ -25,6 +26,7 @@ def __init__(self, pattern, parent_pattern=None): self.path = self.__get_path__(parent_pattern) self.allowed_methods = self.__get_allowed_methods__() self.errors = None + self.nb_recurse = 0 self.fields = self.__get_serializer_fields__() self.fields_json = self.__get_serializer_fields_json__() self.permissions = self.__get_permissions_class__() @@ -62,6 +64,8 @@ def __get_serializer_fields__(self): return fields def __get_fields__(self, serializer): + if serializer in SERIALIZER_FIELDS: + return SERIALIZER_FIELDS.get(serializer) fields = [] for key, field in serializer().get_fields().items(): item = dict( @@ -77,6 +81,7 @@ def __get_fields__(self, serializer): elif isinstance(field, serializers.Serializer): item['fields'] = self.__get_fields__(field.__class__) fields.append(item) + SERIALIZER_FIELDS[serializer] = fields return fields def __get_serializer_fields_json__(self): diff --git a/rest_framework_docs/templates/rest_framework_docs/fields.html b/rest_framework_docs/templates/rest_framework_docs/fields.html index b3c0399..e99251e 100644 --- a/rest_framework_docs/templates/rest_framework_docs/fields.html +++ b/rest_framework_docs/templates/rest_framework_docs/fields.html @@ -1,13 +1,11 @@
    {% for field in item.fields %}
  • - {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %} + {{ field.name }}: {{ field.type }} + {% if field.required %}R{% endif %} {% if field.sub_type %} ({{ field.sub_type }}) {% endif %} {% if field.fields %} - {% with field as item %} - {% include 'rest_framework_docs/fields.html' %} - {% endwith %} + {% include 'rest_framework_docs/fields.html' with item=field only %} {% endif %}
  • {% endfor %} diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index d3ec7e9..63ad702 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -3,102 +3,104 @@ {% block apps_menu %} {% 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 %} -

    +{% 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 %} -
    +
    +
    {% endfor %} -
    +
    - {% endfor %} - {% elif not query %} -

    There are currently no api endpoints to document.

    - {% else %} -

    No endpoints found for {{ query }}.

    - {% endif %} +{% endfor %} +{% elif not query %} +

    There are currently no api endpoints to document.

    +{% else %} +

    No endpoints found for {{ query }}.

    +{% endif %} - - {% endblock %} diff --git a/tests/tests.py b/tests/tests.py index 7b63b34..f714b22 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -39,7 +39,7 @@ def test_index_view_with_endpoints(self): 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"][9].errors), "'test_value'") + self.assertEqual(str(response.context["endpoints"][7].errors), "'test_value'") def test_index_search_with_endpoints(self): response = self.client.get("%s?search=reset-password" % reverse("drfdocs")) @@ -81,7 +81,7 @@ def test_index_view_with_existent_namespace(self): 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'") + self.assertEqual(str(response.context["endpoints"][0].errors), "'test_value'") # Test 'members' namespace response = self.client.get(reverse('drfdocs-filter', args=['members'])) From 85ba4d2623a45a336623c6862bbaa1937704d923 Mon Sep 17 00:00:00 2001 From: Maxence Date: Thu, 21 Jan 2016 09:55:37 +0100 Subject: [PATCH 09/21] Manage allowed_methods on ModelViewSets --- rest_framework_docs/api_endpoint.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index e9f2e10..3b0807f 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -6,6 +6,12 @@ from rest_framework_docs import SERIALIZER_FIELDS +VIEWSET_METHODS = { + 'List': ['get', 'post'], + 'Instance': ['get', 'put', 'patch', 'delete'], +} + + class ApiEndpoint(object): def __init__(self, pattern, parent_pattern=None): @@ -37,7 +43,9 @@ def __get_path__(self, parent_pattern): return simplify_regex(self.pattern.regex.pattern) def __get_allowed_methods__(self): - return [m.upper() for m in self.callback.cls.http_method_names if hasattr(self.callback.cls, m)] + callback_cls = self.callback.cls + return [m.upper() for m in callback_cls.http_method_names if hasattr(callback_cls, m) or + (issubclass(callback_cls, ModelViewSet) and m in VIEWSET_METHODS.get(self.callback.suffix))] def __get_docstring__(self): return inspect.getdoc(self.callback) From a6219434dcdf4eb21941510398c8d507086a4c7c Mon Sep 17 00:00:00 2001 From: Maxence Date: Fri, 22 Jan 2016 15:28:41 +0100 Subject: [PATCH 10/21] Sort allowed_methods --- rest_framework_docs/api_endpoint.py | 4 ++-- tests/tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 3b0807f..de946f8 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -44,8 +44,8 @@ def __get_path__(self, parent_pattern): def __get_allowed_methods__(self): callback_cls = self.callback.cls - return [m.upper() for m in callback_cls.http_method_names if hasattr(callback_cls, m) or - (issubclass(callback_cls, ModelViewSet) and m in VIEWSET_METHODS.get(self.callback.suffix))] + return sorted([m.upper() for m in callback_cls.http_method_names if hasattr(callback_cls, m) or + (issubclass(callback_cls, ModelViewSet) and m in VIEWSET_METHODS.get(self.callback.suffix))]) def __get_docstring__(self): return inspect.getdoc(self.callback) diff --git a/tests/tests.py b/tests/tests.py index f714b22..8d5011c 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -31,7 +31,7 @@ def test_index_view_with_endpoints(self): # Test the login view 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].allowed_methods, ['OPTIONS', 'POST']) 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) @@ -72,7 +72,7 @@ def test_index_view_with_existent_namespace(self): # 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].allowed_methods, ['OPTIONS', 'POST']) self.assertEqual(response.context["endpoints"][0].path, "/accounts/login/") # Test 'organisations' namespace From 8dab8d9acb5a415a63dd56a7fc700f82e0420c01 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 19 Feb 2016 17:13:49 +0100 Subject: [PATCH 11/21] Add line breaks on docstrings --- rest_framework_docs/templates/rest_framework_docs/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index 63ad702..546a95a 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -60,7 +60,7 @@

    {% if endpoint.docstring %} -

    {{ endpoint.docstring }}

    +

    {{ endpoint.docstring|linebreaksbr }}

    {% endif %} {% if endpoint.errors %} From 4d2a37040c7f0164c18d5473465ece86e359217e Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 29 Feb 2016 10:10:00 +0100 Subject: [PATCH 12/21] Nested/List serializers + login_required --- README.md | 3 ++- rest_framework_docs/api_docs.py | 2 ++ rest_framework_docs/api_endpoint.py | 7 ++++++- rest_framework_docs/settings.py | 3 ++- rest_framework_docs/urls.py | 14 ++++++++++++-- rest_framework_docs/views.py | 1 + tests/tests.py | 3 ++- 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7aed7c4..3c17464 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ Finally include the `rest_framework_docs` urls in your `urls.py`: You can find detailed information about the package's settings at [the docs](http://drfdocs.com/docs/settings/). REST_FRAMEWORK_DOCS = { - 'HIDE_DOCS': True # Default: False + 'HIDE_DOCS': True, # Default: False + 'LOGIN_REQUIRED': True, # Default: True } diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index e8e500a..20588c5 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -1,7 +1,9 @@ from operator import attrgetter + 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 import SERIALIZER_FIELDS from rest_framework_docs.api_endpoint import ApiEndpoint diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 9f2872a..15ce38a 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -26,7 +26,7 @@ def __init__(self, pattern, parent_pattern=None): 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 + self.name = '%s (RESTful)' % self.name_parent else: self.name_parent = '' self.name = '' @@ -77,6 +77,7 @@ def __get_serializer_fields__(self): def __get_fields__(self, serializer): if serializer in SERIALIZER_FIELDS: return SERIALIZER_FIELDS.get(serializer) + fields = [] for key, field in serializer().get_fields().items(): item = dict( @@ -84,6 +85,8 @@ def __get_fields__(self, serializer): type=str(field.__class__.__name__), required=field.required ) + + # Nested/List serializer if isinstance(field, (serializers.ListSerializer, serializers.ListField)): sub_type = field.child.__class__ item['sub_type'] = str(sub_type.__name__) @@ -92,6 +95,8 @@ def __get_fields__(self, serializer): elif isinstance(field, serializers.Serializer): item['fields'] = self.__get_fields__(field.__class__) fields.append(item) + + # Keep a copy of serializer fields for optimization purposes SERIALIZER_FIELDS[serializer] = fields return fields diff --git a/rest_framework_docs/settings.py b/rest_framework_docs/settings.py index 2853a7b..c9804c0 100644 --- a/rest_framework_docs/settings.py +++ b/rest_framework_docs/settings.py @@ -5,7 +5,8 @@ class DRFSettings(object): def __init__(self): self.drf_settings = { - "HIDE_DOCS": self.get_setting("HIDE_DOCS") or False + "HIDE_DOCS": self.get_setting("HIDE_DOCS") or False, + "LOGIN_REQUIRED": self.get_setting("LOGIN_REQUIRED") or False, } def get_setting(self, name): diff --git a/rest_framework_docs/urls.py b/rest_framework_docs/urls.py index 5bcfc8b..512aa04 100644 --- a/rest_framework_docs/urls.py +++ b/rest_framework_docs/urls.py @@ -1,9 +1,19 @@ from django.conf.urls import url + +from rest_framework_docs.settings import DRFSettings from rest_framework_docs.views import DRFDocsView + +settings = DRFSettings().settings +if settings["LOGIN_REQUIRED"]: + from django.contrib.auth.decorators import login_required + docs_view = login_required(DRFDocsView.as_view()) +else: + docs_view = DRFDocsView.as_view() + urlpatterns = [ # Url to view the API Docs - url(r'^$', DRFDocsView.as_view(), name='drfdocs'), + url(r'^$', docs_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-]+)/$', docs_view, name='drfdocs-filter'), ] diff --git a/rest_framework_docs/views.py b/rest_framework_docs/views.py index 9d79457..f06bd49 100644 --- a/rest_framework_docs/views.py +++ b/rest_framework_docs/views.py @@ -1,5 +1,6 @@ from django.http import Http404 from django.views.generic.base import TemplateView + from rest_framework_docs.api_docs import ApiDocumentation from rest_framework_docs.settings import DRFSettings diff --git a/tests/tests.py b/tests/tests.py index 2e69297..344abd0 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -6,7 +6,8 @@ class DRFDocsViewTests(TestCase): SETTINGS_HIDE_DOCS = { - 'HIDE_DOCS': True # Default: False + 'HIDE_DOCS': True, # Default: False + 'LOGIN_REQUIRED': False, # Default: False } def setUp(self): From a97f94ad01b96b22c64fea273f3ef5e39ffe3cd1 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 29 Feb 2016 10:32:26 +0100 Subject: [PATCH 13/21] Re-enabling jumbotron block without content (too much space lost) --- .../templates/rest_framework_docs/base.html | 14 ++++++++------ 1 file changed, 8 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 5ebe99d..6042a5e 100644 --- a/rest_framework_docs/templates/rest_framework_docs/base.html +++ b/rest_framework_docs/templates/rest_framework_docs/base.html @@ -54,12 +54,14 @@
    - - - - - - + {% block jumbotron %} + + {% endblock %} {% block content %}{% endblock %} From 3441d6f44fd7bd8cc00eb49d127825bd55fc00b3 Mon Sep 17 00:00:00 2001 From: msaelices Date: Tue, 15 Mar 2016 01:28:46 +0100 Subject: [PATCH 14/21] Fix a TypeError fetching the allowed_methods when the ModelViewSet has no prefix in the callback (when is the class instance). --- rest_framework_docs/api_endpoint.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 15ce38a..71900c6 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -47,8 +47,10 @@ def __get_path__(self, parent_pattern): def __get_allowed_methods__(self): callback_cls = self.callback.cls - return sorted([force_str(m).upper() for m in callback_cls.http_method_names if hasattr(callback_cls, m) or - (issubclass(callback_cls, ModelViewSet) and m in VIEWSET_METHODS.get(self.callback.suffix))]) + return sorted( + [force_str(m).upper() for m in callback_cls.http_method_names + if hasattr(callback_cls, m) or (issubclass(callback_cls, ModelViewSet) + and m in VIEWSET_METHODS.get(self.callback.suffix, ''))]) def __get_docstring__(self): return inspect.getdoc(self.callback) From c6bbf509b3bc23b82fb36d8eef14d0eb7a8f9b10 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 16 Mar 2016 23:19:39 +0100 Subject: [PATCH 15/21] Fixing flake8 issues after PR #1 --- rest_framework_docs/api_endpoint.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 71900c6..73dd5fd 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -47,10 +47,13 @@ def __get_path__(self, parent_pattern): def __get_allowed_methods__(self): callback_cls = self.callback.cls - return sorted( - [force_str(m).upper() for m in callback_cls.http_method_names - if hasattr(callback_cls, m) or (issubclass(callback_cls, ModelViewSet) - and m in VIEWSET_METHODS.get(self.callback.suffix, ''))]) + + def is_method_allowed(method_name): + return hasattr(callback_cls, method_name) or ( + issubclass(callback_cls, ModelViewSet) and + method_name in VIEWSET_METHODS.get(self.callback.suffix, [])) + + return sorted([force_str(name).upper() for name in callback_cls.http_method_names if is_method_allowed(name)]) def __get_docstring__(self): return inspect.getdoc(self.callback) From d08e25b328e3d5f009e2b999d1daea682d4386dd Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 2 Jun 2016 20:25:22 +0200 Subject: [PATCH 16/21] Merge upstream/master 0.0.10 --- rest_framework_docs/api_endpoint.py | 5 ++++- tests/tests.py | 30 ++++++++++++----------------- tests/urls.py | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index acc5bc9..ddaca36 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -9,6 +9,7 @@ from rest_framework_docs import SERIALIZER_FIELDS +METHODS_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] VIEWSET_METHODS = { 'List': ['get', 'post'], 'Instance': ['get', 'put', 'patch', 'delete'], @@ -52,7 +53,9 @@ def is_method_allowed(method_name): issubclass(callback_cls, ModelViewSet) and method_name in VIEWSET_METHODS.get(self.callback.suffix, [])) - return sorted([force_str(name).upper() for name in callback_cls.http_method_names if is_method_allowed(name)]) + return sorted( + [force_str(name).upper() for name in callback_cls.http_method_names if is_method_allowed(name)], + key=lambda e: METHODS_ORDER.index(e)) def __get_docstring__(self): return inspect.getdoc(self.callback) diff --git a/tests/tests.py b/tests/tests.py index 2442720..7ba3557 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -31,24 +31,18 @@ def test_index_view_with_endpoints(self): self.assertEqual(len(response.context["endpoints"]), 11) # 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/login2/") - self.assertEqual(response.context["endpoints"][1].docstring, "A view that allows users to login providing their username and password. Without serializer_class") - 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"]) + endpoint = response.context["endpoints"][1] + self.assertEqual(endpoint.name_parent, "accounts") + self.assertEqual(endpoint.allowed_methods, ['POST', 'OPTIONS']) + self.assertEqual(endpoint.path, "/accounts/login/") + self.assertEqual(endpoint.docstring, "A view that allows users to login providing their username and password.") + self.assertEqual(len(endpoint.fields), 2) + self.assertEqual(endpoint.fields[0]["type"], "CharField") + self.assertTrue(endpoint.fields[0]["required"]) # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error. - self.assertEqual(str(response.context["endpoints"][9].errors), "'test_value'") + endpoint = response.context["endpoints"][8] + self.assertEqual(str(endpoint.errors), "'test_value'") def test_index_search_with_endpoints(self): response = self.client.get("%s?search=reset-password" % reverse("drfdocs")) @@ -77,11 +71,11 @@ def test_index_view_with_existent_namespace(self): # 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) + self.assertEqual(len(response.context["endpoints"]), 6) # Test the login view self.assertEqual(response.context["endpoints"][0].name_parent, "accounts") - self.assertEqual(response.context["endpoints"][0].allowed_methods, ['OPTIONS', 'POST']) + self.assertEqual(response.context["endpoints"][0].allowed_methods, ['POST', 'OPTIONS']) self.assertEqual(response.context["endpoints"][0].path, "/accounts/login/") # Test 'organisations' namespace diff --git a/tests/urls.py b/tests/urls.py index f339c6a..8ae3556 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -32,7 +32,7 @@ url(r'^docs/', include('rest_framework_docs.urls')), # API - url(r'^accounts/', view=include(accounts_urls, namespace='accounts')), + url(r'^accounts/', view=include(accounts_urls, namespace="accounts")), # Endpoints without parents/namespaces url(r'^another-login/$', views.LoginView.as_view(), name="login"), ] From 550b372c8415ec7de47d0c7ee722995937f731b9 Mon Sep 17 00:00:00 2001 From: Maxence Date: Mon, 1 Aug 2016 17:18:18 +0200 Subject: [PATCH 17/21] Merge from upstream + allow list of drf_routers --- demo/project/accounts/urls.py | 16 ++++++-- demo/project/accounts/views.py | 6 +++ demo/project/organisations/urls.py | 11 ++++-- demo/project/organisations/views.py | 6 +++ demo/project/urls.py | 19 +++++++--- rest_framework_docs/api_docs.py | 9 +++-- rest_framework_docs/api_endpoint.py | 57 ++++++----------------------- tests/tests.py | 32 +++++++--------- tests/urls.py | 13 +++++-- 9 files changed, 86 insertions(+), 83 deletions(-) diff --git a/demo/project/accounts/urls.py b/demo/project/accounts/urls.py index 1486675..003fa41 100644 --- a/demo/project/accounts/urls.py +++ b/demo/project/accounts/urls.py @@ -1,15 +1,23 @@ +import django from django.conf.urls import url from project.accounts import views +from rest_framework.routers import SimpleRouter -urlpatterns = [ - url(r'^test/$', views.TestView.as_view(), name="test-view"), +account_router = SimpleRouter() +account_router.register('user-model-viewsets', views.UserModelViewset, base_name='account') +account_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'^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"), +] + account_router.urls -] +# 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, ): + account_urlpatterns = (account_urlpatterns, 'accounts', ) diff --git a/demo/project/accounts/views.py b/demo/project/accounts/views.py index 98531ae..0c0ad0f 100644 --- a/demo/project/accounts/views.py +++ b/demo/project/accounts/views.py @@ -5,6 +5,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, CustomAuthTokenSerializer @@ -80,3 +81,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 diff --git a/demo/project/organisations/urls.py b/demo/project/organisations/urls.py index 667f4be..bc75484 100644 --- a/demo/project/organisations/urls.py +++ b/demo/project/organisations/urls.py @@ -2,13 +2,18 @@ from django.conf.urls import url from project.organisations import views +from rest_framework.routers import SimpleRouter +from .views import OrganisationModelViewset + +organisation_router = SimpleRouter() +organisation_router.register('organisation-model-viewsets', OrganisationModelViewset, base_name='organisation') organisations_urlpatterns = [ url(r'^create/$', view=views.CreateOrganisationView.as_view(), name="create"), url(r'^(?P[\w-]+)/$', view=views.RetrieveOrganisationView.as_view(), name="organisation"), url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"), url(r'^(?P[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave") -] +] + organisation_router.urls members_urlpatterns = [ url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"), @@ -18,5 +23,5 @@ # 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', ) + organisations_urlpatterns = (organisations_urlpatterns, 'organisations', ) + members_urlpatterns = (members_urlpatterns, 'organisations', ) diff --git a/demo/project/organisations/views.py b/demo/project/organisations/views.py index 1e2d5fb..a3c8901 100644 --- a/demo/project/organisations/views.py +++ b/demo/project/organisations/views.py @@ -1,5 +1,6 @@ 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 @@ -34,3 +35,8 @@ def delete(self, request, *args, **kwargs): instance = self.get_object() self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) + + +class OrganisationModelViewset(ModelViewSet): + queryset = Organisation.objects.all() + serializer_class = OrganisationMembersSerializer diff --git a/demo/project/urls.py b/demo/project/urls.py index 8cae347..91e4e97 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -16,14 +16,12 @@ import django from django.conf.urls import include, url from django.contrib import admin -from .organisations.urls import organisations_urlpatterns, members_urlpatterns +from rest_framework_docs.views import DRFDocsView +from .accounts.urls import account_urlpatterns, account_router +from .organisations.urls import organisations_urlpatterns, members_urlpatterns, organisation_router 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')), ] # Django 1.9 Support for the app_name argument is deprecated @@ -31,11 +29,22 @@ django_version = django.VERSION if django.VERSION[:2] >= (1, 9, ): urlpatterns.extend([ + url(r'^accounts/', view=include(account_urlpatterns, namespace='accounts')), url(r'^organisations/', view=include(organisations_urlpatterns, namespace='organisations')), url(r'^members/', view=include(members_urlpatterns, namespace='members')), ]) else: urlpatterns.extend([ + url(r'^accounts/', view=include(account_urlpatterns, namespace='accounts', app_name='account_app')), 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 tests.views import LoginView +routers = [account_router, organisation_router] +urlpatterns.extend([ + url(r'^docs/(?P[\w-]+)/$', DRFDocsView.as_view(drf_router=routers), name='drfdocs-filter'), + url(r'^docs/$', DRFDocsView.as_view(drf_router=routers), name='drfdocs'), + url(r'^another-login/$', LoginView.as_view(), name="login"), +]) diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index fe14a90..bceb7d4 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -30,11 +30,12 @@ def __init__(self, drf_router=None, filter_param=None): def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_param=None): for pattern in urlpatterns: if isinstance(pattern, RegexURLResolver) and (not filter_param or filter_param in [pattern.app_name, pattern.namespace]): - parent_pattern = None if pattern._regex == "^" else pattern - self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=pattern, filter_param=filter_param) + # parent_pattern = None if pattern._regex == "^" else pattern + self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=None if pattern._regex == "^" else pattern, filter_param=filter_param) 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) - self.endpoints.append(api_endpoint) + if not filter_param or parent_pattern: + api_endpoint = ApiEndpoint(pattern, parent_pattern, self.drf_router) + self.endpoints.append(api_endpoint) def _is_drf_view(self, pattern): """ diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 6c33dfd..1d5bd95 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -2,24 +2,17 @@ import inspect from django.contrib.admindocs.views import simplify_regex from django.utils.encoding import force_str -from rest_framework.serializers import BaseSerializer - from rest_framework import serializers from rest_framework.viewsets import ModelViewSet from rest_framework_docs import SERIALIZER_FIELDS -METHODS_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] -VIEWSET_METHODS = { - 'List': ['get', 'post'], - 'Instance': ['get', 'put', 'patch', 'delete'], -} - - class ApiEndpoint(object): def __init__(self, pattern, parent_pattern=None, drf_router=None): - self.drf_router = drf_router + self.drf_router = drf_router or [] + if not isinstance(self.drf_router, (list, tuple)): + self.drf_router = [self.drf_router] self.pattern = pattern self.callback = pattern.callback self.docstring = self.__get_docstring__() @@ -40,9 +33,8 @@ def __init__(self, pattern, parent_pattern=None, drf_router=None): self.serializer_class = self.__get_serializer_class__() if self.serializer_class: self.serializer = self.__get_serializer__() - self.fields = self.__get_serializer_fields__(self.serializer) + self.fields = self.__get_serializer_fields__() self.fields_json = self.__get_serializer_fields_json__() - self.permissions = self.__get_permissions_class__() def __get_path__(self, parent_pattern): @@ -51,30 +43,20 @@ def __get_path__(self, parent_pattern): return simplify_regex(self.pattern.regex.pattern) def __get_allowed_methods__(self): - callback_cls = self.callback.cls - - def is_method_allowed(method_name): - return hasattr(callback_cls, method_name) or ( - issubclass(callback_cls, ModelViewSet) and - method_name in VIEWSET_METHODS.get(self.callback.suffix, [])) - - return sorted( - [force_str(name).upper() for name in callback_cls.http_method_names if is_method_allowed(name)], - key=lambda e: METHODS_ORDER.index(e)) viewset_methods = [] - if self.drf_router: - for prefix, viewset, basename in self.drf_router.registry: + for router in self.drf_router: + for prefix, viewset, basename in router.registry: if self.callback.cls != viewset: continue - lookup = self.drf_router.get_lookup_regex(viewset) - routes = self.drf_router.get_routes(viewset) + lookup = router.get_lookup_regex(viewset) + routes = router.get_routes(viewset) for route in routes: # Only actions which actually exist on the viewset will be bound - mapping = self.drf_router.get_method_map(viewset, route.mapping) + mapping = router.get_method_map(viewset, route.mapping) if not mapping: continue @@ -82,7 +64,7 @@ def is_method_allowed(method_name): regex = route.url.format( prefix=prefix, lookup=lookup, - trailing_slash=self.drf_router.trailing_slash + trailing_slash=router.trailing_slash ) if self.pattern.regex.pattern == regex: funcs, viewset_methods = zip( @@ -92,7 +74,8 @@ def is_method_allowed(method_name): if len(set(funcs)) == 1: self.docstring = inspect.getdoc(getattr(self.callback.cls, funcs[0])) - view_methods = [force_str(m).upper() for m in self.callback.cls.http_method_names if hasattr(self.callback.cls, m)] + view_methods = [force_str(m).upper() for m in self.callback.cls.http_method_names if + hasattr(self.callback.cls, m)] return viewset_methods + view_methods def __get_docstring__(self): @@ -102,22 +85,6 @@ def __get_permissions_class__(self): for perm_class in self.pattern.callback.cls.permission_classes: return perm_class.__name__ - def __get_serializer_fields__(self): - fields = [] - serializer = None - - if hasattr(self.callback.cls, 'serializer_class'): - serializer = self.callback.cls.serializer_class - - elif hasattr(self.callback.cls, 'get_serializer_class'): - serializer = self.callback.cls.get_serializer_class(self.pattern.callback.cls()) - - if hasattr(serializer, 'get_fields'): - try: - fields = self.__get_fields__(serializer) - except KeyError as e: - self.errors = e - fields = [] def __get_serializer__(self): try: return self.serializer_class() diff --git a/tests/tests.py b/tests/tests.py index 03a0986..c0f95a6 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -28,10 +28,10 @@ def test_index_view_with_endpoints(self): response = self.client.get(reverse('drfdocs')) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context["endpoints"]), 15) + self.assertEqual(len(response.context["endpoints"]), 16) # Test the login view - endpoint = response.context["endpoints"][1] + endpoint = response.context["endpoints"][4] self.assertEqual(endpoint.name_parent, "accounts") self.assertEqual(endpoint.allowed_methods, ['POST', 'OPTIONS']) self.assertEqual(endpoint.path, "/accounts/login/") @@ -41,7 +41,7 @@ def test_index_view_with_endpoints(self): self.assertTrue(endpoint.fields[0]["required"]) # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error. - endpoint = response.context["endpoints"][8] + endpoint = response.context["endpoints"][12] self.assertEqual(str(endpoint.errors), "'test_value'") def test_index_search_with_endpoints(self): @@ -81,10 +81,10 @@ def test_index_view_with_existent_namespace(self): # 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) + self.assertEqual(len(response.context["endpoints"]), 5) # The view "OrganisationErroredView" (organisations/(?P[\w-]+)/errored/) should contain an error. - self.assertEqual(str(response.context["endpoints"][0].errors), "'test_value'") + self.assertEqual(str(response.context["endpoints"][1].errors), "'test_value'") # Test 'members' namespace response = self.client.get(reverse('drfdocs-filter', args=['members'])) @@ -107,9 +107,9 @@ def test_index_view_with_existent_app_name(self): # 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) + self.assertEqual(len(response.context["endpoints"]), 6) parents_name = [e.name_parent for e in response.context["endpoints"]] - self.assertEquals(parents_name.count('organisations'), 3) + self.assertEquals(parents_name.count('organisations'), 5) self.assertEquals(parents_name.count('members'), 1) def test_index_search_with_existent_app_name(self): @@ -118,7 +118,7 @@ def test_index_search_with_existent_app_name(self): 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) + self.assertEqual(len(response.context["endpoints"][0].fields), 3) def test_index_view_with_non_existent_namespace_or_app_name(self): """ @@ -130,14 +130,10 @@ def test_index_view_with_non_existent_namespace_or_app_name(self): def test_model_viewset(self): response = self.client.get(reverse('drfdocs')) - self.assertEqual(response.status_code, 200) - - self.assertEqual(response.context["endpoints"][10].path, '/organisations//') - self.assertEqual(response.context['endpoints'][6].fields[2]['to_many_relation'], True) - self.assertEqual(response.context["endpoints"][11].path, '/organisation-model-viewsets/') - self.assertEqual(response.context["endpoints"][12].path, '/organisation-model-viewsets//') - self.assertEqual(response.context["endpoints"][11].allowed_methods, ['GET', 'POST', 'OPTIONS']) - self.assertEqual(response.context["endpoints"][12].allowed_methods, ['GET', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) - self.assertEqual(response.context["endpoints"][13].allowed_methods, ['POST', 'OPTIONS']) - self.assertEqual(response.context["endpoints"][13].docstring, 'This is a test.') + self.assertEqual(response.context["endpoints"][1].path, '/organisation-model-viewsets/') + self.assertEqual(response.context["endpoints"][2].path, '/organisation-model-viewsets//') + self.assertEqual(response.context["endpoints"][1].allowed_methods, ['GET', 'POST', 'OPTIONS']) + self.assertEqual(response.context["endpoints"][2].allowed_methods, ['GET', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) + self.assertEqual(response.context["endpoints"][4].allowed_methods, ['POST', 'OPTIONS']) + self.assertEqual(response.context["endpoints"][3].docstring, 'This is a test.') diff --git a/tests/urls.py b/tests/urls.py index b9d4182..a4cb5b7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -7,6 +7,7 @@ from rest_framework_docs.views import DRFDocsView from tests import views + accounts_urls = [ url(r'^login/$', views.LoginView.as_view(), name="login"), url(r'^login2/$', views.LoginWithSerilaizerClassView.as_view(), name="login2"), @@ -36,12 +37,14 @@ urlpatterns = [ url(r'^admin/', include(admin.site.urls)), - url(r'^docs/', DRFDocsView.as_view(drf_router=router), name='drfdocs'), + + # url(r'^docs/', include('rest_framework_docs.urls')), + url(r'^docs/(?P[\w-]+)/$', DRFDocsView.as_view(drf_router=router), name='drfdocs-filter'), + url(r'^docs/$', DRFDocsView.as_view(drf_router=router), name='drfdocs'), # API - url(r'^accounts/', view=include(accounts_urls, namespace="accounts")), - 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")), + # url(r'^organisations/', view=include(organisations_urls, namespace='organisations')), url(r'^', include(router.urls)), # Endpoints without parents/namespaces @@ -55,11 +58,13 @@ organisations_urls = (organisations_urls, 'organisations_app', ) members_urls = (members_urls, 'organisations_app', ) urlpatterns.extend([ + url(r'^accounts/', view=include(accounts_urls, namespace="accounts")), url(r'^organisations/', view=include(organisations_urls, namespace='organisations')), url(r'^members/', view=include(members_urls, namespace='members')), ]) else: urlpatterns.extend([ + url(r'^accounts/', view=include(accounts_urls, namespace="accounts", app_name='accounts_app')), 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 15dead2c64c61b5dfbffa02b65f069e33d426dfd Mon Sep 17 00:00:00 2001 From: Maxence Date: Mon, 1 Aug 2016 17:29:38 +0200 Subject: [PATCH 18/21] Fix flake8 --- demo/project/urls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/demo/project/urls.py b/demo/project/urls.py index 91e4e97..bc5873c 100644 --- a/demo/project/urls.py +++ b/demo/project/urls.py @@ -41,10 +41,8 @@ ]) -from tests.views import LoginView routers = [account_router, organisation_router] urlpatterns.extend([ url(r'^docs/(?P[\w-]+)/$', DRFDocsView.as_view(drf_router=routers), name='drfdocs-filter'), url(r'^docs/$', DRFDocsView.as_view(drf_router=routers), name='drfdocs'), - url(r'^another-login/$', LoginView.as_view(), name="login"), ]) From 33b41dfb010caa858c9e21e3345f808c59217de4 Mon Sep 17 00:00:00 2001 From: Maxence Date: Mon, 5 Sep 2016 16:36:39 +0200 Subject: [PATCH 19/21] Update to Django 1.10 --- requirements.txt | 8 ++++---- tests/settings.py | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f449bbb..566e6a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Django==1.8.7 -djangorestframework==3.3.2 -coverage==4.0.3 -flake8==2.5.1 +Django==1.10.1 +djangorestframework==3.4.6 +coverage==4.2 +flake8<3.0.0 mkdocs==0.15.3 diff --git a/tests/settings.py b/tests/settings.py index d80e9f2..7e59296 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -39,3 +39,11 @@ # https://docs.djangoproject.com/en/1.8/howto/static-files/ STATIC_URL = '/static/' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + }, +] From e5a8bef5a202a1dba45f00291bfa5ca577bfb171 Mon Sep 17 00:00:00 2001 From: Maxence Date: Thu, 12 Apr 2018 11:46:59 +0200 Subject: [PATCH 20/21] Fix Django 2 --- requirements.txt | 4 ++-- rest_framework_docs/api_docs.py | 10 +++++----- rest_framework_docs/api_endpoint.py | 9 +++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 566e6a7..b799525 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Django==1.10.1 -djangorestframework==3.4.6 +Django==2.1.0 +djangorestframework==3.7.7 coverage==4.2 flake8<3.0.0 mkdocs==0.15.3 diff --git a/rest_framework_docs/api_docs.py b/rest_framework_docs/api_docs.py index bceb7d4..2f2d88d 100644 --- a/rest_framework_docs/api_docs.py +++ b/rest_framework_docs/api_docs.py @@ -1,7 +1,7 @@ from operator import attrgetter from importlib import import_module from django.conf import settings -from django.core.urlresolvers import RegexURLResolver, RegexURLPattern +from django.urls import URLResolver, URLPattern from django.utils.module_loading import import_string from rest_framework.views import APIView from rest_framework_docs import SERIALIZER_FIELDS @@ -29,10 +29,10 @@ def __init__(self, drf_router=None, filter_param=None): def get_all_view_names(self, urlpatterns, parent_pattern=None, filter_param=None): for pattern in urlpatterns: - if isinstance(pattern, RegexURLResolver) and (not filter_param or filter_param in [pattern.app_name, pattern.namespace]): + if isinstance(pattern, URLResolver) and (not filter_param or filter_param in [pattern.app_name, pattern.namespace]): # parent_pattern = None if pattern._regex == "^" else pattern - self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=None if pattern._regex == "^" else pattern, filter_param=filter_param) - elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern): + self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=None if pattern.pattern.regex.pattern == "^" else pattern, filter_param=filter_param) + elif isinstance(pattern, URLPattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern): if not filter_param or parent_pattern: api_endpoint = ApiEndpoint(pattern, parent_pattern, self.drf_router) self.endpoints.append(api_endpoint) @@ -47,7 +47,7 @@ def _is_format_endpoint(self, pattern): """ Exclude endpoints with a "format" parameter """ - return '?P' in pattern._regex + return '?P' in pattern.pattern.regex.pattern def get_endpoints(self): return sorted(self.endpoints, key=attrgetter('name', 'path')) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 1d5bd95..24837c3 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -18,7 +18,7 @@ def __init__(self, pattern, parent_pattern=None, drf_router=None): 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('/', '-') + simplify_regex(parent_pattern.pattern.regex.pattern).replace('/', '-') self.name = self.name_parent if hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, ModelViewSet): self.name = '%s (RESTful)' % self.name_parent @@ -39,8 +39,9 @@ def __init__(self, pattern, parent_pattern=None, drf_router=None): def __get_path__(self, parent_pattern): if parent_pattern: - return simplify_regex(parent_pattern.regex.pattern + self.pattern.regex.pattern) - return simplify_regex(self.pattern.regex.pattern) + parent_regex = parent_pattern.pattern.regex.pattern + return simplify_regex(parent_regex + self.pattern.pattern.regex.pattern) + return simplify_regex(self.pattern.pattern.regex.pattern) def __get_allowed_methods__(self): @@ -66,7 +67,7 @@ def __get_allowed_methods__(self): lookup=lookup, trailing_slash=router.trailing_slash ) - if self.pattern.regex.pattern == regex: + if self.pattern.pattern.regex.pattern == regex: funcs, viewset_methods = zip( *[(mapping[m], m.upper()) for m in self.callback.cls.http_method_names if m in mapping] ) From ada797d8ba1c29be53b896d3f26ea29bc79e87a2 Mon Sep 17 00:00:00 2001 From: Maxence Date: Thu, 11 Oct 2018 12:23:19 +0200 Subject: [PATCH 21/21] Fix _get_allowed_methods --- rest_framework_docs/api_endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 24837c3..3f15a0a 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -76,7 +76,7 @@ def __get_allowed_methods__(self): self.docstring = inspect.getdoc(getattr(self.callback.cls, funcs[0])) view_methods = [force_str(m).upper() for m in self.callback.cls.http_method_names if - hasattr(self.callback.cls, m)] + hasattr(self.callback.cls, m) or m in getattr(self.callback, 'actions', {})] return viewset_methods + view_methods def __get_docstring__(self):