diff --git a/.dockerignore b/.dockerignore index e8f6e66311..1665783ba4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -89,7 +89,6 @@ test_root/paver_logs/ test_root/uploads/ **/django-pyfs **/.tox/ -common/test/db_cache/bok_choy_*.yaml common/test/data/badges/*.png ### Installation artifacts diff --git a/.github/workflows/static-assets-check.yml b/.github/workflows/static-assets-check.yml index 985dd0aab5..271b6ada81 100644 --- a/.github/workflows/static-assets-check.yml +++ b/.github/workflows/static-assets-check.yml @@ -64,8 +64,8 @@ jobs: - name: Run Static Assets Check env: - LMS_CFG: lms/envs/bok_choy.yml - CMS_CFG: cms/envs/bok_choy.yml + LMS_CFG: lms/envs/minimal.yml + CMS_CFG: lms/envs/minimal.yml run: | paver update_assets lms diff --git a/.gitignore b/.gitignore index 1866979edc..1692966900 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,6 @@ test_root/paver_logs/ test_root/uploads/ django-pyfs .tox/ -common/test/db_cache/bok_choy_*.yaml common/test/data/badges/*.png ### Installation artifacts diff --git a/Dockerfile b/Dockerfile index b9da9d6eb4..a159d32464 100644 --- a/Dockerfile +++ b/Dockerfile @@ -153,10 +153,11 @@ FROM base as production USER app ENV EDX_PLATFORM_SETTINGS='docker-production' -ENV SERVICE_VARIANT "${SERVICE_VARIANT}" -ENV SERVICE_PORT "${SERVICE_PORT}" +ENV SERVICE_VARIANT="${SERVICE_VARIANT}" +ENV SERVICE_PORT="${SERVICE_PORT}" ENV DJANGO_SETTINGS_MODULE="${SERVICE_VARIANT}.envs.$EDX_PLATFORM_SETTINGS" EXPOSE ${SERVICE_PORT} + CMD gunicorn \ -c /edx/app/edxapp/edx-platform/${SERVICE_VARIANT}/docker_${SERVICE_VARIANT}_gunicorn.py \ --name ${SERVICE_VARIANT} \ @@ -187,6 +188,6 @@ RUN ln -s "$(pwd)/cms/envs/devstack-experimental.yml" "/edx/etc/studio.yml" RUN touch ../edxapp_env ENV EDX_PLATFORM_SETTINGS='devstack_docker' -ENV SERVICE_VARIANT "${SERVICE_VARIANT}" +ENV SERVICE_VARIANT="${SERVICE_VARIANT}" EXPOSE ${SERVICE_PORT} CMD ./manage.py ${SERVICE_VARIANT} runserver 0.0.0.0:${SERVICE_PORT} diff --git a/README.rst b/README.rst index d8d3a880c5..0a49b634fc 100644 --- a/README.rst +++ b/README.rst @@ -134,7 +134,7 @@ Reporting Security Issues ************************* Please do not report security issues in public. Please email -security@edx.org. +security@openedx.org. .. _individual contributor agreement: https://openedx.org/cla .. _CONTRIBUTING: https://github.com/openedx/.github/blob/master/CONTRIBUTING.md diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py index 2dfa72e2d0..f8a88b45dd 100644 --- a/cms/djangoapps/contentstore/exams.py +++ b/cms/djangoapps/contentstore/exams.py @@ -109,7 +109,7 @@ def get_exam_type(is_proctored, is_practice, is_onboarding): if is_onboarding: exam_type = 'onboarding' elif is_practice: - exam_type = 'practice_proctored' + exam_type = 'practice' else: exam_type = 'proctored' else: 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 0000000000..9d207ee767 --- /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 e69de29bb2..4ceefe6ead 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 0e799ab1cc..4ca1f5f05a 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 e44c31b511..7ecb473d1a 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 2b72f1ff44..bf6ea1d9f3 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 522a34e077..4549326c96 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 0561a781c6..38429910ed 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 @@ from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase 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 @@ class AssetsViewTestCase(AuthorizeStaffTestCase): 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 @@ class AssetsViewTestCase(AuthorizeStaffTestCase): 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 @@ class AssetsViewTestCase(AuthorizeStaffTestCase): ), ) @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 @@ class AssetsViewGetTest(AssetsViewTestCase, ModuleStoreTestCase, APITestCase): 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 @@ class AssetsViewPostTest(AssetsViewTestCase, ModuleStoreTestCase, APITestCase): 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 fe214c2f81..e4c21eb353 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 @@ from cms.djangoapps.contentstore.tests.test_utils import AuthorizeStaffTestCase TEST_LOCATOR = "block-v1:dede+aba+weagi+type@problem+block@ba6327f840da49289fb27a9243913478" +VERSION = "v0" class XBlockViewTestCase(AuthorizeStaffTestCase): @@ -38,7 +39,7 @@ class XBlockViewTestCase(AuthorizeStaffTestCase): 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 @@ class XBlockViewTestCase(AuthorizeStaffTestCase): 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 @@ class XBlockViewTestCase(AuthorizeStaffTestCase): ), ) @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 eb435ef338..8c9a772c34 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 @@ urlpatterns = [ 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 1cc601dca9..0c0c24aeab 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 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 ....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 0000000000..972b6229f5 --- /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 f621929f9c..c97261c429 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 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 ....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 @@ from cms.djangoapps.contentstore.transcript_storage_handlers import ( 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 ffeceb0265..cc26619fb8 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.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 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 2d65d60139..ac1f2cd1fb 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 @@ from .proctoring import ( 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 aefab2c6b1..12816a8cbd 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 rest_framework import serializers 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 742d198a7a..1c9f9f6084 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 @@ API Serializers for course settings 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 657e4339b8..89f566f0ab 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): @@ -58,6 +58,20 @@ class VideoModelSerializer(serializers.Serializer): ) +class VideoActiveTranscriptPreferencesSerializer(serializers.Serializer): + """Serializer for a videos active transcript preferences""" + course_id = serializers.CharField() + provider = serializers.CharField() + cielo24_fidelity = serializers.CharField() + cielo24_turnaround = serializers.CharField() + three_play_turnaround = serializers.CharField() + preferred_languages = serializers.ListField( + child=serializers.CharField() + ) + video_source_language = serializers.CharField() + modified = serializers.CharField() + + class CourseVideosSerializer(serializers.Serializer): """Serializer for course videos""" image_upload_url = serializers.CharField() @@ -72,15 +86,16 @@ class CourseVideosSerializer(serializers.Serializer): video_upload_max_file_size = serializers.CharField() video_image_settings = VideoImageSettingsSerializer(required=True, allow_null=False) is_video_transcript_enabled = serializers.BooleanField() - active_transcript_preferences = serializers.BooleanField(required=False, allow_null=True) + active_transcript_preferences = VideoActiveTranscriptPreferencesSerializer(required=False, allow_null=True) transcript_credentials = serializers.DictField( - child=serializers.CharField() + child=serializers.BooleanField() ) transcript_available_languages = serializers.ListField( child=serializers.DictField( child=serializers.CharField() ) ) + # transcript_available_languages = serializers.BooleanField(required=False, allow_null=True) video_transcript_settings = VideoTranscriptSettingsSerializer() pagination_context = serializers.DictField( child=serializers.CharField(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index b9f68aa3e9..ad5765a673 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 @@ from .views import ( HomePageView, ProctoredExamSettingsView, ProctoringErrorsView, - xblock, - assets, - videos, - transcripts, HelpUrlsView, VideoUsageView ) @@ -84,45 +79,6 @@ urlpatterns = [ 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 dfa87a4a34..780f0059aa 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 .grading import CourseGradingView 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 d4ea444bd1..b6d6dcfbae 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 @@ class VideoUsageView(DeveloperErrorViewMixin, APIView): 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/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 8845874261..20c14089e0 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -13,14 +13,7 @@ from django.dispatch import receiver from edx_toggles.toggles import SettingToggle from opaque_keys.edx.keys import CourseKey from openedx_events.content_authoring.data import CourseCatalogData, CourseScheduleData -from openedx_events.content_authoring.signals import ( - COURSE_CATALOG_INFO_CHANGED, - XBLOCK_DELETED, - XBLOCK_DUPLICATED, - XBLOCK_PUBLISHED, -) -from openedx.core.lib.events import determine_producer_config_for_signal_and_topic -from openedx_events.event_bus import get_producer +from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED from pytz import UTC from cms.djangoapps.contentstore.courseware_index import ( @@ -160,85 +153,6 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= transaction.on_commit(lambda: emit_catalog_info_changed_signal(course_key)) -@receiver(COURSE_CATALOG_INFO_CHANGED) -def listen_for_course_catalog_info_changed(sender, signal, **kwargs): - """ - Publish COURSE_CATALOG_INFO_CHANGED signals onto the event bus. - """ - # temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present - producer_config_setting = determine_producer_config_for_signal_and_topic(COURSE_CATALOG_INFO_CHANGED, - 'course-catalog-info-changed') - if producer_config_setting is True: - log.info("Producing course-catalog-info-changed event via config") - return - log.info("Producing course-catalog-info-changed event via manual send") - get_producer().send( - signal=COURSE_CATALOG_INFO_CHANGED, topic='course-catalog-info-changed', - event_key_field='catalog_info.course_key', event_data={'catalog_info': kwargs['catalog_info']}, - event_metadata=kwargs['metadata'], - ) - - -@receiver(XBLOCK_PUBLISHED) -def listen_for_xblock_published(sender, signal, **kwargs): - """ - Publish XBLOCK_PUBLISHED signals onto the event bus. - """ - # temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present - topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle") - producer_config_setting = determine_producer_config_for_signal_and_topic(XBLOCK_PUBLISHED, topic) - if producer_config_setting is True: - log.info("Producing xblock-published event via config") - return - if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"): - log.info("Producing xblock-published event via manual send") - get_producer().send( - signal=XBLOCK_PUBLISHED, topic=topic, - event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']}, - event_metadata=kwargs['metadata'], - ) - - -@receiver(XBLOCK_DELETED) -def listen_for_xblock_deleted(sender, signal, **kwargs): - """ - Publish XBLOCK_DELETED signals onto the event bus. - """ - # temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present - topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle") - producer_config_setting = determine_producer_config_for_signal_and_topic(XBLOCK_DELETED, topic) - if producer_config_setting is True: - log.info("Producing xblock-deleted event via config") - return - if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"): - log.info("Producing xblock-deleted event via manual send") - get_producer().send( - signal=XBLOCK_DELETED, topic=topic, - event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']}, - event_metadata=kwargs['metadata'], - ) - - -@receiver(XBLOCK_DUPLICATED) -def listen_for_xblock_duplicated(sender, signal, **kwargs): - """ - Publish XBLOCK_DUPLICATED signals onto the event bus. - """ - # temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present - topic = getattr(settings, "EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC", "course-authoring-xblock-lifecycle") - producer_config_setting = determine_producer_config_for_signal_and_topic(XBLOCK_DUPLICATED, topic) - if producer_config_setting is True: - log.info("Producing xblock-duplicated event via config") - return - if settings.FEATURES.get("ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS"): - log.info("Producing xblock-duplicated event via manual send") - get_producer().send( - signal=XBLOCK_DUPLICATED, topic=topic, - event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']}, - event_metadata=kwargs['metadata'], - ) - - @receiver(SignalHandler.course_deleted) def listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument """ diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 1eb7034739..b82605f893 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,7 +45,12 @@ from xmodule.video_block import VideoBlock from cms.djangoapps.contentstore.config import waffle from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json -from cms.djangoapps.contentstore.utils import delete_course, reverse_course_url, reverse_url +from cms.djangoapps.contentstore.utils import ( + delete_course, + reverse_course_url, + reverse_url, + get_taxonomy_tags_widget_url, +) from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager @@ -1410,12 +1415,16 @@ class ContentStoreTest(ContentStoreTestCase): 'assets_handler', course.location.course_key ) + + taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id) + self.assertContains( resp, - '
'.format( # lint-amnesty, pylint: disable=line-too-long + '
'.format( # lint-amnesty, pylint: disable=line-too-long locator=str(course.location), course_key=str(course.id), assets_url=assets_url, + taxonomy_tags_widget_url=taxonomy_tags_widget_url, ), status_code=200, html=True diff --git a/cms/djangoapps/contentstore/tests/test_exams.py b/cms/djangoapps/contentstore/tests/test_exams.py index 6b2f39687e..12a54278d2 100644 --- a/cms/djangoapps/contentstore/tests/test_exams.py +++ b/cms/djangoapps/contentstore/tests/test_exams.py @@ -60,7 +60,7 @@ class TestExamService(ModuleStoreTestCase): @ddt.data( (False, False, False, 'timed'), (True, False, False, 'proctored'), - (True, True, False, 'practice_proctored'), + (True, True, False, 'practice'), (True, True, True, 'onboarding'), ) @ddt.unpack diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 48fc2c962c..ba227ecbec 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -46,7 +46,6 @@ from common.djangoapps.util.milestones_helpers import ( ) from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles -from openedx.core.djangoapps.course_apps.toggles import proctoring_settings_modal_view_enabled from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration @@ -246,10 +245,7 @@ def get_proctored_exam_settings_url(course_locator) -> str: mfe_base_url = get_course_authoring_url(course_locator) course_mfe_url = f'{mfe_base_url}/course/{course_locator}' if mfe_base_url: - if proctoring_settings_modal_view_enabled(course_locator): - proctored_exam_settings_url = f'{course_mfe_url}/pages-and-resources/proctoring/settings' - else: - proctored_exam_settings_url = f'{course_mfe_url}/proctored-exam-settings' + proctored_exam_settings_url = f'{course_mfe_url}/pages-and-resources/proctoring/settings' return proctored_exam_settings_url @@ -446,6 +442,21 @@ def get_taxonomy_list_url(): return taxonomy_list_url +def get_taxonomy_tags_widget_url(course_locator) -> str: + """ + Gets course authoring microfrontend URL for taxonomy tags drawer widget view. + + The `content_id` needs to be appended to the end of the URL when using it. + """ + taxonomy_tags_widget_url = None + # Uses the same waffle flag as taxonomy list page + if use_tagging_taxonomy_list_page(): + mfe_base_url = get_course_authoring_url(course_locator) + if mfe_base_url: + taxonomy_tags_widget_url = f'{mfe_base_url}/tagging/components/widget/' + return taxonomy_tags_widget_url + + def course_import_olx_validation_is_enabled(): """ Check if course olx validation is enabled on course import. diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index a55bb3db9a..b42c011c4b 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -105,6 +105,7 @@ from ..utils import ( get_lms_link_for_item, get_proctored_exam_settings_url, get_course_outline_url, + get_taxonomy_tags_widget_url, get_studio_home_url, get_updates_url, get_advanced_settings_url, @@ -688,6 +689,7 @@ def course_index(request, course_key): 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_block.id), 'proctoring_errors': proctoring_errors, + 'taxonomy_tags_widget_url': get_taxonomy_tags_widget_url(course_block.id), }) diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index 425f97e375..4ed0073bef 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -38,7 +38,7 @@ from xblock.core import XBlock from xblock.fields import Scope from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG -from cms.djangoapps.contentstore.toggles import ENABLE_COPY_PASTE_UNITS +from cms.djangoapps.contentstore.toggles import ENABLE_COPY_PASTE_UNITS, use_tagging_taxonomy_list_page from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig from common.djangoapps.edxmako.services import MakoService @@ -1397,6 +1397,10 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements # If the ENABLE_COPY_PASTE_UNITS feature flag is enabled, we show the newer menu that allows copying/pasting xblock_info["enable_copy_paste_units"] = ENABLE_COPY_PASTE_UNITS.is_enabled() + # If the ENABLE_TAGGING_TAXONOMY_LIST_PAGE feature flag is enabled, we show the "Manage Tags" options + if use_tagging_taxonomy_list_page(): + xblock_info["use_tagging_taxonomy_list_page"] = True + xblock_info[ "has_partition_group_components" ] = has_children_visible_to_specific_partition_groups(xblock) diff --git a/cms/envs/bok_choy.auth.json b/cms/envs/bok_choy.auth.json deleted file mode 100644 index db92b5ba92..0000000000 --- a/cms/envs/bok_choy.auth.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "AWS_ACCESS_KEY_ID": "", - "AWS_SECRET_ACCESS_KEY": "", - "CELERY_BROKER_PASSWORD": "celery", - "CELERY_BROKER_USER": "celery", - "CONTENTSTORE": { - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "localhost" - ], - "port": 27017 - }, - "ENGINE": "xmodule.contentstore.mongo.MongoContentStore", - "OPTIONS": { - "db": "test", - "host": [ - "localhost" - ], - "port": 27017 - } - }, - "DATABASES": { - "default": { - "ENGINE": "django.db.backends.mysql", - "HOST": "localhost", - "NAME": "edxtest", - "PASSWORD": "", - "PORT": "3306", - "USER": "root" - }, - "student_module_history": { - "ENGINE": "django.db.backends.mysql", - "HOST": "localhost", - "NAME": "student_module_history_test", - "PASSWORD": "", - "PORT": "3306", - "USER": "root" - } - }, - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "localhost" - ], - "port": 27017 - }, - "JWT_AUTH": { - "JWT_SECRET_KEY": "super-secret-key", - "JWT_PUBLIC_SIGNING_JWK_SET": "{\"keys\": [{\"kid\": \"BTZ9HA6K\", \"e\": \"AQAB\", \"kty\": \"RSA\", \"n\": \"o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ\"}]}" - }, - "MODULESTORE": { - "default": { - "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", - "OPTIONS": { - "mappings": {}, - "stores": [ - { - "NAME": "draft", - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "localhost" - ], - "port": 27017 - }, - "ENGINE": "xmodule.modulestore.mongo.DraftMongoModuleStore", - "OPTIONS": { - "collection": "modulestore", - "db": "test", - "default_class": "xmodule.hidden_block.HiddenBlock", - "fs_root": "** OVERRIDDEN **", - "host": [ - "localhost" - ], - "port": 27017, - "render_template": "common.djangoapps.edxmako.shortcuts.render_to_string" - } - }, - { - "NAME": "xml", - "ENGINE": "xmodule.modulestore.xml.XMLModuleStore", - "OPTIONS": { - "data_dir": "** OVERRIDDEN **", - "default_class": "xmodule.hidden_block.HiddenBlock" - } - } - ] - } - } - }, - "DJFS": { - "type": "s3fs", - "bucket": "test", - "prefix": "test", - "aws_access_key_id": "test", - "aws_secret_access_key": "test" - }, - "SECRET_KEY": "", - "XQUEUE_INTERFACE": { - "basic_auth": [ - "edx", - "edx" - ], - "django_auth": { - "password": "password", - "username": "lms" - }, - "url": "http://localhost:18040" - }, - "ZENDESK_API_KEY": "", - "ZENDESK_USER": "" -} diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json deleted file mode 100644 index 4defe0ccc1..0000000000 --- a/cms/envs/bok_choy.env.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "BUGS_EMAIL": "bugs@example.com", - "BULK_EMAIL_DEFAULT_FROM_EMAIL": "no-reply@example.com", - "CACHES": { - "celery": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_celery", - "LOCATION": [ - "localhost:11211" - ] - }, - "default": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "sandbox_default", - "LOCATION": [ - "localhost:11211" - ] - }, - "general": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "sandbox_general", - "LOCATION": [ - "localhost:11211" - ] - }, - "mongo_metadata_inheritance": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_mongo_metadata_inheritance", - "LOCATION": [ - "localhost:11211" - ] - }, - "staticfiles": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_static_files", - "LOCATION": [ - "localhost:11211" - ] - } - }, - "CELERY_ALWAYS_EAGER": true, - "CELERY_BROKER_HOSTNAME": "localhost", - "CELERY_BROKER_TRANSPORT": "amqp", - "CERT_QUEUE": "certificates", - "CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION": false, - "CMS_BASE": "localhost:8031", - "CODE_JAIL": { - "limits": { - "REALTIME": 3, - "VMEM": 0 - } - }, - "COMMENTS_SERVICE_KEY": "password", - "COMMENTS_SERVICE_URL": "http://localhost:4567", - "CONTACT_EMAIL": "info@example.com", - "DEFAULT_FEEDBACK_EMAIL": "feedback@example.com", - "DEFAULT_FROM_EMAIL": "registration@example.com", - "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend", - "SOCIAL_SHARING_SETTINGS": { - "CUSTOM_COURSE_URLS": true - }, - "FEATURES": { - "CERTIFICATES_HTML_VIEW": true, - "ENABLE_DISCUSSION_SERVICE": true, - "ENABLE_GRADE_DOWNLOADS": true, - "ENTRANCE_EXAMS": true, - "MILESTONES_APP": true, - "PREVIEW_LMS_BASE": "preview.localhost:8003", - "ENABLE_CONTENT_LIBRARIES": true, - "ENABLE_SPECIAL_EXAMS": true, - "SHOW_HEADER_LANGUAGE_SELECTOR": true, - "ENABLE_EXTENDED_COURSE_DETAILS": true, - "CUSTOM_COURSES_EDX": true - }, - "FEEDBACK_SUBMISSION_EMAIL": "", - "GITHUB_REPO_ROOT": "** OVERRIDDEN **", - "GRADES_DOWNLOAD": { - "BUCKET": "edx-grades", - "ROOT_PATH": "/tmp/edx-s3/grades", - "STORAGE_TYPE": "localfs" - }, - "LMS_BASE": "localhost:8003", - "LMS_ROOT_URL": "http://localhost:8003", - "LOCAL_LOGLEVEL": "INFO", - "LOGGING_ENV": "sandbox", - "LOG_DIR": "** OVERRIDDEN **", - "MEDIA_URL": "/media/", - "MKTG_URL_LINK_MAP": {}, - "SERVER_EMAIL": "devops@example.com", - "SESSION_COOKIE_DOMAIN": null, - "SITE_NAME": "localhost", - "STATIC_URL_BASE": "/static/", - "SYSLOG_SERVER": "", - "TECH_SUPPORT_EMAIL": "technical@example.com", - "TIME_ZONE": "America/New_York", - "WIKI_ENABLED": true, -} diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py deleted file mode 100644 index b6809dd48f..0000000000 --- a/cms/envs/bok_choy.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Settings for Bok Choy tests that are used when running Studio. - -Bok Choy uses two different settings files: -1. test_static_optimized is used when invoking collectstatic -2. bok_choy is used when running the tests - -Note: it isn't possible to have a single settings file, because Django doesn't -support both generating static assets to a directory and also serving static -from the same directory. -""" - - -# Silence noisy logs -import logging -import os - -from django.utils.translation import gettext_lazy -from path import Path as path - -from openedx.core.release import RELEASE_LINE -from xmodule.modulestore.modulestore_settings import update_module_store_settings # lint-amnesty, pylint: disable=wrong-import-order - -########################## Prod-like settings ################################### -# These should be as close as possible to the settings we use in production. -# As in prod, we read in environment and auth variables from JSON files. -# Unlike in prod, we use the JSON files stored in this repo. -# This is a convenience for ensuring (a) that we can consistently find the files -# and (b) that the files are the same in Jenkins as in local dev. -os.environ['SERVICE_VARIANT'] = 'bok_choy_docker' if 'BOK_CHOY_HOSTNAME' in os.environ else 'bok_choy' -CONFIG_ROOT = path(__file__).abspath().dirname() -os.environ['STUDIO_CFG'] = str.format("{config_root}/{service_variant}.yml", - config_root=CONFIG_ROOT, - service_variant=os.environ['SERVICE_VARIANT']) -os.environ['REVISION_CFG'] = f"{CONFIG_ROOT}/revisions.yml" - -from .production import * # pylint: disable=wildcard-import, unused-wildcard-import, wrong-import-position - - -######################### Testing overrides #################################### - -# Redirect to the test_root folder within the repo -TEST_ROOT = REPO_ROOT / "test_root" -GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath() -LOG_DIR = (TEST_ROOT / "log").abspath() -DATA_DIR = TEST_ROOT / "data" - -# Configure modulestore to use the test folder within the repo -update_module_store_settings( - MODULESTORE, - module_store_options={ - 'fs_root': (TEST_ROOT / "data").abspath(), - }, - xml_store_options={ - 'data_dir': (TEST_ROOT / "data").abspath(), - }, - default_store=os.environ.get('DEFAULT_STORE', 'draft'), -) - -# Needed to enable licensing on video blocks -XBLOCK_SETTINGS.update({'VideoBlock': {'licensing_enabled': True}}) - -# Capture the console log via template includes, until webdriver supports log capture again -CAPTURE_CONSOLE_LOG = True - -PLATFORM_NAME = gettext_lazy("édX") -PLATFORM_DESCRIPTION = gettext_lazy("Open édX Platform") -STUDIO_NAME = gettext_lazy("Your Platform 𝓢𝓽𝓾𝓭𝓲𝓸") -STUDIO_SHORT_NAME = gettext_lazy("𝓢𝓽𝓾𝓭𝓲𝓸") - -############################ STATIC FILES ############################# - -# Enable debug so that static assets are served by Django -DEBUG = True - -# Serve static files at /static directly from the staticfiles directory under test root -# Note: optimized files for testing are generated with settings from test_static_optimized -STATIC_URL = "/static/" -STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', -] -STATICFILES_DIRS = [ - (TEST_ROOT / "staticfiles" / "cms").abspath(), -] - -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' -MEDIA_ROOT = TEST_ROOT / "uploads" - -WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = TEST_ROOT / "staticfiles" / "cms" / "webpack-stats.json" - -LOG_OVERRIDES = [ - ('common.djangoapps.track.middleware', logging.CRITICAL), - ('edx.discussion', logging.CRITICAL), -] -for log_name, log_level in LOG_OVERRIDES: - logging.getLogger(log_name).setLevel(log_level) - -# Use the auto_auth workflow for creating users and logging them in -FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True -FEATURES['RESTRICT_AUTOMATIC_AUTH'] = False - -# Enable milestones app -FEATURES['MILESTONES_APP'] = True - -# Enable pre-requisite course -FEATURES['ENABLE_PREREQUISITE_COURSES'] = True - -# Enable student notes -FEATURES['ENABLE_EDXNOTES'] = True - -# Enable teams feature -FEATURES['ENABLE_TEAMS'] = True - -# Enable custom content licensing -FEATURES['LICENSING'] = True - -FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio -FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings - -FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True - -# Whether archived courses (courses with end dates in the past) should be -# shown in Studio in a separate list. -FEATURES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = True - -# Enable partner support link in Studio footer -PARTNER_SUPPORT_EMAIL = 'partner-support@example.com' - -########################### Entrance Exams ################################# -FEATURES['ENTRANCE_EXAMS'] = True - -FEATURES['ENABLE_SPECIAL_EXAMS'] = True - -# Point the URL used to test YouTube availability to our stub YouTube server -YOUTUBE_PORT = 9080 -YOUTUBE['TEST_TIMEOUT'] = 5000 -YOUTUBE_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1') -YOUTUBE['API'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/get_youtube_api/" -YOUTUBE['METADATA_URL'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/test_youtube/" - -FEATURES['ENABLE_COURSEWARE_INDEX'] = True -FEATURES['ENABLE_LIBRARY_INDEX'] = True -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False - -ORGANIZATIONS_AUTOCREATE = False - -SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" -# Path at which to store the mock index -MOCK_SEARCH_BACKING_FILE = ( - TEST_ROOT / "index_file.dat" -).abspath() - -# this secret key should be the same as lms/envs/bok_choy.py's -SECRET_KEY = "very_secret_bok_choy_key" - -LMS_ROOT_URL = "http://localhost:8003" -if RELEASE_LINE == "master": - # On master, acceptance tests use edX books, not the default Open edX books. - HELP_TOKENS_BOOKS = { - 'learner': 'https://edx.readthedocs.io/projects/edx-guide-for-students', - 'course_author': 'https://edx.readthedocs.io/projects/edx-partner-course-staff', - } - -########################## VIDEO TRANSCRIPTS STORAGE ############################ -VIDEO_TRANSCRIPTS_SETTINGS = dict( - VIDEO_TRANSCRIPTS_MAX_BYTES=3 * 1024 * 1024, # 3 MB - STORAGE_KWARGS=dict( - location=MEDIA_ROOT, - base_url=MEDIA_URL, - ), - DIRECTORY_PREFIX='video-transcripts/', -) - -INSTALLED_APPS.append('openedx.testing.coverage_context_listener') - -##################################################################### -# Lastly, see if the developer has any local overrides. -try: - from .private import * # pylint: disable=wildcard-import -except ImportError: - pass diff --git a/cms/envs/bok_choy.yml b/cms/envs/bok_choy.yml deleted file mode 100644 index 735a94c4c2..0000000000 --- a/cms/envs/bok_choy.yml +++ /dev/null @@ -1,155 +0,0 @@ -# ingested bok_choy.env.json -# ingested bok_choy.auth.json -AWS_ACCESS_KEY_ID: '' -AWS_SECRET_ACCESS_KEY: '' -BUGS_EMAIL: bugs@example.com -BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com -CACHES: - celery: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - ignore_exc: true - no_delay: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_celery - LOCATION: ['localhost:11211'] - default: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - ignore_exc: true - no_delay: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: sandbox_default - LOCATION: ['localhost:11211'] - general: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - ignore_exc: true - no_delay: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: sandbox_general - LOCATION: ['localhost:11211'] - mongo_metadata_inheritance: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - ignore_exc: true - no_delay: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_mongo_metadata_inheritance - LOCATION: ['localhost:11211'] - staticfiles: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - ignore_exc: true - no_delay: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_static_files - LOCATION: ['localhost:11211'] -CELERY_ALWAYS_EAGER: true -CELERY_BROKER_HOSTNAME: localhost -CELERY_BROKER_PASSWORD: celery -CELERY_BROKER_TRANSPORT: amqp -CELERY_BROKER_USER: celery -CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION: false -CERT_QUEUE: certificates -CMS_BASE: localhost:8031 -CODE_JAIL: - limits: {REALTIME: 3, VMEM: 0} -COMMENTS_SERVICE_KEY: password -COMMENTS_SERVICE_URL: http://localhost:4567 -CONTACT_EMAIL: info@example.com -CONTENTSTORE: - DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [localhost] - port: 27017 - ENGINE: xmodule.contentstore.mongo.MongoContentStore - OPTIONS: - db: test - host: [localhost] - port: 27017 -DATABASES: - default: {ENGINE: django.db.backends.mysql, HOST: localhost, NAME: edxtest, PASSWORD: '', - PORT: '3306', USER: root} - student_module_history: {ENGINE: django.db.backends.mysql, HOST: localhost, NAME: student_module_history_test, - PASSWORD: '', PORT: '3306', USER: root} -DEFAULT_FEEDBACK_EMAIL: feedback@example.com -DEFAULT_FROM_EMAIL: registration@example.com -DJFS: {aws_access_key_id: test, aws_secret_access_key: test, bucket: test, prefix: test, - type: s3fs} -DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [localhost] - port: 27017 -EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend -FEATURES: {CERTIFICATES_HTML_VIEW: true, CUSTOM_COURSES_EDX: true, - ENABLE_CONTENT_LIBRARIES: true, ENABLE_DISCUSSION_SERVICE: true, ENABLE_EXTENDED_COURSE_DETAILS: true, - ENABLE_GRADE_DOWNLOADS: true, ENABLE_SPECIAL_EXAMS: true, ENTRANCE_EXAMS: true, - MILESTONES_APP: true, PREVIEW_LMS_BASE: 'preview.localhost:8003', SHOW_HEADER_LANGUAGE_SELECTOR: true} -GITHUB_REPO_ROOT: '** OVERRIDDEN **' -GRADES_DOWNLOAD: {BUCKET: edx-grades, ROOT_PATH: /tmp/edx-s3/grades, STORAGE_TYPE: localfs} -JWT_AUTH: {JWT_PUBLIC_SIGNING_JWK_SET: '{"keys": [{"kid": - "BTZ9HA6K", "e": "AQAB", "kty": "RSA", "n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"}]}', - JWT_SECRET_KEY: super-secret-key} -LMS_BASE: localhost:8003 -LMS_ROOT_URL: http://localhost:8003 -LOCAL_LOGLEVEL: INFO -LOGGING_ENV: sandbox -LOG_DIR: '** OVERRIDDEN **' -MEDIA_URL: /media/ -MKTG_URL_LINK_MAP: {} -MODULESTORE: - default: - ENGINE: xmodule.modulestore.mixed.MixedModuleStore - OPTIONS: - mappings: {} - stores: - - DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [localhost] - port: 27017 - ENGINE: xmodule.modulestore.mongo.DraftMongoModuleStore - NAME: draft - OPTIONS: - collection: modulestore - db: test - default_class: xmodule.hidden_block.HiddenBlock - fs_root: '** OVERRIDDEN **' - host: [localhost] - port: 27017 - render_template: common.djangoapps.edxmako.shortcuts.render_to_string - - ENGINE: xmodule.modulestore.xml.XMLModuleStore - NAME: xml - OPTIONS: {data_dir: '** OVERRIDDEN **', default_class: xmodule.hidden_block.HiddenBlock} -# We need to test different scenarios, following setting effectively disbale rate limiting -PASSWORD_RESET_IP_RATE: '1/s' -PASSWORD_RESET_EMAIL_RATE: '1/s' -SECRET_KEY: '' -SERVER_EMAIL: devops@example.com -SESSION_COOKIE_DOMAIN: null -SITE_NAME: localhost -SOCIAL_SHARING_SETTINGS: {CUSTOM_COURSE_URLS: true} -STATIC_URL_BASE: /static/ -SYSLOG_SERVER: '' -TECH_SUPPORT_EMAIL: technical@example.com -TIME_ZONE: America/New_York -WIKI_ENABLED: true -XQUEUE_INTERFACE: - basic_auth: [edx, edx] - django_auth: {password: password, username: lms} - url: http://localhost:18040 -ZENDESK_API_KEY: '' -ZENDESK_USER: '' diff --git a/cms/envs/bok_choy_docker.auth.json b/cms/envs/bok_choy_docker.auth.json deleted file mode 100644 index 9a187c05b1..0000000000 --- a/cms/envs/bok_choy_docker.auth.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "AWS_ACCESS_KEY_ID": "", - "AWS_SECRET_ACCESS_KEY": "", - "CELERY_BROKER_PASSWORD": "celery", - "CELERY_BROKER_USER": "celery", - "CONTENTSTORE": { - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "edx.devstack.mongo" - ], - "port": 27017 - }, - "ENGINE": "xmodule.contentstore.mongo.MongoContentStore", - "OPTIONS": { - "db": "test", - "host": [ - "edx.devstack.mongo" - ], - "port": 27017 - } - }, - "DATABASES": { - "default": { - "ENGINE": "django.db.backends.mysql", - "HOST": "edx.devstack.mysql80", - "NAME": "edxtest", - "PASSWORD": "", - "PORT": "3306", - "USER": "root" - }, - "student_module_history": { - "ENGINE": "django.db.backends.mysql", - "HOST": "edx.devstack.mysql80", - "NAME": "student_module_history_test", - "PASSWORD": "", - "PORT": "3306", - "USER": "root" - } - }, - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "edx.devstack.mongo" - ], - "port": 27017 - }, - "JWT_AUTH": { - "JWT_SECRET_KEY": "super-secret-key", - "JWT_PUBLIC_SIGNING_JWK_SET": "{\"keys\": [{\"kid\": \"BTZ9HA6K\", \"e\": \"AQAB\", \"kty\": \"RSA\", \"n\": \"o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ\"}]}" - }, - "MODULESTORE": { - "default": { - "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", - "OPTIONS": { - "mappings": {}, - "stores": [ - { - "NAME": "draft", - "DOC_STORE_CONFIG": { - "collection": "modulestore", - "db": "test", - "host": [ - "edx.devstack.mongo" - ], - "port": 27017 - }, - "ENGINE": "xmodule.modulestore.mongo.DraftMongoModuleStore", - "OPTIONS": { - "collection": "modulestore", - "db": "test", - "default_class": "xmodule.hidden_block.HiddenBlock", - "fs_root": "** OVERRIDDEN **", - "host": [ - "edx.devstack.mongo" - ], - "port": 27017, - "render_template": "common.djangoapps.edxmako.shortcuts.render_to_string" - } - }, - { - "NAME": "xml", - "ENGINE": "xmodule.modulestore.xml.XMLModuleStore", - "OPTIONS": { - "data_dir": "** OVERRIDDEN **", - "default_class": "xmodule.hidden_block.HiddenBlock" - } - } - ] - } - } - }, - "DJFS": { - "type": "s3fs", - "bucket": "test", - "prefix": "test", - "aws_access_key_id": "test", - "aws_secret_access_key": "test" - }, - "SECRET_KEY": "", - "XQUEUE_INTERFACE": { - "basic_auth": [ - "edx", - "edx" - ], - "django_auth": { - "password": "password", - "username": "lms" - }, - "url": "http://localhost:18040" - }, - "ZENDESK_API_KEY": "", - "ZENDESK_USER": "" -} diff --git a/cms/envs/bok_choy_docker.env.json b/cms/envs/bok_choy_docker.env.json deleted file mode 100644 index 90b4ced635..0000000000 --- a/cms/envs/bok_choy_docker.env.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "BUGS_EMAIL": "bugs@example.com", - "BULK_EMAIL_DEFAULT_FROM_EMAIL": "no-reply@example.com", - "CACHES": { - "celery": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_celery", - "LOCATION": [ - "edx.devstack.memcached:11211" - ] - }, - "default": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "sandbox_default", - "LOCATION": [ - "edx.devstack.memcached:11211" - ] - }, - "general": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "sandbox_general", - "LOCATION": [ - "edx.devstack.memcached:11211" - ] - }, - "mongo_metadata_inheritance": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_mongo_metadata_inheritance", - "LOCATION": [ - "edx.devstack.memcached:11211" - ] - }, - "staticfiles": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "OPTIONS": { - "no_delay": true, - "ignore_exc": true, - "use_pooling": true, - "connect_timeout": 0.5 - }, - "KEY_FUNCTION": "common.djangoapps.util.memcache.safe_key", - "KEY_PREFIX": "integration_static_files", - "LOCATION": [ - "edx.devstack.memcached:11211" - ] - } - }, - "CELERY_ALWAYS_EAGER": true, - "CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION": false, - "CELERY_BROKER_HOSTNAME": "localhost", - "CELERY_BROKER_TRANSPORT": "amqp", - "CERT_QUEUE": "certificates", - "CMS_BASE": "** OVERRIDDEN **", - "CODE_JAIL": { - "limits": { - "REALTIME": 3, - "VMEM": 0 - } - }, - "COMMENTS_SERVICE_KEY": "password", - "COMMENTS_SERVICE_URL": "http://edx.devstack.studio:4567", - "CONTACT_EMAIL": "info@example.com", - "DEFAULT_FEEDBACK_EMAIL": "feedback@example.com", - "DEFAULT_FROM_EMAIL": "registration@example.com", - "EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend", - "SOCIAL_SHARING_SETTINGS": { - "CUSTOM_COURSE_URLS": true - }, - "FEATURES": { - "CERTIFICATES_HTML_VIEW": true, - "ENABLE_DISCUSSION_SERVICE": true, - "ENABLE_GRADE_DOWNLOADS": true, - "ENTRANCE_EXAMS": true, - "MILESTONES_APP": true, - "PREVIEW_LMS_BASE": "preview.localhost:8003", - "ENABLE_CONTENT_LIBRARIES": true, - "ENABLE_SPECIAL_EXAMS": true, - "SHOW_HEADER_LANGUAGE_SELECTOR": true, - "ENABLE_EXTENDED_COURSE_DETAILS": true, - "CUSTOM_COURSES_EDX": true - }, - "GITHUB_REPO_ROOT": "** OVERRIDDEN **", - "GRADES_DOWNLOAD": { - "BUCKET": "edx-grades", - "ROOT_PATH": "/tmp/edx-s3/grades", - "STORAGE_TYPE": "localfs" - }, - "LMS_BASE": "** OVERRIDDEN **", - "LMS_ROOT_URL": "** OVERRIDDEN **", - "LOCAL_LOGLEVEL": "INFO", - "LOGGING_ENV": "sandbox", - "LOG_DIR": "** OVERRIDDEN **", - "MEDIA_URL": "/media/", - "MKTG_URL_LINK_MAP": {}, - "SERVER_EMAIL": "devops@example.com", - "SESSION_COOKIE_DOMAIN": null, - "SITE_NAME": "localhost", - "STATIC_URL_BASE": "/static/", - "SYSLOG_SERVER": "", - "TECH_SUPPORT_EMAIL": "technical@example.com", - "TIME_ZONE": "America/New_York", - "WIKI_ENABLED": true, -} diff --git a/cms/envs/bok_choy_docker.py b/cms/envs/bok_choy_docker.py deleted file mode 100644 index 878537de00..0000000000 --- a/cms/envs/bok_choy_docker.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Settings for Bok Choy tests that are used when running Studio in Docker-based devstack. -""" - -# noinspection PyUnresolvedReferences -from .bok_choy import * # pylint: disable=wildcard-import - -CMS_BASE = '{}:{}'.format(os.environ['BOK_CHOY_HOSTNAME'], os.environ.get('BOK_CHOY_CMS_PORT', 8031)) -LMS_BASE = '{}:{}'.format(os.environ['BOK_CHOY_HOSTNAME'], os.environ.get('BOK_CHOY_LMS_PORT', 8003)) -LMS_ROOT_URL = f'http://{LMS_BASE}' -LOGIN_REDIRECT_WHITELIST = [CMS_BASE] - -COMMENTS_SERVICE_URL = 'http://{}:4567'.format(os.environ['BOK_CHOY_HOSTNAME']) -EDXNOTES_PUBLIC_API = 'http://{}:8042/api/v1'.format(os.environ['BOK_CHOY_HOSTNAME']) - -# Docker does not support the syslog socket at /dev/log. Rely on the console. -LOGGING['handlers']['local'] = LOGGING['handlers']['tracking'] = { - 'class': 'logging.NullHandler', -} - -LOGGING['loggers']['tracking']['handlers'] = ['console'] - -# Point the URL used to test YouTube availability to our stub YouTube server -BOK_CHOY_HOST = os.environ['BOK_CHOY_HOSTNAME'] -YOUTUBE['API'] = f"http://{BOK_CHOY_HOST}:{YOUTUBE_PORT}/get_youtube_api/" -YOUTUBE['METADATA_URL'] = f"http://{BOK_CHOY_HOST}:{YOUTUBE_PORT}/test_youtube/" diff --git a/cms/envs/bok_choy_docker.yml b/cms/envs/bok_choy_docker.yml deleted file mode 100644 index 2b18b5be11..0000000000 --- a/cms/envs/bok_choy_docker.yml +++ /dev/null @@ -1,152 +0,0 @@ -# ingested bok_choy_docker.env.json -# ingested bok_choy_docker.auth.json -AWS_ACCESS_KEY_ID: '' -AWS_SECRET_ACCESS_KEY: '' -BUGS_EMAIL: bugs@example.com -BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com -CACHES: - celery: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - no_delay: true - ignore_exc: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_celery - LOCATION: ['edx.devstack.memcached:11211'] - default: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - no_delay: true - ignore_exc: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: sandbox_default - LOCATION: ['edx.devstack.memcached:11211'] - general: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - no_delay: true - ignore_exc: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: sandbox_general - LOCATION: ['edx.devstack.memcached:11211'] - mongo_metadata_inheritance: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - no_delay: true - ignore_exc: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_mongo_metadata_inheritance - LOCATION: ['edx.devstack.memcached:11211'] - staticfiles: - BACKEND: django.core.cache.backends.memcached.PyMemcacheCache - OPTIONS: - no_delay: true - ignore_exc: true - use_pooling: true - connect_timeout: 0.5 - KEY_FUNCTION: common.djangoapps.util.memcache.safe_key - KEY_PREFIX: integration_static_files - LOCATION: ['edx.devstack.memcached:11211'] -CELERY_ALWAYS_EAGER: true -CELERY_BROKER_HOSTNAME: localhost -CELERY_BROKER_PASSWORD: celery -CELERY_BROKER_TRANSPORT: amqp -CELERY_BROKER_USER: celery -CERT_QUEUE: certificates -CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION: false -CMS_BASE: '** OVERRIDDEN **' -CODE_JAIL: - limits: {REALTIME: 3, VMEM: 0} -COMMENTS_SERVICE_KEY: password -COMMENTS_SERVICE_URL: http://edx.devstack.studio:4567 -CONTACT_EMAIL: info@example.com -CONTENTSTORE: - DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [edx.devstack.mongo] - port: 27017 - ENGINE: xmodule.contentstore.mongo.MongoContentStore - OPTIONS: - db: test - host: [edx.devstack.mongo] - port: 27017 -DATABASES: - default: {ENGINE: django.db.backends.mysql, HOST: edx.devstack.mysql80, NAME: edxtest, - PASSWORD: '', PORT: '3306', USER: root} - student_module_history: {ENGINE: django.db.backends.mysql, HOST: edx.devstack.mysql80, - NAME: student_module_history_test, PASSWORD: '', PORT: '3306', USER: root} -DEFAULT_FEEDBACK_EMAIL: feedback@example.com -DEFAULT_FROM_EMAIL: registration@example.com -DJFS: {aws_access_key_id: test, aws_secret_access_key: test, bucket: test, prefix: test, - type: s3fs} -DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [edx.devstack.mongo] - port: 27017 -EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend -FEATURES: {CERTIFICATES_HTML_VIEW: true, CUSTOM_COURSES_EDX: true, - ENABLE_CONTENT_LIBRARIES: true, ENABLE_DISCUSSION_SERVICE: true, ENABLE_EXTENDED_COURSE_DETAILS: true, - ENABLE_GRADE_DOWNLOADS: true, ENABLE_SPECIAL_EXAMS: true, ENTRANCE_EXAMS: true, - MILESTONES_APP: true, PREVIEW_LMS_BASE: 'preview.localhost:8003', SHOW_HEADER_LANGUAGE_SELECTOR: true} -GITHUB_REPO_ROOT: '** OVERRIDDEN **' -GRADES_DOWNLOAD: {BUCKET: edx-grades, ROOT_PATH: /tmp/edx-s3/grades, STORAGE_TYPE: localfs} -JWT_AUTH: {JWT_PUBLIC_SIGNING_JWK_SET: '{"keys": [{"kid": - "BTZ9HA6K", "e": "AQAB", "kty": "RSA", "n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ"}]}', - JWT_SECRET_KEY: super-secret-key} -LMS_BASE: '** OVERRIDDEN **' -LMS_ROOT_URL: '** OVERRIDDEN **' -LOCAL_LOGLEVEL: INFO -LOGGING_ENV: sandbox -LOG_DIR: '** OVERRIDDEN **' -MEDIA_URL: /media/ -MKTG_URL_LINK_MAP: {} -MODULESTORE: - default: - ENGINE: xmodule.modulestore.mixed.MixedModuleStore - OPTIONS: - mappings: {} - stores: - - DOC_STORE_CONFIG: - collection: modulestore - db: test - host: [edx.devstack.mongo] - port: 27017 - ENGINE: xmodule.modulestore.mongo.DraftMongoModuleStore - NAME: draft - OPTIONS: - collection: modulestore - db: test - default_class: xmodule.hidden_block.HiddenBlock - fs_root: '** OVERRIDDEN **' - host: [edx.devstack.mongo] - port: 27017 - render_template: common.djangoapps.edxmako.shortcuts.render_to_string - - ENGINE: xmodule.modulestore.xml.XMLModuleStore - NAME: xml - OPTIONS: {data_dir: '** OVERRIDDEN **', default_class: xmodule.hidden_block.HiddenBlock} -SECRET_KEY: '' -SERVER_EMAIL: devops@example.com -SESSION_COOKIE_DOMAIN: null -SITE_NAME: localhost -SOCIAL_SHARING_SETTINGS: {CUSTOM_COURSE_URLS: true} -STATIC_URL_BASE: /static/ -SYSLOG_SERVER: '' -TECH_SUPPORT_EMAIL: technical@example.com -TIME_ZONE: America/New_York -WIKI_ENABLED: true -XQUEUE_INTERFACE: - basic_auth: [edx, edx] - django_auth: {password: password, username: lms} - url: http://localhost:18040 -ZENDESK_API_KEY: '' -ZENDESK_USER: '' diff --git a/cms/envs/common.py b/cms/envs/common.py index 3b52ead574..917f6859a6 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2655,6 +2655,9 @@ REGISTRATION_EXTRA_FIELDS = { } EDXAPP_PARSE_KEYS = {} +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + ###################### DEPRECATED URLS ########################## # .. toggle_name: DISABLE_DEPRECATED_SIGNIN_URL @@ -2790,7 +2793,7 @@ SPECTACULAR_SETTINGS = { '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/envs/devstack.py b/cms/envs/devstack.py index 29c7770188..dd2f4522b6 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -300,25 +300,26 @@ CLOSEST_CLIENT_IP_FROM_HEADERS = [] CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:18150' CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:18150' +############################ AI_TRANSLATIONS ################################## +AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1' + #################### Event bus backend ######################## -# .. toggle_name: FEATURES['ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS'] -# .. toggle_implementation: DjangoSetting -# .. toggle_default: False -# .. toggle_description: Temporary configuration which enables sending xblock events over the event bus. -# .. toggle_use_cases: open_edx -# .. toggle_creation_date: 2023-02-21 -# .. toggle_warning: For consistency in user experience, keep the value in sync with the setting of the same name -# in the LMS and CMS. -# This will be deprecated in favor of ENABLE_SEND_XBLOCK_LIFECYCLE_EVENTS_OVER_BUS -# .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/31813' -FEATURES['ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS'] = True -FEATURES['ENABLE_SEND_ENROLLMENT_EVENTS_OVER_BUS'] = True + EVENT_BUS_PRODUCER = 'edx_event_bus_redis.create_producer' EVENT_BUS_REDIS_CONNECTION_URL = 'redis://:password@edx.devstack.redis:6379/' EVENT_BUS_TOPIC_PREFIX = 'dev' EVENT_BUS_CONSUMER = 'edx_event_bus_redis.RedisEventConsumer' -EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC = 'course-authoring-xblock-lifecycle' -EVENT_BUS_ENROLLMENT_LIFECYCLE_TOPIC = 'course-authoring-enrollment-lifecycle' + +course_catalog_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.course.catalog_info.changed.v1'] +course_catalog_event_setting['course-catalog-info-changed']['enabled'] = True + +xblock_published_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.published.v1'] +xblock_published_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True +xblock_deleted_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.deleted.v1'] +xblock_deleted_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True +xblock_duplicated_event_setting = EVENT_BUS_PRODUCER_CONFIG['org.openedx.content_authoring.xblock.duplicated.v1'] +xblock_duplicated_event_setting['course-authoring-xblock-lifecycle']['enabled'] = True + ################# New settings must go ABOVE this line ################# ######################################################################## diff --git a/cms/envs/production.py b/cms/envs/production.py index 7e635c402e..d78971882e 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -660,6 +660,9 @@ DISCUSSIONS_MICROFRONTEND_URL = ENV_TOKENS.get('DISCUSSIONS_MICROFRONTEND_URL', ################### Discussions micro frontend Feedback URL################### DISCUSSIONS_MFE_FEEDBACK_URL = ENV_TOKENS.get('DISCUSSIONS_MFE_FEEDBACK_URL', DISCUSSIONS_MFE_FEEDBACK_URL) +############################ AI_TRANSLATIONS URL ################################## +AI_TRANSLATIONS_API_URL = ENV_TOKENS.get('AI_TRANSLATIONS_API_URL', AI_TRANSLATIONS_API_URL) + ############## DRF overrides ############## REST_FRAMEWORK.update(ENV_TOKENS.get('REST_FRAMEWORK', {})) diff --git a/cms/envs/test_static_optimized.py b/cms/envs/test_static_optimized.py index 61738c9254..c92d9a7262 100644 --- a/cms/envs/test_static_optimized.py +++ b/cms/envs/test_static_optimized.py @@ -1,10 +1,6 @@ """ Settings used when generating static assets for use in tests. -For example, Bok Choy uses two different settings files: -1. test_static_optimized is used when invoking collectstatic -2. bok_choy is used when running CMS and LMS - Note: it isn't possible to have a single settings file, because Django doesn't support both generating static assets to a directory and also serving static from the same directory. diff --git a/cms/lib/spectacular.py b/cms/lib/spectacular.py index d05d713531..4318269beb 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/pytest.ini b/cms/pytest.ini index 7f6e49c83e..c18bf1d66f 100644 --- a/cms/pytest.ini +++ b/cms/pytest.ini @@ -12,6 +12,16 @@ filterwarnings = default ignore:No request passed to the backend, unable to rate-limit:UserWarning ignore::xblock.exceptions.FieldDataDeprecationWarning + # Remove default_app_config warning after updating Django to 4.2 + ignore:.*You can remove default_app_config.*:PendingDeprecationWarning + # ABC deprecation Warning comes from libsass + ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated.*:DeprecationWarning:sass + # declare_namespace Warning comes from XBlock https://github.com/openedx/XBlock/issues/641 + # and also due to dependency: https://github.com/PyFilesystem/pyfilesystem2 + ignore:Deprecated call to `pkg_resources.declare_namespace.*:DeprecationWarning + ignore:.*pkg_resources is deprecated as an API.*:DeprecationWarning + ignore:'etree' is deprecated. Use 'xml.etree.ElementTree' instead.:DeprecationWarning:wiki + norecursedirs = envs python_classes = python_files = test.py tests.py test_*.py *_tests.py diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 0351f2e175..2faccde97e 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -458,6 +458,43 @@ function( event.stopPropagation(); }, + closeManageTagsDrawer(drawer, drawerCover) { + $(drawerCover).css('display', 'none'); + $(drawer).empty(); + $(drawer).css('display', 'none'); + $('body').removeClass('drawer-open'); + }, + + openManageTagsDrawer(event) { + const drawer = document.querySelector("#manage-tags-drawer"); + const drawerCover = document.querySelector(".drawer-cover") + const article = document.querySelector('[data-taxonomy-tags-widget-url]'); + const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url'); + const contentId = this.model.get('id'); + + // Add handler to close drawer when dark background is clicked + $(drawerCover).click(function() { + this.closeManageTagsDrawer(drawer, drawerCover); + }.bind(this)); + + // Add event listen to close drawer when close button is clicked from within the Iframe + window.addEventListener("message", function (event) { + if (event.data === 'closeManageTagsDrawer') { + this.closeManageTagsDrawer(drawer, drawerCover) + } + }.bind(this)); + + $(drawerCover).css('display', 'block'); + // xss-lint: disable=javascript-jquery-html + $(drawer).html( + `` + ); + $(drawer).css('display', 'block'); + + // Prevent background from being scrollable when drawer is open + $('body').addClass('drawer-open'); + }, + addButtonActions: function(element) { XBlockOutlineView.prototype.addButtonActions.apply(this, arguments); element.find('.configure-button').click(function(event) { @@ -478,6 +515,10 @@ function( event.preventDefault(); this.copyXBlock(); }); + element.find('.manage-tags-button').click((event) => { + event.preventDefault(); + this.openManageTagsDrawer(); + }); element.find('.paste-component-button').click((event) => { event.preventDefault(); this.pasteUnit(event); diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 9ded8d66e9..4e826025b9 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -114,6 +114,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldE staffOnlyMessage: this.model.get('staff_only_message'), course: course, enableCopyPasteUnits: this.model.get("enable_copy_paste_units"), // ENABLE_COPY_PASTE_UNITS waffle flag + useTaggingTaxonomyListPage: this.model.get("use_tagging_taxonomy_list_page"), // ENABLE_TAGGING_TAXONOMY_LIST_PAGE waffle flag }; }, diff --git a/cms/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss index fefd095e21..178f6b1674 100644 --- a/cms/static/sass/_build-v1.scss +++ b/cms/static/sass/_build-v1.scss @@ -55,6 +55,7 @@ @import 'elements/uploaded-assets'; // layout for asset tables @import 'elements/creative-commons'; @import 'elements/tooltip'; +@import 'elements/drawer'; // +Base - Specific Views // ==================== diff --git a/cms/static/sass/elements/_drawer.scss b/cms/static/sass/elements/_drawer.scss new file mode 100644 index 0000000000..c18073be98 --- /dev/null +++ b/cms/static/sass/elements/_drawer.scss @@ -0,0 +1,30 @@ +// studio - elements - side drawers +// ==================== + +.drawer-cover { + @extend %ui-depth3; + + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); +} + +.drawer { + @extend %ui-depth4; + + display: none; + position: fixed; + top: 0; + right: 0; + width: 33.33vw; + height: 100vh; + background-color: $gray-l4; +} + +body.drawer-open { + overflow: hidden; +} diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 0d1773e7c1..e5959867be 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -281,7 +281,7 @@ from django.urls import reverse assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)}) %>

${_("Course Outline")}

-
+
@@ -321,4 +321,7 @@ from django.urls import reverse
+ +
+
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index fa3d562196..8bf74b07af 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -169,6 +169,17 @@ if (is_proctored_exam) { <% } %> + + <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %> +
  • + + + ? + <%- gettext('Manage Tags') %> + +
  • + <% } %> + <% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>