From 59854804092b52ada61f20014e77d0549d990ec1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 24 Apr 2019 15:46:50 -0400 Subject: [PATCH 01/13] Add drf-yasg * Install drf-yasg * Add drf-yasg settings and urls * Pin drf to make drf-yasg work * Adjust config-models version to be compatible * Remove django-rest-swagger (the old way) --- lms/envs/common.py | 2 ++ lms/urls.py | 6 ++++-- openedx/core/openapi.py | 20 ++++++++++++++++++++ requirements/edx/base.in | 5 +++-- requirements/edx/github.in | 3 --- 5 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 openedx/core/openapi.py diff --git a/lms/envs/common.py b/lms/envs/common.py index c267b0dd64..7c5cec67f2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2147,6 +2147,8 @@ INSTALLED_APPS = [ # User API 'rest_framework', + 'drf_yasg', + 'openedx.core.djangoapps.user_api', # Shopping cart diff --git a/lms/urls.py b/lms/urls.py index 2c40254d43..dd1190ef19 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -8,7 +8,6 @@ from django.conf.urls.static import static from django.contrib.admin import autodiscover as django_autodiscover from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import RedirectView -from rest_framework_swagger.views import get_swagger_view from branding import views as branding_views from config_models.views import ConfigurationModelCurrentAPIView @@ -42,6 +41,7 @@ from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.verified_track_content import views as verified_track_content_views +from openedx.core.openapi import schema_view from openedx.features.enterprise_support.api import enterprise_enabled from ratelimitbackend import admin from static_template_view import views as static_template_view_views @@ -964,7 +964,9 @@ if settings.BRANCH_IO_KEY: if settings.FEATURES.get('ENABLE_API_DOCS'): urlpatterns += [ - url(r'^api-docs/$', get_swagger_view(title='LMS API')), + url(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)), ] # edx-drf-extensions csrf app diff --git a/openedx/core/openapi.py b/openedx/core/openapi.py new file mode 100644 index 0000000000..c7c9a162e4 --- /dev/null +++ b/openedx/core/openapi.py @@ -0,0 +1,20 @@ +""" +Open API support. +""" + +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) diff --git a/requirements/edx/base.in b/requirements/edx/base.in index e91f65332d..283aea61ce 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -37,7 +37,7 @@ celery==3.1.25 # Asynchronous task execution library defusedxml Django==1.11.21 # Web application framework django-babel-underscore # underscore template extractor for django-babel (internationalization utilities) -django-config-models>=0.2.2 # Configuration models for Django allowing config management with auditing +django-config-models>=1.0.0 # Configuration models for Django allowing config management with auditing django-cors-headers==2.1.0 # Used to allow to configure CORS headers for cross-domain requests django-countries==4.6.1 # Country data for Django forms and model fields django-crum # Middleware that stores the current request and user in thread local storage @@ -54,7 +54,6 @@ django-pyfs django-ratelimit django-ratelimit-backend==1.1.1 django-require -django-rest-swagger # API documentation django-sekizai django-ses==0.8.4 django-simple-history @@ -64,7 +63,9 @@ django-storages==1.4.1 django-user-tasks django-waffle==0.12.0 django-webpack-loader # Used to wire webpack bundles into the django asset pipeline +djangorestframework==3.7.7 djangorestframework-jwt +drf-yasg # Replacement for django-rest-swagger edx-ace==0.1.10 edx-analytics-data-api-client edx-ccx-keys diff --git a/requirements/edx/github.in b/requirements/edx/github.in index d524efd23f..0c45f02614 100644 --- a/requirements/edx/github.in +++ b/requirements/edx/github.in @@ -66,9 +66,6 @@ git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7 # This can go away when we update auth to not use django-rest-framework-oauth git+https://github.com/edx/django-oauth-plus.git@01ec2a161dfc3465f9d35b9211ae790177418316#egg=django-oauth-plus==2.2.9.edx-1 -# Why a DRF fork? See: https://openedx.atlassian.net/browse/PLAT-1581 -git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 - # Why a drf-oauth fork? To add Django 1.11 compatibility to the abandoned repo. # This dependency will be removed by this work: https://openedx.atlassian.net/browse/PLAT-1660 git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 From c60da18de587395fab6adf5910ae03cf20e86a99 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 16 May 2019 11:01:07 -0400 Subject: [PATCH 02/13] make upgrade Because we added drf-yasg and pinned djangorestframework --- requirements/edx/base.txt | 16 +++++++++------- requirements/edx/development.txt | 8 +++++--- requirements/edx/testing.txt | 8 +++++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index fe6b34ee4b..9b8370ca37 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -43,8 +43,8 @@ certifi==2019.3.9 cffi==1.12.3 chardet==3.0.4 click==7.0 # via user-util -coreapi==2.3.3 # via django-rest-swagger, openapi-codec -coreschema==0.0.4 # via coreapi +coreapi==2.3.3 # via drf-yasg +coreschema==0.0.4 # via coreapi, drf-yasg git+https://github.com/edx/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1 cryptography==2.7 cssutils==1.0.2 # via pynliner @@ -77,7 +77,6 @@ django-pyfs==2.0 django-ratelimit-backend==1.1.1 django-ratelimit==2.0.0 django-require==1.0.11 -django-rest-swagger==2.2.0 django-sekizai==1.0.0 django-ses==0.8.4 django-simple-history==2.7.0 @@ -91,9 +90,10 @@ django==1.11.21 djangorestframework-jwt==1.11.0 git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 djangorestframework-xml==1.3.0 # via edx-enterprise -git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 +djangorestframework==3.7.7 docopt==0.6.2 docutils==0.14 # via botocore +drf-yasg==1.15.0 edx-ace==0.1.10 edx-analytics-data-api-client==0.15.3 edx-ccx-keys==0.2.1 @@ -136,6 +136,7 @@ help-tokens==1.0.3 html5lib==1.0.1 httplib2==0.13.0 # via oauth2, zendesk idna==2.8 +inflection==0.3.1 # via drf-yasg ipaddress==1.0.22 isodate==0.6.0 # via python3-saml itypes==1.1.0 # via coreapi @@ -168,7 +169,6 @@ nodeenv==1.1.1 numpy==1.16.4 oauth2==1.9.0.post1 oauthlib==2.1.0 -openapi-codec==1.3.2 # via django-rest-swagger git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3 path.py==8.2.1 pathtools==0.1.2 @@ -210,6 +210,8 @@ requests-oauthlib==1.1.0 requests==2.22.0 rest-condition==1.0.3 rfc6266-parser==0.0.5.post2 +ruamel.ordereddict==0.4.13 # via ruamel.yaml +ruamel.yaml==0.15.96 # via drf-yasg rules==2.0.1 s3transfer==0.1.13 # via boto3 sailthru-client==2.2.3 @@ -217,7 +219,7 @@ scipy==1.2.1 semantic-version==2.6.0 # via edx-drf-extensions shapely==1.6.4.post2 shortuuid==0.5.0 # via edx-django-oauth2-provider -simplejson==3.16.0 # via django-rest-swagger, mailsnake, sailthru-client, zendesk +simplejson==3.16.0 # via mailsnake, sailthru-client, zendesk singledispatch==3.4.0.3 six==1.11.0 slumber==0.7.1 # via edx-rest-api-client @@ -231,7 +233,7 @@ stevedore==1.30.1 sympy==1.4 tincan==0.0.5 # via edx-enterprise unicodecsv==0.14.1 -uritemplate==3.0.0 # via coreapi +uritemplate==3.0.0 # via coreapi, drf-yasg urllib3==1.23 user-util==0.1.5 voluptuous==0.11.5 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 18bc739205..23b105d3ed 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -97,7 +97,6 @@ django-pyfs==2.0 django-ratelimit-backend==1.1.1 django-ratelimit==2.0.0 django-require==1.0.11 -django-rest-swagger==2.2.0 django-sekizai==1.0.0 django-ses==0.8.4 django-simple-history==2.7.0 @@ -111,9 +110,10 @@ django==1.11.21 djangorestframework-jwt==1.11.0 git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 djangorestframework-xml==1.3.0 -git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 +djangorestframework==3.7.7 docopt==0.6.2 docutils==0.14 +drf-yasg==1.15.0 edx-ace==0.1.10 edx-analytics-data-api-client==0.15.3 edx-ccx-keys==0.2.1 @@ -173,6 +173,7 @@ idna==2.8 imagesize==1.1.0 # via sphinx importlib-metadata==0.17 inflect==2.1.0 +inflection==0.3.1 ipaddress==1.0.22 isodate==0.6.0 isort==4.3.20 @@ -214,7 +215,6 @@ nodeenv==1.1.1 numpy==1.16.4 oauth2==1.9.0.post1 oauthlib==2.1.0 -openapi-codec==1.3.2 git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3 packaging==19.0 path.py==8.2.1 @@ -279,6 +279,8 @@ requests-oauthlib==1.1.0 requests==2.22.0 rest-condition==1.0.3 rfc6266-parser==0.0.5.post2 +ruamel.ordereddict==0.4.13 +ruamel.yaml==0.15.96 rules==2.0.1 s3transfer==0.1.13 sailthru-client==2.2.3 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 640bcd2b1e..a77b138a8f 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -94,7 +94,6 @@ django-pyfs==2.0 django-ratelimit-backend==1.1.1 django-ratelimit==2.0.0 django-require==1.0.11 -django-rest-swagger==2.2.0 django-sekizai==1.0.0 django-ses==0.8.4 django-simple-history==2.7.0 @@ -107,9 +106,10 @@ django-webpack-loader==0.6.0 djangorestframework-jwt==1.11.0 git+https://github.com/edx/django-rest-framework-oauth.git@0a43e8525f1e3048efe4bc70c03de308a277197c#egg=djangorestframework-oauth==1.1.1 djangorestframework-xml==1.3.0 -git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3 +djangorestframework==3.7.7 docopt==0.6.2 docutils==0.14 +drf-yasg==1.15.0 edx-ace==0.1.10 edx-analytics-data-api-client==0.15.3 edx-ccx-keys==0.2.1 @@ -167,6 +167,7 @@ httpretty==0.9.6 idna==2.8 importlib-metadata==0.17 # via pluggy inflect==2.1.0 +inflection==0.3.1 ipaddress==1.0.22 isodate==0.6.0 isort==4.3.20 @@ -207,7 +208,6 @@ nodeenv==1.1.1 numpy==1.16.4 oauth2==1.9.0.post1 oauthlib==2.1.0 -openapi-codec==1.3.2 git+https://github.com/edx/edx-ora2.git@2.2.3#egg=ora2==2.2.3 packaging==19.0 # via caniusepython3 path.py==8.2.1 @@ -270,6 +270,8 @@ requests-oauthlib==1.1.0 requests==2.22.0 rest-condition==1.0.3 rfc6266-parser==0.0.5.post2 +ruamel.ordereddict==0.4.13 +ruamel.yaml==0.15.96 rules==2.0.1 s3transfer==0.1.13 sailthru-client==2.2.3 From 9257f68fd81e45b701b963425c9fcc72797957ea Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 13 May 2019 13:20:56 -0400 Subject: [PATCH 03/13] The default TIME_ZONE should be UTC In production, we use UTC as the time zone. DRF 3.7.7 now puts all times in the currently set timezone where it used to use UTC. By setting TIME_ZONE to UTC, we keep the same results we used to get. In a few places, we had to change the expected test results to be UTC. --- cms/envs/common.py | 2 +- .../program_enrollments/api/v1/tests/test_views.py | 4 ++-- lms/envs/common.py | 2 +- openedx/features/content_type_gating/tests/test_models.py | 6 +++--- .../features/course_duration_limits/tests/test_models.py | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 33be61f1af..39478d627e 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -680,7 +680,7 @@ STATICFILES_DIRS = [ # Locale/Internationalization CELERY_TIMEZONE = 'UTC' -TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGES_BIDI = lms.envs.common.LANGUAGES_BIDI diff --git a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py b/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py index ec25886b45..adfa913096 100644 --- a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py +++ b/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py @@ -1738,8 +1738,8 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared course_run_overview = response.data['course_runs'][0] - self.assertEqual(course_run_overview['start_date'], '2018-12-31T05:00:00Z') - self.assertEqual(course_run_overview['end_date'], '2019-01-02T05:00:00Z') + self.assertEqual(course_run_overview['start_date'], '2018-12-31T00:00:00Z') + self.assertEqual(course_run_overview['end_date'], '2019-01-02T00:00:00Z') # course run end date may not exist self.course_overview.end = None diff --git a/lms/envs/common.py b/lms/envs/common.py index 7c5cec67f2..51ca49e960 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -994,7 +994,7 @@ MEDIA_URL = '/media/' # Locale/Internationalization CELERY_TIMEZONE = 'UTC' -TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html # these languages display right to left LANGUAGES_BIDI = ("he", "ar", "fa", "ur", "fa-ir", "rtl") diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py index 59e3bc8c71..e7cb035d6b 100644 --- a/openedx/features/content_type_gating/tests/test_models.py +++ b/openedx/features/content_type_gating/tests/test_models.py @@ -207,7 +207,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): all_configs[CourseLocator('7-True', 'test_course', 'run-None')], { 'enabled': (True, Provenance.org), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), 'studio_override_enabled': (None, Provenance.default), } ) @@ -215,7 +215,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): all_configs[CourseLocator('7-True', 'test_course', 'run-False')], { 'enabled': (False, Provenance.run), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), 'studio_override_enabled': (None, Provenance.default), } ) @@ -223,7 +223,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): all_configs[CourseLocator('7-None', 'test_course', 'run-None')], { 'enabled': (True, Provenance.site), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), 'studio_override_enabled': (None, Provenance.default), } ) diff --git a/openedx/features/course_duration_limits/tests/test_models.py b/openedx/features/course_duration_limits/tests/test_models.py index fb5b22c083..1ec9898b0a 100644 --- a/openedx/features/course_duration_limits/tests/test_models.py +++ b/openedx/features/course_duration_limits/tests/test_models.py @@ -236,21 +236,21 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase): all_configs[CourseLocator('7-True', 'test_course', 'run-None')], { 'enabled': (True, Provenance.org), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), } ) self.assertEqual( all_configs[CourseLocator('7-True', 'test_course', 'run-False')], { 'enabled': (False, Provenance.run), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), } ) self.assertEqual( all_configs[CourseLocator('7-None', 'test_course', 'run-None')], { 'enabled': (True, Provenance.site), - 'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.run), + 'enabled_as_of': (datetime(2018, 1, 1, 0, tzinfo=pytz.UTC), Provenance.run), } ) From de8e158ce8e9a28cfa2a1c947512cd0e7eee872b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 May 2019 12:30:28 -0400 Subject: [PATCH 04/13] Un-truncate test failure diffs --- conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conftest.py b/conftest.py index ec683b154d..41728ac5b9 100644 --- a/conftest.py +++ b/conftest.py @@ -2,6 +2,8 @@ Default unit test configuration and fixtures. """ from __future__ import absolute_import, unicode_literals +from unittest import TestCase + import pytest # Import hooks and fixture overrides from the cms package to @@ -9,6 +11,10 @@ import pytest from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import +# When using self.assertEquals, diffs are truncated. We don't want that, always +# show the whole diff. +TestCase.maxDiff = None + @pytest.fixture(autouse=True) def no_webpack_loader(monkeypatch): From fdd66e5390ac9686105f87e8fc566db89cd921ea Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 May 2019 12:47:34 -0400 Subject: [PATCH 05/13] Adjust the expected error message for DRF 3.7.7 --- openedx/core/djangoapps/user_api/accounts/tests/test_views.py | 2 +- openedx/core/djangoapps/user_api/preferences/tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index b4d2d7dd6d..6b24bd66b8 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -551,7 +551,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ("level_of_education", "none", u"ȻħȺɍłɇs", u'"ȻħȺɍłɇs" is not a valid choice.'), ("country", "GB", "XY", u'"XY" is not a valid choice.'), ("year_of_birth", 2009, "not_an_int", u"A valid integer is required."), - ("name", "bob", "z" * 256, u"Ensure this value has at most 255 characters (it has 256)."), + ("name", "bob", "z" * 256, u"Ensure this field has no more than 255 characters."), ("name", u"ȻħȺɍłɇs", " ", u"The name field must be at least 1 character long."), ("goals", "Smell the roses"), ("mailing_address", "Sesame Street"), diff --git a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py index 092c05aee0..7f8f719994 100644 --- a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py @@ -470,7 +470,7 @@ def get_expected_validation_developer_message(preference_key, preference_value): preference_key=preference_key, preference_value=preference_value, error={ - "key": [u"Ensure this value has at most 255 characters (it has 256)."] + "key": [u"Ensure this field has no more than 255 characters."] } ) From 7359ca4fb2eb6837546a52e0a13617d8084bc9fa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 May 2019 16:33:16 -0400 Subject: [PATCH 06/13] Is this right? It fixes two tests --- .../grades/rest_api/v1/tests/test_grading_policy_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/grades/rest_api/v1/tests/test_grading_policy_view.py b/lms/djangoapps/grades/rest_api/v1/tests/test_grading_policy_view.py index 09c56136a5..975e3f2399 100644 --- a/lms/djangoapps/grades/rest_api/v1/tests/test_grading_policy_view.py +++ b/lms/djangoapps/grades/rest_api/v1/tests/test_grading_policy_view.py @@ -122,7 +122,7 @@ class GradingPolicyTestMixin(object): """ The view should return HTTP status 401 if user is unauthenticated. """ - self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION=None) + self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION="") def test_staff_authorized(self): """ From 64c47856dd0de95e2d425012af1542f4d2428824 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 16 May 2019 09:13:59 -0400 Subject: [PATCH 07/13] DRF 3.7.4 changed how you delegate to another view, so don't The error in the test was: ``` AssertionError: The `request` argument must be an instance of `django.http.HttpRequest`, not `rest_framework.request.Request`. ``` The (controversial) incompatible change was in 3.7.4: https://github.com/encode/django-rest-framework/pull/5618 I'll look into whether there's another way to address it.
Full error report ``` AssertionError: The `request` argument must be an instance of `django.http.HttpRequest`, not `rest_framework.request.Request`. Stacktrace self = def test_profile_image_request_for_null_endorsed_by(self): """ Tests if 'endorsed' is True but 'endorsed_by' is null, the api does not crash. This is the case for some old/stale data in prod/stage environments. """ self.register_get_user_response(self.user) thread = self.make_minimal_cs_thread({ "thread_type": "question", "endorsed_responses": [make_minimal_cs_comment({ "id": "endorsed_comment", "user_id": self.user.id, "username": self.user.username, "endorsed": True, })], "non_endorsed_resp_total": 0, }) self.register_get_thread_response(thread) self.create_profile_image(self.user, get_profile_image_storage()) response = self.client.get(self.url, { "thread_id": thread["id"], "endorsed": True, > "requested_fields": "profile_image", }) lms/djangoapps/discussion_api/tests/test_views.py:1446: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/test.py:291: in get response = super(APIClient, self).get(path, data=data, **extra) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/test.py:208: in get return self.generic('GET', path, **r) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/test.py:237: in generic method, path, data, content_type, secure, **extra) ../venvs/edxapp/local/lib/python2.7/site-packages/django/test/client.py:416: in generic return self.request(**r) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/test.py:288: in request return super(APIClient, self).request(**kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/test.py:240: in request request = super(APIRequestFactory, self).request(**kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/django/test/client.py:501: in request six.reraise(*exc_info) ../venvs/edxapp/local/lib/python2.7/site-packages/django/core/handlers/exception.py:41: in inner response = get_response(request) ../venvs/edxapp/local/lib/python2.7/site-packages/django/core/handlers/base.py:249: in _legacy_get_response response = self._get_response(request) ../venvs/edxapp/local/lib/python2.7/site-packages/django/core/handlers/base.py:187: in _get_response response = self.process_exception_by_middleware(e, request) ../venvs/edxapp/local/lib/python2.7/site-packages/django/core/handlers/base.py:185: in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/django/utils/decorators.py:185: in inner return func(*args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/django/views/decorators/csrf.py:58: in wrapped_view return view_func(*args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/viewsets.py:95: in view return self.dispatch(request, *args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/views.py:494: in dispatch response = self.handle_exception(exc) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/views.py:491: in dispatch response = handler(request, *args, **kwargs) lms/djangoapps/discussion_api/views.py:505: in list form.cleaned_data["requested_fields"], lms/djangoapps/discussion_api/api.py:659: in get_comment_list results = _serialize_discussion_entities(request, context, responses, requested_fields, DiscussionEntity.comment) lms/djangoapps/discussion_api/api.py:468: in _serialize_discussion_entities request, results, usernames, discussion_entity_type, include_profile_image lms/djangoapps/discussion_api/api.py:413: in _add_additional_response_fields username_profile_dict = _get_user_profile_dict(request, usernames=','.join(usernames)) lms/djangoapps/discussion_api/api.py:350: in _get_user_profile_dict user_profile_details = AccountViewSet.as_view({'get': 'list'})(request).data ../venvs/edxapp/local/lib/python2.7/site-packages/django/views/decorators/csrf.py:58: in wrapped_view return view_func(*args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/viewsets.py:95: in view return self.dispatch(request, *args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/views.py:477: in dispatch request = self.initialize_request(request, *args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/viewsets.py:118: in initialize_request request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs) ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/views.py:381: in initialize_request parser_context=parser_context _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = request = parsers = [] authenticators = [, ] negotiator = parser_context = {'args': (), 'kwargs': {}, 'view': } def __init__(self, request, parsers=None, authenticators=None, negotiator=None, parser_context=None): assert isinstance(request, HttpRequest), ( 'The `request` argument must be an instance of ' '`django.http.HttpRequest`, not `{}.{}`.' > .format(request.__class__.__module__, request.__class__.__name__) ) E AssertionError: The `request` argument must be an instance of `django.http.HttpRequest`, not `rest_framework.request.Request`. ../venvs/edxapp/local/lib/python2.7/site-packages/rest_framework/request.py:159: AssertionError ```
--- lms/djangoapps/discussion/rest_api/api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index 27b9bdc04e..1b7a9aa870 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -61,6 +61,7 @@ from openedx.core.djangoapps.django_comment_common.signals import ( thread_voted ) from openedx.core.djangoapps.django_comment_common.utils import get_course_discussion_settings +from openedx.core.djangoapps.user_api.accounts.api import get_account_settings from openedx.core.djangoapps.user_api.accounts.views import AccountViewSet from openedx.core.lib.exceptions import CourseNotFoundError, DiscussionNotFoundError, PageNotFoundError @@ -365,10 +366,11 @@ def _get_user_profile_dict(request, usernames): A dict with username as key and user profile details as value. """ - request.GET = request.GET.copy() # Make a mutable copy of the GET parameters. - request.GET['username'] = usernames - user_profile_details = AccountViewSet.as_view({'get': 'list'})(request).data - + if usernames: + username_list = usernames.split(",") + else: + username_list = [] + user_profile_details = get_account_settings(request, username_list) return {user['username']: user for user in user_profile_details} From 4a1154a7ca5af6c7ed7f0193433febc79f1005b0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 16 May 2019 14:41:28 -0400 Subject: [PATCH 08/13] Give a safer buffer for clearing the rate limiting The rate limiter counts requests in a 5-minute window. To be sure we aren't hitting edge cases, make the future requests 6 minutes plus 1 second in the future. --- lms/djangoapps/shoppingcart/tests/test_views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 0a5cd7bf99..dd18a89a7e 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1749,8 +1749,8 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase): response = self.client.post(url) self.assertEquals(response.status_code, 403) - # now reset the time to 5 mins from now in future in order to unblock - reset_time = datetime.now(UTC) + timedelta(seconds=300) + # now reset the time to 6 mins from now in future in order to unblock + reset_time = datetime.now(UTC) + timedelta(seconds=361) with freeze_time(reset_time): response = self.client.post(url) self.assertEquals(response.status_code, 404) @@ -1773,8 +1773,8 @@ class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase): response = self.client.get(url) self.assertEquals(response.status_code, 403) - # now reset the time to 5 mins from now in future in order to unblock - reset_time = datetime.now(UTC) + timedelta(seconds=300) + # now reset the time to 6 mins from now in future in order to unblock + reset_time = datetime.now(UTC) + timedelta(seconds=361) with freeze_time(reset_time): response = self.client.get(url) self.assertEquals(response.status_code, 404) From 8a443971394869989d3db09df5d1383c4ef0980a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 16 May 2019 16:49:53 -0400 Subject: [PATCH 09/13] Is this field missing because it is None? --- common/djangoapps/entitlements/api/v1/tests/test_serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/djangoapps/entitlements/api/v1/tests/test_serializers.py b/common/djangoapps/entitlements/api/v1/tests/test_serializers.py index 2f487a94ea..f282dc9116 100644 --- a/common/djangoapps/entitlements/api/v1/tests/test_serializers.py +++ b/common/djangoapps/entitlements/api/v1/tests/test_serializers.py @@ -30,7 +30,6 @@ class EntitlementsSerializerTests(ModuleStoreTestCase): 'course_uuid': str(entitlement.course_uuid), 'mode': entitlement.mode, 'refund_locked': False, - 'enrollment_course_run': None, 'order_number': entitlement.order_number, 'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'modified': entitlement.modified.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), From 8774ff1f9b92981d2618d55c5b23857a466b46c3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 May 2019 15:59:18 -0400 Subject: [PATCH 10/13] Use ref_name to disambiguate serializers that drf-yasg would otherwise assume are the same. --- common/djangoapps/course_modes/api/serializers.py | 4 ++++ lms/djangoapps/commerce/api/v1/serializers.py | 6 ++++++ lms/djangoapps/mobile_api/users/serializers.py | 2 ++ openedx/core/djangoapps/enrollments/serializers.py | 4 ++++ openedx/core/djangoapps/user_api/serializers.py | 2 ++ 5 files changed, 18 insertions(+) diff --git a/common/djangoapps/course_modes/api/serializers.py b/common/djangoapps/course_modes/api/serializers.py index 2df0493b79..7c287ac54d 100644 --- a/common/djangoapps/course_modes/api/serializers.py +++ b/common/djangoapps/course_modes/api/serializers.py @@ -28,6 +28,10 @@ class CourseModeSerializer(serializers.Serializer): sku = serializers.CharField(required=False) bulk_sku = serializers.CharField(required=False) + class Meta(object): + # For disambiguating within the drf-yasg swagger schema + ref_name = 'course_modes.CourseMode' + def create(self, validated_data): """ This method must be implemented for use in our diff --git a/lms/djangoapps/commerce/api/v1/serializers.py b/lms/djangoapps/commerce/api/v1/serializers.py index 6c66dfecf3..2eefe85428 100644 --- a/lms/djangoapps/commerce/api/v1/serializers.py +++ b/lms/djangoapps/commerce/api/v1/serializers.py @@ -36,6 +36,8 @@ class CourseModeSerializer(serializers.ModelSerializer): class Meta(object): model = CourseMode fields = ('name', 'currency', 'price', 'sku', 'bulk_sku', 'expires') + # For disambiguating within the drf-yasg swagger schema + ref_name = 'commerce.CourseMode' def validate_course_id(course_id): @@ -77,6 +79,10 @@ class CourseSerializer(serializers.Serializer): verification_deadline = PossiblyUndefinedDateTimeField(format=None, allow_null=True, required=False) modes = CourseModeSerializer(many=True) + class Meta(object): + # For disambiguating within the drf-yasg swagger schema + ref_name = 'commerce.Course' + def validate(self, attrs): """ Ensure the verification deadline occurs AFTER the course mode enrollment deadlines. """ verification_deadline = attrs.get('verification_deadline', None) diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index e6814350e0..da0a7395ed 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -147,3 +147,5 @@ class UserSerializer(serializers.ModelSerializer): model = User fields = ('id', 'username', 'email', 'name', 'course_enrollments') lookup_field = 'username' + # For disambiguating within the drf-yasg swagger schema + ref_name = 'mobile_api.User' diff --git a/openedx/core/djangoapps/enrollments/serializers.py b/openedx/core/djangoapps/enrollments/serializers.py index 5e946527f9..2b7a3a6b55 100644 --- a/openedx/core/djangoapps/enrollments/serializers.py +++ b/openedx/core/djangoapps/enrollments/serializers.py @@ -45,6 +45,10 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth invite_only = serializers.BooleanField(source="invitation_only") course_modes = serializers.SerializerMethodField() + class Meta(object): + # For disambiguating within the drf-yasg swagger schema + ref_name = 'enrollment.Course' + def __init__(self, *args, **kwargs): self.include_expired = kwargs.pop("include_expired", False) super(CourseSerializer, self).__init__(*args, **kwargs) diff --git a/openedx/core/djangoapps/user_api/serializers.py b/openedx/core/djangoapps/user_api/serializers.py index ae515de0ae..76dcdb7fe5 100644 --- a/openedx/core/djangoapps/user_api/serializers.py +++ b/openedx/core/djangoapps/user_api/serializers.py @@ -34,6 +34,8 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): # This list is the minimal set required by the notification service fields = ("id", "url", "email", "name", "username", "preferences") read_only_fields = ("id", "email", "username") + # For disambiguating within the drf-yasg swagger schema + ref_name = 'user_api.User' class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer): From 135cbe76d8d4e9f6c4994de45139891645302397 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 28 May 2019 13:08:27 -0400 Subject: [PATCH 11/13] yasg settings --- openedx/core/openapi.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openedx/core/openapi.py b/openedx/core/openapi.py index c7c9a162e4..d4ab94a08d 100644 --- a/openedx/core/openapi.py +++ b/openedx/core/openapi.py @@ -8,12 +8,12 @@ from drf_yasg import openapi schema_view = get_schema_view( openapi.Info( - title="Snippets API", - default_version='v1', - description="Test description", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="contact@snippets.local"), - license=openapi.License(name="BSD License"), + title="Open edX API", + default_version="v1", + description="APIs for access to Open edX information", + #terms_of_service="https://www.google.com/policies/terms/", # TODO: Do we have these? + contact=openapi.Contact(email="oscm@edx.org"), + #license=openapi.License(name="BSD License"), # TODO: What does this mean? ), public=True, permission_classes=(permissions.AllowAny,), From f0fa5f7169c815108f5fd359b4c2bde47c7ef1de Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 7 Jun 2019 13:57:06 -0400 Subject: [PATCH 12/13] Enable Studio API docs in devstack --- cms/envs/common.py | 2 +- cms/envs/devstack.py | 4 ++++ cms/urls.py | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 39478d627e..be50ede84b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1173,7 +1173,7 @@ INSTALLED_APPS = [ 'pipeline_mako', # API Documentation - 'rest_framework_swagger', + 'drf_yasg', 'openedx.features.course_duration_limits', 'openedx.features.content_type_gating', diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index edc70d1c75..1222a2a21a 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -108,6 +108,10 @@ def should_show_debug_toolbar(request): DEBUG_TOOLBAR_MONGO_STACKTRACES = False +########################### API DOCS ################################# + +FEATURES['ENABLE_API_DOCS'] = True + ################################ MILESTONES ################################ FEATURES['MILESTONES_APP'] = True diff --git a/cms/urls.py b/cms/urls.py index 04edae7027..251aea690d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -3,7 +3,7 @@ from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib.admin import autodiscover as django_autodiscover from django.utils.translation import ugettext_lazy as _ -from rest_framework_swagger.views import get_swagger_view +from openedx.core.openapi import schema_view import contentstore.views from cms.djangoapps.contentstore.views.organization import OrganizationListView @@ -266,7 +266,9 @@ urlpatterns += [ if settings.FEATURES.get('ENABLE_API_DOCS'): urlpatterns += [ - url(r'^api-docs/$', get_swagger_view(title='Studio API')), + url(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)), ] from openedx.core.djangoapps.plugins import constants as plugin_constants, plugin_urls From 7b9040f6b03d41b7a654f6be693638b4ab93d79c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 7 Jun 2019 15:42:06 -0400 Subject: [PATCH 13/13] This enum was backwards --- cms/djangoapps/api/v1/serializers/course_runs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/api/v1/serializers/course_runs.py b/cms/djangoapps/api/v1/serializers/course_runs.py index 44f722e79e..c49659ab92 100644 --- a/cms/djangoapps/api/v1/serializers/course_runs.py +++ b/cms/djangoapps/api/v1/serializers/course_runs.py @@ -125,7 +125,7 @@ class CourseRunImageSerializer(serializers.Serializer): class CourseRunSerializerCommonFieldsMixin(serializers.Serializer): schedule = CourseRunScheduleSerializer(source='*', required=False) pacing_type = CourseRunPacingTypeField(source='self_paced', required=False, - choices=(('instructor_paced', False), ('self_paced', True),)) + choices=((False, 'instructor_paced'), (True, 'self_paced'),)) class CourseRunSerializer(CourseRunSerializerCommonFieldsMixin, CourseRunTeamSerializerMixin, serializers.Serializer):