From e0073f313e9342b1687d371d78d3a4d7cd026530 Mon Sep 17 00:00:00 2001 From: Irfan Ahmad Date: Thu, 11 Jun 2026 17:26:37 +0500 Subject: [PATCH 1/7] feat: remove built-in Video XBlock implementation (slash-n-burn #37819) Removes xmodule/video_block/, CSS assets, the USE_EXTRACTED_VIDEO_BLOCK toggle, and both 'video'/'videoalpha' entrypoints in pyproject.toml now that the extracted version in xblocks-contrib is the only implementation. Inlines rewrite_video_url into video_urls.py (sole production caller). Updates all test file imports and mock.patch paths from xmodule.video_block to xblocks_contrib.video. Deletes test_video_handlers.py and test_video_mongo.py which patch edxval_api internal paths that no longer exist in xmodule. Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/tests/test_contentstore.py | 2 +- .../views/tests/test_course_index.py | 4 +- .../views/tests/test_transcripts.py | 2 +- .../course_api/blocks/tests/test_api.py | 2 +- .../tests/test_video_stream_priority.py | 6 +- .../transformers/tests/test_video_urls.py | 6 +- .../blocks/transformers/video_urls.py | 32 +- .../courseware/tests/test_block_render.py | 2 +- .../courseware/tests/test_video_handlers.py | 1355 --------- .../courseware/tests/test_video_mongo.py | 2605 ----------------- .../courseware/tests/test_video_xml.py | 2 +- lms/djangoapps/support/tests/test_tasks.py | 2 +- .../video_config/tests/test_services.py | 2 +- openedx/envs/common.py | 8 - pyproject.toml | 2 - webpack.builtinblocks.config.js | 7 - xmodule/modulestore/tests/test_api.py | 2 +- .../test_cross_modulestore_import_export.py | 2 +- .../css-builtin-blocks/VideoBlockDisplay.css | 1258 -------- .../css-builtin-blocks/VideoBlockEditor.css | 165 -- xmodule/tests/test_export.py | 2 +- xmodule/tests/test_video.py | 1169 -------- xmodule/video_block/__init__.py | 8 - xmodule/video_block/video_block.py | 1210 -------- xmodule/video_block/video_utils.py | 123 - xmodule/video_block/video_xfields.py | 220 -- 26 files changed, 49 insertions(+), 8149 deletions(-) delete mode 100644 lms/djangoapps/courseware/tests/test_video_handlers.py delete mode 100644 lms/djangoapps/courseware/tests/test_video_mongo.py delete mode 100644 xmodule/static/css-builtin-blocks/VideoBlockDisplay.css delete mode 100644 xmodule/static/css-builtin-blocks/VideoBlockEditor.css delete mode 100644 xmodule/tests/test_video.py delete mode 100644 xmodule/video_block/__init__.py delete mode 100644 xmodule/video_block/video_block.py delete mode 100644 xmodule/video_block/video_utils.py delete mode 100644 xmodule/video_block/video_xfields.py diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 4561926c1b8d..cefe8c07a0d7 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -58,7 +58,7 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint from xmodule.seq_block import SequenceBlock -from xmodule.video_block import VideoBlock +from xblocks_contrib.video import VideoBlock TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex # noqa: UP031 diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 8c33ad3f891f..6b6d89e03918 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -355,7 +355,7 @@ def test_reindex_json_responses(self): course_id=str(self.course.id)) self.assertEqual(response['total'], 1) # noqa: PT009 - @mock.patch('xmodule.video_block.VideoBlock.index_dictionary') + @mock.patch('xblocks_contrib.video.VideoBlock.index_dictionary') def test_reindex_video_error_json_responses(self, mock_index_dictionary): """ Test json response with mocked error data for video @@ -465,7 +465,7 @@ def test_indexing_responses(self): course_id=str(self.course.id)) self.assertEqual(response['total'], 1) # noqa: PT009 - @mock.patch('xmodule.video_block.VideoBlock.index_dictionary') + @mock.patch('xblocks_contrib.video.VideoBlock.index_dictionary') def test_indexing_video_error_responses(self, mock_index_dictionary): """ Test do_course_reindex response with mocked error data for video diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index 8a288c6c8fbb..b8eb2b8219c3 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -32,7 +32,7 @@ from xmodule.contentstore.django import contentstore # pylint: disable=wrong-import-order from xmodule.exceptions import NotFoundError # pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # pylint: disable=wrong-import-order -from xmodule.video_block import VideoBlock # pylint: disable=wrong-import-order +from xblocks_contrib.video import VideoBlock # pylint: disable=wrong-import-order TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex # noqa: UP031 diff --git a/lms/djangoapps/course_api/blocks/tests/test_api.py b/lms/djangoapps/course_api/blocks/tests/test_api.py index 6b6c73e90bd1..c45375bdbc6a 100644 --- a/lms/djangoapps/course_api/blocks/tests/test_api.py +++ b/lms/djangoapps/course_api/blocks/tests/test_api.py @@ -149,7 +149,7 @@ def setUp(self): self.request = RequestFactory().get("/dummy") self.request.user = self.user - @patch('xmodule.video_block.VideoBlock.student_view_data') + @patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_video_urls_rewrite(self, video_data_patch): """ Verify the video blocks returned have their URL re-written for diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_video_stream_priority.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_video_stream_priority.py index 5573faa95616..c34dd595ac65 100644 --- a/lms/djangoapps/course_api/blocks/transformers/tests/test_video_stream_priority.py +++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_video_stream_priority.py @@ -68,7 +68,7 @@ def collect_and_transform(self): @mock.patch('lms.djangoapps.course_blocks.usage_info.CourseUsageInfo') @mock.patch('openedx.core.djangoapps.waffle_utils.CourseWaffleFlag.is_enabled') - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_write_for_deprecated_youtube_flag_on(self, mock_video_data, deprecate_youtube_flag, usage_info): """ Test that video stream priority is written correctly with @@ -129,7 +129,7 @@ def test_write_for_deprecated_youtube_flag_on(self, mock_video_data, deprecate_y @mock.patch('lms.djangoapps.course_blocks.usage_info.CourseUsageInfo') @mock.patch('openedx.core.djangoapps.waffle_utils.CourseWaffleFlag.is_enabled') - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_write_for_deprecated_youtube_flag_off(self, mock_video_data, deprecate_youtube_flag, usage_info): """ Test that video stream priority is written correctly with @@ -188,7 +188,7 @@ def test_write_for_deprecated_youtube_flag_off(self, mock_video_data, deprecate_ else: assert post_transform_data[video_format] == fetched_stream_priority - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_no_priority_for_web_only_videos(self, mock_video_data): """ Verify no write attempt is made for the videos diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_video_urls.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_video_urls.py index 486a3d6c36aa..cdf2b52b8480 100644 --- a/lms/djangoapps/course_api/blocks/transformers/tests/test_video_urls.py +++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_video_urls.py @@ -65,7 +65,7 @@ def collect_and_transform(self): block_structure=self.block_structure, ) - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_rewrite_for_encoded_videos(self, mock_video_data): """ Test that video URLs for videos with available encodings @@ -95,7 +95,7 @@ def test_rewrite_for_encoded_videos(self, mock_video_data): for video_format, video_url in post_transform_data.items(): assert pre_transform_data[video_format] != video_url - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_no_rewrite_for_third_party_vendor(self, mock_video_data): """ Test that video URLs aren't re-written for the videos @@ -125,7 +125,7 @@ def test_no_rewrite_for_third_party_vendor(self, mock_video_data): for video_format, video_url in post_transform_data.items(): assert pre_transform_data[video_format] == video_url - @mock.patch('xmodule.video_block.VideoBlock.student_view_data') + @mock.patch('xblocks_contrib.video.VideoBlock.student_view_data') def test_no_rewrite_for_web_only_videos(self, mock_video_data): """ Verify no rewrite attempt is made for the videos diff --git a/lms/djangoapps/course_api/blocks/transformers/video_urls.py b/lms/djangoapps/course_api/blocks/transformers/video_urls.py index adab00bbfb51..82f9fe795d02 100644 --- a/lms/djangoapps/course_api/blocks/transformers/video_urls.py +++ b/lms/djangoapps/course_api/blocks/transformers/video_urls.py @@ -2,14 +2,44 @@ Video block URL Transformer """ +import logging +from urllib.parse import urlparse from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer -from xmodule.video_block.video_utils import rewrite_video_url # pylint: disable=wrong-import-order from .student_view import StudentViewTransformer +log = logging.getLogger(__name__) + + +def rewrite_video_url(cdn_base_url, original_video_url): + """ + Returns a re-written video URL for cases when an alternate source + has been configured and is selected using factors like user location. + + :param cdn_base_url: The scheme, hostname, port and any relevant path prefix for the alternate CDN. + :param original_video_url: The canonical source for this video. + :return: The re-written URL, or None if the result is not a valid URL. + """ + if (not cdn_base_url) or (not original_video_url): + return None + + parsed = urlparse(original_video_url) + rewritten_url = cdn_base_url.rstrip("/") + "/" + parsed.path.lstrip("/") + validator = URLValidator() + + try: + validator(rewritten_url) + return rewritten_url + except ValidationError: + log.warning("Invalid CDN rewrite URL encountered, %s", rewritten_url) + + return None + class VideoBlockURLTransformer(BlockStructureTransformer): """ diff --git a/lms/djangoapps/courseware/tests/test_block_render.py b/lms/djangoapps/courseware/tests/test_block_render.py index 2744592610ec..606bcf65ba83 100644 --- a/lms/djangoapps/courseware/tests/test_block_render.py +++ b/lms/djangoapps/courseware/tests/test_block_render.py @@ -114,7 +114,7 @@ ) from xmodule.modulestore.tests.test_asides import AsideTestType # pylint: disable=wrong-import-order from xmodule.services import RebindUserServiceError -from xmodule.video_block import VideoBlock # pylint: disable=wrong-import-order +from xblocks_contrib.video import VideoBlock # pylint: disable=wrong-import-order from xmodule.x_module import STUDENT_VIEW, ModuleStoreRuntime # pylint: disable=wrong-import-order TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py deleted file mode 100644 index 9422bcc16354..000000000000 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ /dev/null @@ -1,1355 +0,0 @@ -"""Video xmodule tests in mongo.""" - - -import json -import os -import tempfile -import textwrap -from datetime import timedelta -from unittest.mock import MagicMock, Mock, patch - -import ddt -import freezegun -import pytest -from django.conf import settings -from django.core.files.base import ContentFile -from django.test import RequestFactory -from django.utils.timezone import now -from edxval import api -from webob import Request, Response -from xblock.django.request import DjangoWebobRequest - -from common.djangoapps.student.tests.factories import UserFactory -from common.test.utils import normalize_repr -from openedx.core.djangoapps.contentserver.caching import del_cached_content -from openedx.core.djangoapps.video_config.transcripts_utils import ( # pylint: disable=wrong-import-order - Transcript, - edxval_api, - get_transcript, - subs_filename, -) -from xmodule.contentstore.content import StaticContent # pylint: disable=wrong-import-order -from xmodule.contentstore.django import contentstore # pylint: disable=wrong-import-order -from xmodule.exceptions import NotFoundError # pylint: disable=wrong-import-order -from xmodule.modulestore import ModuleStoreEnum # pylint: disable=wrong-import-order -from xmodule.modulestore.django import modulestore # pylint: disable=wrong-import-order - -# noinspection PyUnresolvedReferences -from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import # noqa: F401 -from xmodule.video_block import VideoBlock # pylint: disable=wrong-import-order -from xmodule.x_module import STUDENT_VIEW - -from .helpers import BaseTestXmodule -from .test_video_xml import SOURCE_XML - -TEST_USERNAME = 'test-user' -TRANSCRIPT = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} -BUMPER_TRANSCRIPT = {"start": [1], "end": [10], "text": ["A bumper"]} -SRT_content = textwrap.dedent(""" - 0 - 00:00:00,12 --> 00:00:00,100 - Привіт, edX вітає вас. - """) -def _create_srt_file(content=None): - """ - Create srt file in filesystem. - """ - content = content or SRT_content - - srt_file = tempfile.NamedTemporaryFile(suffix=".srt") # pylint: disable=consider-using-with - srt_file.content_type = 'application/x-subrip; charset=utf-8' - srt_file.write(content.encode('utf-8')) - srt_file.seek(0) - return srt_file - - -def _check_asset(location, asset_name): - """ - Check that asset with asset_name exists in assets. - """ - content_location = StaticContent.compute_location( - location.course_key, asset_name - ) - try: - contentstore().find(content_location) - except NotFoundError: - return False - else: - return True - - -def _clear_assets(location): - """ - Clear all assets for location. - """ - store = contentstore() - - assets, __ = store.get_all_content_for_course(location.course_key) - for asset in assets: - asset_location = asset['asset_key'] - del_cached_content(asset_location) - store.delete(asset_location) - - -def _get_subs_id(filename): - basename = os.path.splitext(os.path.basename(filename))[0] - return basename.replace('subs_', '').replace('.srt', '') - - -def _create_file(content=''): - """ - Create temporary subs_somevalue.srt.sjson file. - """ - sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson") # pylint: disable=consider-using-with - sjson_file.content_type = 'application/json' - sjson_file.write(textwrap.dedent(content).encode('utf-8')) - sjson_file.seek(0) - return sjson_file - - -def _upload_sjson_file(subs_file, location, default_filename='subs_{}.srt.sjson'): - filename = default_filename.format(_get_subs_id(subs_file.name)) - _upload_file(subs_file, location, filename) - - -def _upload_file(subs_file, location, filename): # pylint: disable=missing-function-docstring - mime_type = subs_file.content_type - content_location = StaticContent.compute_location( - location.course_key, filename - ) - content = StaticContent(content_location, filename, mime_type, subs_file.read()) - contentstore().save(content) - del_cached_content(content.location) - - -@normalize_repr -def attach_sub(item, filename): - """ - Attach `en` transcript. - """ - item.sub = filename - - -@normalize_repr -def attach_bumper_transcript(item, filename, lang="en"): - """ - Attach bumper transcript. - """ - item.video_bumper["transcripts"][lang] = filename - - -@pytest.mark.usefixtures("override_descriptor_system") -class BaseTestVideoXBlock(BaseTestXmodule): - """Base class for VideoXBlock tests.""" - - CATEGORY = 'video' - - def initialize_block(self, data=None, **kwargs): - """ Initialize an XBlock to run tests on. """ - if data: - # VideoBlock data field is no longer used but to avoid needing to re-do - # a lot of tests code, parse and set the values as fields. - fields_data = VideoBlock.parse_video_xml(data) - kwargs.update(fields_data) - kwargs.pop('source', None) - kwargs.get('metadata', {}).pop('source', None) - super().initialize_module(**kwargs) - - def setUp(self): - super().setUp() - self.initialize_block(data=self.DATA, metadata=self.METADATA) - - -class TestVideo(BaseTestVideoXBlock): - """Integration tests: web client + mongo.""" - CATEGORY = "video" - DATA = SOURCE_XML - METADATA = {} - - def test_handle_ajax_wrong_dispatch(self): - responses = { - user.username: self.clients[user.username].post( - self.get_url('whatever'), - {}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - for user in self.users - } - - status_codes = {response.status_code for response in responses.values()} - assert status_codes.pop() == 404 - - def test_handle_ajax_for_speed_with_nan(self): - self.block.handle_ajax('save_user_state', {'speed': json.dumps(1.0)}) - assert self.block.speed == 1.0 - assert self.block.global_speed == 1.0 - - # try to set NaN value for speed. - response = self.block.handle_ajax( - 'save_user_state', {'speed': json.dumps(float('NaN'))} - ) - - assert not json.loads(response)['success'] - expected_error = "Invalid speed value nan, must be a float." - assert json.loads(response)['error'] == expected_error - - # verify that the speed and global speed are still 1.0 - assert self.block.speed == 1.0 - assert self.block.global_speed == 1.0 - - def test_handle_ajax(self): - - data = [ - {'speed': 2.0}, - {'saved_video_position': "00:00:10"}, - {'transcript_language': 'uk'}, - {'bumper_do_not_show_again': True}, - {'bumper_last_view_date': True}, - {'demoo�': 'sample'} - ] - for sample in data: - 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 - self.block.handle_ajax('save_user_state', {'speed': json.dumps(2.0)}) - assert self.block.speed == 2.0 - assert self.block.global_speed == 2.0 - - assert self.block.saved_video_position == timedelta(0) - self.block.handle_ajax('save_user_state', {'saved_video_position': "00:00:10"}) - assert self.block.saved_video_position == timedelta(0, 10) - - assert self.block.transcript_language == 'en' - self.block.handle_ajax('save_user_state', {'transcript_language': "uk"}) - assert self.block.transcript_language == 'uk' - - assert self.block.bumper_do_not_show_again is False - self.block.handle_ajax('save_user_state', {'bumper_do_not_show_again': True}) - assert self.block.bumper_do_not_show_again is True - - with freezegun.freeze_time(now()): - assert self.block.bumper_last_view_date is None - self.block.handle_ajax('save_user_state', {'bumper_last_view_date': True}) - assert self.block.bumper_last_view_date == now() - - response = self.block.handle_ajax('save_user_state', {'demoo�': "sample"}) - assert json.loads(response)['success'] is True - - def get_handler_url(self, handler, suffix): - """ - Return the URL for the specified handler on self.block. - """ - return self.block.runtime.handler_url( - self.block, handler, suffix - ).rstrip('/?') - - def tearDown(self): - _clear_assets(self.block.location) - super().tearDown() - - -@ddt.ddt -class TestTranscriptAvailableTranslationsDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test video handler that provide available translations info. - - Tests for `available_translations` dispatch. - """ - srt_file = _create_srt_file() - DATA = """ - - """.format(os.path.split(srt_file.name)[1]) # noqa: UP032 - - MODEL_DATA = { - 'data': DATA - } - - def setUp(self): - super().setUp() - self.block.render(STUDENT_VIEW) - self.subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} - - def test_available_translation_en(self): - good_sjson = _create_file(json.dumps(self.subs)) - _upload_sjson_file(good_sjson, self.block.location) - self.block.sub = _get_subs_id(good_sjson.name) - - request = Request.blank('/available_translations') - response = self.block.transcript(request=request, dispatch='available_translations') - assert json.loads(response.body.decode('utf-8')) == ['en'] - - def test_available_translation_non_en(self): - _upload_file(_create_srt_file(), self.block.location, os.path.split(self.srt_file.name)[1]) - - request = Request.blank('/available_translations') - response = self.block.transcript(request=request, dispatch='available_translations') - assert json.loads(response.body.decode('utf-8')) == ['uk'] - - @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_transcript_content') - def test_multiple_available_translations(self, mock_get_video_transcript_content): - mock_get_video_transcript_content.return_value = { - 'content': json.dumps({ - "start": [10], - "end": [100], - "text": ["Hi, welcome to Edx."], - }), - 'file_name': 'edx.sjson' - } - - good_sjson = _create_file(json.dumps(self.subs)) - - # Upload english transcript. - _upload_sjson_file(good_sjson, self.block.location) - - # Upload non-english transcript. - _upload_file(self.srt_file, self.block.location, os.path.split(self.srt_file.name)[1]) - - self.block.sub = _get_subs_id(good_sjson.name) - self.block.edx_video_id = 'an-edx-video-id' - - request = Request.blank('/available_translations') - response = self.block.transcript(request=request, dispatch='available_translations') - 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('edxval.api.get_available_transcript_languages') - @ddt.data( - ( - ['en', 'uk', 'ro'], - '', - {}, - ['en', 'uk', 'ro'] - ), - ( - ['uk', 'ro'], - True, - {}, - ['en', 'uk', 'ro'] - ), - ( - ['de', 'ro'], - True, - { - 'uk': True, - 'ro': False, - }, - ['en', 'uk', 'de', 'ro'] - ), - ( - ['de'], - True, - { - 'uk': True, - 'ro': False, - }, - ['en', 'uk', 'de', 'ro'] - ), - ) - @ddt.unpack - def test_val_available_translations( - self, - val_transcripts, - sub, - transcripts, - result, - mock_get_transcript_languages, - mock_get_video_transcript_content - ): - """ - Tests available translations with video component's and val's transcript languages - while the feature is enabled. - """ - for lang_code, in_content_store in dict(transcripts).items(): - if in_content_store: - file_name, __ = os.path.split(self.srt_file.name) - _upload_file(self.srt_file, self.block.location, file_name) - transcripts[lang_code] = file_name - else: - transcripts[lang_code] = 'non_existent.srt.sjson' - if sub: - sjson_transcript = _create_file(json.dumps(self.subs)) - _upload_sjson_file(sjson_transcript, self.block.location) - sub = _get_subs_id(sjson_transcript.name) - - mock_get_video_transcript_content.return_value = { - 'content': json.dumps({ - "start": [10], - "end": [100], - "text": ["Hi, welcome to Edx."], - }), - 'file_name': 'edx.sjson' - } - mock_get_transcript_languages.return_value = val_transcripts - self.block.transcripts = transcripts - self.block.sub = sub - self.block.edx_video_id = 'an-edx-video-id' - # Make request to available translations dispatch. - request = Request.blank('/available_translations') - response = self.block.transcript(request=request, dispatch='available_translations') - self.assertCountEqual(json.loads(response.body.decode('utf-8')), result) # noqa: PT009 - - @patch('openedx.core.djangoapps.video_config.transcripts_utils.edxval_api.get_available_transcript_languages') - def test_val_available_translations_feature_disabled(self, mock_get_available_transcript_languages): - """ - Tests available translations with val transcript languages when feature is disabled. - """ - mock_get_available_transcript_languages.return_value = ['en', 'de', 'ro'] - request = Request.blank('/available_translations') - response = self.block.transcript(request=request, dispatch='available_translations') - assert response.status_code == 404 - - -@ddt.ddt -class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test video handler that provide available translations info. - - Tests for `available_translations_bumper` dispatch. - """ - srt_file = _create_srt_file() - DATA = """ - - """.format(os.path.split(srt_file.name)[1]) # noqa: UP032 - - MODEL_DATA = { - 'data': DATA - } - - def setUp(self): - super().setUp() - self.block.render(STUDENT_VIEW) - self.dispatch = "available_translations/?is_bumper=1" - self.block.video_bumper = {"transcripts": {"en": ""}} - - @ddt.data("en", "uk") - def test_available_translation_en_and_non_en(self, lang): - filename = os.path.split(self.srt_file.name)[1] - _upload_file(self.srt_file, self.block.location, filename) - self.block.video_bumper["transcripts"][lang] = filename - - request = Request.blank('/' + self.dispatch) - response = self.block.transcript(request=request, dispatch=self.dispatch) - assert json.loads(response.body.decode('utf-8')) == [lang] - - @patch('openedx.core.djangoapps.video_config.transcripts_utils.get_available_transcript_languages') - def test_multiple_available_translations(self, mock_get_transcript_languages): - """ - Verify that available translations dispatch works as expected for multiple - translations and returns both content store and edxval translations. - """ - # Assuming that edx-val has German translation available for this video component. - mock_get_transcript_languages.return_value = ['de'] - en_translation = _create_srt_file() - en_translation_filename = os.path.split(en_translation.name)[1] - uk_translation_filename = os.path.split(self.srt_file.name)[1] - # Upload english transcript. - _upload_file(en_translation, self.block.location, en_translation_filename) - - # Upload non-english transcript. - _upload_file(self.srt_file, self.block.location, uk_translation_filename) - - self.block.video_bumper["transcripts"]["en"] = en_translation_filename - self.block.video_bumper["transcripts"]["uk"] = uk_translation_filename - - request = Request.blank('/' + self.dispatch) - response = self.block.transcript(request=request, dispatch=self.dispatch) - # Assert that bumper only get its own translations. - assert sorted(json.loads(response.body.decode('utf-8'))) == sorted(['en', 'uk']) - - -@ddt.ddt -class TestTranscriptDownloadDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test video handler that provide translation transcripts. - - Tests for `download` dispatch. - """ - - DATA = """ - - """ - - MODEL_DATA = { - 'data': DATA - } - - def setUp(self): - super().setUp() - self.block.render(STUDENT_VIEW) - - def test_download_transcript_not_exist(self): - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - assert response.status == '404 Not Found' - - @patch( - 'xblocks_contrib.video.video_handlers.get_transcript', - return_value=('Subs!', 'test_filename.srt', 'application/x-subrip; charset=utf-8') - ) - def test_download_srt_exist(self, __): # noqa: PT019 - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - assert response.body.decode('utf-8') == 'Subs!' - assert response.headers['Content-Type'] == 'application/x-subrip; charset=utf-8' - assert response.headers['Content-Language'] == 'en' - - @patch( - 'xblocks_contrib.video.video_handlers.get_transcript', - return_value=('Subs!', 'txt', 'text/plain; charset=utf-8') - ) - def test_download_txt_exist(self, __): # noqa: PT019 - self.block.transcript_format = 'txt' - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - assert response.body.decode('utf-8') == 'Subs!' - assert response.headers['Content-Type'] == 'text/plain; charset=utf-8' - assert response.headers['Content-Language'] == 'en' - - def test_download_en_no_sub(self): - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - assert response.status == '404 Not Found' - with pytest.raises(NotFoundError): - get_transcript(self.block) - - @patch( - 'openedx.core.djangoapps.video_config.transcripts_utils.get_transcript_for_video', - return_value=(Transcript.SRT, "塞", 'Subs!') - ) - def test_download_non_en_non_ascii_filename(self, __): # noqa: PT019 - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - assert response.body.decode('utf-8') == 'Subs!' - assert response.headers['Content-Type'] == 'application/x-subrip; charset=utf-8' - assert response.headers['Content-Disposition'] == 'attachment; filename="en_塞.srt"' - - @patch('openedx.core.djangoapps.video_config.transcripts_utils.edxval_api.get_video_transcript_data') - def test_download_fallback_transcript(self, mock_get_video_transcript_data): - """ - Verify val transcript is returned as a fallback if it is not found in the content store. - """ - mock_get_video_transcript_data.return_value = { - 'content': json.dumps({ - "start": [10], - "end": [100], - "text": ["Hi, welcome to Edx."], - }), - 'file_name': 'edx.sjson' - } - - # Make request to XModule transcript handler - request = Request.blank('/download') - response = self.block.transcript(request=request, dispatch='download') - - # Expected response - expected_content = '0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n' - expected_headers = { - 'Content-Disposition': 'attachment; filename="edx.srt"', - 'Content-Language': 'en', - 'Content-Type': 'application/x-subrip; charset=utf-8' - } - - # Assert the actual response - assert response.status_code == 200 - assert response.text == expected_content - for attribute, value in expected_headers.items(): - assert response.headers[attribute] == value - - -def _create_djangowebobrequest_object_for_url(url: str) -> DjangoWebobRequest: - """ - create a DjangoWebobRequest (The type of requests used by xblocks) - with a relevant user, - """ - django_request = RequestFactory().get(url) - django_request.user = UserFactory.create(username=TEST_USERNAME) - return DjangoWebobRequest(django_request) - - -@ddt.ddt -class TestTranscriptTranslationGetDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test video handler that provide translation transcripts. - - Tests for `translation` and `translation_bumper` dispatches. - """ - - srt_file = _create_srt_file() - DATA = """ - - """.format(os.path.split(srt_file.name)[1]) # noqa: UP032 - - MODEL_DATA = { - 'data': DATA - } - - def setUp(self): - super().setUp() - self.block.render(STUDENT_VIEW) - self.block.video_bumper = {"transcripts": {"en": ""}} - - @ddt.data( - # No language - ('/translation', 'translation', '400 Bad Request'), - # No videoId - HTML5 video with language that is not in available languages - ('/translation/ru', 'translation/ru', '404 Not Found'), - # Language is not in available languages - ('/translation/ru?videoId=12345', 'translation/ru', '404 Not Found'), - # Youtube_id is invalid or does not exist - ('/translation/uk?videoId=9855256955511225', 'translation/uk', '404 Not Found'), - ('/translation?is_bumper=1', 'translation', '400 Bad Request'), - ('/translation/ru?is_bumper=1', 'translation/ru', '404 Not Found'), - ('/translation/ru?videoId=12345&is_bumper=1', 'translation/ru', '404 Not Found'), - ('/translation/uk?videoId=9855256955511225&is_bumper=1', 'translation/uk', '404 Not Found'), - ) - @ddt.unpack - def test_translation_fails(self, url, dispatch, status_code): - request = _create_djangowebobrequest_object_for_url(url) - response = self.block.transcript(request=request, dispatch=dispatch) - assert response.status == status_code - - @ddt.data( - ('translation/en?videoId={}', 'translation/en', attach_sub), - ('translation/en?videoId={}&is_bumper=1', 'translation/en', attach_bumper_transcript)) - @ddt.unpack - def test_translaton_en_youtube_success(self, url, dispatch, attach): - subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} - good_sjson = _create_file(json.dumps(subs)) - _upload_sjson_file(good_sjson, self.block.location) - subs_id = _get_subs_id(good_sjson.name) - - attach(self.block, subs_id) - request = _create_djangowebobrequest_object_for_url(url.format(subs_id)) - response = self.block.transcript(request=request, dispatch=dispatch) - self.assertDictEqual(json.loads(response.body.decode('utf-8')), subs) # noqa: PT009 - - def test_translation_non_en_youtube_success(self): - subs = { - 'end': [100], - 'start': [12], - 'text': [ - '\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' - ] - } - self.srt_file.seek(0) - _upload_file(self.srt_file, self.block.location, os.path.split(self.srt_file.name)[1]) - subs_id = _get_subs_id(self.srt_file.name) - - # youtube 1_0 request, will generate for all speeds for existing ids - self.block.youtube_id_1_0 = subs_id - self.block.youtube_id_0_75 = '0_75' - self.store.update_item(self.block, self.user.id) - request = _create_djangowebobrequest_object_for_url(f'/translation/uk?videoId={subs_id}') - response = self.block.transcript(request=request, dispatch='translation/uk') - self.assertDictEqual(json.loads(response.body.decode('utf-8')), subs) # noqa: PT009 - - # 0_75 subs are exist - request = _create_djangowebobrequest_object_for_url('/translation/uk?videoId={}'.format('0_75')) - response = self.block.transcript(request=request, dispatch='translation/uk') - calculated_0_75 = { - 'end': [75], - 'start': [9], - 'text': [ - '\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' - ] - } - - self.assertDictEqual(json.loads(response.body.decode('utf-8')), calculated_0_75) # noqa: PT009 - # 1_5 will be generated from 1_0 - self.block.youtube_id_1_5 = '1_5' - self.store.update_item(self.block, self.user.id) - request = _create_djangowebobrequest_object_for_url('/translation/uk?videoId={}'.format('1_5')) - response = self.block.transcript(request=request, dispatch='translation/uk') - calculated_1_5 = { - 'end': [150], - 'start': [18], - 'text': [ - '\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' - ] - } - self.assertDictEqual(json.loads(response.body.decode('utf-8')), calculated_1_5) # noqa: PT009 - - @ddt.data( - ('translation/en', 'translation/en', attach_sub), - ('translation/en?is_bumper=1', 'translation/en', attach_bumper_transcript)) - @ddt.unpack - def test_translaton_en_html5_success(self, url, dispatch, attach): - good_sjson = _create_file(json.dumps(TRANSCRIPT)) - _upload_sjson_file(good_sjson, self.block.location) - subs_id = _get_subs_id(good_sjson.name) - - attach(self.block, subs_id) - self.store.update_item(self.block, self.user.id) - request = _create_djangowebobrequest_object_for_url(url) - response = self.block.transcript(request=request, dispatch=dispatch) - self.assertDictEqual(json.loads(response.body.decode('utf-8')), TRANSCRIPT) # noqa: PT009 - - def test_translaton_non_en_html5_success(self): - subs = { - 'end': [100], - 'start': [12], - 'text': [ - '\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' - ] - } - self.srt_file.seek(0) - _upload_file(self.srt_file, self.block.location, os.path.split(self.srt_file.name)[1]) - - # manually clean youtube_id_1_0, as it has default value - self.block.youtube_id_1_0 = "" - request = _create_djangowebobrequest_object_for_url('/translation/uk') - response = self.block.transcript(request=request, dispatch='translation/uk') - self.assertDictEqual(json.loads(response.body.decode('utf-8')), subs) # noqa: PT009 - - def test_translation_static_transcript_xml_with_data_dirc(self): - """ - Test id data_dir is set in XML course. - - Set course data_dir and ensure we get redirected to that path - if it isn't found in the contentstore. - """ - # Simulate data_dir set in course. - test_modulestore = MagicMock() - attrs = {'get_course.return_value': Mock(data_dir='dummy/static', static_asset_path='')} - test_modulestore.configure_mock(**attrs) - self.block.runtime.modulestore = test_modulestore - - # Test youtube style en - request = _create_djangowebobrequest_object_for_url('/translation/en?videoId=12345') - response = self.block.transcript(request=request, dispatch='translation/en') - assert response.status == '307 Temporary Redirect' - assert ('Location', '/static/dummy/static/subs_12345.srt.sjson') in response.headerlist - - # Test HTML5 video style - self.block.sub = 'OEoXaMPEzfM' - request = _create_djangowebobrequest_object_for_url('/translation/en') - response = self.block.transcript(request=request, dispatch='translation/en') - assert response.status == '307 Temporary Redirect' - assert ('Location', '/static/dummy/static/subs_OEoXaMPEzfM.srt.sjson') in response.headerlist - - # Test different language to ensure we are just ignoring it since we can't - # translate with static fallback - request = _create_djangowebobrequest_object_for_url('/translation/uk') - response = self.block.transcript(request=request, dispatch='translation/uk') - assert response.status == '404 Not Found' - - @ddt.data( - # Test youtube style en - ('/translation/en?videoId=12345', 'translation/en', '307 Temporary Redirect', '12345'), - # Test html5 style en - ('/translation/en', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', attach_sub), - # Test different language to ensure we are just ignoring it since we can't - # translate with static fallback - ('/translation/uk', 'translation/uk', '404 Not Found'), - ( - '/translation/en?is_bumper=1', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', - attach_bumper_transcript - ), - ('/translation/uk?is_bumper=1', 'translation/uk', '404 Not Found'), - ) - @ddt.unpack - def test_translation_static_transcript(self, url, dispatch, status_code, sub=None, attach=None): # noqa: PT028 - """ - Set course static_asset_path and ensure we get redirected to that path - if it isn't found in the contentstore - """ - self._set_static_asset_path() - - if attach: - attach(self.block, sub) - request = _create_djangowebobrequest_object_for_url(url) - response = self.block.transcript(request=request, dispatch=dispatch) - assert response.status == status_code - if sub: - assert ('Location', f'/static/dummy/static/subs_{sub}.srt.sjson') in response.headerlist - - @patch('xmodule.video_block.VideoBlock.context_key', return_value='not_a_course_locator') - def test_translation_static_non_course(self, __): # noqa: PT019 - """ - Test that get_static_transcript short-circuits in the case of a non-CourseLocator. - This fixes a bug for videos inside of content libraries. - """ - self._set_static_asset_path() - - # When context_key is not mocked out, these values would result in 307, as tested above. - request = _create_djangowebobrequest_object_for_url('/translation/en?videoId=12345') - response = self.block.transcript(request=request, dispatch='translation/en') - assert response.status == '404 Not Found' - - def _set_static_asset_path(self): - """ Helper method for setting up the static_asset_path information """ - self.course.static_asset_path = 'dummy/static' - self.course.save() - store = modulestore() - with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id): - store.update_item(self.course, self.user.id) - - @patch('openedx.core.djangoapps.video_config.transcripts_utils.edxval_api.get_video_transcript_data') - @patch('xmodule.video_block.VideoBlock.get_static_transcript', Mock(return_value=Response(status=404))) - def test_translation_fallback_transcript(self, mock_get_video_transcript_data): - """ - Verify that the val transcript is returned as a fallback, - if it is not found in the content store. - """ - transcript = { - 'content': json.dumps({ - "start": [10], - "end": [100], - "text": ["Hi, welcome to Edx."], - }), - 'file_name': 'edx.sjson' - } - mock_get_video_transcript_data.return_value = transcript - - # Make request to XModule transcript handler - request = _create_djangowebobrequest_object_for_url('/translation/en') - response = self.block.transcript(request=request, dispatch='translation/en') - - # Expected headers - expected_headers = { - 'Content-Language': 'en', - 'Content-Type': 'application/json' - } - - # Assert the actual response - assert response.status_code == 200 - assert response.text == transcript['content'] - for attribute, value in expected_headers.items(): - assert response.headers[attribute] == value - - @patch('xmodule.video_block.VideoBlock.get_static_transcript', Mock(return_value=Response(status=404))) - def test_translation_fallback_transcript_feature_disabled(self): - """ - Verify that val transcript is not returned when its feature is disabled. - """ - # Make request to XModule transcript handler - response = self.block.transcript( - request=_create_djangowebobrequest_object_for_url('/translation/en'), - dispatch='translation/en' - ) - # Assert the actual response - assert response.status_code == 404 - - -class TestStudioTranscriptTranslationGetDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test Studio video handler that provide translation transcripts. - - Tests for `translation` dispatch GET HTTP method. - """ - srt_file = _create_srt_file() - DATA = """ - - """.format(os.path.split(srt_file.name)[1], "塞.srt") - - MODEL_DATA = {'data': DATA} - - def test_translation_fails(self): - # No language - request = Request.blank("") - response = self.block.studio_transcript(request=request, dispatch="translation") - assert response.status == '400 Bad Request' - - # No language_code param in request.GET - request = Request.blank("") - response = self.block.studio_transcript(request=request, dispatch="translation") - assert response.status == '400 Bad Request' - assert response.json['error'] == 'Language is required.' - - # Correct case: - filename = os.path.split(self.srt_file.name)[1] - _upload_file(self.srt_file, self.block.location, filename) - request = Request.blank("translation?language_code=uk") - response = self.block.studio_transcript(request=request, dispatch="translation?language_code=uk") - self.srt_file.seek(0) - assert response.body == self.srt_file.read() - assert response.headers['Content-Type'] == 'application/x-subrip; charset=utf-8' - assert response.headers['Content-Disposition'] == f'attachment; filename="uk_{filename}"' - assert response.headers['Content-Language'] == 'uk' - - # Non ascii file name download: - self.srt_file.seek(0) - _upload_file(self.srt_file, self.block.location, "塞.srt") - request = Request.blank("translation?language_code=zh") - response = self.block.studio_transcript(request=request, dispatch="translation?language_code=zh") - self.srt_file.seek(0) - assert response.body == self.srt_file.read() - assert response.headers['Content-Type'] == 'application/x-subrip; charset=utf-8' - assert response.headers['Content-Disposition'] == 'attachment; filename="zh_塞.srt"' - assert response.headers['Content-Language'] == 'zh' - - -@ddt.ddt -class TestStudioTranscriptTranslationPostDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test Studio video handler that provide translation transcripts. - - Tests for `translation` dispatch with HTTP POST method. - """ - DATA = """ - - """ - - MODEL_DATA = { - 'data': DATA - } - - METADATA = {} - - @ddt.data( - { - "post_data": {}, - "error_message": "The following parameters are required: edx_video_id, language_code, new_language_code." - }, - { - "post_data": {"edx_video_id": "111", "language_code": "ar", "new_language_code": "ur"}, - "error_message": 'A transcript with the "ur" language code already exists.' - }, - { - "post_data": {"edx_video_id": "111", "language_code": "ur", "new_language_code": "ur"}, - "error_message": "A transcript file is required." - }, - ) - @patch('openedx.core.djangoapps.video_config.services.VideoConfigService.available_translations') - @ddt.unpack - def test_studio_transcript_post_validations(self, mock_available_translations, post_data, error_message): - """ - Verify that POST request validations works as expected. - """ - # mock available_translations method to return ['ur'] - mock_available_translations.return_value = ['ur'] - request = Request.blank('/translation', POST=post_data) - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.json['error'] == error_message - - @ddt.data( - { - "edx_video_id": "", - }, - { - "edx_video_id": "1234-5678-90", - }, - ) - @ddt.unpack - def test_studio_transcript_post_w_no_edx_video_id(self, edx_video_id): - """ - Verify that POST request works as expected - """ - post_data = { - "edx_video_id": edx_video_id, - "language_code": "ar", - "new_language_code": "uk", - "file": ("filename.srt", SRT_content) - } - - if edx_video_id: - edxval_api.create_video({ - "edx_video_id": edx_video_id, - "status": "uploaded", - "client_video_id": "a video", - "duration": 0, - "encoded_videos": [], - "courses": [] - }) - - request = Request.blank('/translation', POST=post_data) - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status == '201 Created' - response = json.loads(response.text) - assert response['language_code'], 'uk' - self.assertDictEqual(self.block.transcripts, {'uk': f'{response["edx_video_id"]}-uk.srt'}) # noqa: PT009 - assert edxval_api.get_video_transcript_data(video_id=response['edx_video_id'], language_code='uk') - - def test_studio_transcript_post_bad_content(self): - """ - Verify that transcript content encode/decode errors handled as expected - """ - post_data = { - "edx_video_id": "", - "language_code": "ar", - "new_language_code": "uk", - "file": ("filename.srt", SRT_content.encode("cp1251")) - } - - request = Request.blank("/translation", POST=post_data) - response = self.block.studio_transcript(request=request, dispatch="translation") - assert response.status_code == 400 - assert response.json['error'] == 'There is a problem with this transcript file. Try to upload a different file.' - # transcripts fields should not be updated - self.assertDictEqual(self.block.transcripts, {}) # noqa: PT009 - - -@ddt.ddt -class TestStudioTranscriptTranslationDeleteDispatch(TestVideo): # pylint: disable=test-inherits-tests - """ - Test studio video handler that provide translation transcripts. - - Tests for `translation` dispatch DELETE HTTP method. - """ - EDX_VIDEO_ID, LANGUAGE_CODE_UK, LANGUAGE_CODE_EN = 'an_edx_video_id', 'uk', 'en' - REQUEST_META = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'DELETE'} - SRT_FILE = _create_srt_file() - - @ddt.data( - { - 'params': {'lang': 'uk'} - }, - { - 'params': {'edx_video_id': '12345'} - }, - { - 'params': {} - }, - ) - @ddt.unpack - def test_translation_missing_required_params(self, params): - """ - Verify that DELETE dispatch works as expected when required args are missing from request - """ - request = Request(self.REQUEST_META, body=json.dumps(params).encode('utf-8')) - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status_code == 400 - - def test_translation_delete_w_edx_video_id(self): - """ - Verify that DELETE dispatch works as expected when video has edx_video_id - """ - request_body = json.dumps({'lang': self.LANGUAGE_CODE_UK, 'edx_video_id': self.EDX_VIDEO_ID}) - api.create_video({ - 'edx_video_id': self.EDX_VIDEO_ID, - 'status': 'upload', - 'client_video_id': 'awesome.mp4', - 'duration': 0, - 'encoded_videos': [], - 'courses': [str(self.course.id)] - }) - api.create_video_transcript( - video_id=self.EDX_VIDEO_ID, - language_code=self.LANGUAGE_CODE_UK, - file_format='srt', - content=ContentFile(SRT_content) - ) - - # verify that a video transcript exists for expected data - assert api.get_video_transcript_data(video_id=self.EDX_VIDEO_ID, language_code=self.LANGUAGE_CODE_UK) - - request = Request(self.REQUEST_META, body=request_body.encode('utf-8')) - self.block.edx_video_id = self.EDX_VIDEO_ID - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status_code == 200 - - # verify that a video transcript dose not exist for expected data - assert not api.get_video_transcript_data(video_id=self.EDX_VIDEO_ID, language_code=self.LANGUAGE_CODE_UK) - - def test_translation_delete_wo_edx_video_id(self): - """ - Verify that DELETE dispatch works as expected when video has no edx_video_id - """ - request_body = json.dumps({'lang': self.LANGUAGE_CODE_UK, 'edx_video_id': ''}) - srt_file_name_uk = subs_filename('ukrainian_translation.srt', lang=self.LANGUAGE_CODE_UK) - request = Request(self.REQUEST_META, body=request_body.encode('utf-8')) - - # upload and verify that srt file exists in assets - _upload_file(self.SRT_FILE, self.block.location, srt_file_name_uk) - assert _check_asset(self.block.location, srt_file_name_uk) - - # verify transcripts field - assert self.block.transcripts != {} - assert self.LANGUAGE_CODE_UK in self.block.transcripts - - # make request and verify response - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status_code == 200 - - # verify that srt file is deleted - assert self.block.transcripts == {} - assert not _check_asset(self.block.location, srt_file_name_uk) - - def test_translation_delete_w_english_lang(self): - """ - Verify that DELETE dispatch works as expected for english language translation - """ - request_body = json.dumps({'lang': self.LANGUAGE_CODE_EN, 'edx_video_id': ''}) - srt_file_name_en = subs_filename('english_translation.srt', lang=self.LANGUAGE_CODE_EN) - self.block.transcripts['en'] = 'english_translation.srt' - request = Request(self.REQUEST_META, body=request_body.encode('utf-8')) - - # upload and verify that srt file exists in assets - _upload_file(self.SRT_FILE, self.block.location, srt_file_name_en) - assert _check_asset(self.block.location, srt_file_name_en) - - # make request and verify response - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status_code == 200 - - # verify that srt file is deleted - assert self.LANGUAGE_CODE_EN not in self.block.transcripts - assert not _check_asset(self.block.location, srt_file_name_en) - - def test_translation_delete_w_sub(self): - """ - Verify that DELETE dispatch works as expected when translation is present against `sub` field - """ - request_body = json.dumps({'lang': self.LANGUAGE_CODE_EN, 'edx_video_id': ''}) - sub_file_name = subs_filename(self.block.sub, lang=self.LANGUAGE_CODE_EN) - request = Request(self.REQUEST_META, body=request_body.encode('utf-8')) - - # sub should not be empy - assert not self.block.sub == '' - # pylint: disable=wrong-assert-type - - # upload and verify that srt file exists in assets - _upload_file(self.SRT_FILE, self.block.location, sub_file_name) - assert _check_asset(self.block.location, sub_file_name) - - # make request and verify response - response = self.block.studio_transcript(request=request, dispatch='translation') - assert response.status_code == 200 - - # verify that sub is empty and transcript is deleted also - assert self.block.sub == '' - # pylint: disable=wrong-assert-type - assert not _check_asset(self.block.location, sub_file_name) - - -class TestGetTranscript(TestVideo): # pylint: disable=test-inherits-tests - """ - Make sure that `get_transcript` method works correctly - """ - srt_file = _create_srt_file() - DATA = """ - - """.format(os.path.split(srt_file.name)[1], "塞.srt") - - MODEL_DATA = { - 'data': DATA - } - METADATA = {} - - def setUp(self): - super().setUp() - self.block.render(STUDENT_VIEW) - - def test_good_transcript(self): - """ - Test for download 'en' sub with html5 video and self.sub has correct non-empty value. - """ - good_sjson = _create_file(content=textwrap.dedent("""\ - { - "start": [ - 270, - 2720 - ], - "end": [ - 2720, - 5430 - ], - "text": [ - "Hi, welcome to Edx.", - "Let's start with what is on your screen right now." - ] - } - """)) - - _upload_sjson_file(good_sjson, self.block.location) - self.block.sub = _get_subs_id(good_sjson.name) - - text, filename, mime_type = get_transcript(self.block) - - expected_text = textwrap.dedent("""\ - 0 - 00:00:00,270 --> 00:00:02,720 - Hi, welcome to Edx. - - 1 - 00:00:02,720 --> 00:00:05,430 - Let's start with what is on your screen right now. - - """) - - assert text == expected_text - assert filename[:(- 4)] == ('en_' + self.block.sub) - assert mime_type == 'application/x-subrip; charset=utf-8' - - def test_good_txt_transcript(self): - good_sjson = _create_file(content=textwrap.dedent("""\ - { - "start": [ - 270, - 2720 - ], - "end": [ - 2720, - 5430 - ], - "text": [ - "Hi, welcome to Edx.", - "Let's start with what is on your screen right now." - ] - } - """)) - - _upload_sjson_file(good_sjson, self.block.location) - self.block.sub = _get_subs_id(good_sjson.name) - text, filename, mime_type = get_transcript(self.block, output_format=Transcript.TXT) - expected_text = textwrap.dedent("""\ - Hi, welcome to Edx. - Let's start with what is on your screen right now.""") - - assert text == expected_text - assert filename == (('en_' + self.block.sub) + '.txt') - assert mime_type == 'text/plain; charset=utf-8' - - def test_en_with_empty_sub(self): - - self.block.sub = "" - self.block.transcripts = None - # no self.sub, self.youttube_1_0 exist, but no file in assets - with pytest.raises(NotFoundError): - get_transcript(self.block) - - # no self.sub and no self.youtube_1_0, no non-en transcritps - self.block.youtube_id_1_0 = None - with pytest.raises(NotFoundError): - get_transcript(self.block) - - # no self.sub but youtube_1_0 exists with file in assets - good_sjson = _create_file(content=textwrap.dedent("""\ - { - "start": [ - 270, - 2720 - ], - "end": [ - 2720, - 5430 - ], - "text": [ - "Hi, welcome to Edx.", - "Let's start with what is on your screen right now." - ] - } - """)) - _upload_sjson_file(good_sjson, self.block.location) - self.block.youtube_id_1_0 = _get_subs_id(good_sjson.name) - - text, filename, mime_type = get_transcript(self.block) - expected_text = textwrap.dedent("""\ - 0 - 00:00:00,270 --> 00:00:02,720 - Hi, welcome to Edx. - - 1 - 00:00:02,720 --> 00:00:05,430 - Let's start with what is on your screen right now. - - """) - - assert text == expected_text - assert filename == (('en_' + self.block.youtube_id_1_0) + '.srt') - assert mime_type == 'application/x-subrip; charset=utf-8' - - def test_non_en_with_non_ascii_filename(self): - self.block.transcript_language = 'zh' - self.srt_file.seek(0) - _upload_file(self.srt_file, self.block.location, "塞.srt") - - transcripts = self.block.get_transcripts_info() # pylint: disable=unused-variable # noqa: F841 - text, filename, mime_type = get_transcript(self.block) - expected_text = textwrap.dedent(""" - 0 - 00:00:00,12 --> 00:00:00,100 - Привіт, edX вітає вас. - """) - assert text == expected_text - assert filename == 'zh_塞.srt' - assert mime_type == 'application/x-subrip; charset=utf-8' - - def test_value_error_handled(self): - good_sjson = _create_file(content='bad content') - - _upload_sjson_file(good_sjson, self.block.location) - self.block.sub = _get_subs_id(good_sjson.name) - - transcripts = self.block.get_transcripts_info() # pylint: disable=unused-variable # noqa: F841 - error_transcript = {"start": [], "end": [], "text": ["An error occured obtaining the transcript."]} - content, _, _ = get_transcript(self.block) - assert error_transcript["text"][0] in content - - def test_key_error(self): - good_sjson = _create_file(content=""" - { - "start": [ - 270, - 2720 - ], - "end": [ - 2720, - 5430 - ] - } - """) - - _upload_sjson_file(good_sjson, self.block.location) - self.block.sub = _get_subs_id(good_sjson.name) - - transcripts = self.block.get_transcripts_info() # pylint: disable=unused-variable # noqa: F841 - with pytest.raises(KeyError): - get_transcript(self.block) diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py deleted file mode 100644 index d6ca46155db7..000000000000 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ /dev/null @@ -1,2605 +0,0 @@ -""" -Video xmodule tests in mongo. -""" - - -import json -import shutil -from collections import OrderedDict -from contextlib import contextmanager -from tempfile import mkdtemp -from unittest.mock import MagicMock, Mock, patch -from uuid import uuid4 - -import ddt -import pytest -from django.conf import settings -from django.core.files import File -from django.core.files.base import ContentFile -from django.http import Http404 -from django.test import TestCase -from django.test.utils import override_settings -from edx_toggles.toggles.testutils import override_waffle_flag -from edxval.api import ( - ValCannotCreateError, - ValVideoNotFoundError, - create_or_update_video_transcript, - create_profile, - create_video, - create_video_transcript, - get_video_info, - get_video_transcript, - get_video_transcript_data, -) -from edxval.utils import create_file_in_fs -from fs.osfs import OSFS -from fs.path import combine -from lxml import etree -from path import Path as path -from xblocks_contrib.video import bumper_utils - -from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE -from common.test.utils import assert_dict_contains_subset -from lms.djangoapps.courseware.tests.helpers import get_context_from_dict -from openedx.core.djangoapps.video_config import sharing -from openedx.core.djangoapps.video_config.sharing import ( - COURSE_VIDEO_SHARING_ALL_VIDEOS, - COURSE_VIDEO_SHARING_NONE, - COURSE_VIDEO_SHARING_PER_VIDEO, -) -from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE -from openedx.core.djangoapps.video_config.transcripts_utils import Transcript, save_to_store, subs_filename -from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE -from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel -from openedx.core.djangolib.testing.utils import CacheIsolationTestCase -from xmodule.contentstore.content import StaticContent -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 ( # pylint: disable=unused-import # noqa: F401 - mock_render_template, - override_descriptor_system, -) -from xmodule.tests.test_import import DummyModuleStoreRuntime -from xmodule.tests.test_video import VideoBlockTestBase -from xmodule.video_block import VideoBlock, video_utils -from xmodule.video_block.video_block import EXPORT_IMPORT_COURSE_DIR, EXPORT_IMPORT_STATIC_DIR -from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW - -from .test_video_handlers import BaseTestVideoXBlock, TestVideo -from .test_video_xml import PUBLIC_SOURCE_XML, SOURCE_XML - -TRANSCRIPT_FILE_SRT_DATA = """ -1 -00:00:14,370 --> 00:00:16,530 -I am overwatch. - -2 -00:00:16,500 --> 00:00:18,600 -可以用“我不太懂艺术 但我知道我喜欢什么”做比喻. -""" - -TRANSCRIPT_FILE_SJSON_DATA = """{\n "start": [10],\n "end": [100],\n "text": ["Hi, welcome to edxval."]\n}""" - - -class TestVideoYouTube(TestVideo): # pylint: disable=missing-class-docstring, test-inherits-tests - METADATA = {} - - @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""" - self.block.student_view(None) - sources = ['example.mp4', 'example.webm'] - - expected_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'id': self.block.location.html_id(), - 'is_embed': False, - 'metadata': json.dumps(OrderedDict({ - 'autoAdvance': False, - 'saveStateEnabled': True, - 'saveStateUrl': self.block.ajax_url + '/save_user_state', - 'autoplay': False, - 'streams': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg', - 'sources': sources, - 'duration': None, - 'poster': None, - 'captionDataDir': None, - 'showCaptions': 'true', - 'generalSpeed': 1.0, - 'speed': None, - 'savedVideoPosition': 0.0, - 'start': 3603.0, - 'end': 3610.0, - 'transcriptLanguage': 'en', - 'transcriptLanguages': OrderedDict({'en': 'English', 'uk': 'Українська'}), - 'ytMetadataEndpoint': '', - 'ytTestTimeout': 1500, - 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'lmsRootURL': settings.LMS_ROOT_URL, - 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), - 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), - 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, - 'autohideHtml5': False, - 'recordedYoutubeIsAvailable': True, - 'completionEnabled': False, - 'completionPercentage': 0.95, - 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), - 'prioritizeHls': False, - })), - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': '', - } - - # 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 - """Integration tests: web client + mongo.""" - DATA = """ - - """ - MODEL_DATA = { - 'data': DATA, - } - METADATA = {} - - @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. - """ - self.block.student_view(None) - sources = ['example.mp4', 'example.webm'] - - expected_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'is_embed': False, - 'id': self.block.location.html_id(), - 'metadata': json.dumps(OrderedDict({ - 'autoAdvance': False, - 'saveStateEnabled': True, - 'saveStateUrl': self.block.ajax_url + '/save_user_state', - 'autoplay': False, - 'streams': '1.00:3_yD_cEKoCk', - 'sources': sources, - 'duration': None, - 'poster': None, - 'captionDataDir': None, - 'showCaptions': 'true', - 'generalSpeed': 1.0, - 'speed': None, - 'savedVideoPosition': 0.0, - 'start': 3603.0, - 'end': 3610.0, - 'transcriptLanguage': 'en', - 'transcriptLanguages': OrderedDict({'en': 'English'}), - 'ytMetadataEndpoint': '', - 'ytTestTimeout': 1500, - 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'lmsRootURL': settings.LMS_ROOT_URL, - 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), - 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), - 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, - 'autohideHtml5': False, - 'recordedYoutubeIsAvailable': True, - 'completionEnabled': False, - 'completionPercentage': 0.95, - 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), - 'prioritizeHls': False, - })), - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': '', - } - - # 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 -class TestVideoPublicAccess(BaseTestVideoXBlock): - """Test video public access.""" - DATA = PUBLIC_SOURCE_XML - MODEL_DATA = { - 'data': DATA, - } - METADATA = {} - - @contextmanager - def mock_feature_toggle(self, enabled=True): - with patch.object(PUBLIC_VIDEO_SHARE, 'is_enabled', return_value=enabled): - yield - - @ddt.data(True, False) - def test_is_public_sharing_enabled(self, feature_enabled): - """Test public video url.""" - assert self.block.public_access is True - with self.mock_feature_toggle(enabled=feature_enabled): - assert sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) == feature_enabled - - def test_is_public_sharing_enabled__not_public(self): - self.block.public_access = False - with self.mock_feature_toggle(): - assert not sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) - - @patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override') - def test_is_public_sharing_enabled_by_course_override(self, mock_course_sharing_override): - - # Given a course overrides all videos to be shared - mock_course_sharing_override.return_value = COURSE_VIDEO_SHARING_ALL_VIDEOS - self.block.public_access = 'some-arbitrary-value' - - # When I try to determine if public sharing is enabled - with self.mock_feature_toggle(): - is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) - - # Then I will get that course value - self.assertTrue(is_public_sharing_enabled) # noqa: PT009 - - @patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override') - def test_is_public_sharing_disabled_by_course_override(self, mock_course_sharing_override): - # Given a course overrides no videos to be shared - mock_course_sharing_override.return_value = COURSE_VIDEO_SHARING_NONE - self.block.public_access = 'some-arbitrary-value' - - # When I try to determine if public sharing is enabled - with self.mock_feature_toggle(): - is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) - - # Then I will get that course value - self.assertFalse(is_public_sharing_enabled) # noqa: PT009 - - @ddt.data(COURSE_VIDEO_SHARING_PER_VIDEO, None) - @patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override') - def test_is_public_sharing_enabled_per_video(self, mock_override_value, mock_course_sharing_override): - # Given a course does not override per-video settings - mock_course_sharing_override.return_value = mock_override_value - self.block.public_access = 'some-arbitrary-value' - - # When I try to determine if public sharing is enabled - with self.mock_feature_toggle(): - is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) - - # I will get the per-video value - self.assertEqual(self.block.public_access, is_public_sharing_enabled) # noqa: PT009 - - @patch('openedx.core.lib.courses.get_course_by_id') - def test_is_public_sharing_course_not_found(self, mock_get_course): - # Given a course does not override per-video settings - mock_get_course.side_effect = Http404() - self.block.public_access = 'some-arbitrary-value' - - # When I try to determine if public sharing is enabled - with self.mock_feature_toggle(): - is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) - - # I will fall-back to per-video values - self.assertEqual(self.block.public_access, is_public_sharing_enabled) # noqa: PT009 - - @ddt.data(False, True) - @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 - ): - 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 - - -@ddt.ddt -class TestGetHtmlMethod(BaseTestVideoXBlock): - ''' - Make sure that `get_html` works correctly. - ''' - maxDiff = None - CATEGORY = "video" - DATA = SOURCE_XML - METADATA = {} - - def setUp(self): - super().setUp() - self.setup_course() - self.default_metadata_dict = OrderedDict({ - 'autoAdvance': False, - 'saveStateEnabled': True, - 'saveStateUrl': '', - 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), - 'streams': '1.00:3_yD_cEKoCk', - 'sources': '[]', - 'duration': 111.0, - 'poster': None, - 'captionDataDir': None, - 'showCaptions': 'true', - 'generalSpeed': 1.0, - 'speed': None, - 'savedVideoPosition': 0.0, - 'start': 3603.0, - 'end': 3610.0, - 'transcriptLanguage': 'en', - 'transcriptLanguages': OrderedDict({'en': 'English'}), - 'ytMetadataEndpoint': '', - 'ytTestTimeout': 1500, - 'ytApiUrl': 'https://www.youtube.com/iframe_api', - 'lmsRootURL': settings.LMS_ROOT_URL, - 'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'), - 'transcriptAvailableTranslationsUrl': self.get_handler_url('transcript', 'available_translations'), - 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL, - 'autohideHtml5': False, - 'recordedYoutubeIsAvailable': True, - 'completionEnabled': False, - 'completionPercentage': 0.95, - 'publishCompletionUrl': self.get_handler_url('publish_completion', ''), - 'prioritizeHls': False, - }) - - def get_handler_url(self, handler, suffix): - """ - Return the URL for the specified handler on the block represented by - self.block. - """ - return self.block.runtime.handler_url( - self.block, handler, suffix - ).rstrip('/?') - - @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 - # pylint: disable=redefined-outer-name - SOURCE_XML = """ - - """ - - cases = [ - { - 'name': 'video 1', - 'download_track': 'true', - 'track': '', - 'sub': 'a_sub_file.srt.sjson', - 'expected_track_url': 'http://www.example.com/track', - 'transcripts': '', - }, - { - 'name': 'video 2', - 'download_track': 'true', - 'track': '', - 'sub': 'a_sub_file.srt.sjson', - 'expected_track_url': 'a_sub_file.srt.sjson', - 'transcripts': '', - }, - { - 'name': 'video 3', - 'download_track': 'true', - 'track': '', - 'sub': '', - 'expected_track_url': None, - 'transcripts': '', - }, - { - 'name': 'video 4', - 'download_track': 'false', - 'track': '', - 'sub': 'a_sub_file.srt.sjson', - 'expected_track_url': None, - 'transcripts': '', - }, - { - 'name': 'video 5', - 'download_track': 'true', - 'track': '', - 'sub': '', - 'expected_track_url': 'a_sub_file.srt.sjson', - 'transcripts': '', - }, - ] - sources = ['example.mp4', 'example.webm'] - - expected_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'id': self.block.location.html_id(), - 'is_embed': False, - 'metadata': '', - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': '', - } - - for data in cases: - metadata = self.default_metadata_dict - metadata['sources'] = sources - metadata['duration'] = None - DATA = SOURCE_XML.format( - download_track=data['download_track'], - track=data['track'], - sub=data['sub'], - transcripts=data['transcripts'], - name=data['name'], - ) - - self.initialize_block(data=DATA) - track_url = self.get_handler_url('transcript', 'download') - - 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', - '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', - }) - expected_context.update({ - 'display_name': data['name'], - 'transcript_download_format': ( - None if self.block.track and self.block.download_track else 'srt' - ), - 'track': ( - track_url if data['expected_track_url'] == 'a_sub_file.srt.sjson' else data['expected_track_url'] - ), - 'id': self.block.location.html_id(), - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'metadata': json.dumps(metadata) - }) - - # 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) - - @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) - def test_get_html_source(self, mock_render_django_template): - # pylint: disable=invalid-name, redefined-outer-name - SOURCE_XML = """ - - """ - cases = [ - # self.download_video == True - { - 'name': 'video 1', - 'download_video': 'true', - 'source': 'example_source.mp4', - 'sources': """ - - - """, - 'result': { - 'download_video_link': 'example.mp4', - 'sources': ['example.mp4', 'example.webm'], - }, - }, - { - 'name': 'video 2', - 'download_video': 'true', - 'source': '', - 'sources': """ - - - """, - 'result': { - 'download_video_link': 'example.mp4', - 'sources': ['example.mp4', 'example.webm'], - }, - }, - { - 'name': 'video 3', - 'download_video': 'true', - 'source': '', - 'sources': [], - 'result': {}, - }, - - # self.download_video == False - { - 'name': 'video 4', - 'download_video': 'false', - 'source': 'example_source.mp4', - 'sources': """ - - - """, - 'result': { - 'sources': ['example.mp4', 'example.webm'], - }, - }, - ] - - initial_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'id': self.block.location.html_id(), - 'is_embed': False, - 'metadata': self.default_metadata_dict, - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': '', - } - initial_context['metadata']['duration'] = None - - for data in cases: - DATA = SOURCE_XML.format( # pylint: disable=invalid-name - download_video=data['download_video'], - source=data['source'], - sources=data['sources'], - name=data['name'], - ) - self.initialize_block(data=DATA) - 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'].get('sources', []), - }) - expected_context.update({ - 'display_name': data['name'], - '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'].get('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] - - # 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 - """ - # pylint: disable=invalid-name - # pylint: disable=redefined-outer-name - SOURCE_XML = """ - - """ - no_video_data = { - 'name': 'video 1', - 'download_video': 'true', - 'source': 'example_source.mp4', - 'sources': """ - - - """, - 'edx_video_id': "meow", - 'result': { - 'download_video_link': 'example.mp4', - 'sources': ['example.mp4', 'example.webm'], - } - } - DATA = SOURCE_XML.format( - download_video=no_video_data['download_video'], - source=no_video_data['source'], - sources=no_video_data['sources'], - edx_video_id=no_video_data['edx_video_id'], - name=no_video_data['name'], - ) - self.initialize_block(data=DATA) - - # Referencing a non-existent VAL ID in courseware won't cause an error -- - # it'll just fall back to the values in the VideoBlock. - self.block.student_view(None) - - # 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): - # pylint: disable=invalid-name, redefined-outer-name - SOURCE_XML = """ - - """ - - data = { - # test with download_video set to false and make sure download_video_link is not set (is None) - 'download_video': 'false', - 'source': 'example_source.mp4', - 'sources': """ - - - """, - 'edx_video_id': "mock item", - 'result': { - 'download_video_link': None, - # make sure the desktop_mp4 url is included as part of the alternative sources. - 'sources': ['example.mp4', 'example.webm', 'http://www.meowmix.com'], - } - } - - # Video found for edx_video_id - metadata = self.default_metadata_dict - metadata['autoplay'] = False - metadata['sources'] = "" - - initial_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'is_embed': False, - 'id': self.block.location.html_id(), - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'metadata': metadata, - 'transcript_feedback_enabled': False, - 'video_id': 'mock item', - } - - DATA = SOURCE_XML.format( # pylint: disable=invalid-name - download_video=data['download_video'], - source=data['source'], - sources=data['sources'], - edx_video_id=data['edx_video_id'] - ) - self.initialize_block(data=DATA) - - with patch('edxval.api.get_video_info') as mock_get_video_info: - mock_get_video_info.return_value = { - 'url': '/edxval/video/example', - 'edx_video_id': 'example', - 'duration': 111.0, - 'client_video_id': 'The example video', - 'encoded_videos': [ - { - 'url': 'http://www.meowmix.com', - 'file_size': 25556, - 'bitrate': 9600, - 'profile': 'desktop_mp4' - } - ] - } - 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']) - }) - - # 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) - - def encode_and_create_video(self, edx_video_id): - """ - Create and encode video to be used for tests - """ - encoded_videos = [] - for profile, extension in [("desktop_webm", "webm"), ("desktop_mp4", "mp4")]: - create_profile(profile) - encoded_videos.append( - dict( - url=f"http://fake-video.edx.org/{edx_video_id}.{extension}", - file_size=9000, - bitrate=42, - profile=profile, - ) - ) - result = create_video( - dict( - client_video_id='A Client Video id', - duration=111.0, - edx_video_id=edx_video_id, - status='test', - encoded_videos=encoded_videos, - ) - ) - assert result == edx_video_id - return encoded_videos - - def helper_get_html_with_edx_video_id(self, data): - """ - Create expected context and get actual context returned by `get_html` method. - """ - # make sure the urls for the various encodings are included as part of the alternative sources. - # pylint: disable=invalid-name, redefined-outer-name - SOURCE_XML = """ - - """ - - # Video found for edx_video_id - metadata = self.default_metadata_dict - metadata['sources'] = "" - - initial_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': 'example.mp4', - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'is_embed': False, - 'id': self.block.location.html_id(), - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'metadata': metadata, - 'transcript_feedback_enabled': False, - 'video_id': data['edx_video_id'].replace('\t', ' '), - } - - # pylint: disable=invalid-name - DATA = SOURCE_XML.format( - download_video=data['download_video'], - source=data['source'], - sources=data['sources'], - edx_video_id=data['edx_video_id'] - ) - self.initialize_block(data=DATA) - # context returned by get_html - context = self.block.student_view(None).content - - # expected_context, expected context to be returned by get_html - 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'], - 'is_video_from_same_origin': data['result']['is_video_from_same_origin'], - 'metadata': json.dumps(expected_context['metadata']) - }) - return context, expected_context - - # pylint: disable=invalid-name - @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) - @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 - """ - - def side_effect(*args, **kwargs): # pylint: disable=unused-argument - cdn = { - 'http://example.com/example.mp4': 'http://cdn-example.com/example.mp4', - 'http://example.com/example.webm': 'http://cdn-example.com/example.webm', - } - return cdn.get(args[1]) - - mocked_get_video.side_effect = side_effect - - source_xml = """ - - """ - - case_data = { - 'download_video': 'true', - 'source': 'example_source.mp4', - 'sources': """ - - - """, - 'result': { - 'download_video_link': 'http://example.com/example.mp4', - 'sources': [ - 'http://cdn-example.com/example.mp4', - 'http://cdn-example.com/example.webm' - ], - }, - } - - # Only videos with a video id should have their URLs rewritten - # based on CDN settings - cases = [ - dict(case_data, edx_video_id="vid-v1:12345"), - ] - - initial_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'block_id': str(self.block.location), - 'course_id': str(self.block.location.course_key), - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': None, - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'is_embed': False, - 'id': None, - 'metadata': self.default_metadata_dict, - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': 'vid-v1:12345', - } - initial_context['metadata']['duration'] = None - - for data in cases: - DATA = source_xml.format( - download_video=data['download_video'], - source=data['source'], - sources=data['sources'], - edx_video_id=data['edx_video_id'], - ) - self.initialize_block(data=DATA, runtime_kwargs={ - 'user_location': 'CN', - }) - 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' - 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'].get('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'].get('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] - - # Validate and compare contexts - assert get_context_from_dict(actual_context) == get_context_from_dict(expected_context) - - # pylint: disable=invalid-name - @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. - - For a video from a third part, which has 'external' status - in the VAL, the url-rewrite will not happen and URL will - remain unchanged in the get_html() method. - """ - - source_xml = """ - - """ - - case_data = { - 'download_video': 'true', - 'source': 'example_source.mp4', - 'sources': """ - - """, - 'result': { - 'download_video_link': 'http://example.com/example.mp4', - 'sources': [ - 'http://example.com/example.mp4', - ], - }, - } - - cases = [ - dict(case_data, edx_video_id="vid-v1:12345"), - ] - - initial_context = { - 'autoadvance_enabled': False, - 'license': None, - 'bumper_metadata': 'null', - 'cdn_eval': False, - 'cdn_exp_group': None, - 'display_name': 'A Name', - 'download_video_link': None, - 'is_video_from_same_origin': False, - 'handout': None, - 'hide_downloads': False, - 'id': None, - 'is_embed': False, - 'metadata': self.default_metadata_dict, - 'track': None, - 'transcript_download_format': 'srt', - 'transcript_download_formats_list': [ - {'display_name': 'SubRip (.srt) file', 'value': 'srt'}, - {'display_name': 'Text (.txt) file', 'value': 'txt'} - ], - 'poster': 'null', - 'transcript_feedback_enabled': False, - 'video_id': 'vid-v1:12345', - } - initial_context['metadata']['duration'] = None - - for data in cases: - DATA = source_xml.format( - download_video=data['download_video'], - source=data['source'], - sources=data['sources'], - edx_video_id=data['edx_video_id'], - ) - self.initialize_block(data=DATA) - - # Mocking the edxval API call because if not done, - # the method throws exception as no VAL entry is found - # for the corresponding edx-video-id - with patch('edxval.api.get_video_info') as mock_get_video_info: - mock_get_video_info.return_value = { - 'url': 'http://example.com/example.mp4', - 'edx_video_id': 'vid-v1:12345', - 'status': 'external', - 'duration': None, - 'client_video_id': 'external video', - 'encoded_videos': {} - } - 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'].get('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'].get('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] - - # 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 - @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, _): # noqa: PT019 - """ - Verify val profiles on toggling HLS Playback feature. - """ - with patch('xmodule.video_block.video_block.edxval_api.get_urls_for_profiles') as get_urls_for_profiles: - get_urls_for_profiles.return_value = { - 'desktop_webm': 'https://webm.com/dw.webm', - 'hls': 'https://hls.com/hls.m3u8', - 'youtube': 'https://yt.com/?v=v0TFmdO4ZP0', - 'desktop_mp4': 'https://mp4.com/dm.mp4' - } - with patch( - 'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled' - ) as feature_enabled: - feature_enabled.return_value = hls_feature_enabled - video_xml = '' - self.initialize_block(data=video_xml) - self.block.render(STUDENT_VIEW) - get_urls_for_profiles.assert_called_with( - self.block.edx_video_id, - 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, mock_render_django_template): - """ - Verify that hls profile functionality works as expected. - - * HLS source should be added into list of available sources - * HLS source should not be used for download URL If available from edxval - """ - video_xml = '' - - get_urls_for_profiles.return_value = { - 'desktop_webm': 'https://webm.com/dw.webm', - 'hls': 'https://hls.com/hls.m3u8', - 'youtube': 'https://yt.com/?v=v0TFmdO4ZP0', - 'desktop_mp4': 'https://mp4.com/dm.mp4' - } - - self.initialize_block(data=video_xml) - self.block.student_view(None) - - # Get the actual context that was passed to render_django_template - actual_context = mock_render_django_template.call_args.args[1] - - 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 - """ - video_xml = """ - - """ - - self.initialize_block(data=video_xml) - self.block.student_view(None) - - # 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, _): # noqa: PT019 - """ - Verify that `download_video_link` is available if a non HLS videos is available - """ - video_xml = """ - - """ - - self.initialize_block(data=video_xml) - context = self.block.student_view(None).content - assert "'download_video_link': 'http://example.com/example.mp4'" in context - - @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) - def test_html_student_public_view(self, _): # noqa: PT019 - """ - Test the student and public views - """ - video_xml = """ - - """ - - self.initialize_block(data=video_xml) - context = self.block.student_view(None).content - assert '"saveStateEnabled": true' in context - 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, _): # noqa: PT019 - """ - Verify that poster image functionality works as expected. - """ - video_xml = '' - get_course_video_image_url.return_value = '/media/video-images/poster.png' - - self.initialize_block(data=video_xml) - context = self.block.student_view(None).content - - 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, _): # noqa: PT019 - """ - Verify that poster image is set to None and there is no crash when no edx_video_id. - """ - video_xml = '' - get_course_video_image_url.return_value = '/media/video-images/poster.png' - - self.initialize_block(data=video_xml) - context = self.block.student_view(None).content - - 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, _): # noqa: PT019 - """ - Verify that `prioritize_hls` is set to `False` if `HLSPlaybackEnabledFlag` is disabled. - """ - video_xml = '' - self.initialize_block(data=video_xml) - context = self.block.student_view(None).content - assert '"prioritizeHls": false' in context - - @ddt.data( - { - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, - 'waffle_enabled': False, - 'youtube': '3_yD_cEKoCk', - 'hls': ['https://hls.com/hls.m3u8'], - 'result': 'true' - }, - { - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, - 'waffle_enabled': False, - 'youtube': '', - 'hls': ['https://hls.com/hls.m3u8'], - 'result': 'false' - }, - { - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, - 'waffle_enabled': False, - 'youtube': '', - 'hls': [], - 'result': 'false' - }, - { - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, - 'waffle_enabled': False, - 'youtube': '3_yD_cEKoCk', - 'hls': [], - 'result': 'true' - }, - { - 'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, - 'waffle_enabled': True, - 'youtube': '3_yD_cEKoCk', - 'hls': ['https://hls.com/hls.m3u8'], - '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, mock_render_django_template): - """ - Tests various combinations of a `prioritize_hls` flag being set in waffle and overridden for a course. - """ - metadata = { - 'html5_sources': ['http://youtu.be/3_yD_cEKoCk.mp4'] + data['hls'], - } - video_xml = ''.format( - data['youtube'] - ) - 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) - 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 -class TestVideoBlockInitialization(BaseTestVideoXBlock): - """ - Make sure that block initialization works correctly. - """ - CATEGORY = "video" - DATA = SOURCE_XML - METADATA = {} - - def setUp(self): - super().setUp() - self.setup_course() - - @ddt.data( - ( - { - 'youtube': 'v0TFmdO4ZP0', - 'hls': 'https://hls.com/hls.m3u8', - 'desktop_mp4': 'https://mp4.com/dm.mp4', - 'desktop_webm': 'https://webm.com/dw.webm', - }, - ['https://www.youtube.com/watch?v=v0TFmdO4ZP0'] - ), - ( - { - 'youtube': None, - 'hls': 'https://hls.com/hls.m3u8', - 'desktop_mp4': 'https://mp4.com/dm.mp4', - 'desktop_webm': 'https://webm.com/dw.webm', - }, - ['https://www.youtube.com/watch?v=3_yD_cEKoCk'] - ), - ( - { - 'youtube': None, - 'hls': None, - 'desktop_mp4': None, - 'desktop_webm': None, - }, - ['https://www.youtube.com/watch?v=3_yD_cEKoCk'] - ), - ) - @ddt.unpack - @patch( - 'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled', - Mock(return_value=True) - ) - def test_val_encoding_in_context(self, val_video_encodings, video_url): - """ - Tests that the val encodings correctly override the video url when the edx video id is set and - one or more encodings are present. - Accepted order of source priority is: - VAL's youtube source > external youtube source > hls > mp4 > webm. - - Note that `https://www.youtube.com/watch?v=3_yD_cEKoCk` is the default youtube source with which - a video component is initialized. Current implementation considers this youtube source as a valid - external youtube source. - """ - with patch('xmodule.video_block.video_block.edxval_api.get_urls_for_profiles') as get_urls_for_profiles: - get_urls_for_profiles.return_value = val_video_encodings - self.initialize_block( - data='' - ) - context = self.block.get_context() - assert context['transcripts_basic_tab_metadata']['video_url']['value'] == video_url - - @ddt.data( - ( - { - 'youtube': None, - 'hls': 'https://hls.com/hls.m3u8', - 'desktop_mp4': 'https://mp4.com/dm.mp4', - 'desktop_webm': 'https://webm.com/dw.webm', - }, - ['https://hls.com/hls.m3u8'] - ), - ( - { - 'youtube': 'v0TFmdO4ZP0', - 'hls': 'https://hls.com/hls.m3u8', - 'desktop_mp4': None, - 'desktop_webm': 'https://webm.com/dw.webm', - }, - ['https://www.youtube.com/watch?v=v0TFmdO4ZP0'] - ), - ) - @ddt.unpack - @patch( - 'openedx.core.djangoapps.video_config.services.VideoConfigService.is_hls_playback_enabled', - Mock(return_value=True) - ) - def test_val_encoding_in_context_without_external_youtube_source(self, val_video_encodings, video_url): - """ - Tests that the val encodings correctly override the video url when the edx video id is set and - one or more encodings are present. In this scenerio no external youtube source is provided. - Accepted order of source priority is: - VAL's youtube source > external youtube source > hls > mp4 > webm. - """ - with patch('xmodule.video_block.video_block.edxval_api.get_urls_for_profiles') as get_urls_for_profiles: - get_urls_for_profiles.return_value = val_video_encodings - # pylint: disable=line-too-long - self.initialize_block( - data='' - ) - context = self.block.get_context() - assert context['transcripts_basic_tab_metadata']['video_url']['value'] == video_url - - -@ddt.ddt -class TestEditorSavedMethod(BaseTestVideoXBlock): - """ - Make sure that `editor_saved` method works correctly. - """ - CATEGORY = "video" - DATA = SOURCE_XML - METADATA = {} - MODULESTORE = TEST_DATA_SPLIT_MODULESTORE - - def setUp(self): - super().setUp() - self.setup_course() - self.metadata = { - 'source': 'http://youtu.be/3_yD_cEKoCk', - 'html5_sources': ['http://example.org/video.mp4'], - } - # path to subs_3_yD_cEKoCk.srt.sjson file - self.file_name = 'subs_3_yD_cEKoCk.srt.sjson' - self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname() - self.file_path = self.test_dir + '/common/test/data/uploads/' + self.file_name - - def test_editor_saved_when_html5_sub_not_exist(self): - """ - When there is youtube_sub exist but no html5_sub present for - html5_sources, editor_saved function will generate new html5_sub - for video. - """ - self.initialize_block(metadata=self.metadata) - item = self.store.get_item(self.block.location) - with open(self.file_path, "rb") as myfile: # pylint: disable=bad-option-value, open-builtin - save_to_store(myfile.read(), self.file_name, 'text/sjson', item.location) - item.sub = "3_yD_cEKoCk" - # subs_video.srt.sjson does not exist before calling editor_saved function - with pytest.raises(NotFoundError): - Transcript.get_asset(item.location, 'subs_video.srt.sjson') - old_metadata = own_metadata(item) - # calling editor_saved will generate new file subs_video.srt.sjson for html5_sources - item.editor_saved(self.user, old_metadata, None) - assert isinstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent) - - def test_editor_saved_when_youtube_and_html5_subs_exist(self): - """ - When both youtube_sub and html5_sub already exist then no new - sub will be generated by editor_saved function. - """ - self.initialize_block(metadata=self.metadata) - item = self.store.get_item(self.block.location) - with open(self.file_path, "rb") as myfile: # pylint: disable=bad-option-value, open-builtin - save_to_store(myfile.read(), self.file_name, 'text/sjson', item.location) - save_to_store(myfile.read(), 'subs_video.srt.sjson', 'text/sjson', item.location) - item.sub = "3_yD_cEKoCk" - # subs_3_yD_cEKoCk.srt.sjson and subs_video.srt.sjson already exist - assert isinstance(Transcript.get_asset(item.location, self.file_name), StaticContent) - assert isinstance(Transcript.get_asset(item.location, 'subs_video.srt.sjson'), StaticContent) - old_metadata = own_metadata(item) - with patch( - 'openedx.core.djangoapps.video_config.services.manage_video_subtitles_save' - ) as manage_video_subtitles_save: - item.editor_saved(self.user, old_metadata, None) - assert not manage_video_subtitles_save.called - - def test_editor_saved_with_unstripped_video_id(self): - """ - Verify editor saved when video id contains spaces/tabs. - """ - stripped_video_id = str(uuid4()) - unstripped_video_id = '{video_id}{tabs}'.format(video_id=stripped_video_id, tabs='\t\t\t') - self.metadata.update({ - 'edx_video_id': unstripped_video_id - }) - self.initialize_block(metadata=self.metadata) - item = self.store.get_item(self.block.location) - assert item.edx_video_id == unstripped_video_id - - # Now, modifying and saving the video block should strip the video id. - old_metadata = own_metadata(item) - item.display_name = 'New display name' - item.editor_saved(self.user, old_metadata, None) - assert item.edx_video_id == stripped_video_id - - @patch('xmodule.video_block.video_block.edxval_api.get_url_for_profile', Mock(return_value='test_yt_id')) - def test_editor_saved_with_yt_val_profile(self): - """ - Verify editor saved overrides `youtube_id_1_0` when a youtube val profile is there - for a given `edx_video_id`. - """ - self.initialize_block(metadata=self.metadata) - item = self.store.get_item(self.block.location) - assert item.youtube_id_1_0 == '3_yD_cEKoCk' - - # Now, modify `edx_video_id` and save should override `youtube_id_1_0`. - old_metadata = own_metadata(item) - item.edx_video_id = str(uuid4()) - item.editor_saved(self.user, old_metadata, None) - assert item.youtube_id_1_0 == 'test_yt_id' - - -@ddt.ddt -class TestVideoBlockStudentViewJson(BaseTestVideoXBlock, CacheIsolationTestCase): - """ - Tests for the student_view_data method on VideoBlock. - """ - TEST_DURATION = 111.0 - TEST_PROFILE = "mobile" - TEST_SOURCE_URL = "http://www.example.com/source.mp4" - TEST_LANGUAGE = "ge" - TEST_ENCODED_VIDEO = { - 'profile': TEST_PROFILE, - 'bitrate': 333, - 'url': 'http://example.com/video', - 'file_size': 222, - } - TEST_EDX_VIDEO_ID = 'test_edx_video_id' - TEST_YOUTUBE_ID = 'test_youtube_id' - TEST_YOUTUBE_EXPECTED_URL = 'https://www.youtube.com/watch?v=test_youtube_id' - - def setUp(self): - super().setUp() - video_declaration = ( - ""] - ) - self.transcript_url = "transcript_url" - self.initialize_block(data=sample_xml) - self.video = self.block - self.video.runtime.handler_url = Mock(return_value=self.transcript_url) - - def setup_val_video(self, associate_course_in_val=False): - """ - Creates a video entry in VAL. - Arguments: - associate_course - If True, associates the test course with the video in VAL. - """ - create_profile('mobile') - create_video({ - 'edx_video_id': self.TEST_EDX_VIDEO_ID, - 'client_video_id': 'test_client_video_id', - 'duration': self.TEST_DURATION, - 'status': 'dummy', - 'encoded_videos': [self.TEST_ENCODED_VIDEO], - 'courses': [str(self.video.location.course_key)] if associate_course_in_val else [], - }) - self.val_video = get_video_info(self.TEST_EDX_VIDEO_ID) # pylint: disable=attribute-defined-outside-init - - def get_result(self, allow_cache_miss=True): - """ - Returns the result from calling the video's student_view_data method. - Arguments: - allow_cache_miss is passed in the context to the student_view_data method. - """ - context = { - "profiles": [self.TEST_PROFILE], - "allow_cache_miss": "True" if allow_cache_miss else "False" - } - return self.video.student_view_data(context) - - def verify_result_with_fallback_and_youtube(self, result): - """ - Verifies the result is as expected when returning "fallback" video data (not from VAL). - """ - self.assertDictEqual( # noqa: PT009 - result, - { - "only_on_web": False, - "duration": None, - "transcripts": {self.TEST_LANGUAGE: self.transcript_url}, - "encoded_videos": { - "fallback": {"url": self.TEST_SOURCE_URL, "file_size": 0}, - "youtube": {"url": self.TEST_YOUTUBE_EXPECTED_URL, "file_size": 0} - }, - "all_sources": [self.TEST_SOURCE_URL], - } - ) - - def verify_result_with_youtube_url(self, result): - """ - Verifies the result is as expected when returning "fallback" video data (not from VAL). - """ - self.assertDictEqual( # noqa: PT009 - result, - { - "only_on_web": False, - "duration": None, - "transcripts": {self.TEST_LANGUAGE: self.transcript_url}, - "encoded_videos": {"youtube": {"url": self.TEST_YOUTUBE_EXPECTED_URL, "file_size": 0}}, - "all_sources": [], - } - ) - - def verify_result_with_val_profile(self, result): - """ - Verifies the result is as expected when returning video data from VAL. - """ - assert_dict_contains_subset( - self, - result.pop("encoded_videos")[self.TEST_PROFILE], - self.TEST_ENCODED_VIDEO, - ) - self.assertDictEqual( # noqa: PT009 - result, - { - "only_on_web": False, - "duration": self.TEST_DURATION, - "transcripts": {self.TEST_LANGUAGE: self.transcript_url}, - 'all_sources': [self.TEST_SOURCE_URL], - } - ) - - def test_only_on_web(self): - self.video.only_on_web = True - result = self.get_result() - self.assertDictEqual(result, {"only_on_web": True}) # noqa: PT009 - - def test_no_edx_video_id(self): - result = self.get_result() - self.verify_result_with_fallback_and_youtube(result) - - def test_no_edx_video_id_and_no_fallback(self): - video_declaration = f"" - ]) - self.transcript_url = "transcript_url" - self.initialize_block(data=sample_xml) - self.video = self.block - self.video.runtime.handler_url = Mock(return_value=self.transcript_url) - result = self.get_result() - self.verify_result_with_youtube_url(result) - - @ddt.data(True, False) - def test_with_edx_video_id_video_associated_in_val(self, allow_cache_miss): - """ - Tests retrieving a video that is stored in VAL and associated with a course in VAL. - """ - self.video.edx_video_id = self.TEST_EDX_VIDEO_ID - self.setup_val_video(associate_course_in_val=True) - # the video is associated in VAL so no cache miss should ever happen but test retrieval in both contexts - result = self.get_result(allow_cache_miss) - self.verify_result_with_val_profile(result) - - @ddt.data(True, False) - def test_with_edx_video_id_video_unassociated_in_val(self, allow_cache_miss): - """ - Tests retrieving a video that is stored in VAL but not associated with a course in VAL. - """ - self.video.edx_video_id = self.TEST_EDX_VIDEO_ID - self.setup_val_video(associate_course_in_val=False) - result = self.get_result(allow_cache_miss) - if allow_cache_miss: - self.verify_result_with_val_profile(result) - else: - self.verify_result_with_fallback_and_youtube(result) - - @ddt.data(True, False) - def test_with_edx_video_id_video_not_in_val(self, allow_cache_miss): - """ - Tests retrieving a video that is not stored in VAL. - """ - self.video.edx_video_id = self.TEST_EDX_VIDEO_ID - # The video is not in VAL so in contexts that do and don't allow cache misses we should always get a fallback - result = self.get_result(allow_cache_miss) - self.verify_result_with_fallback_and_youtube(result) - - @ddt.data( - ({}, '', [], ['en']), - ({}, '', ['de'], ['de']), - ({}, '', ['en', 'de'], ['en', 'de']), - ({}, 'en-subs', ['de'], ['en', 'de']), - ({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de']), - ({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de']), - ) - @ddt.unpack - @patch('openedx.core.djangoapps.video_config.transcripts_utils.edxval_api.get_available_transcript_languages') - def test_student_view_with_val_transcripts_enabled(self, transcripts, english_sub, val_transcripts, - expected_transcripts, mock_get_transcript_languages): - """ - Test `student_view_data` with edx-val transcripts enabled. - """ - mock_get_transcript_languages.return_value = val_transcripts - self.video.transcripts = transcripts - self.video.sub = english_sub - student_view_response = self.get_result() - self.assertCountEqual(list(student_view_response['transcripts'].keys()), expected_transcripts) # noqa: PT009 - - -@ddt.ddt -class VideoBlockTest(TestCase, VideoBlockTestBase): - """ - Tests for video block that requires access to django settings. - """ - def setUp(self): - super().setUp() - self.block.runtime.handler_url = MagicMock() - self.temp_dir = mkdtemp() - file_system = OSFS(self.temp_dir) - self.file_system = file_system.makedir(EXPORT_IMPORT_COURSE_DIR, recreate=True) - self.addCleanup(shutil.rmtree, self.temp_dir) - - def get_video_transcript_data(self, video_id, language_code='en', file_format='srt', provider='Custom'): - return dict( - video_id=video_id, - language_code=language_code, - provider=provider, - file_format=file_format, - ) - - def test_get_context(self): - """" - Test get_context. - - This test is located here and not in xmodule.tests because get_context calls editable_metadata_fields. - Which, in turn, uses settings.LANGUAGES from django setttings. - """ - correct_tabs = [ - { - 'name': "Basic", - 'template': "video/transcripts.html", - 'current': True - }, - { - 'name': 'Advanced', - 'template': 'tabs/metadata-edit-tab.html' - } - ] - rendered_context = self.block.get_context() - self.assertListEqual(rendered_context['tabs'], correct_tabs) # noqa: PT009 - - # Assert that the Video ID field is present in basic tab metadata context. - assert rendered_context['transcripts_basic_tab_metadata']['edx_video_id'] ==\ - self.block.editable_metadata_fields['edx_video_id'] - - def test_export_val_data_with_internal(self): - """ - Tests that exported VAL videos are working as expected. - """ - language_code = 'ar' - transcript_file_name = 'test_edx_video_id-ar.srt' - expected_transcript_path = combine( - combine(self.temp_dir, EXPORT_IMPORT_COURSE_DIR), - combine(EXPORT_IMPORT_STATIC_DIR, transcript_file_name) - ) - self.block.edx_video_id = 'test_edx_video_id' - - create_profile('mobile') - create_video({ - 'edx_video_id': self.block.edx_video_id, - 'client_video_id': 'test_client_video_id', - 'duration': 111.0, - 'status': 'dummy', - 'encoded_videos': [{ - 'profile': 'mobile', - 'url': 'http://example.com/video', - 'file_size': 222, - 'bitrate': 333, - }], - }) - create_or_update_video_transcript( - video_id=self.block.edx_video_id, - language_code=language_code, - metadata={ - 'provider': 'Cielo24', - 'file_format': 'srt' - }, - file_data=ContentFile(TRANSCRIPT_FILE_SRT_DATA) - ) - - actual = self.block.definition_to_xml(resource_fs=self.file_system) - expected_str = """ - - """.format( # noqa: UP032 - language_code=language_code, - transcript_file=transcript_file_name, - transcripts=json.dumps({language_code: transcript_file_name}) - ) - parser = etree.XMLParser(remove_blank_text=True) - expected = etree.XML(expected_str, parser=parser) - self.assertXmlEqual(expected, actual) - - # Verify transcript file is created. - assert [transcript_file_name] == self.file_system.listdir(EXPORT_IMPORT_STATIC_DIR) - - # Also verify the content of created transcript file. - with open(expected_transcript_path) as transcript_path: - expected_transcript_content = File(transcript_path).read() - transcript = get_video_transcript_data(video_id=self.block.edx_video_id, language_code=language_code) - assert transcript['content'].decode('utf-8') == expected_transcript_content - - @ddt.data( - (['en', 'da'], 'test_sub', ''), - (['da'], 'test_sub', 'test_sub') - ) - @ddt.unpack - def test_export_val_transcripts_backward_compatibility(self, languages, sub, expected_sub): - """ - Tests new transcripts export for backward compatibility. - """ - self.block.edx_video_id = 'test_video_id' - self.block.sub = sub - - # Setup VAL encode profile, video and transcripts - create_profile('mobile') - create_video({ - 'edx_video_id': self.block.edx_video_id, - 'client_video_id': 'test_client_video_id', - 'duration': 111.0, - 'status': 'dummy', - 'encoded_videos': [{ - 'profile': 'mobile', - 'url': 'http://example.com/video', - 'file_size': 222, - 'bitrate': 333, - }], - }) - - for language in languages: - create_video_transcript( - video_id=self.block.edx_video_id, - language_code=language, - file_format=Transcript.SRT, - content=ContentFile(TRANSCRIPT_FILE_SRT_DATA) - ) - - # Export the video block into xml - video_xml = self.block.definition_to_xml(resource_fs=self.file_system) - - # Assert `sub` and `transcripts` attribute in the xml - assert video_xml.get('sub') == expected_sub - - expected_transcripts = { - language: "{edx_video_id}-{language}.srt".format( # noqa: UP032 - edx_video_id=self.block.edx_video_id, - language=language - ) - for language in languages - } - self.assertDictEqual(json.loads(video_xml.get('transcripts')), expected_transcripts) # noqa: PT009 - - # Assert transcript content from course OLX - for language in languages: - expected_transcript_path = combine( - combine(self.temp_dir, EXPORT_IMPORT_COURSE_DIR), - combine(EXPORT_IMPORT_STATIC_DIR, expected_transcripts[language]) - ) - with open(expected_transcript_path) as transcript_path: - expected_transcript_content = File(transcript_path).read() - transcript = get_video_transcript_data(video_id=self.block.edx_video_id, language_code=language) - assert transcript['content'].decode('utf-8') == expected_transcript_content - - def test_export_val_data_not_found(self): - """ - Tests that external video export works as expected. - """ - self.block.edx_video_id = 'nonexistent' - actual = self.block.definition_to_xml(resource_fs=self.file_system) - expected_str = """