Merge branch 'master' of github.com:openedx/edx-platform into HEAD
This commit is contained in:
@@ -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
|
||||
|
||||
4
.github/workflows/static-assets-check.yml
vendored
4
.github/workflows/static-assets-check.yml
vendored
@@ -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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Serializers for all contentstore API versions
|
||||
"""
|
||||
from .common import StrictSerializer
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -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,
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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<edx_video_id>[-\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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}">'.format( # lint-amnesty, pylint: disable=line-too-long
|
||||
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}" data-taxonomy-tags-widget-url="{taxonomy_tags_widget_url}" >'.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: ''
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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/"
|
||||
@@ -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: ''
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 #################
|
||||
########################################################################
|
||||
|
||||
@@ -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', {}))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
`<iframe src="${taxonomyTagsWidgetUrl}${contentId}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"></iframe>`
|
||||
);
|
||||
$(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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
// ====================
|
||||
|
||||
30
cms/static/sass/elements/_drawer.scss
Normal file
30
cms/static/sass/elements/_drawer.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -281,7 +281,7 @@ from django.urls import reverse
|
||||
assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)})
|
||||
%>
|
||||
<h2 class="sr">${_("Course Outline")}</h2>
|
||||
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}">
|
||||
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}" data-taxonomy-tags-widget-url="${taxonomy_tags_widget_url}">
|
||||
</article>
|
||||
</div>
|
||||
<div class="ui-loading">
|
||||
@@ -321,4 +321,7 @@ from django.urls import reverse
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="manage-tags-drawer" class="drawer"></div>
|
||||
<div class="drawer-cover"></div>
|
||||
</%block>
|
||||
|
||||
@@ -169,6 +169,17 @@ if (is_proctored_exam) {
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %>
|
||||
<li class="action-item">
|
||||
<a href="#" data-tooltip="<%- gettext('Manage Tags') %>" class="manage-tags-button action-button">
|
||||
<span class="icon fa fa-tag" aria-hidden="true"></span>
|
||||
<span>?</span>
|
||||
<span class="sr action-button-text"><%- gettext('Manage Tags') %></span>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>
|
||||
<!--
|
||||
If the ENABLE_COPY_PASTE_UNITS feature flag is enabled, all these actions (besides "Publish")
|
||||
@@ -192,6 +203,11 @@ if (is_proctored_exam) {
|
||||
<li class="nav-item">
|
||||
<a class="copy-button" href="#" role="button"><%- gettext('Copy to Clipboard') %></a>
|
||||
</li>
|
||||
<% if (typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %>
|
||||
<li class="nav-item">
|
||||
<a class="manage-tags-button" href="#" role="button"><%- gettext('Manage Tags') %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% if (xblockInfo.isDuplicable()) { %>
|
||||
<li class="nav-item">
|
||||
|
||||
@@ -342,7 +342,10 @@ urlpatterns += [
|
||||
path('api/content_tagging/', include(('openedx.core.djangoapps.content_tagging.urls', 'content_tagging'))),
|
||||
]
|
||||
|
||||
# studio-content-api specific API docs (using drf-spectacular and openapi-v3)
|
||||
# Authoring-api specific API docs (using drf-spectacular and openapi-v3).
|
||||
# This is separate from and in addition to the full studio swagger documentation already existing at /api-docs.
|
||||
# Custom settings are provided in SPECTACULAR_SETTINGS in cms/envs/common.py.
|
||||
# Filter function in cms/lib/spectacular.py determines paths that are swagger-documented.
|
||||
urlpatterns += [
|
||||
re_path('^cms-api/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
re_path('^cms-api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
"""
|
||||
Handlers for student
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
|
||||
from openedx_events.event_bus import get_producer
|
||||
from openedx_events.learning.signals import (
|
||||
COURSE_UNENROLLMENT_COMPLETED,
|
||||
)
|
||||
from openedx.core.lib.events import determine_producer_config_for_signal_and_topic
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(COURSE_UNENROLLMENT_COMPLETED)
|
||||
def course_unenrollment_receiver(sender, signal, **kwargs):
|
||||
"""
|
||||
Removes user notification preference when user un-enrolls from the course
|
||||
"""
|
||||
topic = getattr(settings, "EVENT_BUS_ENROLLMENT_LIFECYCLE_TOPIC", "course-unenrollment-lifecycle")
|
||||
producer_config_setting = determine_producer_config_for_signal_and_topic(COURSE_UNENROLLMENT_COMPLETED, topic)
|
||||
if producer_config_setting is True:
|
||||
log.info("Producing unenrollment-event event via config")
|
||||
return
|
||||
if settings.FEATURES.get("ENABLE_SEND_ENROLLMENT_EVENTS_OVER_BUS"):
|
||||
log.info("Producing unenrollment-event event via manual send")
|
||||
get_producer().send(
|
||||
signal=COURSE_UNENROLLMENT_COMPLETED,
|
||||
topic=topic,
|
||||
event_key_field='enrollment.course.course_key',
|
||||
event_data={'enrollment': kwargs.get('enrollment')},
|
||||
event_metadata=kwargs.get('metadata')
|
||||
)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
Unit tests for event bus tests for course unenrollments
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from common.djangoapps.student.handlers import course_unenrollment_receiver
|
||||
from common.djangoapps.student.tests.factories import (
|
||||
UserFactory,
|
||||
CourseEnrollmentFactory,
|
||||
)
|
||||
|
||||
from openedx_events.data import EventsMetadata
|
||||
from openedx_events.learning.signals import COURSE_UNENROLLMENT_COMPLETED
|
||||
from pytest import mark
|
||||
|
||||
|
||||
@mark.django_db
|
||||
class UnenrollmentEventBusTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for unenrollment events that interact with the event bus.
|
||||
"""
|
||||
@override_settings(ENABLE_SEND_ENROLLMENT_EVENTS_OVER_BUS=False)
|
||||
@mock.patch('common.djangoapps.student.handlers.get_producer', autospec=True)
|
||||
def test_event_disabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we do not push `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
course_unenrollment_receiver(None, None)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_SEND_ENROLLMENT_EVENTS_OVER_BUS': True})
|
||||
@mock.patch('common.djangoapps.student.handlers.get_producer', autospec=True)
|
||||
def test_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `COURSE_UNENROLLMENT_COMPLETED` events to the event bus.
|
||||
"""
|
||||
user = UserFactory()
|
||||
enrollment = CourseEnrollmentFactory(user=user)
|
||||
|
||||
event_metadata = EventsMetadata(
|
||||
event_type=COURSE_UNENROLLMENT_COMPLETED.event_type,
|
||||
id=uuid4(),
|
||||
minorversion=0,
|
||||
source='openedx/lms/web',
|
||||
sourcehost='lms.test',
|
||||
time=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
event_kwargs = {
|
||||
'enrollment': enrollment,
|
||||
'metadata': event_metadata
|
||||
}
|
||||
course_unenrollment_receiver(None, COURSE_UNENROLLMENT_COMPLETED, **event_kwargs)
|
||||
|
||||
# verify that the data sent to the event bus matches what we expect
|
||||
print(mock_producer.return_value)
|
||||
print(mock_producer.return_value.send.call_args)
|
||||
data = mock_producer.return_value.send.call_args.kwargs
|
||||
assert data['event_data']['enrollment'] == enrollment
|
||||
assert data['topic'] == 'course-unenrollment-lifecycle'
|
||||
assert data['event_key_field'] == 'enrollment.course.course_key'
|
||||
@@ -15,6 +15,7 @@ from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pyli
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from markupsafe import escape
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import CourseLocator
|
||||
@@ -33,6 +34,7 @@ from common.djangoapps.student.models import (
|
||||
user_by_anonymous_id
|
||||
)
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.student.toggles import REDIRECT_TO_COURSEWARE_AFTER_ENROLLMENT
|
||||
from common.djangoapps.student.views import complete_course_mode_info
|
||||
from common.djangoapps.util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
|
||||
from common.djangoapps.util.testing import EventTestMixin
|
||||
@@ -893,6 +895,7 @@ class EnrollInCourseTest(EnrollmentEventTestMixin, CacheIsolationTestCase):
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@ddt.ddt
|
||||
class ChangeEnrollmentViewTest(ModuleStoreTestCase):
|
||||
"""Tests the student.views.change_enrollment view"""
|
||||
|
||||
@@ -913,6 +916,17 @@ class ChangeEnrollmentViewTest(ModuleStoreTestCase):
|
||||
)
|
||||
return response
|
||||
|
||||
@ddt.data(
|
||||
(True, 'courseware'),
|
||||
(False, None),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_enrollment_url(self, waffle_flag_enabled, returned_view):
|
||||
with override_waffle_switch(REDIRECT_TO_COURSEWARE_AFTER_ENROLLMENT, waffle_flag_enabled):
|
||||
response = self._enroll_through_view(self.course)
|
||||
data = reverse(returned_view, args=[str(self.course.id)]) if returned_view else ''
|
||||
assert response.content.decode('utf8') == data
|
||||
|
||||
def test_enroll_as_default(self):
|
||||
"""Tests that a student can successfully enroll through this view"""
|
||||
response = self._enroll_through_view(self.course)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Toggles for Dashboard page.
|
||||
"""
|
||||
from edx_toggles.toggles import WaffleFlag
|
||||
from edx_toggles.toggles import WaffleFlag, WaffleSwitch
|
||||
|
||||
# Namespace for student waffle flags.
|
||||
WAFFLE_FLAG_NAMESPACE = 'student'
|
||||
@@ -75,3 +75,21 @@ ENROLLMENT_CONFIRMATION_EMAIL = WaffleFlag(
|
||||
|
||||
def should_send_enrollment_email():
|
||||
return ENROLLMENT_CONFIRMATION_EMAIL.is_enabled()
|
||||
|
||||
|
||||
# Waffle flag to enable control redirecting after enrolment.
|
||||
# .. toggle_name: student.redirect_to_courseware_after_enrollment
|
||||
# .. toggle_implementation: WaffleSwitch
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Redirect to courseware after enrollment instead of dashboard.
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2023-02-06
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_warning: None
|
||||
REDIRECT_TO_COURSEWARE_AFTER_ENROLLMENT = WaffleSwitch(
|
||||
f'{WAFFLE_FLAG_NAMESPACE}.redirect_to_courseware_after_enrollment', __name__
|
||||
)
|
||||
|
||||
|
||||
def should_redirect_to_courseware_after_enrollment():
|
||||
return REDIRECT_TO_COURSEWARE_AFTER_ENROLLMENT.is_enabled()
|
||||
|
||||
@@ -38,6 +38,7 @@ from pytz import UTC
|
||||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from common.djangoapps.student.toggles import should_redirect_to_courseware_after_enrollment
|
||||
from common.djangoapps.track import views as track_views
|
||||
from lms.djangoapps.bulk_email.models import Optout
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
@@ -400,8 +401,10 @@ def change_enrollment(request, check_access=True):
|
||||
reverse("course_modes_choose", kwargs={'course_id': str(course_id)})
|
||||
)
|
||||
|
||||
# Otherwise, there is only one mode available (the default)
|
||||
return HttpResponse()
|
||||
if should_redirect_to_courseware_after_enrollment():
|
||||
return HttpResponse(reverse('courseware', args=[str(course_id)]))
|
||||
else:
|
||||
return HttpResponse()
|
||||
elif action == "unenroll":
|
||||
if configuration_helpers.get_value(
|
||||
"DISABLE_UNENROLLMENT",
|
||||
|
||||
@@ -13,7 +13,6 @@ not possible to have this LTI multiple times on a single page in LMS.
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
@@ -78,7 +77,7 @@ class StubLtiHandler(StubHttpRequestHandler):
|
||||
'callback_url': self.post_dict.get('lis_outcome_service_url').replace('https', 'http'),
|
||||
'sourcedId': self.post_dict.get('lis_result_sourcedid')
|
||||
}
|
||||
host = os.environ.get('BOK_CHOY_HOSTNAME', self.server.server_address[0])
|
||||
host = self.server.server_address[0]
|
||||
submit_url = f'//{host}:{self.server.server_address[1]}'
|
||||
content = self._create_content(status_message, submit_url)
|
||||
self.send_response(200, content)
|
||||
@@ -296,7 +295,7 @@ class StubLtiHandler(StubHttpRequestHandler):
|
||||
|
||||
"""
|
||||
client_secret = str(self.server.config.get('client_secret', self.DEFAULT_CLIENT_SECRET))
|
||||
host = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1')
|
||||
host = '127.0.0.1'
|
||||
port = self.server.server_address[1]
|
||||
lti_base = self.DEFAULT_LTI_ADDRESS.format(host=host, port=port)
|
||||
lti_endpoint = self.server.config.get('lti_endpoint', self.DEFAULT_LTI_ENDPOINT)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from io import StringIO
|
||||
|
||||
import ddt
|
||||
import unittest
|
||||
from django.core.management import call_command
|
||||
from django.db.transaction import TransactionManagementError, atomic
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
@@ -121,7 +120,6 @@ class MigrationTests(TestCase):
|
||||
Tests for migrations.
|
||||
"""
|
||||
|
||||
@unittest.skip("Migration will delete several models. Need to ship not referencing it first")
|
||||
@override_settings(MIGRATION_MODULES={})
|
||||
def test_migrations_are_in_sync(self):
|
||||
"""
|
||||
|
||||
@@ -281,15 +281,6 @@ function getBaseConfig(config, useRequireJs) {
|
||||
'framework:custom': ['factory', initFrameworks]
|
||||
};
|
||||
|
||||
if (process.env.hasOwnProperty('BOK_CHOY_HOSTNAME')) {
|
||||
hostname = process.env.BOK_CHOY_HOSTNAME;
|
||||
if (hostname === 'edx.devstack.lms') {
|
||||
port = 19876;
|
||||
} else {
|
||||
port = 19877;
|
||||
}
|
||||
}
|
||||
|
||||
initFrameworks.$inject = ['config.files'];
|
||||
|
||||
return {
|
||||
@@ -385,7 +376,7 @@ function getBaseConfig(config, useRequireJs) {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: config.singleRun,
|
||||
|
||||
Binary file not shown.
@@ -11,4 +11,14 @@ 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 = .cache
|
||||
|
||||
@@ -59,7 +59,6 @@ and for all environments to use __init__.py to load their settings.
|
||||
The following files should be obviated by this change:
|
||||
|
||||
* bok_choy_docker.py
|
||||
* bok_choy.py
|
||||
* devstack_docker.py
|
||||
* devstack_optimized.py
|
||||
* devstack.py
|
||||
|
||||
41
lms/djangoapps/badges/migrations/0005_delete_all_models.py
Normal file
41
lms/djangoapps/badges/migrations/0005_delete_all_models.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 3.2.22 on 2023-10-24 15:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('badges', '0004_badgeclass_badgr_server_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='badgeassertion',
|
||||
name='badge_class',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='badgeassertion',
|
||||
name='user',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='badgeclass',
|
||||
unique_together=None,
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CourseCompleteImageConfiguration',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='courseeventbadgesconfiguration',
|
||||
name='changed_by',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BadgeAssertion',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BadgeClass',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CourseEventBadgesConfiguration',
|
||||
),
|
||||
]
|
||||
@@ -2,8 +2,7 @@
|
||||
This module contains various configuration settings via
|
||||
waffle switches for the Certificates app.
|
||||
"""
|
||||
|
||||
from edx_toggles.toggles import SettingToggle, WaffleSwitch
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
|
||||
# Namespace
|
||||
WAFFLE_NAMESPACE = 'certificates'
|
||||
@@ -15,31 +14,3 @@ WAFFLE_NAMESPACE = 'certificates'
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2017-09-14
|
||||
AUTO_CERTIFICATE_GENERATION = WaffleSwitch(f"{WAFFLE_NAMESPACE}.auto_certificate_generation", __name__)
|
||||
|
||||
|
||||
# .. toggle_name: SEND_CERTIFICATE_CREATED_SIGNAL
|
||||
# .. toggle_implementation: SettingToggle
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: When True, the system will publish `CERTIFICATE_CREATED` signals to the event bus. The
|
||||
# `CERTIFICATE_CREATED` signal is emit when a certificate has been awarded to a learner and the creation process has
|
||||
# completed.
|
||||
# .. toggle_warning: Will be deprecated in favor of SEND_LEARNING_CERTIFICATE_LIFECYCLE_EVENTS_TO_BUS
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2023-04-11
|
||||
# .. toggle_target_removal_date: 2023-07-31
|
||||
# .. toggle_tickets: TODO
|
||||
SEND_CERTIFICATE_CREATED_SIGNAL = SettingToggle('SEND_CERTIFICATE_CREATED_SIGNAL', default=False, module_name=__name__)
|
||||
|
||||
|
||||
# .. toggle_name: SEND_CERTIFICATE_REVOKED_SIGNAL
|
||||
# .. toggle_implementation: SettingToggle
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: When True, the system will publish `CERTIFICATE_REVOKED` signals to the event bus. The
|
||||
# `CERTIFICATE_REVOKED` signal is emit when a certificate has been revoked from a learner and the revocation process
|
||||
# has completed.
|
||||
# .. toggle_warning: Will be deprecated in favor of SEND_LEARNING_CERTIFICATE_LIFECYCLE_EVENTS_TO_BUS
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2023-09-15
|
||||
# .. toggle_target_removal_date: 2024-01-01
|
||||
# .. toggle_tickets: TODO
|
||||
SEND_CERTIFICATE_REVOKED_SIGNAL = SettingToggle('SEND_CERTIFICATE_REVOKED_SIGNAL', default=False, module_name=__name__)
|
||||
|
||||
@@ -3,17 +3,13 @@ Signal handler for enabling/disabling self-generated certificates based on the c
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from openedx_events.event_bus import get_producer
|
||||
from edx_django_utils.monitoring import set_custom_attribute
|
||||
|
||||
from common.djangoapps.course_modes import api as modes_api
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED
|
||||
from lms.djangoapps.certificates.config import SEND_CERTIFICATE_CREATED_SIGNAL, SEND_CERTIFICATE_REVOKED_SIGNAL
|
||||
from lms.djangoapps.certificates.generation_handler import (
|
||||
CertificateGenerationNotAllowed,
|
||||
generate_allowlist_certificate_task,
|
||||
@@ -29,13 +25,11 @@ from lms.djangoapps.certificates.models import (
|
||||
from lms.djangoapps.certificates.api import auto_certificate_generation_enabled
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from openedx.core.djangoapps.content.course_overviews.signals import COURSE_PACING_CHANGED
|
||||
from openedx.core.lib.events import determine_producer_config_for_signal_and_topic
|
||||
from openedx.core.djangoapps.signals.signals import (
|
||||
COURSE_GRADE_NOW_FAILED,
|
||||
COURSE_GRADE_NOW_PASSED,
|
||||
LEARNER_NOW_VERIFIED
|
||||
)
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED, CERTIFICATE_REVOKED
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -162,71 +156,3 @@ def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs)
|
||||
course_key,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _determine_producer_config_for_signal_and_topic(signal, topic):
|
||||
"""
|
||||
Utility method to determine the setting for the given signal and topic in EVENT_BUS_PRODUCER_CONFIG
|
||||
|
||||
Records to New Relic for later analysis.
|
||||
|
||||
Parameters
|
||||
signal (OpenEdxPublicSignal): The signal being sent to the event bus
|
||||
topic (string): The topic to which the signal is being sent (without environment prefix)
|
||||
|
||||
Returns
|
||||
True if the signal is enabled for that topic in EVENT_BUS_PRODUCER_CONFIG
|
||||
False if the signal is explicitly disabled for that topic in EVENT_BUS_PRODUCER_CONFIG
|
||||
None if the signal/topic pair is not present in EVENT_BUS_PRODUCER_CONFIG
|
||||
"""
|
||||
event_type_producer_configs = getattr(settings, "EVENT_BUS_PRODUCER_CONFIG",
|
||||
{}).get(signal.event_type, {})
|
||||
topic_config = event_type_producer_configs.get(topic, {})
|
||||
topic_setting = topic_config.get('enabled', None)
|
||||
set_custom_attribute(f'producer_config_setting_{topic}_{signal.event_type}',
|
||||
topic_setting if topic_setting is not None else 'Unset')
|
||||
return topic_setting
|
||||
|
||||
|
||||
@receiver(CERTIFICATE_CREATED)
|
||||
def listen_for_certificate_created_event(sender, signal, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Publish `CERTIFICATE_CREATED` events to the event bus.
|
||||
"""
|
||||
# temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present
|
||||
producer_config_setting = determine_producer_config_for_signal_and_topic(CERTIFICATE_CREATED,
|
||||
'learning-certificate-lifecycle')
|
||||
if producer_config_setting is True:
|
||||
log.info("Producing certificate-created event via config")
|
||||
return
|
||||
if SEND_CERTIFICATE_CREATED_SIGNAL.is_enabled():
|
||||
log.info("Producing certificate-created event via manual send")
|
||||
get_producer().send(
|
||||
signal=CERTIFICATE_CREATED,
|
||||
topic='learning-certificate-lifecycle',
|
||||
event_key_field='certificate.course.course_key',
|
||||
event_data={'certificate': kwargs['certificate']},
|
||||
event_metadata=kwargs['metadata']
|
||||
)
|
||||
|
||||
|
||||
@receiver(CERTIFICATE_REVOKED)
|
||||
def listen_for_certificate_revoked_event(sender, signal, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Publish `CERTIFICATE_REVOKED` events to the event bus.
|
||||
"""
|
||||
# temporary: defer to EVENT_BUS_PRODUCER_CONFIG if present
|
||||
producer_config_setting = determine_producer_config_for_signal_and_topic(CERTIFICATE_REVOKED,
|
||||
'learning-certificate-lifecycle')
|
||||
if producer_config_setting is True:
|
||||
log.info("Producing certificate-revoked event via config")
|
||||
return
|
||||
if SEND_CERTIFICATE_REVOKED_SIGNAL.is_enabled():
|
||||
log.info("Producing certificate-revoked event via manual send")
|
||||
get_producer().send(
|
||||
signal=CERTIFICATE_REVOKED,
|
||||
topic='learning-certificate-lifecycle',
|
||||
event_key_field='certificate.course.course_key',
|
||||
event_data={'certificate': kwargs['certificate']},
|
||||
event_metadata=kwargs['metadata']
|
||||
)
|
||||
|
||||
@@ -3,12 +3,9 @@ Unit tests for enabling self-generated certificates for self-paced courses
|
||||
and disabling for instructor-paced courses.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
from django.test.utils import override_settings
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -21,17 +18,10 @@ from lms.djangoapps.certificates.models import (
|
||||
CertificateGenerationConfiguration,
|
||||
GeneratedCertificate
|
||||
)
|
||||
from lms.djangoapps.certificates.signals import (
|
||||
listen_for_certificate_created_event,
|
||||
listen_for_certificate_revoked_event
|
||||
)
|
||||
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from lms.djangoapps.grades.tests.utils import mock_passing_grade
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx_events.data import EventsMetadata
|
||||
from openedx_events.learning.signals import CERTIFICATE_CREATED, CERTIFICATE_REVOKED
|
||||
from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData
|
||||
|
||||
|
||||
class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
|
||||
@@ -443,109 +433,3 @@ class EnrollmentModeChangeCertsTest(ModuleStoreTestCase):
|
||||
) as mock_allowlist_task:
|
||||
self.verified_enrollment.change_mode('audit')
|
||||
mock_allowlist_task.assert_not_called()
|
||||
|
||||
|
||||
class CertificateEventBusTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for Certificate events that interact with the event bus.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.name = f'{self.user.first_name} {self.user.last_name}'
|
||||
self.course = CourseFactory.create(self_paced=True)
|
||||
self.enrollment = CourseEnrollmentFactory(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
is_active=True,
|
||||
mode='verified',
|
||||
)
|
||||
|
||||
def _create_event_data(self, event_type, certificate_status):
|
||||
"""
|
||||
Utility function to create test data for unit tests.
|
||||
"""
|
||||
expected_course_data = CourseData(course_key=self.course.id)
|
||||
expected_user_data = UserData(
|
||||
pii=UserPersonalData(
|
||||
username=self.user.username,
|
||||
email=self.user.email,
|
||||
name=self.name,
|
||||
),
|
||||
id=self.user.id,
|
||||
is_active=self.user.is_active
|
||||
)
|
||||
expected_certificate_data = CertificateData(
|
||||
user=expected_user_data,
|
||||
course=expected_course_data,
|
||||
mode='verified',
|
||||
grade='',
|
||||
current_status=certificate_status,
|
||||
download_url='',
|
||||
name='',
|
||||
)
|
||||
expected_event_metadata = EventsMetadata(
|
||||
event_type=event_type.event_type,
|
||||
id=uuid4(),
|
||||
minorversion=0,
|
||||
source='openedx/lms/web',
|
||||
sourcehost='lms.test',
|
||||
time=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
return {
|
||||
'certificate': expected_certificate_data,
|
||||
'metadata': expected_event_metadata,
|
||||
}
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=False)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_created_event_disabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we do not publish `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_REVOKED_SIGNAL=False)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_revoked_event_disabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we do not publish `CERTIFICATE_REVOKED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_REVOKED_SIGNAL` setting is disabled.
|
||||
"""
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_REVOKED)
|
||||
mock_producer.assert_not_called()
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=True)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_created_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `CERTIFICATE_CREATED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is enabled.
|
||||
"""
|
||||
event_data = self._create_event_data(CERTIFICATE_CREATED, CertificateStatuses.downloadable)
|
||||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED, **event_data)
|
||||
# verify that the data sent to the event bus matches what we expect
|
||||
data = mock_producer.return_value.send.call_args.kwargs
|
||||
assert data['signal'].event_type == CERTIFICATE_CREATED.event_type
|
||||
assert data['event_data']['certificate'] == event_data['certificate']
|
||||
assert data['topic'] == 'learning-certificate-lifecycle'
|
||||
assert data['event_key_field'] == 'certificate.course.course_key'
|
||||
|
||||
@override_settings(SEND_CERTIFICATE_REVOKED_SIGNAL=True)
|
||||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True)
|
||||
def test_certificate_revoked_event_enabled(self, mock_producer):
|
||||
"""
|
||||
Test to verify that we push `CERTIFICATE_REVOKED` events to the event bus if the
|
||||
`SEND_CERTIFICATE_REVOKED_SIGNAL` setting is enabled.
|
||||
"""
|
||||
event_data = self._create_event_data(CERTIFICATE_REVOKED, CertificateStatuses.notpassing)
|
||||
listen_for_certificate_revoked_event(None, CERTIFICATE_REVOKED, **event_data)
|
||||
# verify that the data sent to the event bus matches what we expect
|
||||
data = mock_producer.return_value.send.call_args.kwargs
|
||||
assert data['signal'].event_type == CERTIFICATE_REVOKED.event_type
|
||||
assert data['event_data']['certificate'] == event_data['certificate']
|
||||
assert data['topic'] == 'learning-certificate-lifecycle'
|
||||
assert data['event_key_field'] == 'certificate.course.course_key'
|
||||
|
||||
@@ -307,4 +307,4 @@ class BasketOrderViewTests(UserMixin, TestCase):
|
||||
""" The view should return 403 if the user is not logged in. """
|
||||
self.client.logout()
|
||||
response = self.client.get(self.path)
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
|
||||
@@ -82,7 +82,7 @@ def course_detail(request, username, course_key):
|
||||
return overview
|
||||
|
||||
|
||||
def _filter_by_search(course_queryset, search_term):
|
||||
def _filter_by_search(course_queryset, search_term, mobile_search=False):
|
||||
"""
|
||||
Filters a course queryset by the specified search term.
|
||||
"""
|
||||
@@ -101,6 +101,13 @@ def _filter_by_search(course_queryset, search_term):
|
||||
|
||||
search_courses_ids = {course['data']['id'] for course in search_courses['results']}
|
||||
|
||||
if mobile_search is True:
|
||||
course_limit = getattr(settings, 'MOBILE_SEARCH_COURSE_LIMIT', 100)
|
||||
courses = [course for course in course_queryset[:course_limit] if str(course.id) in search_courses_ids]
|
||||
return LazySequence(
|
||||
iter(courses),
|
||||
est_len=len(courses)
|
||||
)
|
||||
return LazySequence(
|
||||
(
|
||||
course for course in course_queryset
|
||||
@@ -117,7 +124,8 @@ def list_courses(request,
|
||||
search_term=None,
|
||||
permissions=None,
|
||||
active_only=False,
|
||||
course_keys=None):
|
||||
course_keys=None,
|
||||
mobile_search=False):
|
||||
"""
|
||||
Yield all available courses.
|
||||
|
||||
@@ -150,6 +158,9 @@ def list_courses(request,
|
||||
course_keys (list[str]):
|
||||
If specified, it filters visible `CourseOverview` objects by
|
||||
the course keys (ids) provided
|
||||
mobile_search (bool):
|
||||
Optional parameter that limits the number of returned courses
|
||||
to MOBILE_SEARCH_COURSE_LIMIT.
|
||||
|
||||
Return value:
|
||||
Yield `CourseOverview` objects representing the collection of courses.
|
||||
@@ -158,7 +169,7 @@ def list_courses(request,
|
||||
course_qs = get_courses(
|
||||
user, org=org, filter_=filter_, permissions=permissions, active_only=active_only, course_keys=course_keys
|
||||
)
|
||||
course_qs = _filter_by_search(course_qs, search_term)
|
||||
course_qs = _filter_by_search(course_qs, search_term, mobile_search)
|
||||
return course_qs
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ class CourseListGetForm(UsernameValidatorMixin, Form):
|
||||
active_only = ExtendedNullBooleanField(required=False)
|
||||
permissions = MultiValueField(required=False)
|
||||
course_keys = MultiValueField(required=False)
|
||||
mobile_search = ExtendedNullBooleanField(required=False)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
|
||||
@@ -72,6 +72,7 @@ class TestCourseListGetForm(FormTestMixin, UsernameTestMixin, SharedModuleStoreT
|
||||
'permissions': set(),
|
||||
'active_only': None,
|
||||
'course_keys': set(),
|
||||
'mobile_search': None,
|
||||
}
|
||||
|
||||
def test_basic(self):
|
||||
|
||||
@@ -449,6 +449,46 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
|
||||
assert len(response.data['results']) == (30 if (page < 11) else 3)
|
||||
assert [c['id'] for c in response.data['results']] == ordered_course_ids[((page - 1) * 30):(page * 30)]
|
||||
|
||||
def test_count_item_pagination_with_search_term(self):
|
||||
"""
|
||||
Test count items in pagination for api courses list - class CourseListView
|
||||
"""
|
||||
# Create 15 new courses, courses have the word "new" in the title
|
||||
[self.create_and_index_course(f"numb_{number}", f"new_{number}") for number in range(15)] # pylint: disable=expression-not-assigned
|
||||
response = self.verify_response(params={"search_term": "new"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# We don't have 'count' 15 because 'mobile_search' param is None
|
||||
# And LazySequence contains all courses
|
||||
self.assertEqual(response.json()["pagination"]["count"], 18)
|
||||
|
||||
def test_count_item_pagination_with_search_term_and_filter(self):
|
||||
"""
|
||||
Test count items in pagination for api courses list
|
||||
with search_term and filter by organisation -
|
||||
class CourseListView
|
||||
"""
|
||||
# Create 25 new courses with two different organisations
|
||||
[self.create_and_index_course("Org_N", f"new_{number}") for number in range(10)] # pylint: disable=expression-not-assigned
|
||||
[self.create_and_index_course("Org_X", f"new_{number}") for number in range(15)] # pylint: disable=expression-not-assigned
|
||||
response = self.verify_response(params={"org": "Org_X", "search_term": "new"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["pagination"]["count"], 15)
|
||||
|
||||
def test_count_item_pagination_with_search_term_and_mobile_search(self):
|
||||
"""
|
||||
Test count items in pagination for api courses list
|
||||
with search_term and 'mobile_search' is True
|
||||
"""
|
||||
# Create 25 new courses with two different words in titles
|
||||
[self.create_and_index_course("Org_N", f"old_{number}") for number in range(10)] # pylint: disable=expression-not-assigned
|
||||
[self.create_and_index_course("Org_N", f"new_{number}") for number in range(15)] # pylint: disable=expression-not-assigned
|
||||
response = self.verify_response(
|
||||
params={"search_term": "new", "mobile_search": True}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# We have 'count' 15 because 'mobile_search' param is true
|
||||
self.assertEqual(response.json()["pagination"]["count"], 15)
|
||||
|
||||
|
||||
class CourseIdListViewTestCase(CourseApiTestViewMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -290,6 +290,10 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
If specified, it fetches the `CourseOverview` objects for the
|
||||
the specified course keys
|
||||
|
||||
mobile_search (bool):
|
||||
Optional parameter that limits the number of returned courses
|
||||
to MOBILE_SEARCH_COURSE_LIMIT.
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200 on success, with a list of course discovery objects as returned
|
||||
@@ -349,6 +353,7 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
permissions=form.cleaned_data['permissions'],
|
||||
active_only=form.cleaned_data.get('active_only', False),
|
||||
course_keys=form.cleaned_data['course_keys'],
|
||||
mobile_search=form.cleaned_data.get('mobile_search', False),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from cms.djangoapps.contentstore.utils import get_proctored_exam_settings_url
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.course_apps.plugins import CourseApp
|
||||
from openedx.core.djangoapps.course_apps.toggles import proctoring_settings_modal_view_enabled
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
|
||||
User = get_user_model()
|
||||
@@ -211,11 +209,6 @@ class ProctoringCourseApp(CourseApp):
|
||||
"configure": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def legacy_link(course_key: CourseKey):
|
||||
if not proctoring_settings_modal_view_enabled(course_key):
|
||||
return get_proctored_exam_settings_url(course_key)
|
||||
|
||||
|
||||
class CustomPagesCourseApp(CourseApp):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ Helpers for courseware tests.
|
||||
|
||||
|
||||
import ast
|
||||
import re
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
@@ -450,11 +451,15 @@ def get_context_dict_from_string(data):
|
||||
Retrieve dictionary from string.
|
||||
"""
|
||||
# Replace tuple and un-necessary info from inside string and get the dictionary.
|
||||
cleaned_data = ast.literal_eval(data.split('((\'video.html\',')[1].replace("),\n {})", '').strip())
|
||||
cleaned_data['metadata'] = OrderedDict(
|
||||
sorted(json.loads(cleaned_data['metadata']).items(), key=lambda t: t[0])
|
||||
cleaned_data = data.split('((\'video.html\',')[1].replace("),\n {})", '').strip()
|
||||
# Omit user_id validation
|
||||
cleaned_data_without_user = re.sub(".*user_id.*\n?", '', cleaned_data)
|
||||
|
||||
validated_data = ast.literal_eval(cleaned_data_without_user)
|
||||
validated_data['metadata'] = OrderedDict(
|
||||
sorted(json.loads(validated_data['metadata']).items(), key=lambda t: t[0])
|
||||
)
|
||||
return cleaned_data
|
||||
return validated_data
|
||||
|
||||
|
||||
def set_preview_mode(preview_mode: bool):
|
||||
|
||||
@@ -124,6 +124,7 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas
|
||||
'lmsRootURL': settings.LMS_ROOT_URL,
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'autohideHtml5': False,
|
||||
'recordedYoutubeIsAvailable': True,
|
||||
'completionEnabled': False,
|
||||
@@ -138,6 +139,8 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
|
||||
mako_service = self.block.runtime.service(self.block, 'mako')
|
||||
@@ -209,6 +212,7 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
|
||||
'lmsRootURL': settings.LMS_ROOT_URL,
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'autohideHtml5': False,
|
||||
'recordedYoutubeIsAvailable': True,
|
||||
'completionEnabled': False,
|
||||
@@ -223,6 +227,8 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
|
||||
mako_service = self.block.runtime.service(self.block, 'mako')
|
||||
@@ -365,6 +371,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'lmsRootURL': settings.LMS_ROOT_URL,
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'autohideHtml5': False,
|
||||
'recordedYoutubeIsAvailable': True,
|
||||
'completionEnabled': False,
|
||||
@@ -465,6 +472,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
@@ -595,6 +604,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
initial_context['metadata']['duration'] = None
|
||||
|
||||
@@ -707,6 +718,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
metadata = self.default_metadata_dict
|
||||
metadata['autoplay'] = False
|
||||
metadata['sources'] = ""
|
||||
|
||||
initial_context = {
|
||||
'autoadvance_enabled': False,
|
||||
'branding_info': None,
|
||||
@@ -730,6 +742,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
],
|
||||
'poster': 'null',
|
||||
'metadata': metadata,
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': 'mock item',
|
||||
}
|
||||
|
||||
DATA = SOURCE_XML.format( # lint-amnesty, pylint: disable=invalid-name
|
||||
@@ -884,6 +898,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
# Video found for edx_video_id
|
||||
metadata = self.default_metadata_dict
|
||||
metadata['sources'] = ""
|
||||
|
||||
initial_context = {
|
||||
'autoadvance_enabled': False,
|
||||
'branding_info': None,
|
||||
@@ -907,6 +922,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
],
|
||||
'poster': 'null',
|
||||
'metadata': metadata,
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': data['edx_video_id'].replace('\t', ' '),
|
||||
}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@@ -1024,6 +1041,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': 'vid-v1:12345',
|
||||
}
|
||||
initial_context['metadata']['duration'] = None
|
||||
|
||||
@@ -1122,6 +1141,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': 'vid-v1:12345',
|
||||
}
|
||||
initial_context['metadata']['duration'] = None
|
||||
|
||||
@@ -2336,6 +2357,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
|
||||
|
||||
content = self.block.student_view(None).content
|
||||
sources = ['example.mp4', 'example.webm']
|
||||
|
||||
expected_context = {
|
||||
'autoadvance_enabled': False,
|
||||
'branding_info': None,
|
||||
@@ -2391,6 +2413,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
|
||||
'lmsRootURL': settings.LMS_ROOT_URL,
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'autohideHtml5': False,
|
||||
'recordedYoutubeIsAvailable': True,
|
||||
'completionEnabled': False,
|
||||
@@ -2407,7 +2430,9 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
|
||||
'poster': json.dumps(OrderedDict({
|
||||
'url': 'http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg',
|
||||
'type': 'youtube'
|
||||
}))
|
||||
})),
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
|
||||
mako_service = self.block.runtime.service(self.block, 'mako')
|
||||
@@ -2431,6 +2456,7 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
|
||||
Build a dictionary with data expected by some operations in this test.
|
||||
Only parameters related to auto-advance are variable, rest is fixed.
|
||||
"""
|
||||
|
||||
context = {
|
||||
'autoadvance_enabled': autoadvanceenabled_flag,
|
||||
'branding_info': None,
|
||||
@@ -2474,6 +2500,7 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
|
||||
'transcriptAvailableTranslationsUrl': self.block.runtime.handler_url(
|
||||
self.block, 'transcript', 'available_translations'
|
||||
).rstrip('/?'),
|
||||
'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
|
||||
'autohideHtml5': False,
|
||||
'recordedYoutubeIsAvailable': True,
|
||||
'completionEnabled': False,
|
||||
@@ -2487,7 +2514,9 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
|
||||
{'display_name': 'SubRip (.srt) file', 'value': 'srt'},
|
||||
{'display_name': 'Text (.txt) file', 'value': 'txt'}
|
||||
],
|
||||
'poster': 'null'
|
||||
'poster': 'null',
|
||||
'transcript_feedback_enabled': False,
|
||||
'video_id': '',
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.urls import reverse
|
||||
from eventtracking.processors.exceptions import EventEmissionExit
|
||||
from mock import ANY, Mock, patch
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx_events.learning.signals import FORUM_THREAD_CREATED, FORUM_THREAD_RESPONSE_CREATED, FORUM_RESPONSE_COMMENT_CREATED
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
@@ -406,7 +407,7 @@ class ViewsQueryCountTestCase(
|
||||
return inner
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 3, 8, 42),
|
||||
(ModuleStoreEnum.Type.split, 3, 8, 43),
|
||||
)
|
||||
@ddt.unpack
|
||||
@count_queries
|
||||
@@ -1735,6 +1736,8 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
"""
|
||||
Check to make sure an event is fired when a user responds to a thread.
|
||||
"""
|
||||
event_receiver = Mock()
|
||||
FORUM_THREAD_RESPONSE_CREATED.connect(event_receiver)
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"closed": False,
|
||||
"commentable_id": 'test_commentable_id',
|
||||
@@ -1754,12 +1757,29 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
assert event['discussion']['id'] == 'test_thread_id'
|
||||
assert event['options']['followed'] is True
|
||||
|
||||
event_receiver.assert_called_once()
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": FORUM_THREAD_RESPONSE_CREATED,
|
||||
"sender": None,
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"thread",
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@patch('eventtracking.tracker.emit')
|
||||
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
||||
def test_comment_event(self, mock_request, mock_emit):
|
||||
"""
|
||||
Ensure an event is fired when someone comments on a response.
|
||||
"""
|
||||
event_receiver = Mock()
|
||||
FORUM_RESPONSE_COMMENT_CREATED.connect(event_receiver)
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"closed": False,
|
||||
"depth": 1,
|
||||
@@ -1781,6 +1801,19 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
assert event['user_course_roles'] == ['Wizard']
|
||||
assert event['options']['followed'] is False
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": FORUM_RESPONSE_COMMENT_CREATED,
|
||||
"sender": None,
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"thread",
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@patch('eventtracking.tracker.emit')
|
||||
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
||||
@ddt.data((
|
||||
@@ -1809,6 +1842,10 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
team = CourseTeamFactory.create(discussion_topic_id=TEAM_COMMENTABLE_ID)
|
||||
CourseTeamMembershipFactory.create(team=team, user=user)
|
||||
|
||||
event_receiver = Mock()
|
||||
forum_event = views.TRACKING_LOG_TO_EVENT_MAPS.get(event_name)
|
||||
forum_event.connect(event_receiver)
|
||||
|
||||
self._set_mock_request_data(mock_request, {
|
||||
'closed': False,
|
||||
'commentable_id': TEAM_COMMENTABLE_ID,
|
||||
@@ -1825,6 +1862,19 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
assert name == event_name
|
||||
assert event['team_id'] == team.team_id
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": forum_event,
|
||||
"sender": None,
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"thread",
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
('vote_for_thread', 'thread_id', 'thread'),
|
||||
('undo_vote_for_thread', 'thread_id', 'thread'),
|
||||
@@ -1863,6 +1913,10 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
@patch('eventtracking.tracker.emit')
|
||||
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
||||
def test_thread_followed_event(self, view_name, mock_request, mock_emit):
|
||||
event_receiver = Mock()
|
||||
for signal in views.TRACKING_LOG_TO_EVENT_MAPS.values():
|
||||
signal.connect(event_receiver)
|
||||
|
||||
self._set_mock_request_data(mock_request, {
|
||||
'closed': False,
|
||||
'commentable_id': 'test_commentable_id',
|
||||
@@ -1887,6 +1941,11 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque
|
||||
assert event_data['user_forums_roles'] == ['Student']
|
||||
assert event_data['user_course_roles'] == ['Wizard']
|
||||
|
||||
# In case of events that doesn't have a correspondig Open edX events signal
|
||||
# we need to check that none of the openedx signals is called.
|
||||
# This is tested for all the events that are not tested above.
|
||||
event_receiver.assert_not_called()
|
||||
|
||||
|
||||
class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRequestSetupMixin):
|
||||
|
||||
|
||||
@@ -16,12 +16,18 @@ from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from eventtracking import tracker
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx_events.learning.data import DiscussionThreadData, UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import (
|
||||
FORUM_RESPONSE_COMMENT_CREATED,
|
||||
FORUM_THREAD_CREATED,
|
||||
FORUM_THREAD_RESPONSE_CREATED
|
||||
)
|
||||
|
||||
import lms.djangoapps.discussion.django_comment_client.settings as cc_settings
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from common.djangoapps.student.roles import GlobalStaff
|
||||
from common.djangoapps.util.file import store_uploaded_file
|
||||
from common.djangoapps.track import contexts
|
||||
from common.djangoapps.util.file import store_uploaded_file
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import get_course_overview_with_access, get_course_with_access
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||
@@ -42,7 +48,7 @@ from lms.djangoapps.discussion.django_comment_client.utils import (
|
||||
get_user_group_ids,
|
||||
is_comment_too_deep,
|
||||
prepare_content,
|
||||
sanitize_body,
|
||||
sanitize_body
|
||||
)
|
||||
from openedx.core.djangoapps.django_comment_common.signals import (
|
||||
comment_created,
|
||||
@@ -66,6 +72,12 @@ TRACKING_MAX_FORUM_BODY = 2000
|
||||
TRACKING_MAX_FORUM_TITLE = 1000
|
||||
_EVENT_NAME_TEMPLATE = 'edx.forum.{obj_type}.{action_name}'
|
||||
|
||||
TRACKING_LOG_TO_EVENT_MAPS = {
|
||||
'edx.forum.thread.created': FORUM_THREAD_CREATED,
|
||||
'edx.forum.response.created': FORUM_THREAD_RESPONSE_CREATED,
|
||||
'edx.forum.comment.created': FORUM_RESPONSE_COMMENT_CREATED,
|
||||
}
|
||||
|
||||
|
||||
def track_forum_event(request, event_name, course, obj, data, id_map=None):
|
||||
"""
|
||||
@@ -97,6 +109,41 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None):
|
||||
with tracker.get_tracker().context(event_name, context):
|
||||
tracker.emit(event_name, data)
|
||||
|
||||
forum_event = TRACKING_LOG_TO_EVENT_MAPS.get(event_name, None)
|
||||
if forum_event is not None:
|
||||
forum_event.send_event(
|
||||
thread=DiscussionThreadData(
|
||||
anonymous=data.get('anonymous'),
|
||||
anonymous_to_peers=data.get('anonymous_to_peers'),
|
||||
body=data.get('body'),
|
||||
category_id=data.get('category_id'),
|
||||
category_name=data.get('category_name'),
|
||||
commentable_id=data.get('commentable_id'),
|
||||
group_id=data.get('group_id'),
|
||||
id=data.get('id'),
|
||||
team_id=data.get('team_id'),
|
||||
thread_type=data.get('thread_type'),
|
||||
title=data.get('title'),
|
||||
title_truncated=data.get('title_truncated'),
|
||||
truncated=data.get('truncated'),
|
||||
url=data.get('url'),
|
||||
discussion=data.get('discussion'),
|
||||
user_course_roles=data.get('user_course_roles'),
|
||||
user_forums_roles=data.get('user_forums_roles'),
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
name=user.profile.name,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
course_id=str(course.id),
|
||||
options=data.get('options'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_created_event(request, event_name, course, obj, data):
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,7 @@ from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
from opaque_keys.edx.locator import CourseKey
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS, ENABLE_COURSEWIDE_NOTIFICATIONS
|
||||
from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def send_thread_created_notification(thread_id, course_key_str, user_id):
|
||||
Send notification when a new thread is created
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
if not ENABLE_NOTIFICATIONS.is_enabled(course_key):
|
||||
if not (ENABLE_NOTIFICATIONS.is_enabled(course_key) and ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course_key)):
|
||||
return
|
||||
thread = Thread(id=thread_id).retrieve()
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
@@ -25,7 +25,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_STUDENT,
|
||||
CourseDiscussionSettings
|
||||
)
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_COURSEWIDE_NOTIFICATIONS, ENABLE_NOTIFICATIONS
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -44,6 +44,7 @@ def _get_mfe_url(course_id, post_id):
|
||||
@httpretty.activate
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
|
||||
@override_waffle_flag(ENABLE_COURSEWIDE_NOTIFICATIONS, active=True)
|
||||
class TestNewThreadCreatedNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Test cases related to new_discussion_post and new_question_post notification types
|
||||
|
||||
@@ -26,6 +26,7 @@ from common.djangoapps.student.tests.factories import StaffFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.test.utils import XssTestMixin
|
||||
from lms.djangoapps.courseware.courses import get_studio_url
|
||||
from lms.djangoapps.courseware.masquerade import CourseMasquerade
|
||||
from lms.djangoapps.courseware.tabs import get_course_tab_list
|
||||
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
@@ -119,6 +120,11 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT
|
||||
staff = StaffFactory(course_key=self.course.id)
|
||||
assert has_instructor_tab(staff, self.course)
|
||||
|
||||
masquerade_staff = StaffFactory(course_key=self.course.id)
|
||||
masquerade = CourseMasquerade(self.course.id, role='student')
|
||||
masquerade_staff.masquerade_settings = {self.course.id: masquerade}
|
||||
assert not has_instructor_tab(masquerade_staff, self.course)
|
||||
|
||||
student = UserFactory.create()
|
||||
assert not has_instructor_tab(student, self.course)
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ from lms.djangoapps.certificates.models import (
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.courses import get_studio_url
|
||||
from lms.djangoapps.courseware.block_render import get_block_by_usage_id
|
||||
from lms.djangoapps.courseware.masquerade import get_masquerade_role
|
||||
from lms.djangoapps.discussion.django_comment_client.utils import has_forum_access
|
||||
from lms.djangoapps.grades.api import is_writable_gradebook_enabled
|
||||
from lms.djangoapps.instructor.constants import INSTRUCTOR_DASHBOARD_PLUGIN_VIEW_NAME
|
||||
@@ -84,7 +85,9 @@ class InstructorDashboardTab(CourseTab):
|
||||
"""
|
||||
Returns true if the specified user has staff access.
|
||||
"""
|
||||
return bool(user and user.is_authenticated and user.has_perm(permissions.VIEW_DASHBOARD, course.id))
|
||||
return bool(user and user.is_authenticated and
|
||||
get_masquerade_role(user, course.id) != 'student' and
|
||||
user.has_perm(permissions.VIEW_DASHBOARD, course.id))
|
||||
|
||||
|
||||
def show_analytics_dashboard_message(course_key):
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.22 on 2023-11-06 09:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('lti_provider', '0003_auto_20161118_1040'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='lticonsumer',
|
||||
name='require_user_account',
|
||||
field=models.BooleanField(blank=True, default=False, help_text='When checked, the LTI content will load only for learners who have an account in this instance. This is required only for linking learner accounts with the LTI consumer. See the Open edX LTI Provider documentation for more details.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ltiuser',
|
||||
name='edx_user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,7 @@ import logging
|
||||
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
|
||||
|
||||
from openedx.core.djangolib.fields import CharNullField
|
||||
@@ -34,6 +35,11 @@ class LtiConsumer(models.Model):
|
||||
consumer_key = models.CharField(max_length=32, unique=True, db_index=True, default=short_token)
|
||||
consumer_secret = models.CharField(max_length=32, unique=True, default=short_token)
|
||||
instance_guid = CharNullField(max_length=255, blank=True, null=True, unique=True)
|
||||
require_user_account = models.BooleanField(blank=True, default=False, help_text=_(
|
||||
"When checked, the LTI content will load only for learners who have an account "
|
||||
"in this instance. This is required only for linking learner accounts with "
|
||||
"the LTI consumer. See the Open edX LTI Provider documentation for more details."
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def get_or_supplement(instance_guid, consumer_key):
|
||||
@@ -140,7 +146,7 @@ class LtiUser(models.Model):
|
||||
"""
|
||||
lti_consumer = models.ForeignKey(LtiConsumer, on_delete=models.CASCADE)
|
||||
lti_user_id = models.CharField(max_length=255)
|
||||
edx_user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
edx_user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('lti_consumer', 'lti_user_id')
|
||||
|
||||
@@ -7,8 +7,9 @@ import string
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
@@ -92,15 +93,22 @@ class AuthenticateLtiUserTest(TestCase):
|
||||
self.old_user = UserFactory.create()
|
||||
self.request = RequestFactory().post('/')
|
||||
self.request.user = self.old_user
|
||||
self.auto_linking_consumer = LtiConsumer(
|
||||
consumer_name='AutoLinkingConsumer',
|
||||
consumer_key='AutoLinkingKey',
|
||||
consumer_secret='AutoLinkingSecret',
|
||||
require_user_account=True
|
||||
)
|
||||
self.auto_linking_consumer.save()
|
||||
|
||||
def create_lti_user_model(self):
|
||||
def create_lti_user_model(self, consumer=None):
|
||||
"""
|
||||
Generate and save a User and an LTI user model
|
||||
"""
|
||||
edx_user = User(username=self.edx_user_id)
|
||||
edx_user.save()
|
||||
lti_user = LtiUser(
|
||||
lti_consumer=self.lti_consumer,
|
||||
lti_consumer=consumer or self.lti_consumer,
|
||||
lti_user_id=self.lti_user_id,
|
||||
edx_user=edx_user
|
||||
)
|
||||
@@ -140,6 +148,38 @@ class AuthenticateLtiUserTest(TestCase):
|
||||
assert not create_user.called
|
||||
switch_user.assert_called_with(self.request, lti_user, self.lti_consumer)
|
||||
|
||||
def test_auto_linking_of_users_using_lis_person_contact_email_primary(self, create_user, switch_user):
|
||||
request = RequestFactory().post("/", {"lis_person_contact_email_primary": self.old_user.email})
|
||||
request.user = self.old_user
|
||||
|
||||
users.authenticate_lti_user(request, self.lti_user_id, self.lti_consumer)
|
||||
create_user.assert_called_with(self.lti_user_id, self.lti_consumer)
|
||||
|
||||
users.authenticate_lti_user(request, self.lti_user_id, self.auto_linking_consumer)
|
||||
create_user.assert_called_with(self.lti_user_id, self.auto_linking_consumer, self.old_user.email)
|
||||
|
||||
def test_raise_exception_trying_to_auto_link_unauthenticate_user(self, create_user, switch_user):
|
||||
request = RequestFactory().post("/")
|
||||
request.user = AnonymousUser()
|
||||
|
||||
with self.assertRaises(PermissionDenied):
|
||||
users.authenticate_lti_user(request, self.lti_user_id, self.auto_linking_consumer)
|
||||
|
||||
def test_raise_exception_on_mismatched_user_and_lis_email(self, create_user, switch_user):
|
||||
request = RequestFactory().post("/", {"lis_person_contact_email_primary": "wrong_email@example.com"})
|
||||
request.user = self.old_user
|
||||
|
||||
with self.assertRaises(PermissionDenied):
|
||||
users.authenticate_lti_user(request, self.lti_user_id, self.auto_linking_consumer)
|
||||
|
||||
def test_authenticate_unauthenticated_user_after_auto_linking_of_user_account(self, create_user, switch_user):
|
||||
lti_user = self.create_lti_user_model(self.auto_linking_consumer)
|
||||
self.request.user = AnonymousUser()
|
||||
|
||||
users.authenticate_lti_user(self.request, self.lti_user_id, self.auto_linking_consumer)
|
||||
assert not create_user.called
|
||||
switch_user.assert_called_with(self.request, lti_user, self.auto_linking_consumer)
|
||||
|
||||
|
||||
class CreateLtiUserTest(TestCase):
|
||||
"""
|
||||
@@ -154,16 +194,17 @@ class CreateLtiUserTest(TestCase):
|
||||
consumer_secret='TestSecret'
|
||||
)
|
||||
self.lti_consumer.save()
|
||||
self.existing_user = UserFactory.create()
|
||||
|
||||
def test_create_lti_user_creates_auth_user_model(self):
|
||||
users.create_lti_user('lti_user_id', self.lti_consumer)
|
||||
assert User.objects.count() == 1
|
||||
assert User.objects.count() == 2
|
||||
|
||||
@patch('uuid.uuid4', return_value='random_uuid')
|
||||
@patch('lms.djangoapps.lti_provider.users.generate_random_edx_username', return_value='edx_id')
|
||||
def test_create_lti_user_creates_correct_user(self, uuid_mock, _username_mock):
|
||||
users.create_lti_user('lti_user_id', self.lti_consumer)
|
||||
assert User.objects.count() == 1
|
||||
assert User.objects.count() == 2
|
||||
user = User.objects.get(username='edx_id')
|
||||
assert user.email == 'edx_id@lti.example.com'
|
||||
uuid_mock.assert_called_with()
|
||||
@@ -173,10 +214,34 @@ class CreateLtiUserTest(TestCase):
|
||||
User(username='edx_id').save()
|
||||
users.create_lti_user('lti_user_id', self.lti_consumer)
|
||||
assert username_mock.call_count == 2
|
||||
assert User.objects.count() == 2
|
||||
assert User.objects.count() == 3
|
||||
user = User.objects.get(username='new_edx_id')
|
||||
assert user.email == 'new_edx_id@lti.example.com'
|
||||
|
||||
def test_existing_user_is_linked(self):
|
||||
lti_user = users.create_lti_user('lti_user_id', self.lti_consumer, self.existing_user.email)
|
||||
assert lti_user.lti_consumer == self.lti_consumer
|
||||
assert lti_user.edx_user == self.existing_user
|
||||
|
||||
def test_only_one_lti_user_edx_user_for_each_lti_consumer(self):
|
||||
users.create_lti_user('lti_user_id', self.lti_consumer, self.existing_user.email)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
users.create_lti_user('lti_user_id', self.lti_consumer, self.existing_user.email)
|
||||
|
||||
def test_create_multiple_lti_users_for_edx_user_if_lti_consumer_varies(self):
|
||||
lti_consumer_2 = LtiConsumer(
|
||||
consumer_name="SecondConsumer",
|
||||
consumer_key="SecondKey",
|
||||
consumer_secret="SecondSecret",
|
||||
)
|
||||
lti_consumer_2.save()
|
||||
|
||||
lti_user_1 = users.create_lti_user('lti_user_id', self.lti_consumer, self.existing_user.email)
|
||||
lti_user_2 = users.create_lti_user('lti_user_id', lti_consumer_2, self.existing_user.email)
|
||||
|
||||
assert lti_user_1.edx_user == lti_user_2.edx_user
|
||||
|
||||
|
||||
class LtiBackendTest(TestCase):
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,7 @@ Tests for the LTI provider views
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
@@ -58,7 +59,7 @@ def build_launch_request(extra_post_data=None, param_to_delete=None):
|
||||
del post_data[param_to_delete]
|
||||
request = RequestFactory().post('/', data=post_data)
|
||||
request.user = UserFactory.create()
|
||||
request.session = {}
|
||||
request.session = MagicMock()
|
||||
return request
|
||||
|
||||
|
||||
@@ -82,6 +83,14 @@ class LtiTestMixin:
|
||||
)
|
||||
self.consumer.save()
|
||||
|
||||
self.auto_link_consumer = models.LtiConsumer(
|
||||
consumer_name='auto-link-consumer',
|
||||
consumer_key='consumer_key_2',
|
||||
consumer_secret='secret_2',
|
||||
require_user_account=True
|
||||
)
|
||||
self.auto_link_consumer.save()
|
||||
|
||||
|
||||
class LtiLaunchTest(LtiTestMixin, TestCase):
|
||||
"""
|
||||
@@ -189,6 +198,36 @@ class LtiLaunchTest(LtiTestMixin, TestCase):
|
||||
)
|
||||
assert consumer.instance_guid == 'consumer instance guid'
|
||||
|
||||
@patch('lms.djangoapps.lti_provider.views.render_to_response')
|
||||
def test_unauthenticated_user_shown_error_when_require_user_account_is_enabled(self, render_error):
|
||||
"""
|
||||
Verify that an error page is shown instead of LTI Content for an unauthenticated user,
|
||||
when the `require_user_account` flag is enabled for the LTI Consumer.
|
||||
"""
|
||||
request = build_launch_request({'oauth_consumer_key': 'consumer_key_2'})
|
||||
request.user = AnonymousUser()
|
||||
|
||||
views.lti_launch(request, str(COURSE_KEY), str(USAGE_KEY))
|
||||
|
||||
render_error.assert_called()
|
||||
assert render_error.call_args[0][0] == "lti_provider/user-auth-error.html"
|
||||
|
||||
@patch('lms.djangoapps.lti_provider.views.render_to_response')
|
||||
def test_auth_error_shown_when_lis_email_is_different_from_user_email(self, render_error):
|
||||
"""
|
||||
When the `require_user_account` flag is enabled for the LTI Consumer, verify that
|
||||
an error page is shown instead of LTI Content if the authenticated user's email
|
||||
doesn't match the `lis_person_contact_email_primary` value from LTI Launch.
|
||||
"""
|
||||
# lis email different from logged in user
|
||||
request = build_launch_request({
|
||||
'oauth_consumer_key': 'consumer_key_2',
|
||||
'lis_person_contact_email_primary': 'random_email@test.com'
|
||||
})
|
||||
|
||||
views.lti_launch(request, str(COURSE_KEY), str(USAGE_KEY))
|
||||
render_error.assert_called()
|
||||
|
||||
|
||||
class LtiLaunchTestRender(LtiTestMixin, RenderXBlockTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -28,14 +28,25 @@ def authenticate_lti_user(request, lti_user_id, lti_consumer):
|
||||
If the currently logged-in user does not match the user specified by the LTI
|
||||
launch, log out the old user and log in the LTI identity.
|
||||
"""
|
||||
lis_email = request.POST.get("lis_person_contact_email_primary")
|
||||
|
||||
try:
|
||||
lti_user = LtiUser.objects.get(
|
||||
lti_user_id=lti_user_id,
|
||||
lti_consumer=lti_consumer
|
||||
)
|
||||
except LtiUser.DoesNotExist:
|
||||
except LtiUser.DoesNotExist as exc:
|
||||
# This is the first time that the user has been here. Create an account.
|
||||
lti_user = create_lti_user(lti_user_id, lti_consumer)
|
||||
if lti_consumer.require_user_account:
|
||||
# Verify that the email from the LTI Launch and the logged-in user are the same
|
||||
# before linking the LtiUser with the edx_user.
|
||||
if request.user.is_authenticated and request.user.email == lis_email:
|
||||
lti_user = create_lti_user(lti_user_id, lti_consumer, lis_email)
|
||||
else:
|
||||
# Ask the user to login before linking.
|
||||
raise PermissionDenied() from exc
|
||||
else:
|
||||
lti_user = create_lti_user(lti_user_id, lti_consumer)
|
||||
|
||||
if not (request.user.is_authenticated and
|
||||
request.user == lti_user.edx_user):
|
||||
@@ -44,34 +55,36 @@ def authenticate_lti_user(request, lti_user_id, lti_consumer):
|
||||
switch_user(request, lti_user, lti_consumer)
|
||||
|
||||
|
||||
def create_lti_user(lti_user_id, lti_consumer):
|
||||
def create_lti_user(lti_user_id, lti_consumer, email=None):
|
||||
"""
|
||||
Generate a new user on the edX platform with a random username and password,
|
||||
and associates that account with the LTI identity.
|
||||
"""
|
||||
edx_password = str(uuid.uuid4())
|
||||
edx_user = User.objects.filter(email=email).first() if email else None
|
||||
|
||||
created = False
|
||||
while not created:
|
||||
try:
|
||||
edx_user_id = generate_random_edx_username()
|
||||
edx_email = f"{edx_user_id}@{settings.LTI_USER_EMAIL_DOMAIN}"
|
||||
with transaction.atomic():
|
||||
edx_user = User.objects.create_user(
|
||||
username=edx_user_id,
|
||||
password=edx_password,
|
||||
email=edx_email,
|
||||
)
|
||||
# A profile is required if PREVENT_CONCURRENT_LOGINS flag is set.
|
||||
# TODO: We could populate user information from the LTI launch here,
|
||||
# but it's not necessary for our current uses.
|
||||
edx_user_profile = UserProfile(user=edx_user)
|
||||
edx_user_profile.save()
|
||||
created = True
|
||||
except IntegrityError:
|
||||
# The random edx_user_id wasn't unique. Since 'created' is still
|
||||
# False, we will retry with a different random ID.
|
||||
pass
|
||||
if not edx_user:
|
||||
created = False
|
||||
edx_password = str(uuid.uuid4())
|
||||
while not created:
|
||||
try:
|
||||
edx_user_id = generate_random_edx_username()
|
||||
edx_email = f"{edx_user_id}@{settings.LTI_USER_EMAIL_DOMAIN}"
|
||||
with transaction.atomic():
|
||||
edx_user = User.objects.create_user(
|
||||
username=edx_user_id,
|
||||
password=edx_password,
|
||||
email=edx_email,
|
||||
)
|
||||
# A profile is required if PREVENT_CONCURRENT_LOGINS flag is set.
|
||||
# TODO: We could populate user information from the LTI launch here,
|
||||
# but it's not necessary for our current uses.
|
||||
edx_user_profile = UserProfile(user=edx_user)
|
||||
edx_user_profile.save()
|
||||
created = True
|
||||
except IntegrityError:
|
||||
# The random edx_user_id wasn't unique. Since 'created' is still
|
||||
# False, we will retry with a different random ID.
|
||||
pass
|
||||
|
||||
lti_user = LtiUser(
|
||||
lti_consumer=lti_consumer,
|
||||
|
||||
@@ -6,8 +6,10 @@ LTI Provider view functions
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
|
||||
@@ -88,7 +90,17 @@ def lti_launch(request, course_id, usage_id):
|
||||
|
||||
# Create an edX account if the user identifed by the LTI launch doesn't have
|
||||
# one already, and log the edX account into the platform.
|
||||
authenticate_lti_user(request, params['user_id'], lti_consumer)
|
||||
try:
|
||||
authenticate_lti_user(request, params['user_id'], lti_consumer)
|
||||
except PermissionDenied:
|
||||
request.session.flush()
|
||||
context = {
|
||||
"login_link": request.build_absolute_uri(settings.LOGIN_URL),
|
||||
"allow_iframing": True,
|
||||
"disable_header": True,
|
||||
"disable_footer": True,
|
||||
}
|
||||
return render_to_response("lti_provider/user-auth-error.html", context)
|
||||
|
||||
# Store any parameters required by the outcome service in order to report
|
||||
# scores back later. We know that the consumer exists, since the record was
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
"""
|
||||
Settings for Bok Choy tests that are used when running LMS.
|
||||
|
||||
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 tempfile import mkdtemp
|
||||
|
||||
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
|
||||
|
||||
CONFIG_ROOT = path(__file__).abspath().dirname()
|
||||
TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root"
|
||||
|
||||
########################## 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'
|
||||
os.environ['LMS_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
|
||||
GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath()
|
||||
LOG_DIR = (TEST_ROOT / "log").abspath()
|
||||
|
||||
# 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'),
|
||||
)
|
||||
|
||||
PLATFORM_NAME = gettext_lazy("édX")
|
||||
PLATFORM_DESCRIPTION = gettext_lazy("Open édX Platform")
|
||||
|
||||
############################ STATIC FILES #############################
|
||||
|
||||
# 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" / "lms").abspath(),
|
||||
]
|
||||
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = TEST_ROOT / "uploads"
|
||||
|
||||
# Webpack loader must use webpack output setting
|
||||
WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = TEST_ROOT / "staticfiles" / "lms" / "webpack-stats.json"
|
||||
|
||||
# Don't use compression during tests
|
||||
PIPELINE['JS_COMPRESSOR'] = None
|
||||
|
||||
###################### Grades ######################
|
||||
GRADES_DOWNLOAD = {
|
||||
'STORAGE_TYPE': 'localfs',
|
||||
'BUCKET': 'edx-grades',
|
||||
'ROOT_PATH': os.path.join(mkdtemp(), 'edx-s3', 'grades'),
|
||||
}
|
||||
|
||||
|
||||
LOG_OVERRIDES = [
|
||||
('track.middleware', logging.CRITICAL),
|
||||
('common.djangoapps.edxmako.shortcuts', logging.ERROR),
|
||||
('edx.discussion', logging.CRITICAL),
|
||||
]
|
||||
for log_name, log_level in LOG_OVERRIDES:
|
||||
logging.getLogger(log_name).setLevel(log_level)
|
||||
|
||||
|
||||
YOUTUBE_HOSTNAME = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1')
|
||||
# Point the URL used to test YouTube availability to our stub YouTube server
|
||||
YOUTUBE_PORT = 9080
|
||||
YOUTUBE['TEST_TIMEOUT'] = 5000
|
||||
YOUTUBE['API'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/get_youtube_api/"
|
||||
YOUTUBE['METADATA_URL'] = f"http://{YOUTUBE_HOSTNAME}:{YOUTUBE_PORT}/test_youtube/"
|
||||
|
||||
############################# SECURITY SETTINGS ################################
|
||||
# Default to advanced security in common.py, so tests can reset here to use
|
||||
# a simpler security model
|
||||
|
||||
# Path at which to store the mock index
|
||||
MOCK_SEARCH_BACKING_FILE = (
|
||||
TEST_ROOT / "index_file.dat"
|
||||
).abspath()
|
||||
|
||||
# Verify student settings
|
||||
VERIFY_STUDENT["SOFTWARE_SECURE"] = {
|
||||
"API_ACCESS_KEY": "BBBBBBBBBBBBBBBBBBBB",
|
||||
"API_SECRET_KEY": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
|
||||
}
|
||||
|
||||
# Set dummy values for profile image settings.
|
||||
PROFILE_IMAGE_BACKEND = {
|
||||
'class': 'openedx.core.storage.OverwriteStorage',
|
||||
'options': {
|
||||
'location': os.path.join(MEDIA_ROOT, 'profile-images/'),
|
||||
'base_url': os.path.join(MEDIA_URL, 'profile-images/'),
|
||||
},
|
||||
}
|
||||
|
||||
LMS_ROOT_URL = "http://localhost:{}".format(os.environ.get('BOK_CHOY_LMS_PORT', 8003))
|
||||
CMS_BASE = "localhost:{}".format(os.environ.get('BOK_CHOY_CMS_PORT', 8031))
|
||||
LOGIN_REDIRECT_WHITELIST = [CMS_BASE]
|
||||
|
||||
INSTALLED_APPS.append('openedx.testing.coverage_context_listener')
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
# API access management
|
||||
API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
|
||||
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
|
||||
API_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/'
|
||||
AUTH_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html'
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
from .private import * # pylint: disable=wildcard-import
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -1,276 +0,0 @@
|
||||
# ingested edx-platform/lms/envs/bok_choy.auth.json
|
||||
# ingested edx-platform/lms/envs/bok_choy.env.json
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: https://support.example.com/activation-email-help.html
|
||||
ANALYTICS_DASHBOARD_URL: ''
|
||||
AWS_ACCESS_KEY_ID: ''
|
||||
AWS_SECRET_ACCESS_KEY: ''
|
||||
BUGS_EMAIL: bugs@example.com
|
||||
BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com
|
||||
BLOCK_STRUCTURES_SETTINGS:
|
||||
# We have CELERY_ALWAYS_EAGER set to True, so there's no asynchronous
|
||||
# code running and the celery routing is unimportant.
|
||||
# It does not make sense to retry.
|
||||
TASK_MAX_RETRIES: 0
|
||||
# course publish task delay is irrelevant is because the task is run synchronously
|
||||
COURSE_PUBLISH_TASK_DELAY: 0
|
||||
# retry delay is irrelevent because we never retry
|
||||
TASK_DEFAULT_RETRY_DELAY: 0
|
||||
|
||||
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']
|
||||
|
||||
# Capture the console log via template includes, until webdriver supports log capture again
|
||||
CAPTURE_CONSOLE_LOG: True
|
||||
|
||||
CELERY_BROKER_HOSTNAME: localhost
|
||||
CELERY_BROKER_PASSWORD: celery
|
||||
CELERY_BROKER_TRANSPORT: amqp
|
||||
CELERY_BROKER_USER: celery
|
||||
CELERY_ALWAYS_EAGER: True
|
||||
CELERY_RESULT_BACKEND: 'django-cache'
|
||||
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
|
||||
COMPLETION_BY_VIEWING_DELAY_MS: 1000
|
||||
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
|
||||
|
||||
# Enable debug so that static assets are served by Django
|
||||
DEBUG: True
|
||||
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
|
||||
# Configure the LMS to use our stub eCommerce implementation
|
||||
ECOMMERCE_API_URL: 'http://localhost:8043/api/v2/'
|
||||
# Configure the LMS to use our stub EdxNotes implementation
|
||||
EDXNOTES_PUBLIC_API: 'http://localhost:8042/api/v1'
|
||||
EDXNOTES_INTERNAL_API: 'http://localhost:8042/api/v1'
|
||||
|
||||
EDXNOTES_CONNECT_TIMEOUT: 10 # time in seconds
|
||||
EDXNOTES_READ_TIMEOUT: 10 # time in seconds
|
||||
|
||||
NOTES_DISABLED_TABS: []
|
||||
|
||||
EMAIL_BACKEND: django.core.mail.backends.dummy.EmailBackend
|
||||
EVENT_TRACKING_BACKENDS:
|
||||
mongo:
|
||||
ENGINE: eventtracking.backends.mongodb.MongoBackend
|
||||
OPTIONS: {collection: events, database: test}
|
||||
FEATURES:
|
||||
ALLOW_AUTOMATED_SIGNUPS: true
|
||||
AUTOMATIC_AUTH_FOR_TESTING: true
|
||||
AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING: true
|
||||
CERTIFICATES_HTML_VIEW: true
|
||||
CERTIFICATES_INSTRUCTOR_GENERATION: true
|
||||
CUSTOM_COURSES_EDX: true,
|
||||
ENABLE_COURSE_DISCOVERY: true
|
||||
ENABLE_DISCUSSION_SERVICE: true
|
||||
ENABLE_GRADE_DOWNLOADS: true
|
||||
ENABLE_SPECIAL_EXAMS: true
|
||||
ENABLE_THIRD_PARTY_AUTH: true
|
||||
ENABLE_VERIFIED_CERTIFICATES: true
|
||||
EXPOSE_CACHE_PROGRAMS_ENDPOINT: true
|
||||
MODE_CREATION_FOR_TESTING: true
|
||||
PREVIEW_LMS_BASE: 'preview.localhost:8003'
|
||||
RESTRICT_AUTOMATIC_AUTH: false
|
||||
SHOW_HEADER_LANGUAGE_SELECTOR: true
|
||||
ENABLE_MAX_FAILED_LOGIN_ATTEMPTS: False
|
||||
SQUELCH_PII_IN_LOGS: False
|
||||
PREVENT_CONCURRENT_LOGINS: False
|
||||
ENABLE_MOBILE_REST_API: True # Show video bumper in LMS
|
||||
ENABLE_VIDEO_BUMPER: True # Show video bumper in LMS
|
||||
SHOW_BUMPER_PERIODICITY: 1
|
||||
# Enable courseware search for tests
|
||||
ENABLE_COURSEWARE_SEARCH: True
|
||||
# Enable dashboard search for tests
|
||||
ENABLE_DASHBOARD_SEARCH: True
|
||||
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
|
||||
ENABLE_DISCUSSION_HOME_PANEL: True
|
||||
ENABLE_LTI_PROVIDER: True
|
||||
# Enable milestones app
|
||||
MILESTONES_APP: True
|
||||
# Enable oauth authentication, which we test.
|
||||
ENABLE_OAUTH2_PROVIDER: True
|
||||
OAUTH_ENFORCE_SECURE: False
|
||||
ENABLE_PREREQUISITE_COURSES: True
|
||||
ENABLE_COURSE_DISCOVERY: True
|
||||
ENABLE_EDXNOTES: True
|
||||
ENABLE_TEAMS: True
|
||||
LICENSING: True
|
||||
# Use the auto_auth workflow for creating users and logging them in
|
||||
AUTOMATIC_AUTH_FOR_TESTING: True
|
||||
RESTRICT_AUTOMATIC_AUTH: False
|
||||
# Open up endpoint for faking Software Secure responses
|
||||
ENABLE_SOFTWARE_SECURE_FAKE: True
|
||||
# Disable instructor dash buttons for downloading course data when enrollment exceeds this number
|
||||
MAX_ENROLLMENT_INSTR_BUTTONS: 4
|
||||
ENABLE_ENROLLMENT_TRACK_USER_PARTITION: True
|
||||
ENTRANCE_EXAMS: True
|
||||
ENABLE_SPECIAL_EXAMS: True
|
||||
|
||||
|
||||
GITHUB_REPO_ROOT: '** OVERRIDDEN **'
|
||||
JWT_AUTH: {JWT_PRIVATE_SIGNING_JWK: '{"e": "AQAB", "d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"q": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
|
||||
"p": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
|
||||
"kid": "BTZ9HA6K", "kty": "RSA"}', 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: {ABOUT: about, BLOG: blog, CAREERS: careers, CONTACT: contact,
|
||||
COURSES: courses, DONATE: donate, HELP_CENTER: help-center, HONOR: honor, NEWS: news,
|
||||
PRESS: press, PRIVACY: privacy, ROOT: root, SITEMAP.XML: sitemap_xml, TOS: tos,
|
||||
WHAT_IS_VERIFIED_CERT: verified-certificate}
|
||||
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'
|
||||
PASSWORD_RESET_SUPPORT_LINK: https://support.example.com/password-reset-help.html
|
||||
REGISTRATION_EXTENSION_FORM: openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm
|
||||
REGISTRATION_EXTRA_FIELDS: {city: hidden, country: required, gender: optional, goals: optional,
|
||||
honor_code: required, level_of_education: optional, mailing_address: optional, terms_of_service: hidden,
|
||||
year_of_birth: optional}
|
||||
# Use MockSearchEngine as the search engine for test scenario
|
||||
SEARCH_ENGINE: "search.tests.mock_search_engine.MockSearchEngine"
|
||||
# this secret key should be the same as cms/envs/bok_choy.py's
|
||||
SECRET_KEY: "very_secret_bok_choy_key"
|
||||
|
||||
SERVER_EMAIL: devops@example.com
|
||||
SESSION_COOKIE_DOMAIN: null
|
||||
SITE_NAME: localhost:8003
|
||||
SOCIAL_SHARING_SETTINGS: {CERTIFICATE_FACEBOOK: true, CERTIFICATE_FACEBOOK_TEXT: 'Testing
|
||||
facebook feature:', CUSTOM_COURSE_URLS: true, DASHBOARD_FACEBOOK: true, DASHBOARD_TWITTER: true,
|
||||
DASHBOARD_TWITTER_TEXT: 'Testing feature:'}
|
||||
STATIC_URL_BASE: /static/
|
||||
SUPPORT_SITE_LINK: https://support.example.com
|
||||
SYSLOG_SERVER: ''
|
||||
TECH_SUPPORT_EMAIL: technical@example.com
|
||||
THIRD_PARTY_AUTH_BACKENDS: [social_core.backends.google.GoogleOAuth2, social_core.backends.linkedin.LinkedinOAuth2,
|
||||
social_core.backends.facebook.FacebookOAuth2, common.djangoapps.third_party_auth.dummy.DummyBackend,
|
||||
common.djangoapps.third_party_auth.saml.SAMLAuthBackend]
|
||||
THIRD_PARTY_AUTH:
|
||||
Google:
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test"
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test"
|
||||
Facebook:
|
||||
SOCIAL_AUTH_FACEBOOK_KEY": "test"
|
||||
SOCIAL_AUTH_FACEBOOK_SECRET": "test"
|
||||
TIME_ZONE: America/New_York
|
||||
TRACKING_BACKENDS:
|
||||
mongo:
|
||||
ENGINE: common.djangoapps.track.backends.mongodb.MongoBackend
|
||||
OPTIONS: {collection: events, database: test}
|
||||
WIKI_ENABLED: true
|
||||
WAFFLE_OVERRIDE: True
|
||||
XQUEUE_INTERFACE:
|
||||
basic_auth: [edx, edx]
|
||||
django_auth: {password: password, username: lms}
|
||||
# Configure the LMS to use our stub XQueue implementation
|
||||
url: 'http://localhost:8040'
|
||||
|
||||
ZENDESK_API_KEY: ''
|
||||
ZENDESK_USER: ''
|
||||
@@ -1,27 +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]
|
||||
SITE_NAME = LMS_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/"
|
||||
@@ -1,188 +0,0 @@
|
||||
# ingested edx-platform/lms/envs/bok_choy_docker.auth.json
|
||||
# ingested edx-platform/lms/envs/bok_choy_docker.env.json
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: https://support.example.com/activation-email-help.html
|
||||
ANALYTICS_DASHBOARD_URL: ''
|
||||
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
|
||||
KEY_FUNCTION: common.djangoapps.util.memcache.safe_key
|
||||
KEY_PREFIX: integration_celery
|
||||
LOCATION: ['edx.devstack.memcached:11211']
|
||||
OPTIONS:
|
||||
no_delay: true
|
||||
ignore_exc: true
|
||||
use_pooling: true
|
||||
connect_timeout: 0.5
|
||||
default:
|
||||
BACKEND: django.core.cache.backends.memcached.PyMemcacheCache
|
||||
KEY_FUNCTION: common.djangoapps.util.memcache.safe_key
|
||||
KEY_PREFIX: sandbox_default
|
||||
LOCATION: ['edx.devstack.memcached:11211']
|
||||
OPTIONS:
|
||||
no_delay: true
|
||||
ignore_exc: true
|
||||
use_pooling: true
|
||||
connect_timeout: 0.5
|
||||
general:
|
||||
BACKEND: django.core.cache.backends.memcached.PyMemcacheCache
|
||||
KEY_FUNCTION: common.djangoapps.util.memcache.safe_key
|
||||
KEY_PREFIX: sandbox_general
|
||||
LOCATION: ['edx.devstack.memcached:11211']
|
||||
OPTIONS:
|
||||
no_delay: true
|
||||
ignore_exc: true
|
||||
use_pooling: true
|
||||
connect_timeout: 0.5
|
||||
mongo_metadata_inheritance:
|
||||
BACKEND: django.core.cache.backends.memcached.PyMemcacheCache
|
||||
KEY_FUNCTION: common.djangoapps.util.memcache.safe_key
|
||||
KEY_PREFIX: integration_mongo_metadata_inheritance
|
||||
LOCATION: ['edx.devstack.memcached:11211']
|
||||
OPTIONS:
|
||||
no_delay: true
|
||||
ignore_exc: true
|
||||
use_pooling: true
|
||||
connect_timeout: 0.5
|
||||
staticfiles:
|
||||
BACKEND: django.core.cache.backends.memcached.PyMemcacheCache
|
||||
KEY_FUNCTION: common.djangoapps.util.memcache.safe_key
|
||||
KEY_PREFIX: integration_static_files
|
||||
LOCATION: ['edx.devstack.memcached:11211']
|
||||
OPTIONS:
|
||||
no_delay: true
|
||||
ignore_exc: true
|
||||
use_pooling: true
|
||||
connect_timeout: 0.5
|
||||
CELERY_BROKER_HOSTNAME: localhost
|
||||
CELERY_BROKER_PASSWORD: celery
|
||||
CELERY_BROKER_TRANSPORT: amqp
|
||||
CELERY_BROKER_USER: celery
|
||||
CERT_QUEUE: certificates
|
||||
CMS_BASE: '** OVERRIDDEN **'
|
||||
CODE_JAIL:
|
||||
limits: {REALTIME: 3, VMEM: 0}
|
||||
COMMENTS_SERVICE_KEY: password
|
||||
COMMENTS_SERVICE_URL: http://edx.devstack.lms: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.dummy.EmailBackend
|
||||
EVENT_TRACKING_BACKENDS:
|
||||
mongo:
|
||||
ENGINE: eventtracking.backends.mongodb.MongoBackend
|
||||
OPTIONS:
|
||||
collection: events
|
||||
database: test
|
||||
host: [edx.devstack.mongo]
|
||||
port: 27017
|
||||
FEATURES: {ALLOW_AUTOMATED_SIGNUPS: true, AUTOMATIC_AUTH_FOR_TESTING: true,
|
||||
AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING: true, CERTIFICATES_HTML_VIEW: true,
|
||||
CERTIFICATES_INSTRUCTOR_GENERATION: true, CUSTOM_COURSES_EDX: true,
|
||||
ENABLE_COURSE_DISCOVERY: true, ENABLE_DISCUSSION_SERVICE: true, ENABLE_GRADE_DOWNLOADS: true,
|
||||
ENABLE_SPECIAL_EXAMS: true, ENABLE_THIRD_PARTY_AUTH: true,
|
||||
ENABLE_VERIFIED_CERTIFICATES: true, EXPOSE_CACHE_PROGRAMS_ENDPOINT: true, MODE_CREATION_FOR_TESTING: true,
|
||||
PREVIEW_LMS_BASE: 'preview.localhost:8003', RESTRICT_AUTOMATIC_AUTH: false, SHOW_HEADER_LANGUAGE_SELECTOR: true}
|
||||
GITHUB_REPO_ROOT: '** OVERRIDDEN **'
|
||||
JWT_AUTH: {JWT_PRIVATE_SIGNING_JWK: '{"e": "AQAB", "d": "HIiV7KNjcdhVbpn3KT-I9n3JPf5YbGXsCIedmPqDH1d4QhBofuAqZ9zebQuxkRUpmqtYMv0Zi6ECSUqH387GYQF_XvFUFcjQRPycISd8TH0DAKaDpGr-AYNshnKiEtQpINhcP44I1AYNPCwyoxXA1fGTtmkKChsuWea7o8kytwU5xSejvh5-jiqu2SF4GEl0BEXIAPZsgbzoPIWNxgO4_RzNnWs6nJZeszcaDD0CyezVSuH9QcI6g5QFzAC_YuykSsaaFJhZ05DocBsLczShJ9Omf6PnK9xlm26I84xrEh_7x4fVmNBg3xWTLh8qOnHqGko93A1diLRCrKHOvnpvgQ",
|
||||
"n": "o5cn3ljSRi6FaDEKTn0PS-oL9EFyv1pI7dRgffQLD1qf5D6sprmYfWWokSsrWig8u2y0HChSygR6Jn5KXBqQn6FpM0dDJLnWQDRXHLl3Ey1iPYgDSmOIsIGrV9ZyNCQwk03wAgWbfdBTig3QSDYD-sTNOs3pc4UD_PqAvU2nz_1SS2ZiOwOn5F6gulE1L0iE3KEUEvOIagfHNVhz0oxa_VRZILkzV-zr6R_TW1m97h4H8jXl_VJyQGyhMGGypuDrQ9_vaY_RLEulLCyY0INglHWQ7pckxBtI5q55-Vio2wgewe2_qYcGsnBGaDNbySAsvYcWRrqDiFyzrJYivodqTQ",
|
||||
"q": "3T3DEtBUka7hLGdIsDlC96Uadx_q_E4Vb1cxx_4Ss_wGp1Loz3N3ZngGyInsKlmbBgLo1Ykd6T9TRvRNEWEtFSOcm2INIBoVoXk7W5RuPa8Cgq2tjQj9ziGQ08JMejrPlj3Q1wmALJr5VTfvSYBu0WkljhKNCy1KB6fCby0C9WE",
|
||||
"p": "vUqzWPZnDG4IXyo-k5F0bHV0BNL_pVhQoLW7eyFHnw74IOEfSbdsMspNcPSFIrtgPsn7981qv3lN_staZ6JflKfHayjB_lvltHyZxfl0dvruShZOx1N6ykEo7YrAskC_qxUyrIvqmJ64zPW3jkuOYrFs7Ykj3zFx3Zq1H5568G0",
|
||||
"kid": "BTZ9HA6K", "kty": "RSA"}', 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: http://edx.devstack.lms:18003
|
||||
LMS_ROOT_URL: http://edx.devstack.lms:18003
|
||||
LOCAL_LOGLEVEL: INFO
|
||||
LOGGING_ENV: sandbox
|
||||
LOG_DIR: '** OVERRIDDEN **'
|
||||
MEDIA_URL: /media/
|
||||
MKTG_URL_LINK_MAP: {ABOUT: about, BLOG: blog, CAREERS: careers, CONTACT: contact,
|
||||
COURSES: courses, DONATE: donate, HELP_CENTER: help-center, HONOR: honor, NEWS: news,
|
||||
PRESS: press, PRIVACY: privacy, ROOT: root, SITEMAP.XML: sitemap_xml, TOS: tos,
|
||||
WHAT_IS_VERIFIED_CERT: verified-certificate}
|
||||
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}
|
||||
PASSWORD_RESET_SUPPORT_LINK: https://support.example.com/password-reset-help.html
|
||||
REGISTRATION_EXTENSION_FORM: openedx.core.djangoapps.user_api.tests.test_helpers.TestCaseForm
|
||||
REGISTRATION_EXTRA_FIELDS: {city: hidden, country: required, gender: optional, goals: optional,
|
||||
honor_code: required, level_of_education: optional, mailing_address: optional, terms_of_service: hidden,
|
||||
year_of_birth: optional}
|
||||
SECRET_KEY: 'bokchoy_docker_secret_key'
|
||||
SERVER_EMAIL: devops@example.com
|
||||
SESSION_COOKIE_DOMAIN: null
|
||||
SITE_NAME: localhost:8003
|
||||
SOCIAL_SHARING_SETTINGS: {CERTIFICATE_FACEBOOK: true, CERTIFICATE_FACEBOOK_TEXT: 'Testing
|
||||
facebook feature:', CUSTOM_COURSE_URLS: true, DASHBOARD_FACEBOOK: true, DASHBOARD_TWITTER: true,
|
||||
DASHBOARD_TWITTER_TEXT: 'Testing feature:'}
|
||||
STATIC_URL_BASE: /static/
|
||||
SUPPORT_SITE_LINK: https://support.example.com
|
||||
SYSLOG_SERVER: ''
|
||||
TECH_SUPPORT_EMAIL: technical@example.com
|
||||
THIRD_PARTY_AUTH_BACKENDS: [social_core.backends.google.GoogleOAuth2, social_core.backends.linkedin.LinkedinOAuth2,
|
||||
social_core.backends.facebook.FacebookOAuth2, common.djangoapps.third_party_auth.dummy.DummyBackend,
|
||||
common.djangoapps.third_party_auth.saml.SAMLAuthBackend]
|
||||
TIME_ZONE: America/New_York
|
||||
TRACKING_BACKENDS:
|
||||
mongo:
|
||||
ENGINE: common.djangoapps.track.backends.mongodb.MongoBackend
|
||||
OPTIONS:
|
||||
collection: events
|
||||
database: test
|
||||
host: [edx.devstack.mongo]
|
||||
port: 27017
|
||||
WIKI_ENABLED: true
|
||||
XQUEUE_INTERFACE:
|
||||
basic_auth: [edx, edx]
|
||||
django_auth: {password: password, username: lms}
|
||||
url: '** OVERRIDDEN **'
|
||||
ZENDESK_API_KEY: ''
|
||||
ZENDESK_USER: ''
|
||||
@@ -3332,7 +3332,14 @@ CROSS_DOMAIN_CSRF_COOKIE_NAME = ''
|
||||
REST_FRAMEWORK = {
|
||||
# These default classes add observability around endpoints using defaults, and should
|
||||
# not be used anywhere else.
|
||||
# Notes on Order:
|
||||
# 1. `JwtAuthentication` does not check `is_active`, so email validation does not affect it. However,
|
||||
# `SessionAuthentication` does. These work differently, and order changes in what way, which really stinks. See
|
||||
# https://github.com/openedx/public-engineering/issues/165 for details.
|
||||
# 2. `JwtAuthentication` may also update the database based on contents. Since the LMS creates these JWTs, this
|
||||
# shouldn't have any affect at this time. But it could, when and if another service started creating the JWTs.
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'openedx.core.djangolib.default_auth_classes.DefaultJwtAuthentication',
|
||||
'openedx.core.djangolib.default_auth_classes.DefaultSessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'edx_rest_framework_extensions.paginators.DefaultPagination',
|
||||
@@ -4428,6 +4435,9 @@ MOBILE_APP_USER_AGENT_REGEXES = [
|
||||
r'edX/org.edx.mobile',
|
||||
]
|
||||
|
||||
# set course limit for mobile search
|
||||
MOBILE_SEARCH_COURSE_LIMIT = 100
|
||||
|
||||
# cache timeout in seconds for Mobile App Version Upgrade
|
||||
APP_UPGRADE_CACHE_TIMEOUT = 3600
|
||||
|
||||
@@ -5306,6 +5316,10 @@ ENTERPRISE_PLOTLY_SECRET = "I am a secret"
|
||||
|
||||
############## PLOTLY ##############
|
||||
|
||||
############ Internal Enterprise Settings ############
|
||||
ENTERPRISE_VSF_UUID = "e815503343644ac7845bc82325c34460"
|
||||
############ Internal Enterprise Settings ############
|
||||
|
||||
ENTERPRISE_MANUAL_REPORTING_CUSTOMER_UUIDS = []
|
||||
|
||||
AVAILABLE_DISCUSSION_TOURS = []
|
||||
@@ -5326,6 +5340,9 @@ NOTIFICATIONS_EXPIRY = 60
|
||||
EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000
|
||||
NOTIFICATION_CREATION_BATCH_SIZE = 99
|
||||
|
||||
############################ AI_TRANSLATIONS ##################################
|
||||
AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1'
|
||||
|
||||
#### django-simple-history##
|
||||
# disable indexing on date field its coming from django-simple-history.
|
||||
SIMPLE_HISTORY_DATE_INDEX = False
|
||||
|
||||
@@ -509,6 +509,11 @@ EVENT_BUS_REDIS_CONNECTION_URL = 'redis://:password@edx.devstack.redis:6379/'
|
||||
EVENT_BUS_TOPIC_PREFIX = 'dev'
|
||||
EVENT_BUS_CONSUMER = 'edx_event_bus_redis.RedisEventConsumer'
|
||||
|
||||
certificate_revoked_event_config = EVENT_BUS_PRODUCER_CONFIG['org.openedx.learning.certificate.revoked.v1']
|
||||
certificate_revoked_event_config['learning-certificate-lifecycle']['enabled'] = True
|
||||
certificate_created_event_config = EVENT_BUS_PRODUCER_CONFIG['org.openedx.learning.certificate.created.v1']
|
||||
certificate_created_event_config['learning-certificate-lifecycle']['enabled'] = True
|
||||
|
||||
######################## Subscriptions API SETTINGS ########################
|
||||
SUBSCRIPTIONS_ROOT_URL = "http://host.docker.internal:18750"
|
||||
SUBSCRIPTIONS_API_PATH = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscription/"
|
||||
@@ -525,6 +530,9 @@ API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
|
||||
API_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/'
|
||||
AUTH_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html'
|
||||
|
||||
############################ AI_TRANSLATIONS ##################################
|
||||
AI_TRANSLATIONS_API_URL = 'http://localhost:18760/api/v1'
|
||||
|
||||
################# New settings must go ABOVE this line #################
|
||||
########################################################################
|
||||
# See if the developer has any local overrides.
|
||||
|
||||
@@ -1112,6 +1112,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', {}))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -28,16 +28,18 @@ class LmsSearchFilterGenerator(SearchFilterGenerator):
|
||||
def field_dictionary(self, **kwargs):
|
||||
""" add course if provided otherwise add courses in which the user is enrolled in """
|
||||
field_dictionary = super().field_dictionary(**kwargs)
|
||||
course_id = kwargs.get('course_id')
|
||||
if not kwargs.get('user'):
|
||||
field_dictionary['course'] = []
|
||||
elif not kwargs.get('course_id'):
|
||||
elif not course_id:
|
||||
user_enrollments = self._enrollments_for_user(kwargs['user'])
|
||||
field_dictionary['course'] = [str(enrollment.course_id) for enrollment in user_enrollments]
|
||||
|
||||
# if we have an org filter, only include results for this org filter
|
||||
course_org_filter = configuration_helpers.get_current_site_orgs()
|
||||
if course_org_filter:
|
||||
field_dictionary['org'] = course_org_filter
|
||||
# if we have no specific course and an org filter, only include results for this org filter
|
||||
if not course_id:
|
||||
course_org_filter = configuration_helpers.get_current_site_orgs()
|
||||
if course_org_filter:
|
||||
field_dictionary['org'] = course_org_filter
|
||||
|
||||
return field_dictionary
|
||||
|
||||
@@ -46,6 +48,11 @@ class LmsSearchFilterGenerator(SearchFilterGenerator):
|
||||
Exclude any courses defined outside the current org.
|
||||
"""
|
||||
exclude_dictionary = super().exclude_dictionary(**kwargs)
|
||||
# If we are already filtering to a single course, we do not need
|
||||
# the further course filters below.
|
||||
if kwargs.get('course_id'):
|
||||
return exclude_dictionary
|
||||
|
||||
course_org_filter = configuration_helpers.get_current_site_orgs()
|
||||
# If we have a course filter we are ensuring that we only get those courses above
|
||||
if not course_org_filter:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user