Files
edx-platform/lms/djangoapps/courseware/tests/test_video_mongo.py
Muhammad Farhan Khan 9b6445cb7b refactor: use video block utils from xblocks-contrib package (#38088)
* refactor: use bumper_utils from xblocks-contrib package
* refactor: use video_handlers from xblocks-contrib package
* fix: fix test_video_handlers test cases
2026-03-09 17:13:16 +05:00

2602 lines
106 KiB
Python

"""
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 openedx.core.djangoapps.video_config.sharing 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 mock_render_template, override_descriptor_system # pylint: disable=unused-import
from xmodule.tests.test_import import DummyModuleStoreRuntime
from xmodule.tests.test_video import VideoBlockTestBase
from xmodule.video_block import VideoBlock, video_utils
from xblocks_contrib.video import bumper_utils
from openedx.core.djangoapps.video_config.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_from_dict
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
from openedx.core.djangoapps.video_config import sharing
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
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
from common.test.utils import assert_dict_contains_subset
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 = {}
@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 = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson"
download_video="true"
start_time="3603.0" end_time="3610.0"
>
<source src="example.mp4"/>
<source src="example.webm"/>
</video>
"""
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)
@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)
@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)
@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)
@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
# lint-amnesty, pylint: disable=redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
display_name="{name}"
sub="{sub}" download_track="{download_track}"
start_time="3603.0" end_time="3610.0" download_video="true"
>
<source src="example.mp4"/>
<source src="example.webm"/>
{track}
{transcripts}
</video>
"""
cases = [
{
'name': 'video 1',
'download_track': 'true',
'track': '<track src="http://www.example.com/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': '<track src="http://www.example.com/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': '<transcript language="uk" src="ukrainian.srt" />',
},
]
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):
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
display_name="{name}"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="3603.0" end_time="3610.0"
>
{sources}
</video>
"""
cases = [
# self.download_video == True
{
'name': 'video 1',
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'result': {
'download_video_link': 'example.mp4',
'sources': ['example.mp4', 'example.webm'],
},
},
{
'name': 'video 2',
'download_video': 'true',
'source': '',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'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': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'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( # 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)
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
# lint-amnesty, pylint: disable=redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
display_name="{name}"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="3603.0" end_time="3610.0"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
no_video_data = {
'name': 'video 1',
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'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):
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="3603.0" end_time="3610.0"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
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': """
<source src="example.mp4"/>
<source src="example.webm"/>
""",
'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( # 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'
}
]
}
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.
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
SOURCE_XML = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
start_time="3603.0" end_time="3610.0"
edx_video_id="{edx_video_id}"
>
{sources}
</video>
"""
# 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('xmodule.video_block.video_block.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): # 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 = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
edx_video_id="{edx_video_id}"
start_time="3603.0" end_time="3610.0"
>
{sources}
</video>
"""
case_data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="http://example.com/example.mp4"/>
<source src="http://example.com/example.webm"/>
""",
'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 = """
<video show_captions="true"
display_name="A Name"
sub="a_sub_file.srt.sjson" source="{source}"
download_video="{download_video}"
edx_video_id="{edx_video_id}"
start_time="3603.0" end_time="3610.0"
>
{sources}
</video>
"""
case_data = {
'download_video': 'true',
'source': 'example_source.mp4',
'sources': """
<source src="http://example.com/example.mp4"/>
""",
'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, _):
"""
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 = '<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
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 = '<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
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 = """
<video display_name="Video" download_video="true" source="https://hls.com/hls.m3u8">
["https://hls.com/hls2.m3u8", "https://hls.com/hls3.m3u8"]
</video>
"""
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, _):
"""
Verify that `download_video_link` is available if a non HLS videos is available
"""
video_xml = """
<video display_name="Video" download_video="true">
<source src="http://example.com/example.m3u8"/>
<source src="http://example.com/example.mp4"/>
<source src="http://example.com/example.webm"/>
</video>
"""
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, _):
"""
Test the student and public views
"""
video_xml = """
<video display_name="Video" download_video="true" source="https://hls.com/hls.m3u8">
["https://hls.com/hls2.m3u8", "https://hls.com/hls3.m3u8"]
</video>
"""
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, _):
"""
Verify that poster image functionality works as expected.
"""
video_xml = '<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
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, _):
"""
Verify that poster image is set to None and there is no crash when no edx_video_id.
"""
video_xml = '<video display_name="Video" download_video="true" edx_video_id="null">[]</video>'
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, _):
"""
Verify that `prioritize_hls` is set to `False` if `HLSPlaybackEnabledFlag` is disabled.
"""
video_xml = '<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
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 = '<video display_name="Video" edx_video_id="12345-67890" youtube_id_1_0="{}">[]</video>'.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='<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
)
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='<video display_name="Video" youtube_id_1_0="" download_video="true" edx_video_id="12345-67890">[]</video>'
)
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(
'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 = (
"<video display_name='Test Video' edx_video_id='123' youtube_id_1_0=\'" + self.TEST_YOUTUBE_ID + "\'>"
)
sample_xml = ''.join([
video_declaration,
"<source src='", self.TEST_SOURCE_URL, "'/> ",
"<transcript language='", self.TEST_LANGUAGE, "' src='german_translation.srt' /> ",
"</video>"]
)
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.
"""
assert_dict_contains_subset(
self,
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"<video display_name='Test Video 2' youtube_id_1_0=\'{self.TEST_YOUTUBE_ID}\'>"
# the video has no source listed, only a youtube link, so no fallback url will be provided
sample_xml = ''.join([
video_declaration,
"<transcript language='", self.TEST_LANGUAGE, "' src='german_translation.srt' /> ",
"</video>"
])
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)
@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 = """
<video youtube="1.00:3_yD_cEKoCk" url_name="SampleProblem" transcripts='{transcripts}'>
<video_asset client_video_id="test_client_video_id" duration="111.0" image="">
<encoded_video profile="mobile" url="http://example.com/video" file_size="222" bitrate="333"/>
<transcripts>
<transcript file_format="srt" language_code="{language_code}" provider="Cielo24"/>
</transcripts>
</video_asset>
<transcript language="{language_code}" src="{transcript_file}"/>
</video>
""".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 = """<video youtube="1.00:3_yD_cEKoCk" url_name="SampleProblem"/>"""
parser = etree.XMLParser(remove_blank_text=True)
expected = etree.XML(expected_str, parser=parser)
self.assertXmlEqual(expected, actual)
@patch('openedx.core.djangoapps.video_config.transcripts_utils.get_video_ids_info')
def test_export_no_video_ids(self, mock_get_video_ids_info):
"""
Tests export when there is no video id. `export_to_xml` only works in case of video id.
"""
mock_get_video_ids_info.return_value = True, []
actual = self.block.definition_to_xml(resource_fs=self.file_system)
expected_str = '<video youtube="1.00:3_yD_cEKoCk" url_name="SampleProblem"></video>'
parser = etree.XMLParser(remove_blank_text=True)
expected = etree.XML(expected_str, parser=parser)
self.assertXmlEqual(expected, actual)
def test_import_val_data_internal(self):
"""
Test that import val data internal works as expected.
"""
create_profile('mobile')
module_system = DummyModuleStoreRuntime(load_error_blocks=True)
edx_video_id = 'test_edx_video_id'
sub_id = '0CzPOIIdUsA'
external_transcript_name = 'The_Flash.srt'
external_transcript_language_code = 'ur'
val_transcript_language_code = 'ar'
val_transcript_provider = 'Cielo24'
external_transcripts = {
external_transcript_language_code: external_transcript_name
}
# Create static directory in import file system and place transcript files inside it.
module_system.resources_fs.makedirs(EXPORT_IMPORT_STATIC_DIR, recreate=True)
# Create VAL transcript.
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
'test_edx_video_id-ar.srt',
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
# Create self.sub and self.transcripts transcript.
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
subs_filename(sub_id, self.block.transcript_language),
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
external_transcript_name,
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
xml_data = """
<video edx_video_id='{edx_video_id}' sub='{sub_id}' transcripts='{transcripts}'>
<video_asset client_video_id="test_client_video_id" duration="111.0">
<encoded_video profile="mobile" url="http://example.com/video" file_size="222" bitrate="333"/>
<transcripts>
<transcript file_format="srt" language_code="{val_transcript_language_code}" provider="{val_transcript_provider}"/>
</transcripts>
</video_asset>
</video>
""".format(
edx_video_id=edx_video_id,
sub_id=sub_id,
transcripts=json.dumps(external_transcripts),
val_transcript_language_code=val_transcript_language_code,
val_transcript_provider=val_transcript_provider
)
xml_object = etree.fromstring(xml_data)
module_system.id_generator.target_course_id = "test_course_id"
video = self.block.parse_xml(xml_object, module_system, None)
assert video.edx_video_id == 'test_edx_video_id'
video_data = get_video_info(video.edx_video_id)
assert video_data['client_video_id'] == 'test_client_video_id'
assert video_data['duration'] == 111.0
assert video_data['status'] == 'imported'
assert video_data['courses'] == [{module_system.id_generator.target_course_id: None}]
assert video_data['encoded_videos'][0]['profile'] == 'mobile'
assert video_data['encoded_videos'][0]['url'] == 'http://example.com/video'
assert video_data['encoded_videos'][0]['file_size'] == 222
assert video_data['encoded_videos'][0]['bitrate'] == 333
# Verify that VAL transcript is imported.
assert_dict_contains_subset(
self,
self.get_video_transcript_data(
edx_video_id,
language_code=val_transcript_language_code,
provider=val_transcript_provider
),
get_video_transcript(video.edx_video_id, val_transcript_language_code),
)
# Verify that transcript from sub field is imported.
assert_dict_contains_subset(
self,
self.get_video_transcript_data(
edx_video_id,
language_code=self.block.transcript_language
),
get_video_transcript(video.edx_video_id, self.block.transcript_language),
)
# Verify that transcript from transcript field is imported.
assert_dict_contains_subset(
self,
self.get_video_transcript_data(
edx_video_id,
language_code=external_transcript_language_code
),
get_video_transcript(video.edx_video_id, external_transcript_language_code),
)
def test_import_no_video_id(self):
"""
Test that importing a video with no video id, creates a new external video.
"""
xml_data = """<video><video_asset></video_asset></video>"""
xml_object = etree.fromstring(xml_data)
module_system = DummyModuleStoreRuntime(load_error_blocks=True)
# Verify edx_video_id is empty before.
assert self.block.edx_video_id == ''
video = self.block.parse_xml(xml_object, module_system, None)
# Verify edx_video_id is populated after the import.
assert video.edx_video_id != ''
video_data = get_video_info(video.edx_video_id)
assert video_data['client_video_id'] == 'External Video'
assert video_data['duration'] == 0.0
assert video_data['status'] == 'external'
def test_import_val_transcript(self):
"""
Test that importing a video with val transcript, creates a new transcript record.
"""
edx_video_id = 'test_edx_video_id'
val_transcript_language_code = 'es'
val_transcript_provider = 'Cielo24'
xml_data = """
<video edx_video_id='{edx_video_id}'>
<video_asset client_video_id="test_client_video_id" duration="111.0">
<transcripts>
<transcript file_format="srt" language_code="{val_transcript_language_code}" provider="{val_transcript_provider}"/>
</transcripts>
</video_asset>
</video>
""".format(
edx_video_id=edx_video_id,
val_transcript_language_code=val_transcript_language_code,
val_transcript_provider=val_transcript_provider
)
xml_object = etree.fromstring(xml_data)
module_system = DummyModuleStoreRuntime(load_error_blocks=True)
# Create static directory in import file system and place transcript files inside it.
module_system.resources_fs.makedirs(EXPORT_IMPORT_STATIC_DIR, recreate=True)
# Create VAL transcript.
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
'test_edx_video_id-es.srt',
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
# Verify edx_video_id is empty before.
assert self.block.edx_video_id == ''
video = self.block.parse_xml(xml_object, module_system, None)
# Verify edx_video_id is populated after the import.
assert video.edx_video_id != ''
video_data = get_video_info(video.edx_video_id)
assert video_data['status'] == 'external'
# Verify that VAL transcript is imported.
assert_dict_contains_subset(
self,
self.get_video_transcript_data(
edx_video_id,
language_code=val_transcript_language_code,
provider=val_transcript_provider
),
get_video_transcript(video.edx_video_id, val_transcript_language_code),
)
@ddt.data(
(
'test_sub_id',
{'en': 'The_Flash.srt'},
'<transcripts><transcript file_format="srt" language_code="en" provider="Cielo24"/></transcripts>',
# VAL transcript takes priority
{
'video_id': 'test_edx_video_id',
'language_code': 'en',
'file_format': 'srt',
'provider': 'Cielo24'
}
),
(
'',
{'en': 'The_Flash.srt'},
'<transcripts><transcript file_format="srt" language_code="en" provider="Cielo24"/></transcripts>',
# VAL transcript takes priority
{
'video_id': 'test_edx_video_id',
'language_code': 'en',
'file_format': 'srt',
'provider': 'Cielo24'
}
),
(
'test_sub_id',
{},
'<transcripts><transcript file_format="srt" language_code="en" provider="Cielo24"/></transcripts>',
# VAL transcript takes priority
{
'video_id': 'test_edx_video_id',
'language_code': 'en',
'file_format': 'srt',
'provider': 'Cielo24'
}
),
(
'test_sub_id',
{'en': 'The_Flash.srt'},
'',
# self.sub transcript takes priority
{
'video_id': 'test_edx_video_id',
'language_code': 'en',
'file_format': 'sjson',
'provider': 'Custom'
}
),
(
'',
{'en': 'The_Flash.srt'},
'',
# self.transcripts would be saved.
{
'video_id': 'test_edx_video_id',
'language_code': 'en',
'file_format': 'srt',
'provider': 'Custom'
}
)
)
@ddt.unpack
def test_import_val_transcript_priority(self, sub_id, external_transcripts, val_transcripts, expected_transcript):
"""
Test that importing a video with different type of transcripts for same language,
creates expected transcript record.
"""
edx_video_id = 'test_edx_video_id'
language_code = 'en'
module_system = DummyModuleStoreRuntime(load_error_blocks=True)
# Create static directory in import file system and place transcript files inside it.
module_system.resources_fs.makedirs(EXPORT_IMPORT_STATIC_DIR, recreate=True)
xml_data = "<video edx_video_id='test_edx_video_id'"
# Prepare self.sub transcript data.
if sub_id:
create_file_in_fs(
TRANSCRIPT_FILE_SJSON_DATA,
subs_filename(sub_id, language_code),
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
xml_data += " sub='{sub_id}'".format(
sub_id=sub_id
)
# Prepare self.transcripts transcripts data.
if external_transcripts:
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
external_transcripts['en'],
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
xml_data += " transcripts='{transcripts}'".format(
transcripts=json.dumps(external_transcripts),
)
xml_data += '><video_asset client_video_id="test_client_video_id" duration="111.0">'
# Prepare VAL transcripts data.
if val_transcripts:
create_file_in_fs(
TRANSCRIPT_FILE_SRT_DATA,
'{edx_video_id}-{language_code}.srt'.format(
edx_video_id=edx_video_id,
language_code=language_code
),
module_system.resources_fs,
EXPORT_IMPORT_STATIC_DIR
)
xml_data += val_transcripts
xml_data += '</video_asset></video>'
xml_object = etree.fromstring(xml_data)
# Verify edx_video_id is empty before import.
assert self.block.edx_video_id == ''
video = self.block.parse_xml(xml_object, module_system, None)
# Verify edx_video_id is not empty after import.
assert video.edx_video_id != ''
video_data = get_video_info(video.edx_video_id)
assert video_data['status'] == 'external'
# Verify that correct transcripts are imported.
assert_dict_contains_subset(
self,
expected_transcript,
get_video_transcript(video.edx_video_id, language_code),
)
def test_import_val_data_invalid(self):
create_profile('mobile')
module_system = DummyModuleStoreRuntime(load_error_blocks=True)
# Negative file_size is invalid
xml_data = """
<video edx_video_id="test_edx_video_id">
<video_asset client_video_id="test_client_video_id" duration="111.0">
<encoded_video profile="mobile" url="http://example.com/video" file_size="-222" bitrate="333"/>
</video_asset>
</video>
"""
xml_object = etree.fromstring(xml_data)
with pytest.raises(ValCannotCreateError):
VideoBlock.parse_xml(xml_object, module_system, None)
with pytest.raises(ValVideoNotFoundError):
get_video_info("test_edx_video_id")
class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
"""
Tests rendered content in presence of video bumper.
"""
CATEGORY = "video"
METADATA = {}
# Use temporary FEATURES in this test without affecting the original
FEATURES = dict(settings.FEATURES)
@patch('xblocks_contrib.video.bumper_utils.get_bumper_settings')
def test_is_bumper_enabled(self, get_bumper_settings):
"""
Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True
Assume that bumper settings are correct.
"""
self.FEATURES.update({
"SHOW_BUMPER_PERIODICITY": 1,
"ENABLE_VIDEO_BUMPER": True,
})
get_bumper_settings.return_value = {
"video_id": "edx_video_id",
"transcripts": {},
}
with override_settings(FEATURES=self.FEATURES):
assert bumper_utils.is_bumper_enabled(self.block)
self.FEATURES.update({"ENABLE_VIDEO_BUMPER": False})
with override_settings(FEATURES=self.FEATURES):
assert not bumper_utils.is_bumper_enabled(self.block)
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
@patch('xblocks_contrib.video.bumper_utils.is_bumper_enabled')
@patch('xblocks_contrib.video.bumper_utils.get_bumper_settings')
@patch('edxval.api.get_urls_for_profiles')
def test_bumper_metadata(
self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled, mock_render_django_template
):
"""
Test content with rendered bumper metadata.
"""
get_url_for_profiles.return_value = {
'desktop_mp4': 'http://test_bumper.mp4',
'desktop_webm': '',
}
get_bumper_settings.return_value = {
'video_id': 'edx_video_id',
'transcripts': {},
}
is_bumper_enabled.return_value = True
self.block.student_view(None)
sources = ['example.mp4', 'example.webm']
expected_context = {
'autoadvance_enabled': False,
'license': None,
'bumper_metadata': json.dumps(OrderedDict({
'saveStateUrl': self.block.ajax_url + '/save_user_state',
'showCaptions': 'true',
'sources': ['http://test_bumper.mp4'],
'streams': '',
'transcriptLanguage': 'en',
'transcriptLanguages': {'en': 'English'},
'transcriptTranslationUrl': video_utils.set_query_parameter(
self.get_handler_url('transcript', 'translation/__lang__'), 'is_bumper', 1
),
'transcriptAvailableTranslationsUrl': video_utils.set_query_parameter(
self.get_handler_url('transcript', 'available_translations'), 'is_bumper', 1
),
"publishCompletionUrl": video_utils.set_query_parameter(
self.get_handler_url('publish_completion', ''), 'is_bumper', 1
),
})),
'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': '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
'sources': sources,
'poster': None,
'duration': 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': json.dumps(OrderedDict({
'url': 'http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg',
'type': 'youtube'
})),
'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)
@ddt.ddt
class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inherits-tests
"""
Tests the server side of video auto-advance.
"""
maxDiff = None
CATEGORY = "video"
METADATA = {}
# Use temporary FEATURES in this test without affecting the original
FEATURES = dict(settings.FEATURES)
def prepare_expected_context(self, autoadvanceenabled_flag, autoadvance_flag):
"""
Build a dictionary with data expected by some operations in this test.
Only parameters related to auto-advance are variable, rest is fixed.
"""
context = {
'autoadvance_enabled': autoadvanceenabled_flag,
'block_id': str(self.block.location),
'course_id': str(self.block.location.course_key),
'license': None,
'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(),
'bumper_metadata': 'null',
'metadata': json.dumps(OrderedDict({
'autoAdvance': autoadvance_flag,
'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': ['example.mp4', 'example.webm'],
'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.block.runtime.handler_url(
self.block, 'transcript', 'translation/__lang__'
).rstrip('/?'),
'transcriptAvailableTranslationsUrl': self.block.runtime.handler_url(
self.block, 'transcript', 'available_translations'
).rstrip('/?'),
'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': '',
}
return context
def assert_content_matches_expectations(
self, autoadvanceenabled_must_be, autoadvance_must_be, mock_render_django_template
):
"""
Check (assert) that loading video.html produces content that corresponds
to the passed context.
Helper function to avoid code repetition.
"""
with override_settings(FEATURES=self.FEATURES):
self.block.student_view(None)
expected_context = self.prepare_expected_context(
autoadvanceenabled_flag=autoadvanceenabled_must_be,
autoadvance_flag=autoadvance_must_be,
)
# 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 change_course_setting_autoadvance(self, new_value):
"""
Change the .video_auto_advance course setting (a.k.a. advanced setting).
This avoids doing .save(), and instead modifies the instance directly.
Based on test code for video_bumper setting.
"""
# This first render is done to initialize the instance
self.block.render(STUDENT_VIEW)
self.block.video_auto_advance = new_value
self.block._reset_dirty_field(self.block.fields['video_auto_advance']) # pylint: disable=protected-access
# After this step, render() should see the new value
# e.g. use self.block.student_view(None).content
@ddt.data(
(False, False),
(False, True),
(True, False),
(True, True),
)
@ddt.unpack
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_is_autoadvance_available_and_enabled(self, global_setting, course_setting, mock_render_django_template):
"""
Check that the autoadvance is not available when it is disabled via feature flag
(ENABLE_AUTOADVANCE_VIDEOS set to False) or by the course setting.
It checks that:
- only when the feature flag and the course setting are True (at the same time)
the controls are visible
- in that case (when the controls are visible) the video will autoadvance
(because that's the default), in other cases it won't
"""
self.FEATURES.update({"ENABLE_AUTOADVANCE_VIDEOS": global_setting})
self.change_course_setting_autoadvance(new_value=course_setting)
self.assert_content_matches_expectations(
autoadvanceenabled_must_be=(global_setting and course_setting),
autoadvance_must_be=(global_setting and course_setting),
mock_render_django_template=mock_render_django_template,
)