From 5d6e925c831cab92fa337150765d218111b7e95c Mon Sep 17 00:00:00 2001 From: bszabo Date: Fri, 3 Nov 2023 11:49:35 -0400 Subject: [PATCH] feat: TNL-11173 Authoring API is v0 for now (#33644) * feat: TNL-11173 authoring API offered as v0, not v1 * docs: correct swaggerfile for authoring api --------- Co-authored-by: Bernard Szabo Co-authored-by: Jesper Hodge --- .../rest_api/serializers/__init__.py | 4 + .../rest_api/{v1 => }/serializers/common.py | 0 .../contentstore/rest_api/v0/__init__.py | 12 ++ .../rest_api/v0/serializers/__init__.py | 3 + .../rest_api/{v1 => v0}/serializers/assets.py | 2 +- .../{v1 => v0}/serializers/transcripts.py | 2 +- .../rest_api/{v1 => v0}/serializers/xblock.py | 2 +- .../{v1/views => v0}/tests/test_assets.py | 11 +- .../{v1/views => v0}/tests/test_xblock.py | 10 +- .../contentstore/rest_api/v0/urls.py | 52 +++++- .../rest_api/{v1 => v0}/views/assets.py | 4 +- .../rest_api/v0/views/authoring_videos.py | 167 ++++++++++++++++++ .../rest_api/{v1 => v0}/views/transcripts.py | 6 +- .../rest_api/{v1 => v0}/views/utils.py | 0 .../rest_api/{v1 => v0}/views/xblock.py | 2 +- .../rest_api/v1/serializers/__init__.py | 7 +- .../rest_api/v1/serializers/home.py | 2 +- .../rest_api/v1/serializers/settings.py | 2 +- .../rest_api/v1/serializers/videos.py | 2 +- .../contentstore/rest_api/v1/urls.py | 48 +---- .../rest_api/v1/views/__init__.py | 7 - .../contentstore/rest_api/v1/views/videos.py | 152 +--------------- cms/envs/common.py | 2 +- cms/lib/spectacular.py | 10 +- cms/urls.py | 5 +- 25 files changed, 277 insertions(+), 237 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/serializers/__init__.py rename cms/djangoapps/contentstore/rest_api/{v1 => }/serializers/common.py (100%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/serializers/assets.py (78%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/serializers/transcripts.py (83%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/serializers/xblock.py (98%) rename cms/djangoapps/contentstore/rest_api/{v1/views => v0}/tests/test_assets.py (95%) rename cms/djangoapps/contentstore/rest_api/{v1/views => v0}/tests/test_xblock.py (96%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/views/assets.py (96%) create mode 100644 cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/views/transcripts.py (91%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/views/utils.py (100%) rename cms/djangoapps/contentstore/rest_api/{v1 => v0}/views/xblock.py (97%) diff --git a/cms/djangoapps/contentstore/rest_api/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/serializers/__init__.py new file mode 100644 index 000000000000..9d207ee767ea --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/serializers/__init__.py @@ -0,0 +1,4 @@ +""" +Serializers for all contentstore API versions +""" +from .common import StrictSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/common.py b/cms/djangoapps/contentstore/rest_api/serializers/common.py similarity index 100% rename from cms/djangoapps/contentstore/rest_api/v1/serializers/common.py rename to cms/djangoapps/contentstore/rest_api/serializers/common.py diff --git a/cms/djangoapps/contentstore/rest_api/v0/__init__.py b/cms/djangoapps/contentstore/rest_api/v0/__init__.py index e69de29bb2d1..4ceefe6ead69 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v0/__init__.py @@ -0,0 +1,12 @@ +""" +Views for v0 contentstore API. +""" + +from cms.djangoapps.contentstore.rest_api.v0.views.assets import ( + AssetsCreateRetrieveView, + AssetsUpdateDestroyView +) +from cms.djangoapps.contentstore.rest_api.v0.views.xblock import ( + XblockView, + XblockCreateView +) diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py index 0e799ab1cc21..4ca1f5f05a24 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py @@ -2,4 +2,7 @@ Serializers for v0 contentstore API. """ from .advanced_settings import AdvancedSettingsFieldSerializer, CourseAdvancedSettingsSerializer +from .assets import AssetSerializer from .tabs import CourseTabSerializer, CourseTabUpdateSerializer, TabIDLocatorSerializer +from .transcripts import TranscriptSerializer +from .xblock import XblockSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/assets.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/assets.py similarity index 78% rename from cms/djangoapps/contentstore/rest_api/v1/serializers/assets.py rename to cms/djangoapps/contentstore/rest_api/v0/serializers/assets.py index e44c31b511ed..7ecb473d1ade 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/assets.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/assets.py @@ -2,7 +2,7 @@ API Serializers for assets """ from rest_framework import serializers -from .common import StrictSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import StrictSerializer class AssetSerializer(StrictSerializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/transcripts.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/transcripts.py similarity index 83% rename from cms/djangoapps/contentstore/rest_api/v1/serializers/transcripts.py rename to cms/djangoapps/contentstore/rest_api/v0/serializers/transcripts.py index 2b72f1ff441e..bf6ea1d9f382 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/transcripts.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/transcripts.py @@ -2,7 +2,7 @@ API Serializers for transcripts """ from rest_framework import serializers -from .common import StrictSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import StrictSerializer class TranscriptSerializer(StrictSerializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/xblock.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py similarity index 98% rename from cms/djangoapps/contentstore/rest_api/v1/serializers/xblock.py rename to cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py index 522a34e0777d..4549326c9696 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py @@ -2,7 +2,7 @@ API Serializers for xblocks """ from rest_framework import serializers -from .common import StrictSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import StrictSerializer # The XblockSerializer is designed to be scalable and generic. As such, its structure # should remain as general as possible. Avoid indiscriminately adding fields to it, diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_assets.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py similarity index 95% rename from cms/djangoapps/contentstore/rest_api/v1/views/tests/test_assets.py rename to cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py index 0561a781c656..38429910edd1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_assets.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py @@ -21,6 +21,7 @@ ASSET_KEY_STRING = "asset-v1:dede+aba+weagi+type@asset+block@_0e37192a-42c4-441e-a3e1-8e40ec304e2e.jpg" mock_image = MagicMock(file=File) mock_image.name = "test.jpg" +VERSION = "v0" class AssetsViewTestCase(AuthorizeStaffTestCase): @@ -44,7 +45,7 @@ def get_url_params(self): def get_url(self, _course_id=None): return reverse( - "cms.djangoapps.contentstore:v1:cms_api_update_destroy_assets", + f"cms.djangoapps.contentstore:{VERSION}:cms_api_update_destroy_assets", kwargs=self.get_url_params(), ) @@ -52,7 +53,7 @@ def send_request(self, _url, _data): raise NotImplementedError("send_request must be implemented by subclasses") @patch( - "cms.djangoapps.contentstore.rest_api.v1.views.assets.handle_assets", + f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.assets.handle_assets", return_value=JsonResponse( { "locator": ASSET_KEY_STRING, @@ -61,7 +62,7 @@ def send_request(self, _url, _data): ), ) @patch( - "cms.djangoapps.contentstore.rest_api.v1.views.xblock.toggles.use_studio_content_api", + f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.xblock.toggles.use_studio_content_api", return_value=True, ) def make_request( @@ -104,7 +105,7 @@ def get_url_params(self): def get_url(self, _course_id=None): return reverse( - "cms.djangoapps.contentstore:v1:cms_api_create_retrieve_assets", + f"cms.djangoapps.contentstore:{VERSION}:cms_api_create_retrieve_assets", kwargs=self.get_url_params(), ) @@ -156,7 +157,7 @@ def get_url_params(self): def get_url(self, _course_id=None): return reverse( - "cms.djangoapps.contentstore:v1:cms_api_create_retrieve_assets", + f"cms.djangoapps.contentstore:{VERSION}:cms_api_create_retrieve_assets", kwargs=self.get_url_params(), ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py similarity index 96% rename from cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock.py rename to cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py index fe214c2f8113..e4c21eb353b1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py @@ -15,6 +15,7 @@ TEST_LOCATOR = "block-v1:dede+aba+weagi+type@problem+block@ba6327f840da49289fb27a9243913478" +VERSION = "v0" class XBlockViewTestCase(AuthorizeStaffTestCase): @@ -38,7 +39,7 @@ def get_url_params(self): def get_url(self, _course_id=None): return reverse( - "cms.djangoapps.contentstore:v1:cms_api_xblock", + f"cms.djangoapps.contentstore:{VERSION}:cms_api_xblock", kwargs=self.get_url_params(), ) @@ -46,7 +47,7 @@ def send_request(self, _url, _data): raise NotImplementedError("send_request must be implemented by subclasses") @patch( - "cms.djangoapps.contentstore.rest_api.v1.views.xblock.handle_xblock", + f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.xblock.handle_xblock", return_value=JsonResponse( { "locator": TEST_LOCATOR, @@ -55,7 +56,7 @@ def send_request(self, _url, _data): ), ) @patch( - "cms.djangoapps.contentstore.rest_api.v1.views.xblock.toggles.use_studio_content_api", + f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.xblock.toggles.use_studio_content_api", return_value=True, ) def make_request( @@ -134,13 +135,14 @@ class XBlockViewPostTest(XBlockViewTestCase, ModuleStoreTestCase, APITestCase): """ Test POST operation on xblocks - Create a new xblock for a parent xblock """ + VERSION = "v0" def get_url_params(self): return {"course_id": self.get_course_key_string()} def get_url(self, _course_id=None): return reverse( - "cms.djangoapps.contentstore:v1:cms_api_create_xblock", + f"cms.djangoapps.contentstore:{VERSION}:cms_api_create_xblock", kwargs=self.get_url_params(), ) diff --git a/cms/djangoapps/contentstore/rest_api/v0/urls.py b/cms/djangoapps/contentstore/rest_api/v0/urls.py index eb435ef338d8..8c9a772c34fe 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v0/urls.py @@ -1,12 +1,20 @@ """ Contenstore API v0 URLs. """ -from django.urls import re_path +from django.conf import settings +from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN + from .views import AdvancedCourseSettingsView, CourseTabSettingsView, CourseTabListView, CourseTabReorderView +from .views import assets +from .views import transcripts +from .views import authoring_videos +from .views import xblock app_name = "v0" +VIDEO_ID_PATTERN = r'(?P[-\w]+)' + urlpatterns = [ re_path( fr"^advanced_settings/{COURSE_ID_PATTERN}$", @@ -28,4 +36,46 @@ CourseTabReorderView.as_view(), name="course_tab_reorder", ), + + # Authoring API + re_path( + fr'^file_assets/{settings.COURSE_ID_PATTERN}/$', + assets.AssetsCreateRetrieveView.as_view(), name='cms_api_create_retrieve_assets' + ), + re_path( + fr'^file_assets/{settings.COURSE_ID_PATTERN}/{settings.ASSET_KEY_PATTERN}$', + assets.AssetsUpdateDestroyView.as_view(), name='cms_api_update_destroy_assets' + ), + re_path( + fr'^videos/encodings/{settings.COURSE_ID_PATTERN}$', + authoring_videos.VideoEncodingsDownloadView.as_view(), name='cms_api_videos_encodings' + ), + path( + 'videos/features/', + authoring_videos.VideoFeaturesView.as_view(), name='cms_api_videos_features' + ), + re_path( + fr'^videos/images/{settings.COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}$', + authoring_videos.VideoImagesView.as_view(), name='cms_api_videos_images' + ), + re_path( + fr'^videos/uploads/{settings.COURSE_ID_PATTERN}/$', + authoring_videos.VideosCreateUploadView.as_view(), name='cms_api_create_videos_upload' + ), + re_path( + fr'^videos/uploads/{settings.COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}$', + authoring_videos.VideosUploadsView.as_view(), name='cms_api_videos_uploads' + ), + re_path( + fr'^video_transcripts/{settings.COURSE_ID_PATTERN}$', + transcripts.TranscriptView.as_view(), name='cms_api_video_transcripts' + ), + re_path( + fr'^xblock/{settings.COURSE_ID_PATTERN}/$', + xblock.XblockCreateView.as_view(), name='cms_api_create_xblock' + ), + re_path( + fr'^xblock/{settings.COURSE_ID_PATTERN}/{settings.USAGE_KEY_PATTERN}$', + xblock.XblockView.as_view(), name='cms_api_xblock' + ), ] diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/assets.py b/cms/djangoapps/contentstore/rest_api/v0/views/assets.py similarity index 96% rename from cms/djangoapps/contentstore/rest_api/v1/views/assets.py rename to cms/djangoapps/contentstore/rest_api/v0/views/assets.py index 1cc601dca9f6..0c0c24aeab69 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/assets.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/assets.py @@ -9,12 +9,12 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes from common.djangoapps.util.json_request import expect_json_in_class_view -from ....api import course_author_access_required +from cms.djangoapps.contentstore.api import course_author_access_required from cms.djangoapps.contentstore.asset_storage_handlers import handle_assets import cms.djangoapps.contentstore.toggles as contentstore_toggles -from cms.djangoapps.contentstore.rest_api.v1.serializers import AssetSerializer +from ..serializers.assets import AssetSerializer from .utils import validate_request_with_serializer from rest_framework.parsers import (MultiPartParser, FormParser, JSONParser) from openedx.core.lib.api.parsers import TypedFileUploadParser diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py new file mode 100644 index 000000000000..972b6229f55a --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py @@ -0,0 +1,167 @@ +""" +Public rest API endpoints for the Authoring API video assets. +""" +import logging +from rest_framework.generics import ( + CreateAPIView, + RetrieveAPIView, + DestroyAPIView +) +from rest_framework.parsers import (MultiPartParser, FormParser) +from django.views.decorators.csrf import csrf_exempt +from django.http import Http404 + +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.core.lib.api.parsers import TypedFileUploadParser +from common.djangoapps.util.json_request import expect_json_in_class_view + +from ....api import course_author_access_required + +from cms.djangoapps.contentstore.video_storage_handlers import ( + handle_videos, + get_video_encodings_download, + handle_video_images, + enabled_video_features +) +from cms.djangoapps.contentstore.rest_api.v1.serializers import ( + VideoUploadSerializer, + VideoImageSerializer, +) +import cms.djangoapps.contentstore.toggles as contentstore_toggles +from .utils import validate_request_with_serializer + + +log = logging.getLogger(__name__) +toggles = contentstore_toggles + + +@view_auth_classes() +class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView): + """ + public rest API endpoints for the CMS API video assets. + course_key: required argument, needed to authorize course authors and identify the video. + video_id: required argument, needed to identify the video. + """ + serializer_class = VideoUploadSerializer + + def dispatch(self, request, *args, **kwargs): + # TODO: probably want to refactor this to a decorator. + """ + The dispatch method of a View class handles HTTP requests in general + and calls other methods to handle specific HTTP methods. + We use this to raise a 404 if the content api is disabled. + """ + if not toggles.use_studio_content_api(): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + @course_author_access_required + def retrieve(self, request, course_key, edx_video_id=None): # pylint: disable=arguments-differ + return handle_videos(request, course_key.html_id(), edx_video_id) + + @course_author_access_required + @expect_json_in_class_view + def destroy(self, request, course_key, edx_video_id): # pylint: disable=arguments-differ + return handle_videos(request, course_key.html_id(), edx_video_id) + + +@view_auth_classes() +class VideosCreateUploadView(DeveloperErrorViewMixin, CreateAPIView): + """ + public rest API endpoints for the CMS API video assets. + course_key: required argument, needed to authorize course authors and identify the video. + """ + serializer_class = VideoUploadSerializer + + def dispatch(self, request, *args, **kwargs): + # TODO: probably want to refactor this to a decorator. + """ + The dispatch method of a View class handles HTTP requests in general + and calls other methods to handle specific HTTP methods. + We use this to raise a 404 if the content api is disabled. + """ + if not toggles.use_studio_content_api(): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + @csrf_exempt + @course_author_access_required + @expect_json_in_class_view + @validate_request_with_serializer + def create(self, request, course_key): # pylint: disable=arguments-differ + return handle_videos(request, course_key.html_id()) + + +@view_auth_classes() +class VideoImagesView(DeveloperErrorViewMixin, CreateAPIView): + """ + public rest API endpoint for uploading a video image. + course_key: required argument, needed to authorize course authors and identify the video. + video_id: required argument, needed to identify the video. + """ + serializer_class = VideoImageSerializer + parser_classes = (MultiPartParser, FormParser, TypedFileUploadParser) + + def dispatch(self, request, *args, **kwargs): + # TODO: probably want to refactor this to a decorator. + """ + The dispatch method of a View class handles HTTP requests in general + and calls other methods to handle specific HTTP methods. + We use this to raise a 404 if the content api is disabled. + """ + if not toggles.use_studio_content_api(): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + @csrf_exempt + @course_author_access_required + @expect_json_in_class_view + @validate_request_with_serializer + def create(self, request, course_key, edx_video_id=None): # pylint: disable=arguments-differ + return handle_video_images(request, course_key.html_id(), edx_video_id) + + +@view_auth_classes() +class VideoEncodingsDownloadView(DeveloperErrorViewMixin, RetrieveAPIView): + """ + public rest API endpoint providing a CSV report containing the encoded video URLs for video uploads. + course_key: required argument, needed to authorize course authors and identify relevant videos. + """ + + def dispatch(self, request, *args, **kwargs): + # TODO: probably want to refactor this to a decorator. + """ + The dispatch method of a View class handles HTTP requests in general + and calls other methods to handle specific HTTP methods. + We use this to raise a 404 if the content api is disabled. + """ + if not toggles.use_studio_content_api(): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + @csrf_exempt + @course_author_access_required + def retrieve(self, request, course_key): # pylint: disable=arguments-differ + return get_video_encodings_download(request, course_key.html_id()) + + +@view_auth_classes() +class VideoFeaturesView(DeveloperErrorViewMixin, RetrieveAPIView): + """ + public rest API endpoint providing a list of enabled video features. + """ + + def dispatch(self, request, *args, **kwargs): + # TODO: probably want to refactor this to a decorator. + """ + The dispatch method of a View class handles HTTP requests in general + and calls other methods to handle specific HTTP methods. + We use this to raise a 404 if the content api is disabled. + """ + if not toggles.use_studio_content_api(): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + @csrf_exempt + def retrieve(self, request): # pylint: disable=arguments-differ + return enabled_video_features(request) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/transcripts.py b/cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py similarity index 91% rename from cms/djangoapps/contentstore/rest_api/v1/views/transcripts.py rename to cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py index f621929f9cc7..c97261c4292e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/transcripts.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py @@ -13,7 +13,7 @@ from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes from common.djangoapps.util.json_request import expect_json_in_class_view -from ....api import course_author_access_required +from cms.djangoapps.contentstore.api import course_author_access_required from cms.djangoapps.contentstore.transcript_storage_handlers import ( upload_transcript, @@ -21,11 +21,11 @@ handle_transcript_download, ) import cms.djangoapps.contentstore.toggles as contentstore_toggles -from cms.djangoapps.contentstore.rest_api.v1.serializers import TranscriptSerializer +from ..serializers import TranscriptSerializer from rest_framework.parsers import (MultiPartParser, FormParser) from openedx.core.lib.api.parsers import TypedFileUploadParser -from .utils import validate_request_with_serializer +from cms.djangoapps.contentstore.rest_api.v0.views.utils import validate_request_with_serializer log = logging.getLogger(__name__) toggles = contentstore_toggles diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/utils.py b/cms/djangoapps/contentstore/rest_api/v0/views/utils.py similarity index 100% rename from cms/djangoapps/contentstore/rest_api/v1/views/utils.py rename to cms/djangoapps/contentstore/rest_api/v0/views/utils.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/xblock.py b/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py similarity index 97% rename from cms/djangoapps/contentstore/rest_api/v1/views/xblock.py rename to cms/djangoapps/contentstore/rest_api/v0/views/xblock.py index ffeceb0265fc..cc26619fb83a 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py @@ -13,7 +13,7 @@ from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers import cms.djangoapps.contentstore.toggles as contentstore_toggles -from cms.djangoapps.contentstore.rest_api.v1.serializers import XblockSerializer +from ..serializers import XblockSerializer from .utils import validate_request_with_serializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 2d65d60139a7..ac1f2cd1fb54 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -1,11 +1,11 @@ """ Serializers for v1 contentstore API. """ -from .home import CourseHomeSerializer from .course_details import CourseDetailsSerializer -from .course_team import CourseTeamSerializer from .course_rerun import CourseRerunSerializer +from .course_team import CourseTeamSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer +from .home import CourseHomeSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, @@ -13,12 +13,9 @@ ProctoringErrorsSerializer ) from .settings import CourseSettingsSerializer -from .xblock import XblockSerializer from .videos import ( CourseVideosSerializer, VideoUploadSerializer, VideoImageSerializer, VideoUsageSerializer ) -from .transcripts import TranscriptSerializer -from .assets import AssetSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index aefab2c6b1df..12816a8cbd1d 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -6,7 +6,7 @@ from openedx.core.lib.api.serializers import CourseKeyField -from .common import CourseCommonSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import CourseCommonSerializer class UnsucceededCourseSerializer(serializers.Serializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py index 742d198a7ad4..1c9f9f608478 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py @@ -4,7 +4,7 @@ from rest_framework import serializers -from .common import CourseCommonSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import CourseCommonSerializer class CourseSettingsSerializer(serializers.Serializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py index 657e4339b8cd..852117629b68 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -2,7 +2,7 @@ API Serializers for videos """ from rest_framework import serializers -from .common import StrictSerializer +from cms.djangoapps.contentstore.rest_api.serializers.common import StrictSerializer class FileSpecSerializer(StrictSerializer): diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index b9f68aa3e982..ad5765a67396 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,6 +1,5 @@ """ Contenstore API v1 URLs. """ -from django.conf import settings from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN @@ -15,10 +14,6 @@ HomePageView, ProctoredExamSettingsView, ProctoringErrorsView, - xblock, - assets, - videos, - transcripts, HelpUrlsView, VideoUsageView ) @@ -84,45 +79,6 @@ name="course_rerun" ), - # CMS API - re_path( - fr'^file_assets/{settings.COURSE_ID_PATTERN}/$', - assets.AssetsCreateRetrieveView.as_view(), name='cms_api_create_retrieve_assets' - ), - re_path( - fr'^file_assets/{settings.COURSE_ID_PATTERN}/{settings.ASSET_KEY_PATTERN}$', - assets.AssetsUpdateDestroyView.as_view(), name='cms_api_update_destroy_assets' - ), - re_path( - fr'^videos/encodings/{settings.COURSE_ID_PATTERN}$', - videos.VideoEncodingsDownloadView.as_view(), name='cms_api_videos_encodings' - ), - path( - 'videos/features/', - videos.VideoFeaturesView.as_view(), name='cms_api_videos_features' - ), - re_path( - fr'^videos/images/{settings.COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}$', - videos.VideoImagesView.as_view(), name='cms_api_videos_images' - ), - re_path( - fr'^videos/uploads/{settings.COURSE_ID_PATTERN}/$', - videos.VideosCreateUploadView.as_view(), name='cms_api_create_videos_upload' - ), - re_path( - fr'^videos/uploads/{settings.COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}$', - videos.VideosUploadsView.as_view(), name='cms_api_videos_uploads' - ), - re_path( - fr'^video_transcripts/{settings.COURSE_ID_PATTERN}$', - transcripts.TranscriptView.as_view(), name='cms_api_video_transcripts' - ), - re_path( - fr'^xblock/{settings.COURSE_ID_PATTERN}/$', - xblock.XblockCreateView.as_view(), name='cms_api_create_xblock' - ), - re_path( - fr'^xblock/{settings.COURSE_ID_PATTERN}/{settings.USAGE_KEY_PATTERN}$', - xblock.XblockView.as_view(), name='cms_api_xblock' - ), + # Authoring API + # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used ] diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index dfa87a4a34b3..780f0059aaed 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -8,15 +8,8 @@ from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView from .home import HomePageView from .settings import CourseSettingsView -from .xblock import XblockView, XblockCreateView -from .assets import AssetsCreateRetrieveView, AssetsUpdateDestroyView from .videos import ( CourseVideosView, - VideosUploadsView, - VideosCreateUploadView, - VideoImagesView, - VideoEncodingsDownloadView, - VideoFeaturesView, VideoUsageView, ) from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index d4ea444bd134..b6d6dcfbae20 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -1,44 +1,26 @@ """ -Public rest API endpoints for the CMS API video assets. +Public rest API endpoints for contentstore API video assets (outside authoring API) """ import edx_api_doc_tools as apidocs import logging from opaque_keys.edx.keys import CourseKey -from rest_framework.generics import ( - CreateAPIView, - RetrieveAPIView, - DestroyAPIView -) from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.parsers import (MultiPartParser, FormParser) -from django.views.decorators.csrf import csrf_exempt -from django.http import Http404 from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes, verify_course_exists -from openedx.core.lib.api.parsers import TypedFileUploadParser from common.djangoapps.student.auth import has_studio_read_access -from common.djangoapps.util.json_request import expect_json_in_class_view -from ....api import course_author_access_required from ....utils import get_course_videos_context from cms.djangoapps.contentstore.video_storage_handlers import ( - handle_videos, - get_video_encodings_download, - handle_video_images, - enabled_video_features, get_video_usage_path ) from cms.djangoapps.contentstore.rest_api.v1.serializers import ( CourseVideosSerializer, - VideoUploadSerializer, - VideoImageSerializer, VideoUsageSerializer, ) import cms.djangoapps.contentstore.toggles as contentstore_toggles -from .utils import validate_request_with_serializer log = logging.getLogger(__name__) @@ -198,135 +180,3 @@ def get(self, request: Request, course_id: str, edx_video_id: str): usage_locations = get_video_usage_path(request, course_key, edx_video_id) serializer = VideoUsageSerializer(usage_locations) return Response(serializer.data) - - -@view_auth_classes() -class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView): - """ - public rest API endpoints for the CMS API video assets. - course_key: required argument, needed to authorize course authors and identify the video. - video_id: required argument, needed to identify the video. - """ - serializer_class = VideoUploadSerializer - - def dispatch(self, request, *args, **kwargs): - # TODO: probably want to refactor this to a decorator. - """ - The dispatch method of a View class handles HTTP requests in general - and calls other methods to handle specific HTTP methods. - We use this to raise a 404 if the content api is disabled. - """ - if not toggles.use_studio_content_api(): - raise Http404 - return super().dispatch(request, *args, **kwargs) - - @course_author_access_required - def retrieve(self, request, course_key, edx_video_id=None): # pylint: disable=arguments-differ - return handle_videos(request, course_key.html_id(), edx_video_id) - - @course_author_access_required - @expect_json_in_class_view - def destroy(self, request, course_key, edx_video_id): # pylint: disable=arguments-differ - return handle_videos(request, course_key.html_id(), edx_video_id) - - -@view_auth_classes() -class VideosCreateUploadView(DeveloperErrorViewMixin, CreateAPIView): - """ - public rest API endpoints for the CMS API video assets. - course_key: required argument, needed to authorize course authors and identify the video. - """ - serializer_class = VideoUploadSerializer - - def dispatch(self, request, *args, **kwargs): - # TODO: probably want to refactor this to a decorator. - """ - The dispatch method of a View class handles HTTP requests in general - and calls other methods to handle specific HTTP methods. - We use this to raise a 404 if the content api is disabled. - """ - if not toggles.use_studio_content_api(): - raise Http404 - return super().dispatch(request, *args, **kwargs) - - @csrf_exempt - @course_author_access_required - @expect_json_in_class_view - @validate_request_with_serializer - def create(self, request, course_key): # pylint: disable=arguments-differ - return handle_videos(request, course_key.html_id()) - - -@view_auth_classes() -class VideoImagesView(DeveloperErrorViewMixin, CreateAPIView): - """ - public rest API endpoint for uploading a video image. - course_key: required argument, needed to authorize course authors and identify the video. - video_id: required argument, needed to identify the video. - """ - serializer_class = VideoImageSerializer - parser_classes = (MultiPartParser, FormParser, TypedFileUploadParser) - - def dispatch(self, request, *args, **kwargs): - # TODO: probably want to refactor this to a decorator. - """ - The dispatch method of a View class handles HTTP requests in general - and calls other methods to handle specific HTTP methods. - We use this to raise a 404 if the content api is disabled. - """ - if not toggles.use_studio_content_api(): - raise Http404 - return super().dispatch(request, *args, **kwargs) - - @csrf_exempt - @course_author_access_required - @expect_json_in_class_view - @validate_request_with_serializer - def create(self, request, course_key, edx_video_id=None): # pylint: disable=arguments-differ - return handle_video_images(request, course_key.html_id(), edx_video_id) - - -@view_auth_classes() -class VideoEncodingsDownloadView(DeveloperErrorViewMixin, RetrieveAPIView): - """ - public rest API endpoint providing a CSV report containing the encoded video URLs for video uploads. - course_key: required argument, needed to authorize course authors and identify relevant videos. - """ - - def dispatch(self, request, *args, **kwargs): - # TODO: probably want to refactor this to a decorator. - """ - The dispatch method of a View class handles HTTP requests in general - and calls other methods to handle specific HTTP methods. - We use this to raise a 404 if the content api is disabled. - """ - if not toggles.use_studio_content_api(): - raise Http404 - return super().dispatch(request, *args, **kwargs) - - @csrf_exempt - @course_author_access_required - def retrieve(self, request, course_key): # pylint: disable=arguments-differ - return get_video_encodings_download(request, course_key.html_id()) - - -@view_auth_classes() -class VideoFeaturesView(DeveloperErrorViewMixin, RetrieveAPIView): - """ - public rest API endpoint providing a list of enabled video features. - """ - - def dispatch(self, request, *args, **kwargs): - # TODO: probably want to refactor this to a decorator. - """ - The dispatch method of a View class handles HTTP requests in general - and calls other methods to handle specific HTTP methods. - We use this to raise a 404 if the content api is disabled. - """ - if not toggles.use_studio_content_api(): - raise Http404 - return super().dispatch(request, *args, **kwargs) - - @csrf_exempt - def retrieve(self, request): # pylint: disable=arguments-differ - return enabled_video_features(request) diff --git a/cms/envs/common.py b/cms/envs/common.py index 3b52ead574c5..64c2c24538dd 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2790,7 +2790,7 @@ 'DESCRIPTION': 'Experimental API to edit xblocks and course content. Danger: Do not use on running courses!', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, - 'PREPROCESSING_HOOKS': ['cms.lib.spectacular.cms_api_filter'], # restrict spectacular to CMS API endpoints + 'PREPROCESSING_HOOKS': ['cms.lib.spectacular.cms_api_filter'], # restrict spectacular to CMS API endpoints. (cms/lib/spectacular.py) } diff --git a/cms/lib/spectacular.py b/cms/lib/spectacular.py index d05d71353157..4318269bebcc 100644 --- a/cms/lib/spectacular.py +++ b/cms/lib/spectacular.py @@ -10,10 +10,12 @@ def cms_api_filter(endpoints): for (path, path_regex, method, callback) in endpoints: # Add only paths to the list that are part of the CMS API if ( - path.startswith("/api/contentstore/v1/xblock") or - path.startswith("/api/contentstore/v1/videos") or - path.startswith("/api/contentstore/v1/video_transcripts") or - path.startswith("/api/contentstore/v1/file_assets") + # Don't just replace this with /v1 when switching to a later version of the CMS API. + # That would include some unintended endpoints. + path.startswith("/api/contentstore/v0/xblock") or + path.startswith("/api/contentstore/v0/videos") or + path.startswith("/api/contentstore/v0/video_transcripts") or + path.startswith("/api/contentstore/v0/file_assets") ): filtered.append((path, path_regex, method, callback)) return filtered diff --git a/cms/urls.py b/cms/urls.py index b353e095ea27..973311b1c271 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -342,7 +342,10 @@ path('api/content_tagging/', include(('openedx.core.djangoapps.content_tagging.urls', 'content_tagging'))), ] -# studio-content-api specific API docs (using drf-spectacular and openapi-v3) +# Authoring-api specific API docs (using drf-spectacular and openapi-v3). +# This is separate from and in addition to the full studio swagger documentation already existing at /api-docs. +# Custom settings are provided in SPECTACULAR_SETTINGS in cms/envs/common.py. +# Filter function in cms/lib/spectacular.py determines paths that are swagger-documented. urlpatterns += [ re_path('^cms-api/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), re_path('^cms-api/schema/', SpectacularAPIView.as_view(), name='schema'),