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 pytest
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied
from django.test import RequestFactory, TestCase
from django.urls import reverse
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
......
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_extensions.cache.mixins import CacheResponseMixin
......
......@@ -2,6 +2,43 @@ from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.urls import reverse
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):
......
from django.conf import settings
from django.core.cache import cache
from django.urls import reverse
from rest_framework.test import APITestCase
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
......@@ -14,7 +15,10 @@ class RateLimitingTest(APITestCase):
def setUp(self):
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.client.login(username=self.user.username, password=USER_PASSWORD)
......@@ -40,7 +44,8 @@ class RateLimitingTest(APITestCase):
def test_rate_limiting(self):
""" Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """
response = self._make_requests()
self.assertEqual(response.status_code, 429)
assert response.status_code == 429
def test_user_throttle_rate(self):
""" Verify the UserThrottleRate can be used to override the default rate limit. """
......@@ -50,7 +55,8 @@ class RateLimitingTest(APITestCase):
def assert_rate_limit_successfully_exceeded(self):
""" Asserts that the throttle's rate limit can be exceeded without encountering an error. """
response = self._make_requests()
self.assertEqual(response.status_code, 200)
assert response.status_code == 200
def test_superuser_throttling(self):
""" Verify superusers are not throttled. """
......
......@@ -320,7 +320,6 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissions',
),
'PAGE_SIZE': 20,
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework_swagger.views.get_restructuredtext',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer',
......@@ -353,10 +352,7 @@ JWT_AUTH = {
}
SWAGGER_SETTINGS = {
'api_version': 'v1',
'doc_expansion': 'list',
'is_authenticated': True,
'permission_denied_handler': 'course_discovery.apps.api.views.api_docs_permission_denied_handler'
'DOC_EXPANSION': 'list',
}
# Elasticsearch uses index settings to specify available analyzers.
......
body {
margin: 0;
}
#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;
.footer {
display: none;
}
{% extends 'rest_framework_swagger/base.html' %}
{% load static %}
{% load staticfiles %}
{% block style %}
{{ block.super }}
<link href='{% static "css/edx-swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
{% block extra_styles %}
<link href='{% static "css/edx-swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
{% endblock %}
{% block branding %}
<span id="api-name">edX Catalog API</span>
{% endblock %}
{% block api_selector %}
{% block header %}
{% endblock %}
......@@ -22,9 +22,11 @@ from django.conf.urls.static import static
from django.contrib import admin
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.course_metadata.views import QueryPreviewView
admin.autodiscover()
urlpatterns = auth_urlpatterns + [
......@@ -34,7 +36,7 @@ urlpatterns = auth_urlpatterns + [
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.
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'^health/$', core_views.health, name='health'),
url('^$', QueryPreviewView.as_view()),
......
......@@ -25,11 +25,7 @@ djangorestframework==3.4.7
djangorestframework-csv==1.4.1
djangorestframework-jwt==1.8.0
djangorestframework-xml==1.3.0
django-rest-swagger[reST]==0.3.10
# 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
django-rest-swagger==2.0.7
drf-extensions==0.3.1
drf-haystack==1.6.0rc1
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