Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
**********************************************

Expand Down
2 changes: 1 addition & 1 deletion xblocks_contrib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
from .video import VideoBlock
from .word_cloud import WordCloudBlock

__version__ = "0.13.0"
__version__ = "0.13.1"
9 changes: 3 additions & 6 deletions xblocks_contrib/video/bumper_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 get_edxval_api, set_query_parameter

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 23 additions & 18 deletions xblocks_contrib/video/tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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": {}}
Expand Down Expand Up @@ -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)
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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.
"""
Expand All @@ -860,8 +865,8 @@ def test_export_to_xml_empty_parameters(self):
expected = '<video youtube="1.00:3_yD_cEKoCk" url_name="SampleProblem"/>\n'
assert expected == etree.tostring(xml, pretty_print=True).decode('utf-8')

@patch('xblocks_contrib.video.video.edxval_api', None)
def test_export_to_xml_with_transcripts_as_none(self):
@patch('xblocks_contrib.video.video.get_edxval_api', return_value=None)
def test_export_to_xml_with_transcripts_as_none(self, _mock_get_edxval_api):
"""
Test XML export with transcripts being overridden to None.
"""
Expand All @@ -870,8 +875,8 @@ def test_export_to_xml_with_transcripts_as_none(self):
expected = b'<video youtube="1.00:3_yD_cEKoCk" url_name="SampleProblem"/>\n'
assert expected == etree.tostring(xml, pretty_print=True)

@patch('xblocks_contrib.video.video.edxval_api', None)
def test_export_to_xml_invalid_characters_in_attributes(self):
@patch('xblocks_contrib.video.video.get_edxval_api', return_value=None)
def test_export_to_xml_invalid_characters_in_attributes(self, _mock_get_edxval_api):
"""
Test XML export will *not* raise TypeError by lxml library if contains illegal characters.
The illegal characters in a String field are removed from the string instead.
Expand All @@ -880,8 +885,8 @@ def test_export_to_xml_invalid_characters_in_attributes(self):
xml = self.block.definition_to_xml(self.file_system)
assert xml.get('display_name') == 'DisplayName'

@patch('xblocks_contrib.video.video.edxval_api', None)
def test_export_to_xml_unicode_characters(self):
@patch('xblocks_contrib.video.video.get_edxval_api', return_value=None)
def test_export_to_xml_unicode_characters(self, _mock_get_edxval_api):
"""
Test XML export handles the unicode characters.
"""
Expand Down
12 changes: 8 additions & 4 deletions xblocks_contrib/video/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from xblocks_contrib.video.video_utils import (
create_youtube_string,
format_xml_exception_message,
get_edxval_api,
get_poster,
get_resource_url,
rewrite_video_url,
Expand Down Expand Up @@ -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__)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1105,13 +1106,15 @@ 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):
"""
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
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 2 additions & 6 deletions xblocks_contrib/video/video_transcripts_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@

from xblocks_contrib.video.bumper_utils import get_bumper_settings
from xblocks_contrib.video.exceptions import TranscriptNotFoundError

try:
from edxval import api as edxval_api
except ImportError:
edxval_api = None

from xblocks_contrib.video.video_utils import get_edxval_api

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -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)

Expand Down
12 changes: 12 additions & 0 deletions xblocks_contrib/video/video_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 # pylint: disable=import-outside-toplevel
return edxval_api
except ImportError:
return None