Skip to content
Prev Previous commit
Next Next commit
- 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)
  • Loading branch information
Maxence committed Jan 5, 2016
commit e2744a0f65fa0ee24e2e60a2de87ade592159a65
15 changes: 12 additions & 3 deletions demo/project/organisations/urls.py
Original file line number Diff line number Diff line change
@@ -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<slug>[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"),
url(r'^(?P<slug>[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave")
]

members_urlpatterns = [
url(r'^(?P<slug>[\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', )
20 changes: 17 additions & 3 deletions demo/project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
])
17 changes: 10 additions & 7 deletions rest_framework_docs/api_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
27 changes: 15 additions & 12 deletions rest_framework_docs/templates/rest_framework_docs/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@

{% block apps_menu %}
{% regroup endpoints by name_parent as endpoints_grouped %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Jump To <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for group in endpoints_grouped %}
<li><a href="#{{ group.grouper|lower }}-group">{{ group.grouper }}</a></li>
{% endfor %}
</ul>
</li>
{% if endpoints_grouped|length > 1 %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Jump To <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for group in endpoints_grouped %}
<li><a href="#{{ group.grouper|lower }}-group">{{ group.grouper }}</a></li>
{% endfor %}
</ul>
</li>
{% endif %}
{% endblock %}


{% block content %}

{% regroup endpoints by name_parent as endpoints_grouped %}

{% if endpoints_grouped %}
{% for group in endpoints_grouped %}

<h1 id="{{ group.grouper|lower }}-group">{{group.grouper}}</h1>
<h1 id="{{ group.grouper|lower }}-group">
{% if group.grouper %}
<a href="{% url 'drfdocs-filter' group.grouper %}">{{group.grouper}}</a>
{% endif %}
</h1>

<div class="panel-group" role="tablist">

Expand Down
3 changes: 2 additions & 1 deletion rest_framework_docs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
urlpatterns = [
# Url to view the API Docs
url(r'^$', DRFDocsView.as_view(), name='drfdocs'),
url(r'^(?P<app_name>\w+)/$', DRFDocsView.as_view(), name='drfdocs-ns'),
# Url to view the API Docs with a specific namespace or app_name
url(r'^(?P<filter_param>\w+)/$', DRFDocsView.as_view(), name='drfdocs-filter'),
]
4 changes: 2 additions & 2 deletions rest_framework_docs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
50 changes: 38 additions & 12 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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<slug>[\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)
26 changes: 22 additions & 4 deletions tests/urls.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,19 +18,36 @@

organisations_urls = [
url(r'^create/$', view=views.CreateOrganisationView.as_view(), name="create"),
url(r'^(?P<slug>[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"),
url(r'^(?P<slug>[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave"),
url(r'^(?P<slug>[\w-]+)/errored/$', view=views.OrganisationErroredView.as_view(), name="errored")
]

members_urls = [
url(r'^(?P<slug>[\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')),
])