""" Video xmodule tests in mongo. """ from contextlib import contextmanager import json import shutil from collections import OrderedDict 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 xmodule.contentstore.content import StaticContent from xmodule.course_block import ( COURSE_VIDEO_SHARING_ALL_VIDEOS, COURSE_VIDEO_SHARING_NONE, COURSE_VIDEO_SHARING_PER_VIDEO ) from xmodule.exceptions import NotFoundError from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE # noinspection PyUnresolvedReferences from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import from xmodule.tests.test_import import DummySystem from xmodule.tests.test_video import VideoBlockTestBase from xmodule.video_block import VideoBlock, bumper_utils, video_utils from xmodule.video_block.transcripts_utils import Transcript, save_to_store, subs_filename from xmodule.video_block.video_block import EXPORT_IMPORT_COURSE_DIR, EXPORT_IMPORT_STATIC_DIR from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE from lms.djangoapps.courseware.tests.helpers import get_context_dict_from_string from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE 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 .test_video_handlers import BaseTestVideoXBlock, TestVideo from .test_video_xml import SOURCE_XML, PUBLIC_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): # lint-amnesty, pylint: disable=missing-class-docstring, test-inherits-tests METADATA = {} def test_video_constructor(self): """Make sure that all parameters extracted correctly from xml""" context = self.block.student_view(None).content sources = ['example.mp4', 'example.webm'] expected_context = { 'autoadvance_enabled': False, 'branding_info': None, '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', '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'), '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', } mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests """Integration tests: web client + mongo.""" DATA = """ """ MODEL_DATA = { 'data': DATA, } METADATA = {} def test_video_constructor(self): """Make sure that if the 'youtube' attribute is omitted in XML, then the template generates an empty string for the YouTube streams. """ context = self.block.student_view(None).content sources = ['example.mp4', 'example.webm'] expected_context = { 'autoadvance_enabled': False, 'branding_info': None, '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', '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'), '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', } mako_service = self.block.runtime.service(self.block, 'mako') expected_result = get_context_dict_from_string( mako_service.render_lms_template('video.html', expected_context) ) assert get_context_dict_from_string(context) == expected_result assert expected_result['download_video_link'] == 'example.mp4' assert expected_result['display_name'] == 'A Name' @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 self.block.is_public_sharing_enabled() == feature_enabled def test_is_public_sharing_enabled__not_public(self): self.block.public_access = False with self.mock_feature_toggle(): assert not self.block.is_public_sharing_enabled() @patch('xmodule.video_block.video_block.VideoBlock.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 = self.block.is_public_sharing_enabled() # Then I will get that course value self.assertTrue(is_public_sharing_enabled) @patch('xmodule.video_block.video_block.VideoBlock.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 = self.block.is_public_sharing_enabled() # Then I will get that course value self.assertFalse(is_public_sharing_enabled) @ddt.data(COURSE_VIDEO_SHARING_PER_VIDEO, None) @patch('xmodule.video_block.video_block.VideoBlock.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 = self.block.is_public_sharing_enabled() # I will get the per-video value self.assertEqual(self.block.public_access, is_public_sharing_enabled) @patch('xmodule.video_block.video_block.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 = self.block.is_public_sharing_enabled() # I will fall-back to per-video values self.assertEqual(self.block.public_access, is_public_sharing_enabled) @ddt.data(False, True) def test_context(self, is_public_sharing_enabled): with self.mock_feature_toggle(): with patch.object( self.block, 'is_public_sharing_enabled', return_value=is_public_sharing_enabled ): content = self.block.student_view(None).content context = get_context_dict_from_string(content) 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'), '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('/?') def test_get_html_track(self): # pylint: disable=invalid-name # lint-amnesty, 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, 'branding_info': None, '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', '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', } 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') context = self.block.student_view(None).content 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) }) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) def test_get_html_source(self): # lint-amnesty, 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, 'branding_info': None, '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', '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', } initial_context['metadata']['duration'] = None for data in cases: DATA = SOURCE_XML.format( # lint-amnesty, pylint: disable=invalid-name download_video=data['download_video'], source=data['source'], sources=data['sources'], name=data['name'], ) self.initialize_block(data=DATA) context = self.block.student_view(None).content 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']) }) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) def test_get_html_with_non_existent_edx_video_id(self): """ Tests the VideoBlock get_html where a edx_video_id is given but a video is not found """ # pylint: disable=invalid-name # lint-amnesty, 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. assert 'example.mp4' in self.block.student_view(None).content def test_get_html_with_mocked_edx_video_id(self): # lint-amnesty, 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, 'branding_info': None, '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', '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, } DATA = SOURCE_XML.format( # lint-amnesty, 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' } ] } context = self.block.student_view(None).content 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']) }) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) def test_get_html_with_existing_edx_video_id(self): """ Tests the `VideoBlock` `get_html` where `edx_video_id` is given and related video is found """ edx_video_id = 'thundercats' # create video with provided edx_video_id and return encoded_videos encoded_videos = self.encode_and_create_video(edx_video_id) # data to be used to retrieve video by edxval API data = { 'download_video': 'true', 'source': 'example_source.mp4', 'sources': """ """, 'edx_video_id': edx_video_id, 'result': { 'download_video_link': f'http://fake-video.edx.org/{edx_video_id}.mp4', 'sources': ['example.mp4', 'example.webm'] + [video['url'] for video in encoded_videos], }, } # context returned by get_html when provided with above data # expected_context, a dict to assert with context context, expected_context = self.helper_get_html_with_edx_video_id(data) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) def test_get_html_with_existing_unstripped_edx_video_id(self): """ Tests the `VideoBlock` `get_html` where `edx_video_id` with some unwanted tab(\t) is given and related video is found """ edx_video_id = 'thundercats' # create video with provided edx_video_id and return encoded_videos encoded_videos = self.encode_and_create_video(edx_video_id) # data to be used to retrieve video by edxval API # unstripped edx_video_id is provided here data = { 'download_video': 'true', 'source': 'example_source.mp4', 'sources': """ """, 'edx_video_id': f"{edx_video_id}\t", 'result': { 'download_video_link': f'http://fake-video.edx.org/{edx_video_id}.mp4', 'sources': ['example.mp4', 'example.webm'] + [video['url'] for video in encoded_videos], }, } # context returned by get_html when provided with above data # expected_context, a dict to assert with context context, expected_context = self.helper_get_html_with_edx_video_id(data) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) def 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. # lint-amnesty, 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, 'branding_info': None, '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', '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, } # 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'], 'metadata': json.dumps(expected_context['metadata']) }) return context, expected_context # pylint: disable=invalid-name @patch('xmodule.video_block.video_block.BrandingInfoConfig') @patch('xmodule.video_block.video_block.rewrite_video_url') def test_get_html_cdn_source(self, mocked_get_video, mock_BrandingInfoConfig): """ Test if sources got from CDN """ mock_BrandingInfoConfig.get_config.return_value = { "CN": { 'url': 'http://www.xuetangx.com', 'logo_src': 'http://www.xuetangx.com/static/images/logo.png', 'logo_tag': 'Video hosted by XuetangX.com' } } def side_effect(*args, **kwargs): # lint-amnesty, 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, 'branding_info': { 'logo_src': 'http://www.xuetangx.com/static/images/logo.png', 'logo_tag': 'Video hosted by XuetangX.com', 'url': 'http://www.xuetangx.com' }, '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, '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', } 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' context = self.block.student_view(None).content 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']) }) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) # pylint: disable=invalid-name def test_get_html_cdn_source_external_video(self): """ 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, 'branding_info': None, 'license': None, 'bumper_metadata': 'null', 'cdn_eval': False, 'cdn_exp_group': None, 'display_name': 'A Name', 'download_video_link': None, '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', } 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': {} } context = self.block.student_view(None).content 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']) }) mako_service = self.block.runtime.service(self.block, 'mako') assert get_context_dict_from_string(context) ==\ get_context_dict_from_string(mako_service.render_lms_template('video.html', expected_context)) @ddt.data( (True, ['youtube', 'desktop_webm', 'desktop_mp4', 'hls']), (False, ['youtube', 'desktop_webm', 'desktop_mp4']) ) @ddt.unpack def test_get_html_on_toggling_hls_feature(self, hls_feature_enabled, expected_val_profiles): """ 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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_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): """ 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) context = self.block.student_view(None).content assert "'download_video_link': 'https://mp4.com/dm.mp4'" in context assert '"streams": "1.00:https://yt.com/?v=v0TFmdO4ZP0"' in context assert sorted(['https://webm.com/dw.webm', 'https://mp4.com/dm.mp4', 'https://hls.com/hls.m3u8']) ==\ sorted(get_context_dict_from_string(context)['metadata']['sources']) def test_get_html_hls_no_video_id(self): """ Verify that `download_video_link` is set to None for HLS videos if no video id """ video_xml = """ """ self.initialize_block(data=video_xml) context = self.block.student_view(None).content assert "'download_video_link': None" in context def test_get_html_non_hls_video_download(self): """ 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 def test_html_student_public_view(self): """ 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('xmodule.video_block.video_block.edxval_api.get_course_video_image_url') def test_poster_image(self, get_course_video_image_url): """ 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('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): """ 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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_enabled', Mock(return_value=False)) def test_hls_primary_playback_on_toggling_hls_feature(self): """ 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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_enabled', Mock(return_value=True)) def test_deprecate_youtube_course_waffle_flag(self, data): """ 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) context = self.block.student_view(None).content assert '"prioritizeHls": {}'.format(data['result']) in context @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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_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('xmodule.video_block.video_block.HLSPlaybackEnabledFlag.feature_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: # lint-amnesty, 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: # lint-amnesty, 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('xmodule.video_block.video_block.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( 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( 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. """ self.assertDictContainsSubset( result.pop("encoded_videos")[self.TEST_PROFILE], self.TEST_ENCODED_VIDEO, ) self.assertDictEqual( 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}) 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('xmodule.video_block.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) @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) # 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( 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( edx_video_id=self.block.edx_video_id, language=language ) for language in languages } self.assertDictEqual(json.loads(video_xml.get('transcripts')), expected_transcripts) # 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 = """