diff --git a/common/lib/xmodule/xmodule/video_module/video_handlers.py b/common/lib/xmodule/xmodule/video_module/video_handlers.py index 4ed6e16e18..0438c15f71 100644 --- a/common/lib/xmodule/xmodule/video_module/video_handlers.py +++ b/common/lib/xmodule/xmodule/video_module/video_handlers.py @@ -250,9 +250,10 @@ class VideoStudentViewHandlers(object): response.content_type = Transcript.mime_types['sjson'] elif dispatch == 'download': + lang = request.GET.get('lang', None) try: transcript_content, transcript_filename, transcript_mime_type = self.get_transcript( - transcripts, transcript_format=self.transcript_download_format + transcripts, transcript_format=self.transcript_download_format, lang=lang ) except (NotFoundError, ValueError, KeyError, UnicodeDecodeError): log.debug("Video@download exception") diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 574a4ffcf9..cffc8ad13c 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -20,12 +20,12 @@ import logging import random from collections import OrderedDict from operator import itemgetter - from lxml import etree from pkg_resources import resource_string from django.conf import settings +from openedx.core.lib.cache_utils import memoize_in_request_cache from xblock.core import XBlock from xblock.fields import ScopeIds from xblock.runtime import KvsFieldData @@ -329,6 +329,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, return self.system.render_template('video.html', context) +@XBlock.wants("request_cache") @XBlock.wants("settings") class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, TabsEditingDescriptor, EmptyDataRawDescriptor): @@ -722,7 +723,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler if self.sub: _update_transcript_for_index() - # check to see if there are transcripts in other languages besides default transcript + # Check to see if there are transcripts in other languages besides default transcript if self.transcripts: for language in self.transcripts.keys(): _update_transcript_for_index(language) @@ -734,3 +735,78 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler xblock_body["content_type"] = "Video" return xblock_body + + @property + def request_cache(self): + """ + Returns the request_cache from the runtime. + """ + return self.runtime.service(self, "request_cache") + + @memoize_in_request_cache('request_cache') + def get_cached_val_data_for_course(self, video_profile_names, course_id): + """ + Returns the VAL data for the requested video profiles for the given course. + """ + return edxval_api.get_video_info_for_course_and_profiles(unicode(course_id), video_profile_names) + + def student_view_json(self, context): + """ + Returns a JSON representation of the student_view of this XModule. + The contract of the JSON content is between the caller and the particular XModule. + """ + # Honor only_on_web + if self.only_on_web: + return {"only_on_web": True} + + encoded_videos = {} + val_video_data = {} + + # Check in VAL data first if edx_video_id exists + if self.edx_video_id: + video_profile_names = context.get("profiles", []) + + # get and cache bulk VAL data for course + val_course_data = self.get_cached_val_data_for_course(video_profile_names, self.location.course_key) + val_video_data = val_course_data.get(self.edx_video_id, {}) + + # Get the encoded videos if data from VAL is found + if val_video_data: + encoded_videos = val_video_data.get('profiles', {}) + + # If information for this edx_video_id is not found in the bulk course data, make a + # separate request for this individual edx_video_id, unless cache misses are disabled. + # This is useful/required for videos that don't have a course designated, such as the introductory video + # that is shared across many courses. However, this results in a separate database request so watch + # out for any performance hit if many such videos exist in a course. Set the 'allow_cache_miss' parameter + # to False to disable this fall back. + elif context.get("allow_cache_miss", "True").lower() == "true": + try: + val_video_data = edxval_api.get_video_info(self.edx_video_id) + # Unfortunately, the VAL API is inconsistent in how it returns the encodings, so remap here. + for enc_vid in val_video_data.pop('encoded_videos'): + encoded_videos[enc_vid['profile']] = {key: enc_vid[key] for key in ["url", "file_size"]} + except edxval_api.ValVideoNotFoundError: + pass + + # Fall back to other video URLs in the video module if not found in VAL + if not encoded_videos: + video_url = self.html5_sources[0] if self.html5_sources else self.source + if video_url: + encoded_videos["fallback"] = { + "url": video_url, + "file_size": 0, # File size is unknown for fallback URLs + } + + transcripts_info = self.get_transcripts_info() + transcripts = { + lang: self.runtime.handler_url(self, 'transcript', 'download', query="lang=" + lang, thirdparty=True) + for lang in self.available_translations(transcripts_info, verify_assets=False) + } + + return { + "only_on_web": self.only_on_web, + "duration": val_video_data.get('duration', None), + "transcripts": transcripts, + "encoded_videos": encoded_videos, + } diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 36c1eacb2e..cb06a2cbed 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """Video xmodule tests in mongo.""" +import ddt +import itertools import json from collections import OrderedDict @@ -13,7 +15,7 @@ from django.test.utils import override_settings from xmodule.video_module import VideoDescriptor, bumper_utils, video_utils from xmodule.x_module import STUDENT_VIEW -from xmodule.tests.test_video import VideoDescriptorTestBase +from xmodule.tests.test_video import VideoDescriptorTestBase, instantiate_descriptor from xmodule.tests.test_import import DummySystem from edxval.api import ( @@ -861,6 +863,119 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): self.assertFalse(self.item_descriptor.download_video) +@ddt.ddt +class TestVideoDescriptorStudentViewJson(TestCase): + """ + Tests for the student_view_json method on VideoDescriptor. + """ + 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' + + def setUp(self): + super(TestVideoDescriptorStudentViewJson, self).setUp() + sample_xml = ( + "" + ) + self.transcript_url = "transcript_url" + self.video = instantiate_descriptor(data=sample_xml) + 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': [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_json method. + Arguments: + allow_cache_miss is passed in the context to the student_view_json method. + """ + context = { + "profiles": [self.TEST_PROFILE], + "allow_cache_miss": "True" if allow_cache_miss else "False" + } + return self.video.student_view_json(context) + + def verify_result_with_fallback_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": {"fallback": {"url": self.TEST_SOURCE_URL, "file_size": 0}}, + } + ) + + 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}, + } + ) + + 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_url(result) + + @ddt.data( + *itertools.product([True, False], [True, False], [True, False]) + ) + @ddt.unpack + def test_with_edx_video_id(self, allow_cache_miss, video_exists_in_val, associate_course_in_val): + self.video.edx_video_id = self.TEST_EDX_VIDEO_ID + if video_exists_in_val: + self.setup_val_video(associate_course_in_val) + result = self.get_result(allow_cache_miss) + if video_exists_in_val and (associate_course_in_val or allow_cache_miss): + self.verify_result_with_val_profile(result) + else: + self.verify_result_with_fallback_url(result) + + @attr('shard_1') class VideoDescriptorTest(TestCase, VideoDescriptorTestBase): """