From e16925e8676c2b67aad4a917c198945b9773d0af Mon Sep 17 00:00:00 2001 From: Kyle D McCormick Date: Mon, 7 Apr 2025 09:07:49 -0400 Subject: [PATCH] fix: Globally Enable Studio Content REST API There was a waffle flag `contentstore.enable_studio_content_api`, intended to gate the "experimental" REST APIs at `/api/contentstore/v{0,1,2}/*`. In practice, these APIs are no longer experimental: for the past few named releases, they have been enabled in Tutor and used to power the Authoring MFE. We are making the Authoring MFE default-on in all Open edX sites starting in Teak, with the legacy authoring frontend slated for removal by Ulmo. Therefore, we need to remove flag which is gating the REST API. We are _not_ introducing a temporary opt-out toggle, as we do need feel it is necessary. Part of: https://github.com/openedx/edx-platform/issues/36275 --- .../rest_api/v0/tests/test_assets.py | 33 ----------- .../rest_api/v0/tests/test_xblock.py | 40 ------------- .../rest_api/v0/views/api_heartbeat.py | 5 +- .../contentstore/rest_api/v0/views/assets.py | 25 -------- .../rest_api/v0/views/authoring_videos.py | 58 ------------------- .../rest_api/v0/views/transcripts.py | 18 ------ .../contentstore/rest_api/v0/views/xblock.py | 25 -------- cms/djangoapps/contentstore/toggles.py | 25 +------- 8 files changed, 2 insertions(+), 227 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py index 46b636916df8..3772dbb64e77 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_assets.py @@ -60,13 +60,8 @@ def send_request(self, _url, _data): } ), ) - @patch( - f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.xblock.toggles.use_studio_content_api", - return_value=True, - ) def make_request( self, - mock_use_studio_content_api, mock_handle_assets, run_assertions=None, course_id=None, @@ -125,13 +120,6 @@ def assert_assets_handler_called(self, *, mock_handle_assets, response): def send_request(self, url, data): return self.client.get(url) - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_assets_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -182,13 +170,6 @@ def assert_assets_handler_called(self, *, mock_handle_assets, response): def send_request(self, url, data): return self.client.post(url, data=data, format="multipart") - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.post(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_assets_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -232,13 +213,6 @@ def assert_assets_handler_called(self, *, mock_handle_assets, response): def send_request(self, url, data): return self.client.put(url, data=data, format="json") - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.put(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_assets_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -277,13 +251,6 @@ def assert_assets_handler_called(self, *, mock_handle_assets, response): def send_request(self, url, data): return self.client.delete(url) - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_assets_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password diff --git a/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py b/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py index e4c21eb353b1..512c92f6ffe9 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/tests/test_xblock.py @@ -55,13 +55,8 @@ def send_request(self, _url, _data): } ), ) - @patch( - f"cms.djangoapps.contentstore.rest_api.{VERSION}.views.xblock.toggles.use_studio_content_api", - return_value=True, - ) def make_request( self, - mock_use_studio_content_api, mock_handle_xblock, run_assertions=None, course_id=None, @@ -111,13 +106,6 @@ def assert_xblock_handler_called(self, *, mock_handle_xblock, response): def send_request(self, url, data): return self.client.get(url) - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_xblock_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -167,13 +155,6 @@ def assert_xblock_handler_called(self, *, mock_handle_xblock, response): def send_request(self, url, data): return self.client.post(url, data=data, format="json") - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.post(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_xblock_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -218,13 +199,6 @@ def assert_xblock_handler_called(self, *, mock_handle_xblock, response): def send_request(self, url, data): return self.client.put(url, data=data, format="json") - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.put(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_xblock_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -269,13 +243,6 @@ def assert_xblock_handler_called(self, *, mock_handle_xblock, response): def send_request(self, url, data): return self.client.patch(url, data=data, format="json") - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.patch(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_xblock_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password @@ -310,13 +277,6 @@ def assert_xblock_handler_called(self, *, mock_handle_xblock, response): def send_request(self, url, data): return self.client.delete(url) - def test_api_behind_feature_flag(self): - # should return 404 if the feature flag is not enabled - url = self.get_url() - - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_xblock_handler_called_with_correct_arguments(self): self.client.login( username=self.course_instructor.username, password=self.password diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/api_heartbeat.py b/cms/djangoapps/contentstore/rest_api/v0/views/api_heartbeat.py index f8b539557e5e..f322f2360939 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/api_heartbeat.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/api_heartbeat.py @@ -5,7 +5,6 @@ from rest_framework.response import Response from rest_framework import status from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes -import cms.djangoapps.contentstore.toggles as toggles class APIHeartBeatView(DeveloperErrorViewMixin, APIView): @@ -43,6 +42,4 @@ def get(self, request: Request): } ``` """ - if toggles.use_studio_content_api(): - return Response({'status': 'heartbeat successful'}, status=status.HTTP_200_OK) - return Response(status=status.HTTP_403_FORBIDDEN) + return Response({'status': 'heartbeat successful'}, status=status.HTTP_200_OK) diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/assets.py b/cms/djangoapps/contentstore/rest_api/v0/views/assets.py index 0c0c24aeab69..a7a495256514 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/assets.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/assets.py @@ -4,7 +4,6 @@ import logging from rest_framework.generics import CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView 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 common.djangoapps.util.json_request import expect_json_in_class_view @@ -12,7 +11,6 @@ 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 ..serializers.assets import AssetSerializer from .utils import validate_request_with_serializer @@ -20,7 +18,6 @@ from openedx.core.lib.api.parsers import TypedFileUploadParser log = logging.getLogger(__name__) -toggles = contentstore_toggles @view_auth_classes() @@ -33,17 +30,6 @@ class AssetsCreateRetrieveView(DeveloperErrorViewMixin, CreateAPIView, RetrieveA serializer_class = AssetSerializer parser_classes = (JSONParser, 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 @validate_request_with_serializer @@ -66,17 +52,6 @@ class AssetsUpdateDestroyView(DeveloperErrorViewMixin, UpdateAPIView, DestroyAPI serializer_class = AssetSerializer parser_classes = (JSONParser, 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) - @course_author_access_required @expect_json_in_class_view @validate_request_with_serializer diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py index 8fb66070f3bf..f97bf7a98d16 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_videos.py @@ -9,7 +9,6 @@ ) 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 @@ -27,12 +26,10 @@ 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() @@ -44,17 +41,6 @@ class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView """ 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) @@ -73,17 +59,6 @@ class VideosCreateUploadView(DeveloperErrorViewMixin, CreateAPIView): """ 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 @@ -102,17 +77,6 @@ class VideoImagesView(DeveloperErrorViewMixin, CreateAPIView): 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 @@ -133,17 +97,6 @@ class VideoEncodingsDownloadView(DeveloperErrorViewMixin, RetrieveAPIView): # does not specify a serializer class. swagger_schema = None - 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 @@ -161,17 +114,6 @@ class VideoFeaturesView(DeveloperErrorViewMixin, RetrieveAPIView): # does not specify a serializer class. swagger_schema = None - 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/v0/views/transcripts.py b/cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py index 9a63693e12b9..3344fdbd1f43 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/transcripts.py @@ -8,7 +8,6 @@ DestroyAPIView ) 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 common.djangoapps.util.json_request import expect_json_in_class_view @@ -20,7 +19,6 @@ delete_video_transcript_or_404, handle_transcript_download, ) -import cms.djangoapps.contentstore.toggles as contentstore_toggles from ..serializers import TranscriptSerializer, YoutubeTranscriptCheckSerializer, YoutubeTranscriptUploadSerializer from rest_framework.parsers import (MultiPartParser, FormParser) from openedx.core.lib.api.parsers import TypedFileUploadParser @@ -28,7 +26,6 @@ from cms.djangoapps.contentstore.rest_api.v0.views.utils import validate_request_with_serializer log = logging.getLogger(__name__) -toggles = contentstore_toggles @view_auth_classes() @@ -42,11 +39,6 @@ class TranscriptView(DeveloperErrorViewMixin, CreateAPIView, RetrieveAPIView, De serializer_class = TranscriptSerializer parser_classes = (MultiPartParser, FormParser, TypedFileUploadParser) - def dispatch(self, request, *args, **kwargs): - 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 @@ -81,11 +73,6 @@ class YoutubeTranscriptCheckView(DeveloperErrorViewMixin, RetrieveAPIView): serializer_class = YoutubeTranscriptCheckSerializer parser_classes = (MultiPartParser, FormParser, TypedFileUploadParser) - def dispatch(self, request, *args, **kwargs): - 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_string): # pylint: disable=arguments-differ """ @@ -104,11 +91,6 @@ class YoutubeTranscriptUploadView(DeveloperErrorViewMixin, RetrieveAPIView): serializer_class = YoutubeTranscriptUploadSerializer parser_classes = (MultiPartParser, FormParser, TypedFileUploadParser) - def dispatch(self, request, *args, **kwargs): - 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_string): # pylint: disable=arguments-differ """ diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py b/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py index cc26619fb83a..8e678ae845e0 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/xblock.py @@ -4,21 +4,18 @@ import logging from rest_framework.generics import RetrieveUpdateDestroyAPIView, CreateAPIView 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 common.djangoapps.util.json_request import expect_json_in_class_view from cms.djangoapps.contentstore.api import course_author_access_required from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers -import cms.djangoapps.contentstore.toggles as contentstore_toggles from ..serializers import XblockSerializer from .utils import validate_request_with_serializer log = logging.getLogger(__name__) -toggles = contentstore_toggles handle_xblock = view_handlers.handle_xblock @@ -32,17 +29,6 @@ class XblockView(DeveloperErrorViewMixin, RetrieveUpdateDestroyAPIView): """ serializer_class = XblockSerializer - 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) - # pylint: disable=arguments-differ @course_author_access_required @expect_json_in_class_view @@ -77,17 +63,6 @@ class XblockCreateView(DeveloperErrorViewMixin, CreateAPIView): """ serializer_class = XblockSerializer - 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) - # pylint: disable=arguments-differ @csrf_exempt @course_author_access_required diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index af125e0d9b7a..33db42638f0e 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -5,6 +5,7 @@ from openedx.core.djangoapps.content.search import api as search_api from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag + # .. toggle_name: FEATURES['ENABLE_EXPORT_GIT'] # .. toggle_implementation: SettingDictToggle # .. toggle_default: False @@ -211,30 +212,6 @@ def individualize_anonymous_user_id(course_id): return INDIVIDUALIZE_ANONYMOUS_USER_ID.is_enabled(course_id) -# .. toggle_name: contentstore.enable_studio_content_api -# .. toggle_implementation: WaffleFlag -# .. toggle_default: False -# .. toggle_description: Enables the new (experimental and unsafe!) Studio Content REST API for course authors, -# .. which provides CRUD capabilities for course content and xblock editing. -# .. Use at your own peril - you can easily delete learner data when editing running courses. -# .. This can be triggered by deleting blocks, editing subsections, problems, assignments, discussions, -# .. creating new problems or graded sections, and by other things you do. -# .. toggle_use_cases: open_edx -# .. toggle_creation_date: 2023-05-26 -# .. toggle_tickets: TNL-10208 -ENABLE_STUDIO_CONTENT_API = WaffleFlag( - f'{CONTENTSTORE_NAMESPACE}.enable_studio_content_api', - __name__, -) - - -def use_studio_content_api(): - """ - Returns a boolean if studio editing API is enabled - """ - return ENABLE_STUDIO_CONTENT_API.is_enabled() - - # .. toggle_name: new_studio_mfe.use_new_home_page # .. toggle_implementation: WaffleFlag # .. toggle_default: False