refactor: Convert BuiltIn Video Block's html from mako to django template (#37509)

Closes: https://github.com/openedx/public-engineering/issues/427
This commit is contained in:
salmannawaz
2025-11-18 20:59:11 +05:00
committed by GitHub
parent afe5dab47d
commit d516736482
4 changed files with 293 additions and 269 deletions

View File

@@ -3,8 +3,6 @@ Helpers for courseware tests.
"""
import ast
import re
import json
from collections import OrderedDict
from datetime import timedelta
@@ -444,17 +442,27 @@ def get_expiration_banner_text(user, course, language='en'): # lint-amnesty, py
return bannerText
def get_context_dict_from_string(data):
def get_context_from_dict(data):
"""
Retrieve dictionary from string.
"""
# Replace tuple and un-necessary info from inside string and get the dictionary.
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)
Retrieve validated dictionary from template's contextual data.
Args:
data: The context dictionary to validate
Returns:
dict: context dictionary
"""
# Make a copy to avoid modifying the original dict
validated_data = data.copy()
# Omit user_id validation
validated_data.pop('user_id', None)
# Handle metadata field - parse and sort to ensure consistent ordering
if 'metadata' in validated_data and validated_data['metadata'] is not None:
metadata_dict = json.loads(validated_data['metadata'])
validated_data['metadata'] = OrderedDict(
sorted(metadata_dict.items(), key=lambda t: t[0])
)
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 validated_data

View File

@@ -46,7 +46,7 @@ from xmodule.exceptions import NotFoundError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
# noinspection PyUnresolvedReferences
from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import
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
@@ -55,7 +55,7 @@ from xmodule.video_block.video_block import EXPORT_IMPORT_COURSE_DIR, EXPORT_IMP
from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW
from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE
from lms.djangoapps.courseware.tests.helpers import get_context_dict_from_string
from lms.djangoapps.courseware.tests.helpers import get_context_from_dict
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
from openedx.core.djangoapps.video_config import sharing
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
@@ -82,9 +82,10 @@ TRANSCRIPT_FILE_SJSON_DATA = """{\n "start": [10],\n "end": [100],\n "text
class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-class-docstring, test-inherits-tests
METADATA = {}
def test_video_constructor(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_video_constructor(self, mock_render_django_template):
"""Make sure that all parameters extracted correctly from xml"""
context = self.block.student_view(None).content
self.block.student_view(None)
sources = ['example.mp4', 'example.webm']
expected_context = {
@@ -145,9 +146,11 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas
'video_id': '',
}
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
@@ -168,11 +171,12 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
}
METADATA = {}
def test_video_constructor(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_video_constructor(self, mock_render_django_template):
"""Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams.
"""
context = self.block.student_view(None).content
self.block.student_view(None)
sources = ['example.mp4', 'example.webm']
expected_context = {
@@ -233,13 +237,17 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
'video_id': '',
}
mako_service = self.block.runtime.service(self.block, 'mako')
expected_result = get_context_dict_from_string(
mako_service.render_lms_template('video.html', expected_context)
)
assert get_context_dict_from_string(context) == expected_result
assert expected_result['download_video_link'] == 'example.mp4'
assert expected_result['display_name'] == 'A Name'
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Validate and compare contexts
validated_actual = get_context_from_dict(actual_context)
validated_expected = get_context_from_dict(expected_context)
assert validated_actual == validated_expected
# Verify specific fields
assert validated_actual['download_video_link'] == 'example.mp4'
assert validated_actual['display_name'] == 'A Name'
@ddt.ddt
@@ -323,15 +331,19 @@ class TestVideoPublicAccess(BaseTestVideoXBlock):
self.assertEqual(self.block.public_access, is_public_sharing_enabled)
@ddt.data(False, True)
def test_context(self, is_public_sharing_enabled):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_context(self, is_public_sharing_enabled, mock_render_django_template):
with self.mock_feature_toggle():
with patch.object(
sharing,
'is_public_sharing_enabled',
return_value=is_public_sharing_enabled
):
content = self.block.student_view(None).content
context = get_context_dict_from_string(content)
self.block.student_view(None)
# Get the actual context that was passed to render_django_template
context = mock_render_django_template.call_args.args[1]
assert ('public_sharing_enabled' in context) == is_public_sharing_enabled
assert ('public_video_url' in context) == is_public_sharing_enabled
@@ -391,7 +403,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
self.block, handler, suffix
).rstrip('/?')
def test_get_html_track(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_track(self, mock_render_django_template):
# pylint: disable=invalid-name
# lint-amnesty, pylint: disable=redefined-outer-name
SOURCE_XML = """
@@ -493,7 +506,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
self.initialize_block(data=DATA)
track_url = self.get_handler_url('transcript', 'download')
context = self.block.student_view(None).content
self.block.student_view(None)
metadata.update({
'transcriptLanguages': {"en": "English"} if not data['transcripts'] else {"uk": 'Українська'},
'transcriptLanguage': 'en' if not data['transcripts'] or data.get('sub') else 'uk',
@@ -516,11 +529,14 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'metadata': json.dumps(metadata)
})
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
def test_get_html_source(self):
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_source(self, mock_render_django_template):
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
@@ -619,7 +635,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
name=data['name'],
)
self.initialize_block(data=DATA)
context = self.block.student_view(None).content
self.block.student_view(None)
expected_context = dict(initial_context)
expected_context['metadata'].update({
@@ -638,11 +654,14 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'metadata': json.dumps(expected_context['metadata'])
})
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
def test_get_html_with_non_existent_edx_video_id(self):
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_with_non_existent_edx_video_id(self, mock_render_django_template):
"""
Tests the VideoBlock get_html where a edx_video_id is given but a video is not found
"""
@@ -684,9 +703,18 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
# Referencing a non-existent VAL ID in courseware won't cause an error --
# it'll just fall back to the values in the VideoBlock.
assert 'example.mp4' in self.block.student_view(None).content
self.block.student_view(None)
def test_get_html_with_mocked_edx_video_id(self):
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Verify it falls back to the sources defined in the VideoBlock XML
assert actual_context['download_video_link'] == 'example.mp4'
metadata_dict = json.loads(actual_context['metadata'])
assert sorted(metadata_dict['sources']) == sorted(['example.mp4', 'example.webm'])
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_with_mocked_edx_video_id(self, mock_render_django_template):
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
@@ -771,93 +799,28 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
}
]
}
context = self.block.student_view(None).content
self.block.student_view(None)
expected_context = dict(initial_context)
expected_context['metadata'].update({
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
'saveStateUrl': self.block.ajax_url + '/save_user_state',
'sources': data['result']['sources'],
})
expected_context.update({
'id': self.block.location.html_id(),
'block_id': str(self.block.location),
'course_id': str(self.block.location.course_key),
'download_video_link': data['result']['download_video_link'],
'metadata': json.dumps(expected_context['metadata'])
})
expected_context = dict(initial_context)
expected_context['metadata'].update({
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'),
'publishCompletionUrl': self.get_handler_url('publish_completion', ''),
'saveStateUrl': self.block.ajax_url + '/save_user_state',
'sources': data['result']['sources'],
})
expected_context.update({
'id': self.block.location.html_id(),
'block_id': str(self.block.location),
'course_id': str(self.block.location.course_key),
'download_video_link': data['result']['download_video_link'],
'metadata': json.dumps(expected_context['metadata'])
})
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
def test_get_html_with_existing_edx_video_id(self):
"""
Tests the `VideoBlock` `get_html` where `edx_video_id` is given and related video is found
"""
edx_video_id = 'thundercats'
# create video with provided edx_video_id and return encoded_videos
encoded_videos = self.encode_and_create_video(edx_video_id)
# data to be used to retrieve video by edxval API
data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'edx_video_id': edx_video_id,
'result': {
'download_video_link': f'http://fake-video.edx.org/{edx_video_id}.mp4',
'is_video_from_same_origin': True,
'sources': ['http://fake-video.edx.org/example.mp4', 'http://fake-video.edx.org/example.webm'] +
[video['url'] for video in encoded_videos],
},
}
with override_settings(VIDEO_CDN_URL={'default': 'http://fake-video.edx.org'}):
# context returned by get_html when provided with above data
# expected_context, a dict to assert with context
context, expected_context = self.helper_get_html_with_edx_video_id(data)
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
def test_get_html_with_existing_unstripped_edx_video_id(self):
"""
Tests the `VideoBlock` `get_html` where `edx_video_id` with some unwanted tab(\t)
is given and related video is found
"""
edx_video_id = 'thundercats'
# create video with provided edx_video_id and return encoded_videos
encoded_videos = self.encode_and_create_video(edx_video_id)
# data to be used to retrieve video by edxval API
# unstripped edx_video_id is provided here
data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'edx_video_id': f"{edx_video_id}\t",
'result': {
'download_video_link': f'http://fake-video.edx.org/{edx_video_id}.mp4',
'is_video_from_same_origin': True,
'sources': ['http://fake-video.edx.org/example.mp4', 'http://fake-video.edx.org/example.webm'] +
[video['url'] for video in encoded_videos],
},
}
with override_settings(VIDEO_CDN_URL={'default': 'http://fake-video.edx.org'}):
# context returned by get_html when provided with above data
# expected_context, a dict to assert with context
context, expected_context = self.helper_get_html_with_edx_video_id(data)
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
def encode_and_create_video(self, edx_video_id):
"""
@@ -966,8 +929,9 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
return context, expected_context
# 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')
def test_get_html_cdn_source(self, mocked_get_video):
def test_get_html_cdn_source(self, mocked_get_video, mock_render_django_template):
"""
Test if sources got from CDN
"""
@@ -1056,7 +1020,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
user_service = self.block.runtime.service(self.block, 'user')
user_location = user_service.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE]
assert user_location == 'CN'
context = self.block.student_view(None).content
self.block.student_view(None)
expected_context = dict(initial_context)
expected_context['metadata'].update({
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
@@ -1073,12 +1037,15 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'metadata': json.dumps(expected_context['metadata'])
})
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
# pylint: disable=invalid-name
def test_get_html_cdn_source_external_video(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_cdn_source_external_video(self, mock_render_django_template):
"""
Test that video from an external source loads successfully.
@@ -1164,7 +1131,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'client_video_id': 'external video',
'encoded_videos': {}
}
context = self.block.student_view(None).content
self.block.student_view(None)
expected_context = dict(initial_context)
expected_context['metadata'].update({
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
@@ -1181,16 +1148,19 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'metadata': json.dumps(expected_context['metadata'])
})
mako_service = self.block.runtime.service(self.block, 'mako')
assert get_context_dict_from_string(context) ==\
get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context))
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Validate and compare contexts (handles user_id and metadata ordering)
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
@ddt.data(
(True, ['youtube', 'desktop_webm', 'desktop_mp4', 'hls']),
(False, ['youtube', 'desktop_webm', 'desktop_mp4'])
)
@ddt.unpack
def test_get_html_on_toggling_hls_feature(self, hls_feature_enabled, expected_val_profiles):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_on_toggling_hls_feature(self, hls_feature_enabled, expected_val_profiles, _):
"""
Verify val profiles on toggling HLS Playback feature.
"""
@@ -1213,12 +1183,13 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
expected_val_profiles,
)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch(
'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled',
Mock(return_value=True)
)
@patch('xmodule.video_block.video_block.edxval_api.get_urls_for_profiles')
def test_get_html_hls(self, get_urls_for_profiles):
def test_get_html_hls(self, get_urls_for_profiles, mock_render_django_template):
"""
Verify that hls profile functionality works as expected.
@@ -1235,14 +1206,23 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
}
self.initialize_block(data=video_xml)
context = self.block.student_view(None).content
self.block.student_view(None)
assert "'download_video_link': 'https://mp4.com/dm.mp4'" in context
assert '"streams": "1.00:https://yt.com/?v=v0TFmdO4ZP0"' in context
assert sorted(['https://webm.com/dw.webm', 'https://mp4.com/dm.mp4', 'https://hls.com/hls.m3u8']) ==\
sorted(get_context_dict_from_string(context)['metadata']['sources'])
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
def test_get_html_hls_no_video_id(self):
metadata_dict = json.loads(actual_context['metadata'])
assert actual_context['download_video_link'] == 'https://mp4.com/dm.mp4'
assert metadata_dict['streams'] == '1.00:https://yt.com/?v=v0TFmdO4ZP0'
assert sorted(metadata_dict['sources']) == sorted([
'https://webm.com/dw.webm',
'https://mp4.com/dm.mp4',
'https://hls.com/hls.m3u8',
])
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_hls_no_video_id(self, mock_render_django_template):
"""
Verify that `download_video_link` is set to None for HLS videos if no video id
"""
@@ -1253,10 +1233,15 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
"""
self.initialize_block(data=video_xml)
context = self.block.student_view(None).content
assert "'download_video_link': None" in context
self.block.student_view(None)
def test_get_html_non_hls_video_download(self):
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
assert actual_context['download_video_link'] is None
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_get_html_non_hls_video_download(self, _):
"""
Verify that `download_video_link` is available if a non HLS videos is available
"""
@@ -1272,7 +1257,8 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
context = self.block.student_view(None).content
assert "'download_video_link': 'http://example.com/example.mp4'" in context
def test_html_student_public_view(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_html_student_public_view(self, _):
"""
Test the student and public views
"""
@@ -1288,8 +1274,9 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
context = self.block.render(PUBLIC_VIEW).content
assert '"saveStateEnabled": false' in context
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch('xmodule.video_block.video_block.edxval_api.get_course_video_image_url')
def test_poster_image(self, get_course_video_image_url):
def test_poster_image(self, get_course_video_image_url, _):
"""
Verify that poster image functionality works as expected.
"""
@@ -1301,8 +1288,9 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
assert '"poster": "/media/video-images/poster.png"' in context
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch('xmodule.video_block.video_block.edxval_api.get_course_video_image_url')
def test_poster_image_without_edx_video_id(self, get_course_video_image_url):
def test_poster_image_without_edx_video_id(self, get_course_video_image_url, _):
"""
Verify that poster image is set to None and there is no crash when no edx_video_id.
"""
@@ -1314,11 +1302,12 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
assert "'poster': 'null'" in context
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch(
'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled',
Mock(return_value=False)
)
def test_hls_primary_playback_on_toggling_hls_feature(self):
def test_hls_primary_playback_on_toggling_hls_feature(self, _):
"""
Verify that `prioritize_hls` is set to `False` if `HLSPlaybackEnabledFlag` is disabled.
"""
@@ -1364,11 +1353,12 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
'result': 'false'
},
)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch(
'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled',
Mock(return_value=True)
)
def test_deprecate_youtube_course_waffle_flag(self, data):
def test_deprecate_youtube_course_waffle_flag(self, data, mock_render_django_template):
"""
Tests various combinations of a `prioritize_hls` flag being set in waffle and overridden for a course.
"""
@@ -1381,8 +1371,10 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']):
with override_waffle_flag(DEPRECATE_YOUTUBE, active=data['waffle_enabled']):
self.initialize_block(data=video_xml, metadata=metadata)
context = self.block.student_view(None).content
assert '"prioritizeHls": {}'.format(data['result']) in context
self.block.student_view(None)
context = mock_render_django_template.call_args.args[1]
metadata_dict = json.loads(context['metadata'])
assert metadata_dict['prioritizeHls'] == (data['result'] == 'true')
@ddt.ddt
@@ -2353,10 +2345,13 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
with override_settings(FEATURES=self.FEATURES):
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('edxval.api.get_urls_for_profiles')
def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled):
def test_bumper_metadata(
self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled, mock_render_django_template
):
"""
Test content with rendered bumper metadata.
"""
@@ -2372,7 +2367,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
is_bumper_enabled.return_value = True
content = self.block.student_view(None).content
self.block.student_view(None)
sources = ['example.mp4', 'example.webm']
expected_context = {
@@ -2452,9 +2447,11 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
'video_id': '',
}
mako_service = self.block.runtime.service(self.block, 'mako')
expected_content = mako_service.render_lms_template('video.html', expected_context)
assert get_context_dict_from_string(content) == get_context_dict_from_string(expected_content)
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
@ddt.ddt
@@ -2537,7 +2534,9 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
}
return context
def assert_content_matches_expectations(self, autoadvanceenabled_must_be, autoadvance_must_be):
def assert_content_matches_expectations(
self, autoadvanceenabled_must_be, autoadvance_must_be, mock_render_django_template
):
"""
Check (assert) that loading video.html produces content that corresponds
to the passed context.
@@ -2545,18 +2544,18 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
"""
with override_settings(FEATURES=self.FEATURES):
content = self.block.student_view(None).content
self.block.student_view(None)
expected_context = self.prepare_expected_context(
autoadvanceenabled_flag=autoadvanceenabled_must_be,
autoadvance_flag=autoadvance_must_be,
)
mako_service = self.block.runtime.service(self.block, 'mako')
with override_settings(FEATURES=self.FEATURES):
expected_content = mako_service.render_lms_template('video.html', expected_context)
# Get the actual context that was passed to render_django_template
actual_context = mock_render_django_template.call_args.args[1]
assert get_context_dict_from_string(content) == get_context_dict_from_string(expected_content)
# Validate and compare contexts
assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context)
def change_course_setting_autoadvance(self, new_value):
"""
@@ -2578,7 +2577,8 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
(True, True),
)
@ddt.unpack
def test_is_autoadvance_available_and_enabled(self, global_setting, course_setting):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_is_autoadvance_available_and_enabled(self, global_setting, course_setting, mock_render_django_template):
"""
Check that the autoadvance is not available when it is disabled via feature flag
(ENABLE_AUTOADVANCE_VIDEOS set to False) or by the course setting.
@@ -2590,7 +2590,9 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
"""
self.FEATURES.update({"ENABLE_AUTOADVANCE_VIDEOS": global_setting})
self.change_course_setting_autoadvance(new_value=course_setting)
self.assert_content_matches_expectations(
autoadvanceenabled_must_be=(global_setting and course_setting),
autoadvance_must_be=(global_setting and course_setting),
mock_render_django_template=mock_render_django_template,
)

View File

@@ -1,45 +1,43 @@
<%page expression_filter="h"/>
{% load i18n %}
<%!
from django.utils.translation import gettext as _
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
%>
% if display_name is not UNDEFINED and display_name is not None and not is_embed:
<h3 class="hd hd-2">${display_name}</h3>
% endif
{% if display_name is not None and not is_embed %}
<h3 class="hd hd-2">{{ display_name|escape }}</h3>
{% endif %}
<div
id="video_${id}"
id="video_{{ id|escape }}"
class="video closed"
data-metadata='${metadata}'
data-bumper-metadata='${bumper_metadata}'
data-autoadvance-enabled="${autoadvance_enabled}"
data-poster='${poster}'
data-block-id='${block_id}'
data-course-id='${course_id}'
data-metadata="{{ metadata|escape }}"
data-bumper-metadata='{{ bumper_metadata|escape }}'
data-autoadvance-enabled="{{ autoadvance_enabled|escape }}"
data-poster='{{ poster|escape }}'
data-block-id='{{ block_id|escape }}'
data-course-id='{{ course_id|escape }}'
tabindex="-1"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<div class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<span tabindex="0" class="spinner" aria-hidden="false"
aria-label="{% trans 'Loading video player' as tmsg %}{{tmsg|force_escape}}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true"
aria-label="{% trans 'Play video' as tmsg %}{{tmsg|force_escape}}"></span>
<div class="video-player-pre"></div>
<div class="video-player">
<div id="${id}"></div>
<h4 class="hd hd-4 video-error is-hidden">${_('No playable video sources found.')}</h4>
<div id="{{ id|escape }}"></div>
<h4 class="hd hd-4 video-error is-hidden">{% trans 'No playable video sources found.' as tmsg %}{{tmsg|force_escape}}</h4>
<h4 class="hd hd-4 video-hls-error is-hidden">
${_('Your browser does not support this video format. Try using a different browser.')}
{% trans 'Your browser does not support this video format. Try using a different browser.' as tmsg %}{{tmsg|force_escape}}
</h4>
</div>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<div class="video-controls is-hidden">
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="vcr">
<div class="vidtime">0:00 / 0:00</div>
</div>
<div class="secondary-controls"></div>
</div>
</div>
@@ -49,42 +47,44 @@ from openedx.core.djangolib.js_utils import (
<div class="focus_grabber last"></div>
<div class="wrapper-video-bottom-section">
% if ((download_video_link or track or handout or branding_info or public_sharing_enabled) and not hide_downloads):
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
% if download_video_link or public_sharing_enabled:
{% if download_video_link or track or handout or public_sharing_enabled and not hide_downloads %}
<h3 class="hd hd-4 downloads-heading sr"
id="video-download-transcripts_{{ id|escape }}">{% trans 'Downloads and transcripts' as tmsg %}{{tmsg|force_escape}}</h3>
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_{{ id|escape }}">
{% if download_video_link or public_sharing_enabled %}
<div class="wrapper-download-video">
<h4 class="hd hd-5">${_('Video')}</h4>
% if download_video_link:
<span class="icon fa fa-download" aria-hidden="true"></span>
<a class="btn-link video-sources video-download-button" href="${download_video_link}" target="${'_blank' if not is_video_from_same_origin else '_self'}">
${_('Download video file')}
</a>
% endif
% if download_video_link and public_sharing_enabled:
<br>
% endif
% if sharing_sites_info:
<div class="wrapper-social-share">
<button class="social-toggle-btn btn">
<h4 class="hd hd-5">{% trans 'Video' as tmsg %}{{tmsg|force_escape}}</h4>
{% if download_video_link %}
<span class="icon fa fa-download" aria-hidden="true"></span>
<a class="btn-link video-sources video-download-button"
href="{{ download_video_link|escape }}"
target="{% if not is_video_from_same_origin %}_blank{% else %}_self{% endif %}">
{% trans "Download video file" as tmsg %}{{ tmsg|force_escape }}
</a>
{% endif %}
{% if download_video_link and public_sharing_enabled %}
<br>{% endif %}
{% if sharing_sites_info %}
<div class="wrapper-social-share">
<button class="social-toggle-btn btn">
<span class="icon fa fa-share-alt"></span>
${_('Share this video')}
{% trans 'Share this video' as tmsg %}{{tmsg|force_escape}}
</button>
<div hidden class="container-social-share">
${_('Share this video')}
{% trans "Share this video" as tmsg %}{{tmsg|force_escape}}
<div class="btn-link close-btn">
<span class="icon fa fa-close"></span>
</div>
% for sharing_site_info in sharing_sites_info:
{% for sharing_site_info in sharing_sites_info %}
<a
class="social-share-link"
data-source="${sharing_site_info['name']}"
href="${sharing_site_info['sharing_url']}"
target="_blank"
rel="noopener noreferrer"
data-source="{{ sharing_site_info.name|escape }}"
href="{{ sharing_site_info.sharing_url|escape }}"
target="_blank"
rel="noopener noreferrer"
>
% if (sharing_site_info['name'] == 'twitter'):
{% if sharing_site_info.name == "twitter" %}
<!--
Twitter now uses the X brand icon, but Font Awesome does not include fa-x-twitter until 6.0
Upgrading to 6.0 would require lots of leg work becuase all square icons have new name patterns
@@ -95,56 +95,63 @@ from openedx.core.djangolib.js_utils import (
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm297.1 84L257.3 234.6 379.4 396H283.8L209 298.1 123.3 396H75.8l111-126.9L69.7 116h98l67.7 89.5L313.6 116h47.5zM323.3 367.6L153.4 142.9H125.1L296.9 367.6h26.3z"/>
</svg>
</span>
{% else %}
<span class="icon fa {{ sharing_site_info.fa_icon_name|escape }}"
aria-hidden="true"></span>
{% endif %}
<span class="sr">
{% filter force_escape %}{% blocktrans with site=sharing_site_info.name %}Share on {{ site }}{% endblocktrans %}{% endfilter %}
</span>
% else:
<span class="icon fa ${sharing_site_info['fa_icon_name']}" aria-hidden="true"></span>
% endif
<span class="sr">${_("Share on {site}").format(site=sharing_site_info['name'])}</span>
</a>
% endfor
<div class="public-video-url-container">
<a href=${public_video_url} class="public-video-url-link">
${public_video_url}
</a>
<div class="public-video-copy-btn" data-url=${public_video_url}>
</a>
{% endfor %}
<div class="public-video-url-container">
<a href="{{ public_video_url|escape }}" class="public-video-url-link">
{{ public_video_url|escape }}
</a>
<div
class="public-video-copy-btn" data-url="{{ public_video_url|escape }}">
<span class="icon fa fa-link"></span>
<span>${_('Copy')}</span>
<span>{% trans "Copy" as tmsg %}{{tmsg|force_escape}}</span>
</div>
</div>
</div>
</div>
% endif
{% endif %}
</div>
% endif
% if track:
{% endif %}
{% if track %}
<div class="wrapper-download-transcripts">
<h4 class="hd hd-5">${_('Transcripts')}</h4>
% if transcript_download_format:
<ul class="list-download-transcripts">
% for item in transcript_download_formats_list:
<li class="transcript-option">
<h4 class="hd hd-5">{% trans 'Transcripts' as tmsg %}{{tmsg|force_escape}}</h4>
{% if transcript_download_format %}
<ul class="list-download-transcripts">
{% for item in transcript_download_formats_list %}
<li class="transcript-option">
<span class="icon fa fa-download" aria-hidden="true"></span>
<% dname = _("Download {file}").format(file=item['display_name']) %>
<a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
</li>
% endfor
</ul>
% else:
<a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
% endif
<a class="btn btn-link" href="{{ track|escape }}"
data-value="{{ item.value|escape }}">
{% filter force_escape %}{% blocktrans with file=item.display_name %}Download {{ file }}{% endblocktrans %}{% endfilter %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<a class="btn-link external-track" href="{{ track|escape }}">{% trans 'Download transcript' as tmsg %}{{tmsg|force_escape}}</a>
{% endif %}
</div>
% endif
% if handout:
{% endif %}
{% if handout %}
<div class="wrapper-handouts">
<h4 class="hd hd-5">${_('Handouts')}</h4>
<a class="btn-link" href="${handout}">${_('Download Handout')}</a>
<h4 class="hd hd-5">{% trans 'Handouts' as tmsg %}{{tmsg|force_escape}}</h4>
<a class="btn-link" href="{{ handout|escape }}">{% trans 'Download Handout' as tmsg %}{{tmsg|force_escape}}</a>
</div>
{% endif %}
</div>
% endif
</div>
% endif
% if transcript_feedback_enabled and video_id:
<div class="wrapper-transcript-feedback" data-video-id='${video_id}' data-user-id='${user_id}'>
<h4 class="hd hd-5">${_('How is the transcript quality ?')}</h4>
{% endif %}
{% if transcript_feedback_enabled and video_id %}
<div class="wrapper-transcript-feedback" data-video-id="{{ video_id|escape }}" data-user-id="{{ user_id|escape }}">
<h4 class="hd hd-5">{% trans 'How is the transcript quality?' as tmsg %}{{tmsg|force_escape}}</h4>
<div class="transcript-feedback-buttons">
<div class="transcript-feedback-btn-wrapper">
<button class="thumbs-up-btn">
@@ -158,7 +165,7 @@ from openedx.core.djangolib.js_utils import (
</div>
</div>
</div>
% endif
{% endif %}
<div class="google-disclaimer">
<a href="https://translate.google.com/" target="_blank">
<img
@@ -172,7 +179,9 @@ from openedx.core.djangolib.js_utils import (
</div>
</div>
% if cdn_eval:
{% if cdn_eval %}
{{ cdn_exp_group|json_script:"cdnExpGroup" }}
<script>
//TODO: refactor this js into a separate file.
function sendPerformanceBeacon(id, expgroup, value, event_name) {
@@ -181,16 +190,18 @@ from openedx.core.djangolib.js_utils import (
}
var cdnStartTime;
var salt = Math.floor((1 + Math.random()) * 0x100000).toString(36);
var id = "${id | n, js_escaped_string}";
var id = "{{ id|escapejs }}";
function initializeCDNExperiment() {
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, "", "load");
var cdnData = JSON.parse(document.getElementById('cdnExpGroup').textContent);
sendPerformanceBeacon(id + "_" + salt, cdnData, "", "load");
cdnStartTime = Date.now();
$.each(['loadstart', 'abort', 'error', 'stalled', 'loadedmetadata',
'loadeddata', 'canplay', 'canplaythrough', 'seeked'],
function(index, eventName) {
$("#video_" + id).bind("html5:" + eventName, null, function() {
timeElapsed = Date.now() - cdnStartTime;
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, timeElapsed, eventName);
'loadeddata', 'canplay', 'canplaythrough', 'seeked'], function(index, eventName) {
$("#video_" + id).bind("html5:" + eventName, null, function () {
var timeElapsed = Date.now() - cdnStartTime;
sendPerformanceBeacon(id + "_" + salt, cdnData, timeElapsed, eventName);
});
});
}
@@ -199,4 +210,4 @@ from openedx.core.djangolib.js_utils import (
initializeCDNExperiment();
}
</script>
% endif
{% endif %}

View File

@@ -30,6 +30,7 @@ from xblock.core import XBlock
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData
from xblocks_contrib.video import VideoBlock as _ExtractedVideoBlock
from xblock.utils.resources import ResourceLoader
from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID
from openedx.core.lib.cache_utils import request_cached
@@ -37,7 +38,6 @@ from openedx.core.lib.license import LicenseMixin
from xmodule.contentstore.content import StaticContent
from xmodule.editing_block import EditingMixin
from xmodule.exceptions import NotFoundError
from xmodule.mako_block import MakoTemplateBlockBase
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, own_metadata
from xmodule.raw_block import EmptyDataRawMixin
from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment
@@ -95,6 +95,7 @@ except ImportError:
edxval_api = None
log = logging.getLogger(__name__)
loader = ResourceLoader("lms")
# Make '_' a no-op so we can scrape strings. Using lambda instead of
# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file
@@ -497,7 +498,7 @@ class _BuiltInVideoBlock(
if video_config_service:
template_context.update(video_config_service.get_public_sharing_context(self, self.course_id))
return self.runtime.service(self, 'mako').render_lms_template('video.html', template_context)
return loader.render_django_template("templates/video.html", template_context)
def is_transcript_feedback_enabled(self):
"""
@@ -848,7 +849,9 @@ class _BuiltInVideoBlock(
"""
Extend context by data for transcript basic tab.
"""
_context = MakoTemplateBlockBase.get_context(self)
_context = {
'editable_metadata_fields': self.editable_metadata_fields
}
_context.update({
'tabs': self.tabs,
'html_id': self.location.html_id(), # element_id