From 611fc7ca2890a3f2443387f9a02c241d5089edc1 Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Mon, 9 Mar 2026 15:46:30 +0500 Subject: [PATCH 1/6] fix: apps installation issue --- xblocks_contrib/video/bumper_utils.py | 9 +++------ xblocks_contrib/video/video.py | 12 ++++++++---- xblocks_contrib/video/video_transcripts_utils.py | 8 ++------ xblocks_contrib/video/video_utils.py | 12 ++++++++++++ 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/xblocks_contrib/video/bumper_utils.py b/xblocks_contrib/video/bumper_utils.py index 64b02e91..c023437b 100644 --- a/xblocks_contrib/video/bumper_utils.py +++ b/xblocks_contrib/video/bumper_utils.py @@ -12,12 +12,7 @@ from django.conf import settings -from .video_utils import set_query_parameter - -try: - import edxval.api as edxval_api -except ImportError: - edxval_api = None +from .video_utils import set_query_parameter, _get_edxval_api log = logging.getLogger(__name__) @@ -55,6 +50,7 @@ def is_bumper_enabled(video): (bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now) ]) is_studio = getattr(video.runtime, "is_author_mode", False) + edxval_api = _get_edxval_api() return bool( not is_studio and settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and @@ -105,6 +101,7 @@ def get_bumper_sources(video): Returns list of sources. """ + edxval_api = _get_edxval_api() try: val_profiles = ["desktop_webm", "desktop_mp4"] val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles) diff --git a/xblocks_contrib/video/video.py b/xblocks_contrib/video/video.py index 31303e37..7364668b 100644 --- a/xblocks_contrib/video/video.py +++ b/xblocks_contrib/video/video.py @@ -64,6 +64,7 @@ get_poster, get_resource_url, rewrite_video_url, + _get_edxval_api ) from xblocks_contrib.video.video_xfields import VideoFields @@ -93,10 +94,6 @@ # (4) is one of the next items on the backlog for edxval, and should get rid # of this particular import silliness. It's just that I haven't made one before, # and I was worried about trying it with my deadline constraints. -try: - import edxval.api as edxval_api -except ImportError: - edxval_api = None log = logging.getLogger(__name__) loader = ResourceLoader(__name__) @@ -356,6 +353,7 @@ def get_html(self, view=STUDENT_VIEW, context=None): # pylint: disable=too-many # If we have an edx_video_id, we prefer its values over what we store # internally for download links (source, html5_sources) and the youtube # stream. + edxval_api = _get_edxval_api() if self.edx_video_id and edxval_api: # lint-amnesty, pylint: disable=too-many-nested-blocks try: val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] @@ -752,6 +750,7 @@ def definition_to_xml(self, resource_fs): # lint-amnesty, pylint: disable=too-m transcripts.update(self.transcripts) edx_video_id = clean_video_id(self.edx_video_id) + edxval_api = _get_edxval_api() if edxval_api and edx_video_id: try: # Create static dir if not created earlier. @@ -819,6 +818,7 @@ def get_context(self): """ Extend context by data for transcript basic tab. """ + edxval_api = _get_edxval_api() _context = { 'editable_metadata_fields': self.editable_metadata_fields } @@ -1043,6 +1043,7 @@ def import_video_info_into_val(self, xml, resource_fs, course_id): for language_code, transcript in self.transcripts.items(): external_transcripts[language_code].append(transcript) + edxval_api = _get_edxval_api() if edxval_api: edx_video_id = edxval_api.import_from_xml( video_asset_elem, @@ -1105,6 +1106,7 @@ def get_cached_val_data_for_course(cls, request_cache, video_profile_names, cour """ Returns the VAL data for the requested video profiles for the given course. """ + edxval_api = _get_edxval_api() return edxval_api.get_video_info_for_course_and_profiles(str(course_id), video_profile_names) def student_view_data(self, context=None): @@ -1112,6 +1114,7 @@ def student_view_data(self, context=None): Returns a JSON representation of the student_view of this XModule. The contract of the JSON content is between the caller and the particular XModule. """ + edxval_api = _get_edxval_api() context = context or {} # If the "only_on_web" field is set on this video, do not return the rest of the video's data @@ -1198,6 +1201,7 @@ def _poster(self): """ Helper to get poster info from edxval """ + edxval_api = _get_edxval_api() if edxval_api and self.edx_video_id: return edxval_api.get_course_video_image_url( course_id=self.scope_ids.usage_id.context_key.for_branch(None), diff --git a/xblocks_contrib/video/video_transcripts_utils.py b/xblocks_contrib/video/video_transcripts_utils.py index c7fc58b4..54dfcaa0 100644 --- a/xblocks_contrib/video/video_transcripts_utils.py +++ b/xblocks_contrib/video/video_transcripts_utils.py @@ -11,14 +11,9 @@ from django.utils.translation import get_language_info from xblocks_contrib.video.bumper_utils import get_bumper_settings +from xblocks_contrib.video.video_utils import _get_edxval_api from xblocks_contrib.video.exceptions import TranscriptNotFoundError -try: - from edxval import api as edxval_api -except ImportError: - edxval_api = None - - log = logging.getLogger(__name__) NON_EXISTENT_TRANSCRIPT = 'non_existent_dummy_file_name' @@ -79,6 +74,7 @@ def get_available_transcript_languages(edx_video_id): """ available_languages = [] edx_video_id = clean_video_id(edx_video_id) + edxval_api = _get_edxval_api() if edxval_api and edx_video_id: available_languages = edxval_api.get_available_transcript_languages(video_id=edx_video_id) diff --git a/xblocks_contrib/video/video_utils.py b/xblocks_contrib/video/video_utils.py index c3d6e8ce..3ea3a87a 100644 --- a/xblocks_contrib/video/video_utils.py +++ b/xblocks_contrib/video/video_utils.py @@ -212,3 +212,15 @@ def get_resource_url(xblock, path, package_scope=None): else: resource_path = dev_path return xblock.runtime.local_resource_url(xblock, resource_path) + + +def _get_edxval_api(): + """ + Lazy import for edxval_api to prevent AppRegistryNotReady errors + during Django startup. + """ + try: + import edxval.api as edxval_api + return edxval_api + except ImportError: + return None From a2746102a81ce9fa0fdec90a4302f73016f28bab Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Mon, 9 Mar 2026 15:48:21 +0500 Subject: [PATCH 2/6] fix: bump version --- CHANGELOG.rst | 8 ++++++++ xblocks_contrib/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4085ee8b..4668e654 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Change Log Unreleased ********** +0.13.1 - 2026-03-09 +********************************************** + +Fixed +===== + +* Fix TemplateDoesNotExist Error for capa templates. + 0.13.0 - 2026-03-03 ********************************************** diff --git a/xblocks_contrib/__init__.py b/xblocks_contrib/__init__.py index 7313f6e0..8a83c7a7 100644 --- a/xblocks_contrib/__init__.py +++ b/xblocks_contrib/__init__.py @@ -9,4 +9,4 @@ from .video import VideoBlock from .word_cloud import WordCloudBlock -__version__ = "0.13.0" +__version__ = "0.13.1" From 142d476418fd8b14a12a58bf79ab537aa2765b5a Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Mon, 9 Mar 2026 16:06:56 +0500 Subject: [PATCH 3/6] fix: quality issues --- xblocks_contrib/video/video_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xblocks_contrib/video/video_utils.py b/xblocks_contrib/video/video_utils.py index 3ea3a87a..d299f3ae 100644 --- a/xblocks_contrib/video/video_utils.py +++ b/xblocks_contrib/video/video_utils.py @@ -220,7 +220,7 @@ def _get_edxval_api(): during Django startup. """ try: - import edxval.api as edxval_api + import edxval.api as edxval_api # pylint: disable=import-outside-toplevel return edxval_api except ImportError: return None From 537f8ad1068aa9371b73dbeaabd1ca1774e3e0b3 Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Mon, 9 Mar 2026 16:58:06 +0500 Subject: [PATCH 4/6] fix: quality issues --- xblocks_contrib/video/bumper_utils.py | 2 +- xblocks_contrib/video/video.py | 2 +- xblocks_contrib/video/video_transcripts_utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xblocks_contrib/video/bumper_utils.py b/xblocks_contrib/video/bumper_utils.py index c023437b..f8be1e6f 100644 --- a/xblocks_contrib/video/bumper_utils.py +++ b/xblocks_contrib/video/bumper_utils.py @@ -12,7 +12,7 @@ from django.conf import settings -from .video_utils import set_query_parameter, _get_edxval_api +from .video_utils import _get_edxval_api, set_query_parameter log = logging.getLogger(__name__) diff --git a/xblocks_contrib/video/video.py b/xblocks_contrib/video/video.py index 7364668b..55eed3af 100644 --- a/xblocks_contrib/video/video.py +++ b/xblocks_contrib/video/video.py @@ -59,12 +59,12 @@ subs_filename, ) from xblocks_contrib.video.video_utils import ( + _get_edxval_api, create_youtube_string, format_xml_exception_message, get_poster, get_resource_url, rewrite_video_url, - _get_edxval_api ) from xblocks_contrib.video.video_xfields import VideoFields diff --git a/xblocks_contrib/video/video_transcripts_utils.py b/xblocks_contrib/video/video_transcripts_utils.py index 54dfcaa0..6f917d53 100644 --- a/xblocks_contrib/video/video_transcripts_utils.py +++ b/xblocks_contrib/video/video_transcripts_utils.py @@ -11,8 +11,8 @@ from django.utils.translation import get_language_info from xblocks_contrib.video.bumper_utils import get_bumper_settings -from xblocks_contrib.video.video_utils import _get_edxval_api from xblocks_contrib.video.exceptions import TranscriptNotFoundError +from xblocks_contrib.video.video_utils import _get_edxval_api log = logging.getLogger(__name__) From 69d85e46988519ffa588dc66015277ad0283d33d Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Tue, 10 Mar 2026 11:18:23 +0500 Subject: [PATCH 5/6] fix: update util name --- xblocks_contrib/video/bumper_utils.py | 6 +++--- xblocks_contrib/video/video.py | 16 ++++++++-------- xblocks_contrib/video/video_transcripts_utils.py | 4 ++-- xblocks_contrib/video/video_utils.py | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/xblocks_contrib/video/bumper_utils.py b/xblocks_contrib/video/bumper_utils.py index f8be1e6f..4d1f75be 100644 --- a/xblocks_contrib/video/bumper_utils.py +++ b/xblocks_contrib/video/bumper_utils.py @@ -12,7 +12,7 @@ from django.conf import settings -from .video_utils import _get_edxval_api, set_query_parameter +from .video_utils import get_edxval_api, set_query_parameter log = logging.getLogger(__name__) @@ -50,7 +50,7 @@ def is_bumper_enabled(video): (bumper_last_view_date and bumper_last_view_date + timedelta(seconds=periodicity) > utc_now) ]) is_studio = getattr(video.runtime, "is_author_mode", False) - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() return bool( not is_studio and settings.FEATURES.get('ENABLE_VIDEO_BUMPER') and @@ -101,7 +101,7 @@ def get_bumper_sources(video): Returns list of sources. """ - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() try: val_profiles = ["desktop_webm", "desktop_mp4"] val_video_urls = edxval_api.get_urls_for_profiles(video.bumper['edx_video_id'], val_profiles) diff --git a/xblocks_contrib/video/video.py b/xblocks_contrib/video/video.py index 55eed3af..174c63cd 100644 --- a/xblocks_contrib/video/video.py +++ b/xblocks_contrib/video/video.py @@ -59,9 +59,9 @@ subs_filename, ) from xblocks_contrib.video.video_utils import ( - _get_edxval_api, create_youtube_string, format_xml_exception_message, + get_edxval_api, get_poster, get_resource_url, rewrite_video_url, @@ -353,7 +353,7 @@ def get_html(self, view=STUDENT_VIEW, context=None): # pylint: disable=too-many # If we have an edx_video_id, we prefer its values over what we store # internally for download links (source, html5_sources) and the youtube # stream. - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() if self.edx_video_id and edxval_api: # lint-amnesty, pylint: disable=too-many-nested-blocks try: val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] @@ -750,7 +750,7 @@ def definition_to_xml(self, resource_fs): # lint-amnesty, pylint: disable=too-m transcripts.update(self.transcripts) edx_video_id = clean_video_id(self.edx_video_id) - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() if edxval_api and edx_video_id: try: # Create static dir if not created earlier. @@ -818,7 +818,7 @@ def get_context(self): """ Extend context by data for transcript basic tab. """ - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() _context = { 'editable_metadata_fields': self.editable_metadata_fields } @@ -1043,7 +1043,7 @@ def import_video_info_into_val(self, xml, resource_fs, course_id): for language_code, transcript in self.transcripts.items(): external_transcripts[language_code].append(transcript) - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() if edxval_api: edx_video_id = edxval_api.import_from_xml( video_asset_elem, @@ -1106,7 +1106,7 @@ def get_cached_val_data_for_course(cls, request_cache, video_profile_names, cour """ Returns the VAL data for the requested video profiles for the given course. """ - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() return edxval_api.get_video_info_for_course_and_profiles(str(course_id), video_profile_names) def student_view_data(self, context=None): @@ -1114,7 +1114,7 @@ def student_view_data(self, context=None): Returns a JSON representation of the student_view of this XModule. The contract of the JSON content is between the caller and the particular XModule. """ - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() context = context or {} # If the "only_on_web" field is set on this video, do not return the rest of the video's data @@ -1201,7 +1201,7 @@ def _poster(self): """ Helper to get poster info from edxval """ - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() if edxval_api and self.edx_video_id: return edxval_api.get_course_video_image_url( course_id=self.scope_ids.usage_id.context_key.for_branch(None), diff --git a/xblocks_contrib/video/video_transcripts_utils.py b/xblocks_contrib/video/video_transcripts_utils.py index 6f917d53..8fd58a94 100644 --- a/xblocks_contrib/video/video_transcripts_utils.py +++ b/xblocks_contrib/video/video_transcripts_utils.py @@ -12,7 +12,7 @@ from xblocks_contrib.video.bumper_utils import get_bumper_settings from xblocks_contrib.video.exceptions import TranscriptNotFoundError -from xblocks_contrib.video.video_utils import _get_edxval_api +from xblocks_contrib.video.video_utils import get_edxval_api log = logging.getLogger(__name__) @@ -74,7 +74,7 @@ def get_available_transcript_languages(edx_video_id): """ available_languages = [] edx_video_id = clean_video_id(edx_video_id) - edxval_api = _get_edxval_api() + edxval_api = get_edxval_api() if edxval_api and edx_video_id: available_languages = edxval_api.get_available_transcript_languages(video_id=edx_video_id) diff --git a/xblocks_contrib/video/video_utils.py b/xblocks_contrib/video/video_utils.py index d299f3ae..148553c2 100644 --- a/xblocks_contrib/video/video_utils.py +++ b/xblocks_contrib/video/video_utils.py @@ -214,7 +214,7 @@ def get_resource_url(xblock, path, package_scope=None): return xblock.runtime.local_resource_url(xblock, resource_path) -def _get_edxval_api(): +def get_edxval_api(): """ Lazy import for edxval_api to prevent AppRegistryNotReady errors during Django startup. From f133fc20e7730d9916b9cc4f173885859407467e Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Tue, 10 Mar 2026 11:42:43 +0500 Subject: [PATCH 6/6] fix: update tests --- xblocks_contrib/video/tests/test_video.py | 41 +++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/xblocks_contrib/video/tests/test_video.py b/xblocks_contrib/video/tests/test_video.py index 76578753..bb8629fb 100644 --- a/xblocks_contrib/video/tests/test_video.py +++ b/xblocks_contrib/video/tests/test_video.py @@ -636,11 +636,13 @@ def test_import_with_float_times(self): 'data': '' }) - @patch('xblocks_contrib.video.video.edxval_api') - def test_import_val_data(self, mock_val_api): + @patch('xblocks_contrib.video.video.get_edxval_api') + def test_import_val_data(self, mock_get_edxval_api): """ Test that `parse_xml` works method works as expected. """ + mock_val_api = mock_get_edxval_api.return_value + def mock_val_import(xml, edx_video_id, resource_fs, static_dir, external_transcripts, *, course_id=None): """Mock edxval.api.import_parse_xml""" assert xml.tag == 'video_asset' @@ -681,8 +683,9 @@ def mock_val_import(xml, edx_video_id, resource_fs, static_dir, external_transcr course_id='test_course_id' ) - @patch('xblocks_contrib.video.video.edxval_api') - def test_import_val_data_invalid(self, mock_val_api): + @patch('xblocks_contrib.video.video.get_edxval_api') + def test_import_val_data_invalid(self, mock_get_edxval_api): + mock_val_api = mock_get_edxval_api.return_value mock_val_api.ValCannotCreateError = _MockValCannotCreateError mock_val_api.import_from_xml = Mock(side_effect=mock_val_api.ValCannotCreateError) module_system = DummyRuntime(load_error_blocks=True) @@ -709,11 +712,12 @@ def setUp(self): self.file_system = OSFS(self.temp_dir) self.addCleanup(shutil.rmtree, self.temp_dir) - @patch('xblocks_contrib.video.video.edxval_api') - def test_export_to_xml(self, mock_val_api): + @patch('xblocks_contrib.video.video.get_edxval_api') + def test_export_to_xml(self, mock_get_edxval_api): """ Test that we write the correct XML on export. """ + mock_val_api = mock_get_edxval_api.return_value edx_video_id = 'test_edx_video_id' mock_val_api.export_to_xml = Mock( return_value={"xml": etree.Element('video_asset'), "transcripts": {}} @@ -806,8 +810,9 @@ def test_export_to_xml_without_video_id(self): expected = etree.XML(xml_string, parser=parser) self.assertXmlEqual(expected, xml) - @patch('xblocks_contrib.video.video.edxval_api') - def test_export_to_xml_val_error(self, mock_val_api): + @patch('xblocks_contrib.video.video.get_edxval_api') + def test_export_to_xml_val_error(self, mock_get_edxval_api): + mock_val_api = mock_get_edxval_api.return_value # Export should succeed without VAL data if video does not exist mock_val_api.ValVideoNotFoundError = _MockValVideoNotFoundError mock_val_api.export_to_xml = Mock(side_effect=mock_val_api.ValVideoNotFoundError) @@ -819,8 +824,8 @@ def test_export_to_xml_val_error(self, mock_val_api): expected = etree.XML(xml_string, parser=parser) self.assertXmlEqual(expected, xml) - @patch('xblocks_contrib.video.video.edxval_api', None) - def test_export_to_xml_empty_end_time(self): + @patch('xblocks_contrib.video.video.get_edxval_api', return_value=None) + def test_export_to_xml_empty_end_time(self, _mock_get_edxval_api): """ Test that we write the correct XML on export. """ @@ -850,8 +855,8 @@ def test_export_to_xml_empty_end_time(self): expected = etree.XML(xml_string, parser=parser) self.assertXmlEqual(expected, xml) - @patch('xblocks_contrib.video.video.edxval_api', None) - def test_export_to_xml_empty_parameters(self): + @patch('xblocks_contrib.video.video.get_edxval_api', return_value=None) + def test_export_to_xml_empty_parameters(self, _mock_get_edxval_api): """ Test XML export with defaults. """ @@ -860,8 +865,8 @@ def test_export_to_xml_empty_parameters(self): expected = '