# -*- coding: utf-8 -*- """ Tests for video outline API """ import ddt import itertools import json from collections import namedtuple from mock import Mock from uuid import uuid4 from django.conf import settings from edxval import api from milestones.tests.utils import MilestonesTestCaseMixin from mock import patch from nose.plugins.attrib import attr from mobile_api.models import MobileApiConfig from mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, remove_user_from_cohort from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory from xmodule.partitions.partitions import Group, UserPartition from xmodule.video_module import transcripts_utils class TestVideoAPITestCase(MobileAPITestCase): """ Base test class for video related mobile APIs """ def setUp(self): super(TestVideoAPITestCase, self).setUp() self.section = ItemFactory.create( parent=self.course, category="chapter", display_name=u"test factory section omega \u03a9", ) self.sub_section = ItemFactory.create( parent=self.section, category="sequential", display_name=u"test subsection omega \u03a9", ) self.unit = ItemFactory.create( parent=self.sub_section, category="vertical", metadata={'graded': True, 'format': 'Homework'}, display_name=u"test unit omega \u03a9", ) self.other_unit = ItemFactory.create( parent=self.sub_section, category="vertical", metadata={'graded': True, 'format': 'Homework'}, display_name=u"test unit omega 2 \u03a9", ) self.nameless_unit = ItemFactory.create( parent=self.sub_section, category="vertical", metadata={'graded': True, 'format': 'Homework'}, display_name=None, ) self.edx_video_id = 'testing-123' self.video_url = 'http://val.edx.org/val/video.mp4' self.video_url_high = 'http://val.edx.org/val/video_high.mp4' self.video_url_low = 'http://val.edx.org/val/video_low.mp4' self.youtube_url = 'http://val.edx.org/val/youtube.mp4' self.html5_video_url = 'http://video.edx.org/html5/video.mp4' api.create_profile('youtube') api.create_profile('mobile_high') api.create_profile('mobile_low') # create the video in VAL api.create_video({ 'edx_video_id': self.edx_video_id, 'status': 'test', 'client_video_id': u"test video omega \u03a9", 'duration': 12, 'courses': [unicode(self.course.id)], 'encoded_videos': [ { 'profile': 'youtube', 'url': 'xyz123', 'file_size': 0, 'bitrate': 1500 }, { 'profile': 'mobile_low', 'url': self.video_url, 'file_size': 12345, 'bitrate': 250 }, { 'profile': 'mobile_high', 'url': self.video_url_high, 'file_size': 99999, 'bitrate': 250 }, ]}) # Set requested profiles MobileApiConfig(video_profiles="mobile_low,mobile_high,youtube").save() class TestVideoAPIMixin(object): """ Mixin class that provides helpers for testing video related mobile APIs """ def _create_video_with_subs(self, custom_subid=None): """ Creates and returns a video with stored subtitles. """ subid = custom_subid or uuid4().hex transcripts_utils.save_subs_to_store( { 'start': [100, 200, 240, 390, 1000], 'end': [200, 240, 380, 1000, 1500], 'text': [ 'subs #1', 'subs #2', 'subs #3', 'subs #4', 'subs #5' ] }, subid, self.course) return ItemFactory.create( parent=self.unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test video omega \u03a9", sub=subid ) def _verify_paths(self, course_outline, path_list, outline_index=0): """ Takes a path_list and compares it against the course_outline Attributes: course_outline (list): A list of dictionaries that includes a 'path' and 'named_path' field which we will be comparing path_list to path_list (list): A list of the expected strings outline_index (int): Index into the course_outline list for which the path is being tested. """ path = course_outline[outline_index]['path'] self.assertEqual(len(path), len(path_list)) for i in range(len(path_list)): self.assertEqual(path_list[i], path[i]['name']) #named_path will be deprecated eventually named_path = course_outline[outline_index]['named_path'] self.assertEqual(len(named_path), len(path_list)) for i in range(len(path_list)): self.assertEqual(path_list[i], named_path[i]) def _setup_course_partitions(self, scheme_id='random', is_cohorted=False): """Helper method to configure the user partitions in the course.""" self.partition_id = 0 # pylint: disable=attribute-defined-outside-init self.course.user_partitions = [ UserPartition( self.partition_id, 'first_partition', 'First Partition', [Group(0, 'alpha'), Group(1, 'beta')], scheme=None, scheme_id=scheme_id ), ] self.course.cohort_config = {'cohorted': is_cohorted} self.store.update_item(self.course, self.user.id) def _setup_group_access(self, xblock, partition_id, group_ids): """Helper method to configure the partition and group mapping for the given xblock.""" xblock.group_access = {partition_id: group_ids} self.store.update_item(xblock, self.user.id) def _setup_split_module(self, sub_block_category): """Helper method to configure a split_test unit with children of type sub_block_category.""" self._setup_course_partitions() self.split_test = ItemFactory.create( # pylint: disable=attribute-defined-outside-init parent=self.unit, category="split_test", display_name=u"split test unit", user_partition_id=0, ) sub_block_a = ItemFactory.create( parent=self.split_test, category=sub_block_category, display_name=u"split test block a", ) sub_block_b = ItemFactory.create( parent=self.split_test, category=sub_block_category, display_name=u"split test block b", ) self.split_test.group_id_to_child = { str(index): url for index, url in enumerate([sub_block_a.location, sub_block_b.location]) } self.store.update_item(self.split_test, self.user.id) return sub_block_a, sub_block_b @attr(shard=2) class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, MilestonesTestCaseMixin): """ Tests /api/mobile/v0.5/video_outlines/courses/{course_id} with no course set """ REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id']} def setUp(self): super(TestNonStandardCourseStructure, self).setUp() self.chapter_under_course = ItemFactory.create( parent=self.course, category="chapter", display_name=u"test factory chapter under course omega \u03a9", ) self.section_under_course = ItemFactory.create( parent=self.course, category="sequential", display_name=u"test factory section under course omega \u03a9", ) self.section_under_chapter = ItemFactory.create( parent=self.chapter_under_course, category="sequential", display_name=u"test factory section under chapter omega \u03a9", ) self.vertical_under_course = ItemFactory.create( parent=self.course, category="vertical", display_name=u"test factory vertical under course omega \u03a9", ) self.vertical_under_section = ItemFactory.create( parent=self.section_under_chapter, category="vertical", display_name=u"test factory vertical under section omega \u03a9", ) def test_structure_course_video(self): """ Tests when there is a video without a vertical directly under course """ self.login_and_enroll() ItemFactory.create( parent=self.course, category="video", display_name=u"test factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches(section_url, r'courseware$') self.assertEqual(section_url, unit_url) self._verify_paths(course_outline, []) def test_structure_course_vert_video(self): """ Tests when there is a video under vertical directly under course """ self.login_and_enroll() ItemFactory.create( parent=self.vertical_under_course, category="video", display_name=u"test factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches( section_url, r'courseware/test_factory_vertical_under_course_omega_%CE%A9/$' ) self.assertEqual(section_url, unit_url) self._verify_paths( course_outline, [ u'test factory vertical under course omega \u03a9' ] ) def test_structure_course_chap_video(self): """ Tests when there is a video directly under chapter """ self.login_and_enroll() ItemFactory.create( parent=self.chapter_under_course, category="video", display_name=u"test factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches( section_url, r'courseware/test_factory_chapter_under_course_omega_%CE%A9/$' ) self.assertEqual(section_url, unit_url) self._verify_paths( course_outline, [ u'test factory chapter under course omega \u03a9', ] ) def test_structure_course_section_video(self): """ Tests when chapter is none, and video under section under course """ self.login_and_enroll() ItemFactory.create( parent=self.section_under_course, category="video", display_name=u"test factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches( section_url, r'courseware/test_factory_section_under_course_omega_%CE%A9/$' ) self.assertEqual(section_url, unit_url) self._verify_paths( course_outline, [ u'test factory section under course omega \u03a9', ] ) def test_structure_course_chap_section_video(self): """ Tests when chapter and sequential exists, with a video with no vertical. """ self.login_and_enroll() ItemFactory.create( parent=self.section_under_chapter, category="video", display_name=u"meow factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches( section_url, ( r'courseware/test_factory_chapter_under_course_omega_%CE%A9/' + 'test_factory_section_under_chapter_omega_%CE%A9/$' ) ) self.assertEqual(section_url, unit_url) self._verify_paths( course_outline, [ u'test factory chapter under course omega \u03a9', u'test factory section under chapter omega \u03a9', ] ) def test_structure_course_section_vert_video(self): """ Tests chapter->section->vertical->unit """ self.login_and_enroll() ItemFactory.create( parent=self.vertical_under_section, category="video", display_name=u"test factory video omega \u03a9", ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertRegexpMatches( section_url, ( r'courseware/test_factory_chapter_under_course_omega_%CE%A9/' + 'test_factory_section_under_chapter_omega_%CE%A9/$' ) ) self.assertRegexpMatches( unit_url, ( r'courseware/test_factory_chapter_under_course_omega_%CE%A9/' + 'test_factory_section_under_chapter_omega_%CE%A9/1$' ) ) self._verify_paths( course_outline, [ u'test factory chapter under course omega \u03a9', u'test factory section under chapter omega \u03a9', u'test factory vertical under section omega \u03a9' ] ) @attr(shard=2) @ddt.ddt class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, TestVideoAPIMixin, MilestonesTestCaseMixin): """ Tests for /api/mobile/v0.5/video_outlines/courses/{course_id}.. """ REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id']} def test_only_on_web(self): self.login_and_enroll() course_outline = self.api_response().data self.assertEqual(len(course_outline), 0) subid = uuid4().hex transcripts_utils.save_subs_to_store( { 'start': [100], 'end': [200], 'text': [ 'subs #1', ] }, subid, self.course) ItemFactory.create( parent=self.unit, category="video", display_name=u"test video", only_on_web=True, subid=subid ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertIsNone(course_outline[0]["summary"]["video_url"]) self.assertIsNone(course_outline[0]["summary"]["video_thumbnail_url"]) self.assertEqual(course_outline[0]["summary"]["duration"], 0) self.assertEqual(course_outline[0]["summary"]["size"], 0) self.assertEqual(course_outline[0]["summary"]["name"], "test video") self.assertEqual(course_outline[0]["summary"]["transcripts"], {}) self.assertIsNone(course_outline[0]["summary"]["language"]) self.assertEqual(course_outline[0]["summary"]["category"], "video") self.assertTrue(course_outline[0]["summary"]["only_on_web"]) def test_mobile_api_video_profiles(self): """ Tests VideoSummaryList with different MobileApiConfig video_profiles """ self.login_and_enroll() edx_video_id = "testing_mobile_high" api.create_video({ 'edx_video_id': edx_video_id, 'status': 'test', 'client_video_id': u"test video omega \u03a9", 'duration': 12, 'courses': [unicode(self.course.id)], 'encoded_videos': [ { 'profile': 'youtube', 'url': self.youtube_url, 'file_size': 2222, 'bitrate': 4444 }, { 'profile': 'mobile_high', 'url': self.video_url_high, 'file_size': 111, 'bitrate': 333 }, ]}) ItemFactory.create( parent=self.other_unit, category="video", display_name=u"testing mobile high video", edx_video_id=edx_video_id, ) expected_output = { 'all_sources': [], 'category': u'video', 'video_thumbnail_url': None, 'language': u'en', 'name': u'testing mobile high video', 'video_url': self.video_url_high, 'duration': 12.0, 'transcripts': { 'en': 'http://testserver/api/mobile/v0.5/video_outlines/transcripts/{}/testing_mobile_high_video/en'.format(self.course.id) # pylint: disable=line-too-long }, 'only_on_web': False, 'encoded_videos': { u'mobile_high': { 'url': self.video_url_high, 'file_size': 111 }, u'youtube': { 'url': self.youtube_url, 'file_size': 2222 } }, 'size': 111 } # The transcript was not entered, so it should not be found! # This is the default behaviour at courses.edX.org, based on `FALLBACK_TO_ENGLISH_TRANSCRIPTS` transcripts_response = self.client.get(expected_output['transcripts']['en']) self.assertEqual(404, transcripts_response.status_code) with patch.dict(settings.FEATURES, FALLBACK_TO_ENGLISH_TRANSCRIPTS=False): # Other platform installations may override this setting # This ensures that the server don't return empty English transcripts when there's none! self.assertFalse(self.api_response().data[0]['summary'].get('transcripts')) # Testing when video_profiles='mobile_low,mobile_high,youtube' course_outline = self.api_response().data course_outline[0]['summary'].pop("id") self.assertEqual(course_outline[0]['summary'], expected_output) # Testing when there is no mobile_low, and that mobile_high doesn't show MobileApiConfig(video_profiles="mobile_low,youtube").save() course_outline = self.api_response().data expected_output['encoded_videos'].pop('mobile_high') expected_output['video_url'] = self.youtube_url expected_output['size'] = 2222 course_outline[0]['summary'].pop("id") self.assertEqual(course_outline[0]['summary'], expected_output) # Testing where youtube is the default video over mobile_high MobileApiConfig(video_profiles="youtube,mobile_high").save() course_outline = self.api_response().data expected_output['encoded_videos']['mobile_high'] = { 'url': self.video_url_high, 'file_size': 111 } course_outline[0]['summary'].pop("id") self.assertEqual(course_outline[0]['summary'], expected_output) def test_mobile_api_html5_sources(self): """ Tests VideoSummaryList without the video pipeline, using fallback HTML5 video URLs """ self.login_and_enroll() descriptor = ItemFactory.create( parent=self.other_unit, category="video", display_name=u"testing html5 sources", edx_video_id=None, source=self.video_url_high, html5_sources=[self.video_url_low], ) expected_output = { 'all_sources': [self.video_url_low, self.video_url_high], 'category': u'video', 'video_thumbnail_url': None, 'language': u'en', 'id': unicode(descriptor.scope_ids.usage_id), 'name': u'testing html5 sources', 'video_url': self.video_url_low, 'duration': None, 'transcripts': { 'en': 'http://testserver/api/mobile/v0.5/video_outlines/transcripts/{}/testing_html5_sources/en'.format(self.course.id) # pylint: disable=line-too-long }, 'only_on_web': False, 'encoded_videos': None, 'size': 0, } course_outline = self.api_response().data self.assertEqual(course_outline[0]['summary'], expected_output) def test_video_not_in_val(self): self.login_and_enroll() self._create_video_with_subs() ItemFactory.create( parent=self.other_unit, category="video", edx_video_id="some_non_existent_id_in_val", display_name=u"some non existent video in val", html5_sources=[self.html5_video_url] ) summary = self.api_response().data[1]['summary'] self.assertEqual(summary['name'], "some non existent video in val") self.assertIsNone(summary['encoded_videos']) self.assertIsNone(summary['duration']) self.assertEqual(summary['size'], 0) self.assertEqual(summary['video_url'], self.html5_video_url) def test_course_list(self): self.login_and_enroll() self._create_video_with_subs() ItemFactory.create( parent=self.other_unit, category="video", display_name=u"test video omega 2 \u03a9", html5_sources=[self.html5_video_url] ) ItemFactory.create( parent=self.other_unit, category="video", display_name=u"test video omega 3 \u03a9", source=self.html5_video_url ) ItemFactory.create( parent=self.unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test draft video omega \u03a9", visible_to_staff_only=True, ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 3) vid = course_outline[0] self.assertIn('test_subsection_omega_%CE%A9', vid['section_url']) self.assertIn('test_subsection_omega_%CE%A9/1', vid['unit_url']) self.assertIn(u'test_video_omega_\u03a9', vid['summary']['id']) self.assertEqual(vid['summary']['video_url'], self.video_url) self.assertEqual(vid['summary']['size'], 12345) self.assertIn('en', vid['summary']['transcripts']) self.assertFalse(vid['summary']['only_on_web']) self.assertEqual(course_outline[1]['summary']['video_url'], self.html5_video_url) self.assertEqual(course_outline[1]['summary']['size'], 0) self.assertFalse(course_outline[1]['summary']['only_on_web']) self.assertEqual(course_outline[1]['path'][2]['name'], self.other_unit.display_name) self.assertEqual(course_outline[1]['path'][2]['id'], unicode(self.other_unit.location)) self.assertEqual(course_outline[2]['summary']['video_url'], self.html5_video_url) self.assertEqual(course_outline[2]['summary']['size'], 0) self.assertFalse(course_outline[2]['summary']['only_on_web']) def test_with_nameless_unit(self): self.login_and_enroll() ItemFactory.create( parent=self.nameless_unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test draft video omega 2 \u03a9" ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertEqual(course_outline[0]['path'][2]['name'], self.nameless_unit.location.block_id) def test_with_video_in_sub_section(self): """ Tests a non standard xml format where a video is underneath a sequential We are expecting to return the same unit and section url since there is no unit vertical. """ self.login_and_enroll() ItemFactory.create( parent=self.sub_section, category="video", edx_video_id=self.edx_video_id, display_name=u"video in the sub section" ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertEqual(len(course_outline[0]['path']), 2) section_url = course_outline[0]["section_url"] unit_url = course_outline[0]["unit_url"] self.assertIn( u'courseware/test_factory_section_omega_%CE%A9/test_subsection_omega_%CE%A9', section_url ) self.assertTrue(section_url) self.assertTrue(unit_url) self.assertEqual(section_url, unit_url) @ddt.data( *itertools.product([True, False], ["video", "problem"]) ) @ddt.unpack def test_with_split_block(self, is_user_staff, sub_block_category): """Test with split_module->sub_block_category and for both staff and non-staff users.""" self.login_and_enroll() self.user.is_staff = is_user_staff self.user.save() self._setup_split_module(sub_block_category) video_outline = self.api_response().data num_video_blocks = 1 if sub_block_category == "video" else 0 self.assertEqual(len(video_outline), num_video_blocks) for block_index in range(num_video_blocks): self._verify_paths( video_outline, [ self.section.display_name, self.sub_section.display_name, self.unit.display_name, self.split_test.display_name ], block_index ) self.assertIn(u"split test block", video_outline[block_index]["summary"]["name"]) def test_with_split_vertical(self): """Test with split_module->vertical->video structure.""" self.login_and_enroll() split_vertical_a, split_vertical_b = self._setup_split_module("vertical") ItemFactory.create( parent=split_vertical_a, category="video", display_name=u"video in vertical a", ) ItemFactory.create( parent=split_vertical_b, category="video", display_name=u"video in vertical b", ) video_outline = self.api_response().data # user should see only one of the videos (a or b). self.assertEqual(len(video_outline), 1) self.assertIn(u"video in vertical", video_outline[0]["summary"]["name"]) a_or_b = video_outline[0]["summary"]["name"][-1:] self._verify_paths( video_outline, [ self.section.display_name, self.sub_section.display_name, self.unit.display_name, self.split_test.display_name, u"split test block " + a_or_b ], ) def _create_cohorted_video(self, group_id): """Creates a cohorted video block, giving access to only the given group_id.""" video_block = ItemFactory.create( parent=self.unit, category="video", display_name=u"video for group " + unicode(group_id), ) self._setup_group_access(video_block, self.partition_id, [group_id]) def _create_cohorted_vertical_with_video(self, group_id): """Creates a cohorted vertical with a child video block, giving access to only the given group_id.""" vertical_block = ItemFactory.create( parent=self.sub_section, category="vertical", display_name=u"vertical for group " + unicode(group_id), ) self._setup_group_access(vertical_block, self.partition_id, [group_id]) ItemFactory.create( parent=vertical_block, category="video", display_name=u"video for group " + unicode(group_id), ) @ddt.data("_create_cohorted_video", "_create_cohorted_vertical_with_video") def test_with_cohorted_content(self, content_creator_method_name): self.login_and_enroll() self._setup_course_partitions(scheme_id='cohort', is_cohorted=True) cohorts = [] for group_id in [0, 1]: getattr(self, content_creator_method_name)(group_id) cohorts.append(CohortFactory(course_id=self.course.id, name=u"Cohort " + unicode(group_id))) link = CourseUserGroupPartitionGroup( course_user_group=cohorts[group_id], partition_id=self.partition_id, group_id=group_id, ) link.save() for cohort_index in range(len(cohorts)): # add user to this cohort add_user_to_cohort(cohorts[cohort_index], self.user.username) # should only see video for this cohort video_outline = self.api_response().data self.assertEqual(len(video_outline), 1) self.assertEquals( u"video for group " + unicode(cohort_index), video_outline[0]["summary"]["name"] ) # remove user from this cohort remove_user_from_cohort(cohorts[cohort_index], self.user.username) # un-cohorted user should see no videos video_outline = self.api_response().data self.assertEqual(len(video_outline), 0) # staff user sees all videos self.user.is_staff = True self.user.save() video_outline = self.api_response().data self.assertEqual(len(video_outline), 2) def test_with_hidden_blocks(self): self.login_and_enroll() hidden_subsection = ItemFactory.create( parent=self.section, category="sequential", hide_from_toc=True, ) unit_within_hidden_subsection = ItemFactory.create( parent=hidden_subsection, category="vertical", ) hidden_unit = ItemFactory.create( parent=self.sub_section, category="vertical", hide_from_toc=True, ) ItemFactory.create( parent=unit_within_hidden_subsection, category="video", edx_video_id=self.edx_video_id, ) ItemFactory.create( parent=hidden_unit, category="video", edx_video_id=self.edx_video_id, ) course_outline = self.api_response().data self.assertEqual(len(course_outline), 0) def test_language(self): self.login_and_enroll() video = ItemFactory.create( parent=self.nameless_unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test draft video omega 2 \u03a9" ) language_case = namedtuple('language_case', ['transcripts', 'expected_language']) language_cases = [ # defaults to english language_case({}, "en"), # supports english language_case({"en": 1}, "en"), # supports another language language_case({"lang1": 1}, "lang1"), # returns first alphabetically-sorted language language_case({"lang1": 1, "en": 2}, "en"), language_case({"lang1": 1, "lang2": 2}, "lang1"), ] for case in language_cases: video.transcripts = case.transcripts modulestore().update_item(video, self.user.id) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertEqual(course_outline[0]['summary']['language'], case.expected_language) def test_transcripts(self): self.login_and_enroll() video = ItemFactory.create( parent=self.nameless_unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test draft video omega 2 \u03a9" ) transcript_case = namedtuple('transcript_case', ['transcripts', 'english_subtitle', 'expected_transcripts']) transcript_cases = [ # defaults to english transcript_case({}, "", ["en"]), transcript_case({}, "en-sub", ["en"]), # supports english transcript_case({"en": 1}, "", ["en"]), transcript_case({"en": 1}, "en-sub", ["en"]), # keeps both english and other languages transcript_case({"lang1": 1, "en": 2}, "", ["lang1", "en"]), transcript_case({"lang1": 1, "en": 2}, "en-sub", ["lang1", "en"]), # adds english to list of languages only if english_subtitle is specified transcript_case({"lang1": 1, "lang2": 2}, "", ["lang1", "lang2"]), transcript_case({"lang1": 1, "lang2": 2}, "en-sub", ["lang1", "lang2", "en"]), ] for case in transcript_cases: video.transcripts = case.transcripts video.sub = case.english_subtitle modulestore().update_item(video, self.user.id) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertSetEqual( set(course_outline[0]['summary']['transcripts'].keys()), set(case.expected_transcripts) ) @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.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True)) @patch('xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages') def test_val_transcripts_with_feature_enabled(self, transcripts, english_sub, val_transcripts, expected_transcripts, mock_get_transcript_languages): self.login_and_enroll() video = ItemFactory.create( parent=self.nameless_unit, category="video", edx_video_id=self.edx_video_id, display_name=u"test draft video omega 2 \u03a9" ) mock_get_transcript_languages.return_value = val_transcripts video.transcripts = transcripts video.sub = english_sub modulestore().update_item(video, self.user.id) course_outline = self.api_response().data self.assertEqual(len(course_outline), 1) self.assertItemsEqual(course_outline[0]['summary']['transcripts'].keys(), expected_transcripts) @attr(shard=2) class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin, TestVideoAPIMixin, MilestonesTestCaseMixin): """ Tests for /api/mobile/v0.5/video_outlines/transcripts/{course_id}.. """ REVERSE_INFO = {'name': 'video-transcripts-detail', 'params': ['course_id']} def setUp(self): super(TestTranscriptsDetail, self).setUp() self.video = self._create_video_with_subs() def reverse_url(self, reverse_args=None, **kwargs): reverse_args = reverse_args or {} reverse_args.update({ 'block_id': self.video.location.block_id, 'lang': kwargs.get('lang', 'en'), }) return super(TestTranscriptsDetail, self).reverse_url(reverse_args, **kwargs) def test_incorrect_language(self): self.login_and_enroll() self.api_response(expected_response_code=404, lang='pl') def test_transcript_with_unicode_file_name(self): self.video = self._create_video_with_subs(custom_subid=u'你好') self.login_and_enroll() self.api_response(expected_response_code=200, lang='en') @patch( 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=True), ) @patch( 'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages', Mock(return_value=['uk']), ) @patch('xmodule.video_module.transcripts_utils.edxval_api.get_video_transcript_data') def test_val_transcript(self, mock_get_video_transcript_content): """ Tests transcript retrieval view with val transcripts. """ mock_get_video_transcript_content.return_value = { 'content': json.dumps({ 'start': [10], 'end': [100], 'text': [u'Hi, welcome to Edx.'], }), 'file_name': 'edx.sjson' } self.login_and_enroll() # Now, make request to retrieval endpoint response = self.api_response(expected_response_code=200, lang='uk') # Expected headers expected_content = u'0\n00:00:00,010 --> 00:00:00,100\nHi, welcome to Edx.\n\n' expected_headers = { 'Content-Disposition': 'attachment; filename="edx.srt"', 'Content-Type': 'application/x-subrip; charset=utf-8' } # Assert the actual response self.assertEqual(response.content, expected_content) for attribute, value in expected_headers.iteritems(): self.assertEqual(response.get(attribute), value) @patch( 'openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled', Mock(return_value=False), ) @patch( 'xmodule.video_module.transcripts_utils.edxval_api.get_available_transcript_languages', Mock(return_value=['uk']), ) def test_val_transcript_feature_disabled(self): """ Tests transcript retrieval view with val transcripts when the corresponding feature is disabled. """ self.login_and_enroll() # request to retrieval endpoint will result in 404 as val transcripts are disabled. self.api_response(expected_response_code=404, lang='uk')