Merge branch 'master' of github.com:openedx/edx-platform into HEAD

This commit is contained in:
Taras Lytvynenko
2023-11-15 16:50:51 +02:00
163 changed files with 3298 additions and 3460 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,4 @@
"""
Serializers for all contentstore API versions
"""
from .common import StrictSerializer

View File

@@ -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
)

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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,

View File

@@ -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(),
)

View File

@@ -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(),
)

View File

@@ -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'
),
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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(),

View File

@@ -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
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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),
})

View File

@@ -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)

View File

@@ -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": ""
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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: ''

View File

@@ -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": ""
}

View File

@@ -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,
}

View File

@@ -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/"

View File

@@ -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: ''

View File

@@ -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)
}

View File

@@ -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 #################
########################################################################

View File

@@ -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', {}))

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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
};
},

View File

@@ -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
// ====================

View 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;
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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'),

View File

@@ -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')
)

View File

@@ -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'

View File

@@ -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)

View File

@@ -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()

View File

@@ -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",

View File

@@ -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)

View File

@@ -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):
"""

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View 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',
),
]

View File

@@ -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__)

View File

@@ -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']
)

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -72,6 +72,7 @@ class TestCourseListGetForm(FormTestMixin, UsernameTestMixin, SharedModuleStoreT
'permissions': set(),
'active_only': None,
'course_keys': set(),
'mobile_search': None,
}
def test_basic(self):

View File

@@ -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):
"""

View File

@@ -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),
)

View File

@@ -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):
"""

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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),
),
]

View File

@@ -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')

View File

@@ -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):
"""

View File

@@ -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):
"""

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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: ''

View File

@@ -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/"

View File

@@ -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: ''

View File

@@ -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

View File

@@ -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.

View File

@@ -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', {}))

View File

@@ -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.

View File

@@ -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