From ebd77155504df2c957dad749aaf2e9b133eba03a Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:00:13 +0500 Subject: [PATCH 1/7] enable extracted video block --- openedx/envs/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/envs/common.py b/openedx/envs/common.py index 5d7c105025ed..2bb81fa5108d 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -2105,7 +2105,7 @@ def add_optional_apps(optional_apps, installed_apps): # .. 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 ############################## From 6b8305444f939c4af461b999b08be5acb231fd4a Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:01:35 +0500 Subject: [PATCH 2/7] chore: adds tagging app config --- cms/envs/common.py | 2 +- cms/lib/xblock/tagging/apps.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 cms/lib/xblock/tagging/apps.py diff --git a/cms/envs/common.py b/cms/envs/common.py index f0371952f833..5e04ad61848a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -804,7 +804,7 @@ def make_lms_template_path(settings): '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..123e706eea46 --- /dev/null +++ b/cms/lib/xblock/tagging/apps.py @@ -0,0 +1,12 @@ +"""Expand commentComment on line R1Code has comments. Press enter to view. +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' From 3e7edefbaf99dea7dccba941d082b0794725df0d Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:03:05 +0500 Subject: [PATCH 3/7] fix: udpate test cases to fix for the extracted video block --- .../courseware/tests/test_video_handlers.py | 29 ++++++++++++++----- .../courseware/tests/test_video_mongo.py | 17 +++++++---- xmodule/modulestore/tests/test_api.py | 5 +++- xmodule/tests/test_video.py | 22 +++++++++----- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 6353e2aec79f..d0a5de4a91f7 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -10,6 +10,7 @@ import pytest import ddt import freezegun +from django.conf import settings from django.core.files.base import ContentFile from django.utils.timezone import now from django.test import RequestFactory @@ -48,6 +49,13 @@ Привіт, edX вітає вас. """) +if settings.USE_EXTRACTED_VIDEO_BLOCK: + path_video_handlers = 'xblocks_contrib.video.video_handlers' + path_transcripts_utils = 'xblocks_contrib.video.video_transcripts_utils' +else: + path_video_handlers = 'xmodule.video_block.video_handlers' + path_transcripts_utils = 'openedx.core.djangoapps.video_config.transcripts_utils' + def _create_srt_file(content=None): """ @@ -206,10 +214,17 @@ def test_handle_ajax(self): {'demoo�': 'sample'} ] for sample in data: - response = self.clients[self.users[0].username].post( - self.get_url('save_user_state'), - sample, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') + if settings.USE_EXTRACTED_VIDEO_BLOCK: + handler_url = self.get_url('save_user_state', handler_name='ajax_handler') + response = self.clients[self.users[0].username].post( + handler_url, + sample, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + else: + response = self.clients[self.users[0].username].post( + self.get_url('save_user_state'), + sample, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') assert response.status_code == 200 assert self.block.speed is None @@ -320,7 +335,7 @@ def test_multiple_available_translations(self, mock_get_video_transcript_content assert sorted(json.loads(response.body.decode('utf-8'))) == sorted(['en', 'uk']) @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_transcript_content') - @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_available_transcript_languages') + @patch(f'{path_transcripts_utils}.get_available_transcript_languages') @ddt.data( ( ['en', 'uk', 'ro'], @@ -504,7 +519,7 @@ def test_download_transcript_not_exist(self): assert response.status == '404 Not Found' @patch( - 'xmodule.video_block.video_handlers.get_transcript', + f'{path_video_handlers}.get_transcript', return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8') ) def test_download_srt_exist(self, __): @@ -515,7 +530,7 @@ def test_download_srt_exist(self, __): assert response.headers['Content-Language'] == 'en' @patch( - 'xmodule.video_block.video_handlers.get_transcript', + f'{path_video_handlers}.get_transcript', return_value=('Subs!', 'txt', 'text/plain; charset=utf-8') ) def test_download_txt_exist(self, __): diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index f4d2847cff0f..1a72a7e5b18a 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -49,7 +49,7 @@ from xmodule.tests.helpers import mock_render_template, override_descriptor_system # pylint: disable=unused-import from xmodule.tests.test_import import DummyModuleStoreRuntime from xmodule.tests.test_video import VideoBlockTestBase -from xmodule.video_block import VideoBlock, bumper_utils, video_utils +from xmodule.video_block import VideoBlock, video_utils from openedx.core.djangoapps.video_config.transcripts_utils import Transcript, save_to_store, subs_filename from xmodule.video_block.video_block import EXPORT_IMPORT_COURSE_DIR, EXPORT_IMPORT_STATIC_DIR from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW @@ -66,6 +66,13 @@ from .test_video_xml import SOURCE_XML, PUBLIC_SOURCE_XML from common.test.utils import assert_dict_contains_subset +if settings.USE_EXTRACTED_VIDEO_BLOCK: + from xblocks_contrib.video import bumper_utils + bumper_utils_path = 'xblocks_contrib.video.bumper_utils' +else: + from xmodule.video_block import bumper_utils + bumper_utils_path = 'xmodule.video_block.bumper_utils' + TRANSCRIPT_FILE_SRT_DATA = """ 1 00:00:14,370 --> 00:00:16,530 @@ -930,7 +937,7 @@ def helper_get_html_with_edx_video_id(self, data): # pylint: disable=invalid-name @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) - @patch('xmodule.video_block.video_block.rewrite_video_url') + @patch(f'{VideoBlock.__module__}.rewrite_video_url') def test_get_html_cdn_source(self, mocked_get_video, mock_render_django_template): """ Test if sources got from CDN @@ -2323,7 +2330,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests # Use temporary FEATURES in this test without affecting the original FEATURES = dict(settings.FEATURES) - @patch('xmodule.video_block.bumper_utils.get_bumper_settings') + @patch(f'{bumper_utils_path}.get_bumper_settings') def test_is_bumper_enabled(self, get_bumper_settings): """ Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True @@ -2348,8 +2355,8 @@ def test_is_bumper_enabled(self, get_bumper_settings): assert not bumper_utils.is_bumper_enabled(self.block) @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) - @patch('xmodule.video_block.bumper_utils.is_bumper_enabled') - @patch('xmodule.video_block.bumper_utils.get_bumper_settings') + @patch(f'{bumper_utils_path}.is_bumper_enabled') + @patch(f'{bumper_utils_path}.get_bumper_settings') @patch('edxval.api.get_urls_for_profiles') def test_bumper_metadata( self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled, mock_render_django_template diff --git a/xmodule/modulestore/tests/test_api.py b/xmodule/modulestore/tests/test_api.py index 03dd79d4ffa4..61aa4ef40177 100644 --- a/xmodule/modulestore/tests/test_api.py +++ b/xmodule/modulestore/tests/test_api.py @@ -26,7 +26,10 @@ def test_get_root_module_name(): Ensure the module name function works with different xblocks. """ assert get_root_module_name(LtiConsumerXBlock) == 'lti_consumer' - assert get_root_module_name(VideoBlock) == 'xmodule' + if settings.USE_EXTRACTED_VIDEO_BLOCK: + assert get_root_module_name(VideoBlock) == 'xblocks_contrib' + else: + assert get_root_module_name(VideoBlock) == 'xmodule' assert get_root_module_name(DoneXBlock) == 'done' diff --git a/xmodule/tests/test_video.py b/xmodule/tests/test_video.py index aa03c15851cb..b85d9f723a70 100644 --- a/xmodule/tests/test_video.py +++ b/xmodule/tests/test_video.py @@ -82,6 +82,15 @@ ["ur", "Urdu"] ) +if settings.USE_EXTRACTED_VIDEO_BLOCK: + get_available_transcript_languages_path = ( + 'xblocks_contrib.video.video_transcripts_utils.get_available_transcript_languages' + ) +else: + get_available_transcript_languages_path = ( + 'openedx.core.djangoapps.video_config.transcripts_utils.get_available_transcript_languages' + ) + def instantiate_block(**field_data): """ @@ -320,7 +329,7 @@ def test_parse_xml(self): @XBlockAside.register_temp_plugin(AsideTestType, "test_aside") @patch('xmodule.video_block.video_block.VideoBlock.load_file') - @patch('xmodule.video_block.video_block.is_pointer_tag') + @patch(f'{VideoBlock.__module__}.is_pointer_tag') @ddt.data(True, False) def test_parse_xml_with_asides(self, video_xml_has_aside, mock_is_pointer_tag, mock_load_file): """Test that `parse_xml` parses asides from the video xml""" @@ -642,7 +651,7 @@ def test_import_with_float_times(self): 'data': '' }) - @patch('xmodule.video_block.video_block.edxval_api') + @patch(f'{VideoBlock.__module__}.edxval_api') def test_import_val_data(self, mock_val_api): """ Test that `parse_xml` works method works as expected. @@ -687,7 +696,7 @@ def mock_val_import(xml, edx_video_id, resource_fs, static_dir, external_transcr course_id='test_course_id' ) - @patch('xmodule.video_block.video_block.edxval_api') + @patch(f'{VideoBlock.__module__}.edxval_api') def test_import_val_data_invalid(self, mock_val_api): mock_val_api.ValCannotCreateError = _MockValCannotCreateError mock_val_api.import_from_xml = Mock(side_effect=mock_val_api.ValCannotCreateError) @@ -715,7 +724,7 @@ def setUp(self): self.file_system = OSFS(self.temp_dir) self.addCleanup(shutil.rmtree, self.temp_dir) - @patch('xmodule.video_block.video_block.edxval_api') + @patch(f'{VideoBlock.__module__}.edxval_api') def test_export_to_xml(self, mock_val_api): """ Test that we write the correct XML on export. @@ -815,7 +824,7 @@ def test_export_to_xml_without_video_id(self): expected = etree.XML(xml_string, parser=parser) self.assertXmlEqual(expected, xml) - @patch('xmodule.video_block.video_block.edxval_api') + @patch(f'{VideoBlock.__module__}.edxval_api') def test_export_to_xml_val_error(self, mock_val_api): # Export should succeed without VAL data if video does not exist mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError @@ -948,8 +957,7 @@ def test_student_view_data(self, field_data, expected_student_view_data): 'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled', Mock(return_value=True) ) - @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_available_transcript_languages', - Mock(return_value=['es'])) + @patch(get_available_transcript_languages_path, Mock(return_value=['es'])) @patch('edxval.api.get_video_info_for_course_and_profiles', Mock(return_value={})) @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_transcript_content') @patch('edxval.api.get_video_info') From a215f2881156b6461a448d54646feeec7602e905 Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:04:12 +0500 Subject: [PATCH 4/7] chore: point xblocks-contrib to the required testing branch in requirements --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6a04252e4203..e2f7534a9b6b 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -1267,7 +1267,7 @@ xblock-utils==4.0.0 # via # edx-sga # xblock-poll -xblocks-contrib==0.10.2 +git+https://github.com/openedx/xblocks-contrib.git@farhan/update-shifted-video-block-code#egg=xblocks-contrib # via -r requirements/edx/bundled.in xmlsec==1.3.14 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 566f9df1bc2c..56acb722cefd 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -2295,7 +2295,7 @@ xblock-utils==4.0.0 # -r requirements/edx/testing.txt # edx-sga # xblock-poll -xblocks-contrib==0.10.2 +git+https://github.com/openedx/xblocks-contrib.git@farhan/update-shifted-video-block-code#egg=xblocks-contrib # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index a7f3fb2f094b..1a58eb574cb2 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1606,7 +1606,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.10.2 +git+https://github.com/openedx/xblocks-contrib.git@farhan/update-shifted-video-block-code#egg=xblocks-contrib # via -r requirements/edx/base.txt xmlsec==1.3.14 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 307d80fffa3c..6154aa2aa812 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1696,7 +1696,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.10.2 +git+https://github.com/openedx/xblocks-contrib.git@farhan/update-shifted-video-block-code#egg=xblocks-contrib # via -r requirements/edx/base.txt xmlsec==1.3.14 # via From 65178e14ba8e85a5e2bd80b72663a9d90f4f3f1c Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:13:11 +0500 Subject: [PATCH 5/7] chore: remove bumper_utils from edx-platform --- .../courseware/tests/test_video_mongo.py | 8 +- .../video_config/transcripts_utils.py | 2 +- xmodule/video_block/__init__.py | 2 +- xmodule/video_block/bumper_utils.py | 147 ------------------ xmodule/video_block/video_block.py | 2 +- 5 files changed, 5 insertions(+), 156 deletions(-) delete mode 100644 xmodule/video_block/bumper_utils.py diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 1a72a7e5b18a..6120b42ff3a3 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -66,12 +66,8 @@ from .test_video_xml import SOURCE_XML, PUBLIC_SOURCE_XML from common.test.utils import assert_dict_contains_subset -if settings.USE_EXTRACTED_VIDEO_BLOCK: - from xblocks_contrib.video import bumper_utils - bumper_utils_path = 'xblocks_contrib.video.bumper_utils' -else: - from xmodule.video_block import bumper_utils - bumper_utils_path = 'xmodule.video_block.bumper_utils' +from xblocks_contrib.video import bumper_utils +bumper_utils_path = 'xblocks_contrib.video.bumper_utils' TRANSCRIPT_FILE_SRT_DATA = """ 1 diff --git a/openedx/core/djangoapps/video_config/transcripts_utils.py b/openedx/core/djangoapps/video_config/transcripts_utils.py index 7714680a3192..84b596093414 100644 --- a/openedx/core/djangoapps/video_config/transcripts_utils.py +++ b/openedx/core/djangoapps/video_config/transcripts_utils.py @@ -789,7 +789,7 @@ def get_transcripts_info(self, is_bumper=False): # TODO: This causes a circular import when imported at the top-level. # This import will be removed as part of the VideoBlock extraction. # https://github.com/openedx/edx-platform/issues/36282 - from xmodule.video_block.bumper_utils import get_bumper_settings + from xblocks_contrib.video.bumper_utils import get_bumper_settings if is_bumper: transcripts = copy.deepcopy(get_bumper_settings(self).get('transcripts', {})) diff --git a/xmodule/video_block/__init__.py b/xmodule/video_block/__init__.py index 0bb52cb9cb3e..48d4ce6cda1b 100644 --- a/xmodule/video_block/__init__.py +++ b/xmodule/video_block/__init__.py @@ -2,7 +2,7 @@ Container for video block and its utils. """ -from .bumper_utils import * +from xblocks_contrib.video.bumper_utils import * from openedx.core.djangoapps.video_config.transcripts_utils import * # lint-amnesty, pylint: disable=redefined-builtin from .video_block import * from .video_utils import * diff --git a/xmodule/video_block/bumper_utils.py b/xmodule/video_block/bumper_utils.py deleted file mode 100644 index 64b02e915573..000000000000 --- a/xmodule/video_block/bumper_utils.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Utils for video bumper -""" - - -import copy -import json -import logging -from collections import OrderedDict -from datetime import datetime, timedelta -from zoneinfo import ZoneInfo - -from django.conf import settings - -from .video_utils import set_query_parameter - -try: - import edxval.api as edxval_api -except ImportError: - edxval_api = None - -log = logging.getLogger(__name__) - - -def get_bumper_settings(video): - """ - Get bumper settings from video instance. - """ - bumper_settings = copy.deepcopy(getattr(video, 'video_bumper', {})) - - # clean up /static/ prefix from bumper transcripts - for lang, transcript_url in bumper_settings.get('transcripts', {}).items(): - bumper_settings['transcripts'][lang] = transcript_url.replace("/static/", "") - - return bumper_settings - - -def is_bumper_enabled(video): - """ - Check if bumper enabled. - - - Feature flag ENABLE_VIDEO_BUMPER should be set to True - - Do not show again button should not be clicked by user. - - Current time minus periodicity must be greater that last time viewed - - edxval_api should be presented - - Returns: - bool. - """ - bumper_last_view_date = getattr(video, 'bumper_last_view_date', None) - utc_now = datetime.now(ZoneInfo("UTC")) - periodicity = settings.FEATURES.get('SHOW_BUMPER_PERIODICITY', 0) - has_viewed = any([ - video.bumper_do_not_show_again, - (bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now) - ]) - is_studio = getattr(video.runtime, "is_author_mode", False) - return bool( - not is_studio and - settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and - get_bumper_settings(video) and - edxval_api and - not has_viewed - ) - - -def bumperize(video): - """ - Populate video with bumper settings, if they are presented. - """ - video.bumper = { - 'enabled': False, - 'edx_video_id': "", - 'transcripts': {}, - 'metadata': None, - } - - if not is_bumper_enabled(video): - return - - bumper_settings = get_bumper_settings(video) - - try: - video.bumper['edx_video_id'] = bumper_settings['video_id'] - video.bumper['transcripts'] = bumper_settings['transcripts'] - except (TypeError, KeyError): - log.warning( - "Could not retrieve video bumper information from course settings" - ) - return - - sources = get_bumper_sources(video) - if not sources: - return - - video.bumper.update({ - 'metadata': bumper_metadata(video, sources), - 'enabled': True, # Video poster needs this. - }) - - -def get_bumper_sources(video): - """ - Get bumper sources from edxval. - - Returns list of sources. - """ - try: - val_profiles = ["desktop_webm", "desktop_mp4"] - val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles) - bumper_sources = [url for url in [val_video_urls[p] for p in val_profiles] if url] - except edxval_api.ValInternalError: - # if no bumper sources, nothing will be showed - log.warning( - "Could not retrieve information from VAL for Bumper edx Video ID: %s.", video.bumper['edx_video_id'] - ) - return [] - - return bumper_sources - - -def bumper_metadata(video, sources): - """ - Generate bumper metadata. - """ - transcripts = video.get_transcripts_info(is_bumper=True) - unused_track_url, bumper_transcript_language, bumper_languages = video.get_transcripts_for_student(transcripts) - - metadata = OrderedDict({ - 'saveStateUrl': video.ajax_url + '/save_user_state', - 'showCaptions': json.dumps(video.show_captions), - 'sources': sources, - 'streams': '', - 'transcriptLanguage': bumper_transcript_language, - 'transcriptLanguages': bumper_languages, - 'transcriptTranslationUrl': set_query_parameter( - video.runtime.handler_url(video, 'transcript', 'translation/__lang__').rstrip('/?'), 'is_bumper', 1 - ), - 'transcriptAvailableTranslationsUrl': set_query_parameter( - video.runtime.handler_url(video, 'transcript', 'available_translations').rstrip('/?'), 'is_bumper', 1 - ), - 'publishCompletionUrl': set_query_parameter( - video.runtime.handler_url(video, 'publish_completion', '').rstrip('?'), 'is_bumper', 1 - ), - }) - - return metadata diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py index 4cf58420b69e..49ba3ebd4077 100644 --- a/xmodule/video_block/video_block.py +++ b/xmodule/video_block/video_block.py @@ -48,7 +48,7 @@ XModuleMixin, XModuleToXBlockMixin, ) from xmodule.xml_block import XmlMixin, deserialize_field, is_pointer_tag, name_to_pathname -from .bumper_utils import bumperize +from xblocks_contrib.video.bumper_utils import bumperize from openedx.core.djangoapps.video_config.transcripts_utils import ( Transcript, VideoTranscriptsMixin, From a00e7b9af2c8ce5891f7d8cb65a6a34f5fbcee16 Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 13:34:26 +0500 Subject: [PATCH 6/7] chore: remove transcript_utils condition from test_video_handlers --- lms/djangoapps/courseware/tests/test_video_handlers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index d0a5de4a91f7..6ba4e48267dc 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -51,10 +51,8 @@ if settings.USE_EXTRACTED_VIDEO_BLOCK: path_video_handlers = 'xblocks_contrib.video.video_handlers' - path_transcripts_utils = 'xblocks_contrib.video.video_transcripts_utils' else: path_video_handlers = 'xmodule.video_block.video_handlers' - path_transcripts_utils = 'openedx.core.djangoapps.video_config.transcripts_utils' def _create_srt_file(content=None): @@ -335,7 +333,7 @@ def test_multiple_available_translations(self, mock_get_video_transcript_content assert sorted(json.loads(response.body.decode('utf-8'))) == sorted(['en', 'uk']) @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_transcript_content') - @patch(f'{path_transcripts_utils}.get_available_transcript_languages') + @patch('edxval.api.get_available_transcript_languages') @ddt.data( ( ['en', 'uk', 'ro'], From c0e9338ae5bd8a99356cdeff308e1d76c6545b34 Mon Sep 17 00:00:00 2001 From: farhan Date: Sat, 21 Feb 2026 14:05:15 +0500 Subject: [PATCH 7/7] chore: remove video_handers from the edx-platform --- .../courseware/tests/test_video_handlers.py | 5 +- xmodule/video_block/video_block.py | 2 +- xmodule/video_block/video_handlers.py | 551 ------------------ 3 files changed, 2 insertions(+), 556 deletions(-) delete mode 100644 xmodule/video_block/video_handlers.py diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index 6ba4e48267dc..2d580dd315e8 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -49,10 +49,7 @@ Привіт, edX вітає вас. """) -if settings.USE_EXTRACTED_VIDEO_BLOCK: - path_video_handlers = 'xblocks_contrib.video.video_handlers' -else: - path_video_handlers = 'xmodule.video_block.video_handlers' +path_video_handlers = 'xblocks_contrib.video.video_handlers' def _create_srt_file(content=None): diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py index 49ba3ebd4077..edaf8b5f19df 100644 --- a/xmodule/video_block/video_block.py +++ b/xmodule/video_block/video_block.py @@ -57,7 +57,7 @@ get_html5_ids, subs_filename ) -from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers +from xblocks_contrib.video.video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers from .video_utils import create_youtube_string, format_xml_exception_message, get_poster, rewrite_video_url from .video_xfields import VideoFields diff --git a/xmodule/video_block/video_handlers.py b/xmodule/video_block/video_handlers.py deleted file mode 100644 index adc59ae740d6..000000000000 --- a/xmodule/video_block/video_handlers.py +++ /dev/null @@ -1,551 +0,0 @@ -""" -Handlers for video block. - -StudentViewHandlers are handlers for video block instance. -StudioViewHandlers are handlers for video descriptor instance. -""" - - -import json -import logging -import math - -from django.utils.timezone import now -from opaque_keys.edx.locator import CourseLocator -from webob import Response -from xblock.core import XBlock -from xblock.exceptions import JsonHandlerError -from xblock.fields import RelativeTime - -from xmodule.exceptions import NotFoundError - -from openedx.core.djangoapps.video_config.transcripts_utils import ( - Transcript, - clean_video_id, - subs_filename, -) -from xblocks_contrib.video.exceptions import ( - TranscriptsGenerationException, - TranscriptNotFoundError, -) - -log = logging.getLogger(__name__) - - -def get_transcript( - video_block, - lang: str | None = None, - output_format: str = 'srt', - youtube_id: str | None = None, - is_bumper: bool = False, -) -> tuple[bytes, str, str]: - """ - Retrieve a transcript using a video block's configuration service. - - Returns: - tuple(bytes, str, str): transcript content, filename, and mimetype. - - Raises: - Exception: If the video config service is not available or the transcript cannot be retrieved. - """ - video_config_service = video_block.runtime.service(video_block, 'video_config') - if not video_config_service: - raise Exception("Video config service not found") - return video_config_service.get_transcript(video_block, lang, output_format, youtube_id, is_bumper) - - -# Disable no-member warning: -# pylint: disable=no-member - -def to_boolean(value): - """ - Convert a value from a GET or POST request parameter to a bool - """ - if isinstance(value, bytes): - value = value.decode('ascii', errors='replace') - if isinstance(value, str): - return value.lower() == 'true' - else: - return bool(value) - - -class VideoStudentViewHandlers: - """ - Handlers for video block instance. - """ - global_speed = None - transcript_language = None - - def handle_ajax(self, dispatch, data): - """ - Update values of xfields, that were changed by student. - """ - accepted_keys = [ - 'speed', 'auto_advance', 'saved_video_position', 'transcript_language', - 'transcript_download_format', 'youtube_is_available', - 'bumper_last_view_date', 'bumper_do_not_show_again' - ] - - conversions = { - 'speed': json.loads, - 'auto_advance': json.loads, - 'saved_video_position': RelativeTime.isotime_to_timedelta, - 'youtube_is_available': json.loads, - 'bumper_last_view_date': to_boolean, - 'bumper_do_not_show_again': to_boolean, - } - - if dispatch == 'save_user_state': - for key in data: - if key in accepted_keys: - if key in conversions: - value = conversions[key](data[key]) - else: - value = data[key] - - if key == 'bumper_last_view_date': - value = now() - - if key == 'speed' and math.isnan(value): - message = f"Invalid speed value {value}, must be a float." - log.warning(message) - return json.dumps({'success': False, 'error': message}) - - setattr(self, key, value) - - if key == 'speed': - self.global_speed = self.speed - - return json.dumps({'success': True}) - - log.debug(f"GET {data}") - log.debug(f"DISPATCH {dispatch}") - - raise NotFoundError('Unexpected dispatch type') - - def get_static_transcript(self, request, transcripts): - """ - Courses that are imported with the --nostatic flag do not show - transcripts/captions properly even if those captions are stored inside - their static folder. This adds a last resort method of redirecting to - the static asset path of the course if the transcript can't be found - inside the contentstore and the course has the static_asset_path field - set. - - transcripts (dict): A dict with all transcripts and a sub. - """ - response = Response(status=404) - # Only do redirect for English - if not self.transcript_language == 'en': - return response - - # If this video lives in library, the code below is not relevant and will error. - if not isinstance(self.course_id, CourseLocator): - return response - - video_id = request.GET.get('videoId', None) - if video_id: - transcript_name = video_id - else: - transcript_name = transcripts["sub"] - - if transcript_name: - # Get the asset path for course - asset_path = None - course = self.runtime.modulestore.get_course(self.course_id) - if course.static_asset_path: - asset_path = course.static_asset_path - else: - # It seems static_asset_path is not set in any XMLModuleStore courses. - asset_path = getattr(course, 'data_dir', '') - - if asset_path: - response = Response( - status=307, - location='/static/{}/{}'.format( - asset_path, - subs_filename(transcript_name, self.transcript_language) - ) - ) - return response - - @XBlock.json_handler - def publish_completion(self, data, dispatch): # pylint: disable=unused-argument - """ - Entry point for completion for student_view. - - Parameters: - data: JSON dict: - key: "completion" - value: float in range [0.0, 1.0] - - dispatch: Ignored. - Return value: JSON response (200 on success, 400 for malformed data) - """ - completion_service = self.runtime.service(self, 'completion') - if completion_service is None: - raise JsonHandlerError(500, "No completion service found") - if not completion_service.completion_tracking_enabled(): - raise JsonHandlerError(404, "Completion tracking is not enabled and API calls are unexpected") - if not isinstance(data['completion'], (int, float)): - message = "Invalid completion value {}. Must be a float in range [0.0, 1.0]" - raise JsonHandlerError(400, message.format(data['completion'])) - if not 0.0 <= data['completion'] <= 1.0: - message = "Invalid completion value {}. Must be in range [0.0, 1.0]" - raise JsonHandlerError(400, message.format(data['completion'])) - self.runtime.publish(self, "completion", data) - return {"result": "ok"} - - @staticmethod - def make_transcript_http_response(content, filename, language, content_type, add_attachment_header=True): - """ - Construct `Response` object. - - Arguments: - content (unicode): transcript content - filename (unicode): transcript filename - language (unicode): transcript language - mimetype (unicode): transcript content type - add_attachment_header (bool): whether to add attachment header or not - """ - headerlist = [ - ('Content-Language', language), - ] - - if add_attachment_header: - headerlist.append( - ( - 'Content-Disposition', - f'attachment; filename="{filename}"' - ) - ) - - response = Response( - content, - headerlist=headerlist, - charset='utf8' - ) - response.content_type = content_type - - return response - - @XBlock.handler - def transcript(self, request, dispatch): - """ - Entry point for transcript handlers for student_view. - - Request GET contains: - (optional) `videoId` for `translation` dispatch. - `is_bumper=1` flag for bumper case. - - Dispatches, (HTTP GET): - /translation/[language_id] - /download - /available_translations/ - - Explanations: - `download`: returns SRT or TXT file. - `translation`: depends on HTTP methods: - Provide translation for requested language, SJSON format is sent back on success, - Proper language_id should be in url. - `available_translations`: - Returns list of languages, for which transcript files exist. - For 'en' check if SJSON exists. For non-`en` check if SRT file exists. - """ - is_bumper = request.GET.get('is_bumper', False) - transcripts = self.get_transcripts_info(is_bumper) - - if dispatch.startswith('translation'): - language = dispatch.replace('translation', '').strip('/') - - # Because scrapers hit video blocks, verify that a user exists. - # use the _request attr to get the django request object. - if not request._request.user: # pylint: disable=protected-access - log.info("Transcript: user must be logged or public view enabled to get transcript") - return Response(status=403) - - if not language: - log.info("Invalid /translation request: no language.") - return Response(status=400) - - if language not in ['en'] + list(transcripts["transcripts"].keys()): - log.info("Video: transcript facilities are not available for given language.") - return Response(status=404) - - if language != self.transcript_language: - self.transcript_language = language - - try: - youtube_id = None if is_bumper else request.GET.get('videoId') - content, filename, mimetype = get_transcript( - self, - lang=self.transcript_language, - output_format=Transcript.SJSON, - youtube_id=youtube_id, - is_bumper=is_bumper - ) - response = self.make_transcript_http_response( - content, - filename, - self.transcript_language, - mimetype, - add_attachment_header=False - ) - except TranscriptNotFoundError as exc: - edx_video_id = clean_video_id(self.edx_video_id) - log.warning( - '[Translation Dispatch] %s: %s', - self.location, - exc if is_bumper else f'Transcript not found for {edx_video_id}, lang: {self.transcript_language}', - ) - response = self.get_static_transcript(request, transcripts) - - elif dispatch == 'download': - lang = request.GET.get('lang', None) - - try: - content, filename, mimetype = get_transcript(self, lang, output_format=self.transcript_download_format) - except TranscriptNotFoundError: - return Response(status=404) - - response = self.make_transcript_http_response( - content, - filename, - self.transcript_language, - mimetype - ) - elif dispatch.startswith('available_translations'): - video_config_service = self.runtime.service(self, 'video_config') - if not video_config_service: - return Response(status=404) - available_translations = video_config_service.available_translations( - self, - transcripts, - verify_assets=True, - is_bumper=is_bumper - ) - if available_translations: - response = Response(json.dumps(available_translations)) - response.content_type = 'application/json' - else: - response = Response(status=404) - else: # unknown dispatch - log.debug("Dispatch is not allowed") - response = Response(status=404) - - return response - - @XBlock.handler - def student_view_user_state(self, request, suffix=''): # lint-amnesty, pylint: disable=unused-argument - """ - Endpoint to get user-specific state, like current position and playback speed, - without rendering the full student_view HTML. This is similar to student_view_state, - but that one cannot contain user-specific info. - """ - view_state = self.student_view_data() - view_state.update({ - "saved_video_position": self.saved_video_position.total_seconds(), - "speed": self.speed, - }) - return Response( - json.dumps(view_state), - content_type='application/json', - charset='UTF-8' - ) - - @XBlock.handler - def yt_video_metadata(self, request, suffix=''): # lint-amnesty, pylint: disable=unused-argument - """ - Endpoint to get YouTube metadata. - This handler is only used in the Learning-Core-based runtime. The old - runtime uses a similar REST API that's not an XBlock handler. - """ - from lms.djangoapps.courseware.views.views import load_metadata_from_youtube - if not self.youtube_id_1_0: - # TODO: more informational response to explain that yt_video_metadata not supported for non-youtube videos. - return Response('{}', status=400) - - metadata, status_code = load_metadata_from_youtube(video_id=self.youtube_id_1_0, request=request) - response = Response(json.dumps(metadata), status=status_code) - response.content_type = 'application/json' - return response - - -class VideoStudioViewHandlers: - """ - Handlers for Studio view. - """ - def validate_transcript_upload_data(self, data): - """ - Validates video transcript file. - Arguments: - data: Transcript data to be validated. - Returns: - None or String - If there is error returns error message otherwise None. - """ - error = None - _ = self.runtime.service(self, "i18n").ugettext - # Validate the must have attributes - this error is unlikely to be faced by common users. - must_have_attrs = ['edx_video_id', 'language_code', 'new_language_code'] - missing = [attr for attr in must_have_attrs if attr not in data] - - # Get available transcript languages. - transcripts = self.get_transcripts_info() - video_config_service = self.runtime.service(self, 'video_config') - if not video_config_service: - return error - available_translations = video_config_service.available_translations( - self, - transcripts, - verify_assets=True - ) - - if missing: - error = _('The following parameters are required: {missing}.').format(missing=', '.join(missing)) - elif ( - data['language_code'] != data['new_language_code'] and data['new_language_code'] in available_translations - ): - error = _('A transcript with the "{language_code}" language code already exists.').format( - language_code=data['new_language_code'], - ) - elif 'file' not in data: - error = _('A transcript file is required.') - - return error - - @XBlock.handler - def studio_transcript(self, request, dispatch): - """ - Entry point for Studio transcript handlers. - - Dispatches: - /translation/[language_id] - language_id sould be in url. - - `translation` dispatch support following HTTP methods: - `POST`: - Upload srt file. Check possibility of generation of proper sjson files. - For now, it works only for self.transcripts, not for `en`. - `GET: - Return filename from storage. SRT format is sent back on success. Filename should be in GET dict. - - We raise all exceptions right in Studio: - NotFoundError: - Video or asset was deleted from module/contentstore, but request came later. - Seems impossible to be raised. block_render.py catches NotFoundErrors from here. - - /translation POST: - TypeError: - Unjsonable filename or content. - TranscriptsGenerationException, TranscriptException: - no SRT extension or not parse-able by PySRT - UnicodeDecodeError: non-UTF8 uploaded file content encoding. - """ - if dispatch.startswith('translation'): - - if request.method == 'POST': - response = self._studio_transcript_upload(request) - elif request.method == 'DELETE': - response = self._studio_transcript_delete(request) - elif request.method == 'GET': - response = self._studio_transcript_get(request) - else: - # Any other HTTP method is not allowed. - response = Response(status=404) - - else: # unknown dispatch - log.debug("Dispatch is not allowed") - response = Response(status=404) - - return response - - def _studio_transcript_upload(self, request): - """ - Upload transcript. Used in "POST" method in `studio_transcript` - """ - _ = self.runtime.service(self, "i18n").ugettext - video_config_service = self.runtime.service(self, 'video_config') - if not video_config_service: - return Response(json={'error': _('Runtime does not support transcripts.')}, status=400) - error = self.validate_transcript_upload_data(data=request.POST) - if error: - return Response(json={'error': error}, status=400) - edx_video_id = (request.POST['edx_video_id'] or "").strip() - language_code = request.POST['language_code'] - new_language_code = request.POST['new_language_code'] - try: - video_config_service.upload_transcript( - video_block=self, # NOTE: .edx_video_id and .transcripts may get mutated - edx_video_id=edx_video_id, - language_code=language_code, - new_language_code=new_language_code, - transcript_file=request.POST['file'].file, - ) - return Response( - json.dumps( - { - "edx_video_id": edx_video_id or self.edx_video_id, - "language_code": new_language_code, - } - ), - status=201, - ) - except (TranscriptsGenerationException, UnicodeDecodeError): - return Response( - json={ - 'error': _( - 'There is a problem with this transcript file. Try to upload a different file.' - ) - }, - status=400 - ) - - def _studio_transcript_delete(self, request): - """ - Delete transcript. Used in "DELETE" method in `studio_transcript` - """ - _ = self.runtime.service(self, "i18n").ugettext - video_config_service = self.runtime.service(self, 'video_config') - if not video_config_service: - return Response(json={'error': _('Runtime does not support transcripts.')}, status=400) - request_data = request.json - if 'lang' not in request_data or 'edx_video_id' not in request_data: - return Response(status=400) - video_config_service.delete_transcript( - video_block=self, - edx_video_id=request_data['edx_video_id'], - language_code=request_data['lang'], - ) - return Response(status=200) - - def _studio_transcript_get(self, request): - """ - Get transcript. Used in "GET" method in `studio_transcript` - """ - _ = self.runtime.service(self, "i18n").ugettext - language = request.GET.get('language_code') - if not language: - return Response(json={'error': _('Language is required.')}, status=400) - - try: - video_config_service = self.runtime.service(self, 'video_config') - if not video_config_service: - return Response(status=404) - transcript_content, transcript_name, mime_type = video_config_service.get_transcript( - self, lang=language, output_format=Transcript.SRT - ) - response = Response(transcript_content, headerlist=[ - ( - 'Content-Disposition', - f'attachment; filename="{transcript_name}"' - ), - ('Content-Language', language), - ('Content-Type', mime_type) - ]) - except ( - UnicodeDecodeError, - TranscriptsGenerationException, - TranscriptNotFoundError - ): - response = Response(status=404) - return response