diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7fba6b6b4cf1..78af44c738e2 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,6 +20,7 @@ jobs: name: ${{ matrix.shard_name }}(py=${{ matrix.python-version }},dj=${{ matrix.django-version }},mongo=${{ matrix.mongo-version }}) runs-on: ${{ matrix.os-version }} strategy: + fail-fast: false matrix: python-version: - "3.11" diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 1c64cfb660e2..dec76205c359 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -29,6 +29,7 @@ from xmodule.util.builtin_assets import add_webpack_js_to_fragment from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, XModuleMixin from cms.djangoapps.xblock_config.models import StudioConfig +from openedx.core.djangoapps.video_config.services import VideoConfigService from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id from cms.lib.xblock.field_data import CmsFieldData from cms.lib.xblock.upstream_sync import UpstreamLink diff --git a/cms/envs/common.py b/cms/envs/common.py index 7a2a3343a8db..57303f3e264a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1089,7 +1089,7 @@ 'statici18n', # Tagging - 'cms.lib.xblock.tagging', + 'cms.lib.xblock.tagging.apps.TaggingConfig', # Enables default site and redirects 'django_sites_extensions', diff --git a/cms/lib/xblock/tagging/apps.py b/cms/lib/xblock/tagging/apps.py new file mode 100644 index 000000000000..d329343e445c --- /dev/null +++ b/cms/lib/xblock/tagging/apps.py @@ -0,0 +1,12 @@ +""" +Django app configuration for the XBlock tagging app +""" +from django.apps import AppConfig + + +class TaggingConfig(AppConfig): + """ + Django app configuration for the XBlock tagging app + """ + name = 'cms.lib.xblock.tagging' + verbose_name = 'XBlock Tagging' \ No newline at end of file diff --git a/lms/djangoapps/courseware/block_render.py b/lms/djangoapps/courseware/block_render.py index ad04bcb19246..7904b93e07d0 100644 --- a/lms/djangoapps/courseware/block_render.py +++ b/lms/djangoapps/courseware/block_render.py @@ -44,6 +44,7 @@ from lms.djangoapps.teams.services import TeamsService from openedx.core.djangoapps.video_config.services import VideoConfigService from openedx.core.lib.xblock_services.call_to_action import CallToActionService +from openedx.core.djangoapps.video_config.services import VideoConfigService from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.library_tools import LegacyLibraryToolsService diff --git a/openedx/core/djangoapps/video_config/services.py b/openedx/core/djangoapps/video_config/services.py index 66799d7393ce..74642c2d4bf4 100644 --- a/openedx/core/djangoapps/video_config/services.py +++ b/openedx/core/djangoapps/video_config/services.py @@ -20,6 +20,10 @@ from openedx.core.djangoapps.video_config.toggles import TRANSCRIPT_FEEDBACK from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE +from xmodule.contentstore.content import StaticContent +from xmodule.contentstore.django import contentstore +from xmodule.exceptions import NotFoundError + log = logging.getLogger(__name__) @@ -120,3 +124,86 @@ def get_transcript( raise TranscriptNotFoundError( f"Failed to get transcript: {exc}" ) from exc + + def get_transcript_from_store(self, course_key, filename): + """ + Return transcript from static content store by course key and filename. + + Args: + course_key: Course key + filename (str): filename of the asset + + Returns: + transcript from store + + Raises: + TranscriptNotFoundError: If transcript not found + """ + content_location = StaticContent.compute_location(course_key, filename) + try: + return contentstore().find(content_location) + except NotFoundError as exc: + raise TranscriptNotFoundError( + f"Failed to get transcript: {exc}" + ) from exc + + def delete_transcript_from_store(self, course_key, filename): + """ + Delete transcript from store by course key and filename. + + Args: + course_key: Course key + filename (str): filename of the asset + + Returns: + Asset location + """ + try: + content_location = StaticContent.compute_location(course_key, filename) + contentstore().delete(content_location) + log.info("Transcript asset %s was removed from store.", filename) + except NotFoundError as exc: + raise TranscriptNotFoundError( + f"Failed to get transcript: {exc}" + ) from exc + return StaticContent.compute_location(course_key, filename) + + def find_transcript_from_store(self, course_key, filename): + """ + Find transcript from store by course key and filename. + + Args: + course_key: Course key + filename (str): filename of the asset + + Returns: + transcript from store + + Raises: + TranscriptNotFoundError: If transcript not found + """ + try: + content_location = StaticContent.compute_location(course_key, filename) + return contentstore().find(content_location).data.decode('utf-8') + except NotFoundError as exc: + raise TranscriptNotFoundError( + f"Failed to get transcript: {exc}" + ) from exc + + def save_transcript_into_store(self, content, filename, mime_type, course_key): + """ + Save transcript into store by course key. + + Args: + content: The content to save + filename: The filename + mime_type: The MIME type of the content + course_key: The course key + + Returns: + Content location of saved transcript in store + """ + content_location = StaticContent.compute_location(course_key, filename) + content = StaticContent(content_location, filename, mime_type, content) + contentstore().save(content) + return content_location diff --git a/openedx/core/djangoapps/video_config/transcripts_utils.py b/openedx/core/djangoapps/video_config/transcripts_utils.py index be86324cd6e4..98b4765abf58 100644 --- a/openedx/core/djangoapps/video_config/transcripts_utils.py +++ b/openedx/core/djangoapps/video_config/transcripts_utils.py @@ -23,7 +23,6 @@ from pysrt.srtexc import Error from opaque_keys.edx.locator import LibraryLocatorV2 -from openedx.core.djangoapps.xblock.api import get_component_from_usage_key from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError @@ -796,6 +795,14 @@ def get_asset(location, filename): """ return contentstore().find(Transcript.asset_location(location, filename)) + @staticmethod + def get_asset_by_course_key(course_key, filename): + """ + Return asset by location and filename. + """ + content_location = StaticContent.compute_location(course_key, filename) + return contentstore().find(content_location) + @staticmethod def asset_location(location, filename): """ @@ -818,6 +825,37 @@ def delete_asset(location, filename): pass return StaticContent.compute_location(location.course_key, filename) + @staticmethod + def delete_asset_by_course_key(course_key, filename): + """ + Delete asset by location and filename. + """ + try: + content_location = StaticContent.compute_location(course_key, filename) + contentstore().delete(content_location) + log.info("Transcript asset %s was removed from store.", filename) + except NotFoundError: + pass + return StaticContent.compute_location(course_key, filename) + + @staticmethod + def find_asset(course_key, filename): + """ + Finds asset by course_key and filename. + """ + content_location = StaticContent.compute_location(course_key, filename) + return contentstore().find(content_location).data.decode('utf-8') + + @staticmethod + def save_transcript(content, filename, mime_type, course_key): + """ + Save transcript to store by course_key and filename. + """ + content_location = StaticContent.compute_location(course_key, filename) + content = StaticContent(content_location, filename, mime_type, content) + contentstore().save(content) + return content_location + class VideoTranscriptsMixin: """Mixin class for transcript functionality. @@ -1117,6 +1155,7 @@ def get_transcript_from_learning_core(video_block, language, output_format, tran # Grab the underlying Component. There's no version parameter to this call, # so we're just going to grab the file associated with the latest draft # version for now. + from openedx.core.djangoapps.xblock.api import get_component_from_usage_key component = get_component_from_usage_key(usage_key) component_version = component.versioning.draft if not component_version: diff --git a/openedx/core/djangoapps/video_config/utils.py b/openedx/core/djangoapps/video_config/utils.py new file mode 100644 index 000000000000..863c2e95731e --- /dev/null +++ b/openedx/core/djangoapps/video_config/utils.py @@ -0,0 +1,96 @@ +""" +Video configuration utilities +""" + +import logging + +from django.conf import settings + +from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE +from openedx.core.lib.courses import get_course_by_id +from xmodule.course_block import ( + COURSE_VIDEO_SHARING_ALL_VIDEOS, + COURSE_VIDEO_SHARING_NONE, +) + +log = logging.getLogger(__name__) + + +class VideoSharingUtils: + """ + Provides utility methods for video sharing functionality. + """ + + @staticmethod + def get_public_video_url(video_block): + """ + Returns the public video url for a video block. + + Args: + video_block: The video XBlock instance + + Returns: + str: The public video URL + """ + return fr'{settings.LMS_ROOT_URL}/videos/{str(video_block.location)}' + + @staticmethod + def is_public_sharing_enabled(video_block): + """ + Check if public sharing is enabled for a video. + + Args: + video_block: The video XBlock instance + + Returns: + bool: True if public sharing is enabled, False otherwise + """ + if not video_block.context_key.is_course: + return False # Only courses support this feature (not libraries) + + try: + # Video share feature must be enabled for sharing settings to take effect + feature_enabled = PUBLIC_VIDEO_SHARE.is_enabled(video_block.context_key) + except Exception as err: # pylint: disable=broad-except + log.exception(f"Error retrieving course for course ID: {video_block.context_key}") + return False + + if not feature_enabled: + return False + + # Check if the course specifies a general setting + course_video_sharing_option = VideoSharingUtils.get_course_video_sharing_override(video_block) + + # Course can override all videos to be shared + if course_video_sharing_option == COURSE_VIDEO_SHARING_ALL_VIDEOS: + return True + + # ... or no videos to be shared + elif course_video_sharing_option == COURSE_VIDEO_SHARING_NONE: + return False + + # ... or can fall back to per-video setting + # Equivalent to COURSE_VIDEO_SHARING_PER_VIDEO or None / unset + else: + return video_block.public_access + + @staticmethod + def get_course_video_sharing_override(video_block): + """ + Return course video sharing options override or None + + Args: + video_block: The video XBlock instance + + Returns: + Course video sharing option or None + """ + if not video_block.context_key.is_course: + return False # Only courses support this feature (not libraries) + + try: + course = get_course_by_id(video_block.context_key) + return getattr(course, 'video_sharing_options', None) + except Exception as err: # pylint: disable=broad-except + log.exception(f"Error retrieving course for course ID: {video_block.course_id}") + return None diff --git a/openedx/envs/common.py b/openedx/envs/common.py index de9e45f235be..e6458af11285 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -1736,7 +1736,7 @@ def _make_locale_paths(settings): # .. toggle_warning: Not production-ready until relevant subtask https://github.com/openedx/edx-platform/issues/34827 is done. # .. toggle_creation_date: 2024-11-10 # .. toggle_target_removal_date: 2025-06-01 -USE_EXTRACTED_VIDEO_BLOCK = False +USE_EXTRACTED_VIDEO_BLOCK = True ############################## Marketing Site ############################## diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 7dbf1e37e2f2..854b9c82d88c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -1279,7 +1279,7 @@ xblock-utils==4.0.0 # via # edx-sga # xblock-poll -xblocks-contrib==0.8.1 +git+https://github.com/openedx/xblocks-contrib.git@farhan/video-xblock-extraction # via -r requirements/edx/bundled.in xmlsec==1.3.14 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 80428b0f1493..ba875406cf6b 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -2312,7 +2312,7 @@ xblock-utils==4.0.0 # -r requirements/edx/testing.txt # edx-sga # xblock-poll -xblocks-contrib==0.8.1 +git+https://github.com/openedx/xblocks-contrib.git@farhan/video-xblock-extraction # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 26e182dc16ed..c44dd97a1e25 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1617,7 +1617,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.8.1 +git+https://github.com/openedx/xblocks-contrib.git@farhan/video-xblock-extraction # via -r requirements/edx/base.txt xmlsec==1.3.14 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 7680738cda3a..49990336b180 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1710,7 +1710,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.8.1 +git+https://github.com/openedx/xblocks-contrib.git@farhan/video-xblock-extraction # via -r requirements/edx/base.txt xmlsec==1.3.14 # via