Commit 51757ed8 by Renzo Lucioni

Fix API docs

The API docs were broken by the Django 1.11 upgrade. This change includes an upgrade to the most recent version of django-rest-swagger that supports DRF 3.4. Newer versions of django-rest-swagger require DRF 3.5.

This change is complicated by a decision made by the maintainers of the django-filter package to avoid subclassing DRF's DjangoFilterBackend. For more on that decision, see https://github.com/carltongibson/django-filter/pull/576. The SchemaGenerator from DRF 3.4.7 expects instances of DjangoFilterBackend to have a get_fields() method. The DjangoFilterBackend from django-filter 1.0.4 doesn't have this method, instead replacing it with the get_schema_fields() used by the SchemaGenerator in DRF 3.5. To work around this, I've replaced our one use django-filter's DjangoFilterBackend with DRF's DjangoFilterBackend. Note that DRF 3.5 removes its implementation of DjangoFilterBackend in favor of django-filter's, so we will have to change how we import DjangoFilterBackend when we upgrade to 3.5.

LEARNER-1590
parent b02c1cde
import ddt import ddt
import pytest
from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from course_discovery.apps.api.views import api_docs_permission_denied_handler from course_discovery.apps.api.views import api_docs_permission_denied_handler
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import PartnerFactory, UserFactory
@pytest.mark.django_db
class TestApiDocs:
"""
Regression tests introduced following LEARNER-1590.
"""
path = reverse('api_docs')
def test_api_docs(self, admin_client):
"""
Verify that the API docs are available to authenticated clients.
"""
PartnerFactory(pk=settings.DEFAULT_PARTNER_ID)
response = admin_client.get(self.path)
assert response.status_code == 200
def test_api_docs_redirect(self, client):
"""
Verify that unauthenticated clients are redirected.
"""
response = client.get(self.path)
assert response.status_code == 302
@ddt.ddt @ddt.ddt
......
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_extensions.cache.mixins import CacheResponseMixin from rest_framework_extensions.cache.mixins import CacheResponseMixin
......
...@@ -2,6 +2,43 @@ from django.core.exceptions import PermissionDenied ...@@ -2,6 +2,43 @@ from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny
from rest_framework.renderers import CoreJSONRenderer
from rest_framework.response import Response
from rest_framework.schemas import SchemaGenerator
from rest_framework.views import APIView
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
class SwaggerSchemaView(APIView):
permission_classes = [AllowAny]
renderer_classes = [
CoreJSONRenderer,
OpenAPIRenderer,
SwaggerUIRenderer,
]
# Added in DRF 3.5.0. Does nothing until we upgrade.
exclude_from_schema = True
def get(self, request):
generator = SchemaGenerator(
title='Discovery API',
# TODO: Remove these kwargs after upgrading to DRF 3.5. exclude_from_schema
# will be sufficient at that point.
url='/api',
urlconf='course_discovery.apps.api.urls',
)
schema = generator.get_schema(request=request)
if not schema:
# get_schema() uses the same permissions check as the API endpoints.
# If we don't get a schema document back, it means the user is not
# authenticated or doesn't have permission to access the API.
# api_docs_permission_denied_handler() handles both of these cases.
return api_docs_permission_denied_handler(request)
return Response(schema)
def api_docs_permission_denied_handler(request): def api_docs_permission_denied_handler(request):
......
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from course_discovery.apps.core.models import UserThrottleRate from course_discovery.apps.core.models import UserThrottleRate
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.factories import USER_PASSWORD, PartnerFactory, UserFactory
from course_discovery.apps.core.throttles import OverridableUserRateThrottle from course_discovery.apps.core.throttles import OverridableUserRateThrottle
...@@ -14,7 +15,10 @@ class RateLimitingTest(APITestCase): ...@@ -14,7 +15,10 @@ class RateLimitingTest(APITestCase):
def setUp(self): def setUp(self):
super(RateLimitingTest, self).setUp() super(RateLimitingTest, self).setUp()
self.url = reverse('django.swagger.resources.view')
PartnerFactory(pk=settings.DEFAULT_PARTNER_ID)
self.url = reverse('api_docs')
self.user = UserFactory() self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
...@@ -40,7 +44,8 @@ class RateLimitingTest(APITestCase): ...@@ -40,7 +44,8 @@ class RateLimitingTest(APITestCase):
def test_rate_limiting(self): def test_rate_limiting(self):
""" Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """ """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
response = self._make_requests() response = self._make_requests()
self.assertEqual(response.status_code, 429)
assert response.status_code == 429
def test_user_throttle_rate(self): def test_user_throttle_rate(self):
""" Verify the UserThrottleRate can be used to override the default rate limit. """ """ Verify the UserThrottleRate can be used to override the default rate limit. """
...@@ -50,7 +55,8 @@ class RateLimitingTest(APITestCase): ...@@ -50,7 +55,8 @@ class RateLimitingTest(APITestCase):
def assert_rate_limit_successfully_exceeded(self): def assert_rate_limit_successfully_exceeded(self):
""" Asserts that the throttle's rate limit can be exceeded without encountering an error. """ """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """
response = self._make_requests() response = self._make_requests()
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
def test_superuser_throttling(self): def test_superuser_throttling(self):
""" Verify superusers are not throttled. """ """ Verify superusers are not throttled. """
......
...@@ -320,7 +320,6 @@ REST_FRAMEWORK = { ...@@ -320,7 +320,6 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissions', 'rest_framework.permissions.DjangoModelPermissions',
), ),
'PAGE_SIZE': 20, 'PAGE_SIZE': 20,
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework_swagger.views.get_restructuredtext',
'TEST_REQUEST_RENDERER_CLASSES': ( 'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer', 'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
...@@ -353,10 +352,7 @@ JWT_AUTH = { ...@@ -353,10 +352,7 @@ JWT_AUTH = {
} }
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'api_version': 'v1', 'DOC_EXPANSION': 'list',
'doc_expansion': 'list',
'is_authenticated': True,
'permission_denied_handler': 'course_discovery.apps.api.views.api_docs_permission_denied_handler'
} }
# Elasticsearch uses index settings to specify available analyzers. # Elasticsearch uses index settings to specify available analyzers.
......
body { .footer {
margin: 0; display: none;
}
#header {
background-color: #fcfcfc;
border-bottom: 4px solid #0079bc;
color: #999999;
}
#django-rest-swagger {
background-color: white;
color: #999999;
}
#django-rest-swagger a {
color: inherit;
} }
{% extends 'rest_framework_swagger/base.html' %} {% extends 'rest_framework_swagger/base.html' %}
{% load static %} {% load staticfiles %}
{% block style %} {% block extra_styles %}
{{ block.super }} <link href='{% static "css/edx-swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
<link href='{% static "css/edx-swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
{% endblock %} {% endblock %}
{% block header %}
{% block branding %}
<span id="api-name">edX Catalog API</span>
{% endblock %}
{% block api_selector %}
{% endblock %} {% endblock %}
...@@ -22,9 +22,11 @@ from django.conf.urls.static import static ...@@ -22,9 +22,11 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.i18n import javascript_catalog from django.views.i18n import javascript_catalog
from course_discovery.apps.api.views import SwaggerSchemaView
from course_discovery.apps.core import views as core_views from course_discovery.apps.core import views as core_views
from course_discovery.apps.course_metadata.views import QueryPreviewView from course_discovery.apps.course_metadata.views import QueryPreviewView
admin.autodiscover() admin.autodiscover()
urlpatterns = auth_urlpatterns + [ urlpatterns = auth_urlpatterns + [
...@@ -34,7 +36,7 @@ urlpatterns = auth_urlpatterns + [ ...@@ -34,7 +36,7 @@ urlpatterns = auth_urlpatterns + [
url(r'^api/', include('course_discovery.apps.api.urls', namespace='api')), url(r'^api/', include('course_discovery.apps.api.urls', namespace='api')),
# Use the same auth views for all logins, including those originating from the browseable API. # Use the same auth views for all logins, including those originating from the browseable API.
url(r'^api-auth/', include(auth_urlpatterns, namespace='rest_framework')), url(r'^api-auth/', include(auth_urlpatterns, namespace='rest_framework')),
url(r'^api-docs/', include('rest_framework_swagger.urls')), url(r'^api-docs/', SwaggerSchemaView.as_view(), name='api_docs'),
url(r'^auto_auth/$', core_views.AutoAuth.as_view(), name='auto_auth'), url(r'^auto_auth/$', core_views.AutoAuth.as_view(), name='auto_auth'),
url(r'^health/$', core_views.health, name='health'), url(r'^health/$', core_views.health, name='health'),
url('^$', QueryPreviewView.as_view()), url('^$', QueryPreviewView.as_view()),
......
...@@ -25,11 +25,7 @@ djangorestframework==3.4.7 ...@@ -25,11 +25,7 @@ djangorestframework==3.4.7
djangorestframework-csv==1.4.1 djangorestframework-csv==1.4.1
djangorestframework-jwt==1.8.0 djangorestframework-jwt==1.8.0
djangorestframework-xml==1.3.0 djangorestframework-xml==1.3.0
django-rest-swagger[reST]==0.3.10 django-rest-swagger==2.0.7
# django-rest-swagger[reST] should install docutils, but doesn't
# (see https://github.com/pypa/pip/issues/4617), so we force it here.
docutils>=0.8
drf-extensions==0.3.1 drf-extensions==0.3.1
drf-haystack==1.6.0rc1 drf-haystack==1.6.0rc1
dry-rest-permissions==0.1.6 dry-rest-permissions==0.1.6
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment