DEPR-4: Remove Mobile Video Outlines API

This commit is contained in:
Nimisha Asthagiri
2019-04-02 13:59:26 -04:00
parent 5a1105bb43
commit 3d8b8a05ad
10 changed files with 4 additions and 1457 deletions

View File

@@ -164,8 +164,6 @@ class CountMongoCallsCourseTraversal(TestCase):
with store_builder.build(request_cache=request_cache) as (content_store, modulestore):
course_key = self._import_course(content_store, modulestore)
# Course traversal modeled after the traversal done here:
# lms/djangoapps/mobile_api/video_outlines/serializers.py:BlockOutline
# Starting at the root course block, do a breadth-first traversal using
# get_children() to retrieve each block's children.
with check_mongo_calls(num_mongo_calls):

View File

@@ -9,6 +9,5 @@ from .users.views import my_user_info
urlpatterns = [
url(r'^users/', include('mobile_api.users.urls')),
url(r'^my_user_info', my_user_info, name='user-info'),
url(r'^video_outlines/', include('mobile_api.video_outlines.urls')),
url(r'^course_info/', include('mobile_api.course_info.urls')),
]

View File

@@ -73,11 +73,10 @@ class CourseOverviewField(serializers.RelatedField):
request=request,
) if course_overview.is_discussion_tab_enabled() else None,
'video_outline': reverse(
'video-summary-list',
kwargs={'api_version': api_version, 'course_id': course_id},
request=request,
),
# This is an old API that was removed as part of DEPR-4. We keep the
# field present in case API parsers expect it, but this API is now
# removed.
'video_outline': None,
}

View File

@@ -111,7 +111,6 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
self.assertIn('courses/{}/about'.format(self.course.id), found_course['course_about'])
self.assertIn('course_info/{}/updates'.format(self.course.id), found_course['course_updates'])
self.assertIn('course_info/{}/handouts'.format(self.course.id), found_course['course_handouts'])
self.assertIn('video_outlines/courses/{}'.format(self.course.id), found_course['video_outline'])
self.assertEqual(found_course['id'], unicode(self.course.id))
self.assertEqual(courses[0]['mode'], CourseMode.DEFAULT_MODE_SLUG)
self.assertEqual(courses[0]['course']['subscription_id'], self.course.clean_id(padding_char='_'))

View File

@@ -1,3 +0,0 @@
"""
Video outline API
"""

View File

@@ -1,3 +0,0 @@
"""
A models.py is required to make this an app (until we move to Django 1.7)
"""

View File

@@ -1,253 +0,0 @@
"""
Serializer for video outline
"""
from edxval.api import ValInternalError, get_video_info_for_course_and_profiles
from rest_framework.reverse import reverse
from courseware.access import has_access
from courseware.courses import get_course_by_id
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from util.module_utils import get_dynamic_descriptor_children
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo.base import BLOCK_TYPES_WITH_CHILDREN
class BlockOutline(object):
"""
Serializes course videos, pulling data from VAL and the video modules.
"""
def __init__(self, course_id, start_block, block_types, request, video_profiles, api_version):
"""Create a BlockOutline using `start_block` as a starting point."""
self.start_block = start_block
self.block_types = block_types
self.course_id = course_id
self.api_version = api_version
self.request = request # needed for making full URLS
self.local_cache = {}
try:
self.local_cache['course_videos'] = get_video_info_for_course_and_profiles(
unicode(course_id), video_profiles
)
except ValInternalError: # pragma: nocover
self.local_cache['course_videos'] = {}
def __iter__(self):
def parent_or_requested_block_type(usage_key):
"""
Returns whether the usage_key's block_type is one of self.block_types or a parent type.
"""
return (
usage_key.block_type in self.block_types or
usage_key.block_type in BLOCK_TYPES_WITH_CHILDREN
)
def create_module(descriptor):
"""
Factory method for creating and binding a module for the given descriptor.
"""
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course_id, self.request.user, descriptor, depth=0,
)
course = get_course_by_id(self.course_id)
return get_module_for_descriptor(
self.request.user, self.request, descriptor, field_data_cache, self.course_id, course=course
)
with modulestore().bulk_operations(self.course_id):
child_to_parent = {}
stack = [self.start_block]
while stack:
curr_block = stack.pop()
if curr_block.hide_from_toc:
# For now, if the 'hide_from_toc' setting is set on the block, do not traverse down
# the hierarchy. The reason being is that these blocks may not have human-readable names
# to display on the mobile clients.
# Eventually, we'll need to figure out how we want these blocks to be displayed on the
# mobile clients. As they are still accessible in the browser, just not navigatable
# from the table-of-contents.
continue
if curr_block.location.block_type in self.block_types:
if not has_access(self.request.user, 'load', curr_block, course_key=self.course_id):
continue
summary_fn = self.block_types[curr_block.category]
block_path = list(path(curr_block, child_to_parent, self.start_block))
unit_url, section_url = find_urls(self.course_id, curr_block, child_to_parent, self.request)
yield {
"path": block_path,
"named_path": [b["name"] for b in block_path],
"unit_url": unit_url,
"section_url": section_url,
"summary": summary_fn(
self.course_id,
curr_block,
self.request,
self.local_cache,
self.api_version
)
}
if curr_block.has_children:
children = get_dynamic_descriptor_children(
curr_block,
self.request.user.id,
create_module,
usage_key_filter=parent_or_requested_block_type
)
for block in reversed(children):
stack.append(block)
child_to_parent[block] = curr_block
def path(block, child_to_parent, start_block):
"""path for block"""
block_path = []
while block in child_to_parent:
block = child_to_parent[block]
if block is not start_block:
block_path.append({
# to be consistent with other edx-platform clients, return the defaulted display name
'name': block.display_name_with_default_escaped, # xss-lint: disable=python-deprecated-display-name
'category': block.category,
'id': unicode(block.location)
})
return reversed(block_path)
def find_urls(course_id, block, child_to_parent, request):
"""
Find the section and unit urls for a block.
Returns:
unit_url, section_url:
unit_url (str): The url of a unit
section_url (str): The url of a section
"""
block_path = []
while block in child_to_parent:
block = child_to_parent[block]
block_path.append(block)
block_list = list(reversed(block_path))
block_count = len(block_list)
chapter_id = block_list[1].location.block_id if block_count > 1 else None
section = block_list[2] if block_count > 2 else None
position = None
if block_count > 3:
position = 1
for block in section.children:
if block.block_id == block_list[3].url_name:
break
position += 1
kwargs = {'course_id': unicode(course_id)}
if chapter_id is None:
course_url = reverse("courseware", kwargs=kwargs, request=request)
return course_url, course_url
kwargs['chapter'] = chapter_id
if section is None:
chapter_url = reverse("courseware_chapter", kwargs=kwargs, request=request)
return chapter_url, chapter_url
kwargs['section'] = section.url_name
section_url = reverse("courseware_section", kwargs=kwargs, request=request)
if position is None:
return section_url, section_url
kwargs['position'] = position
unit_url = reverse("courseware_position", kwargs=kwargs, request=request)
return unit_url, section_url
def video_summary(video_profiles, course_id, video_descriptor, request, local_cache, api_version):
"""
returns summary dict for the given video module
"""
always_available_data = {
"name": video_descriptor.display_name,
"category": video_descriptor.category,
"id": unicode(video_descriptor.scope_ids.usage_id),
"only_on_web": video_descriptor.only_on_web,
}
all_sources = []
if video_descriptor.only_on_web:
ret = {
"video_url": None,
"video_thumbnail_url": None,
"duration": 0,
"size": 0,
"transcripts": {},
"language": None,
"all_sources": all_sources,
}
ret.update(always_available_data)
return ret
# Get encoded videos
video_data = local_cache['course_videos'].get(video_descriptor.edx_video_id, {})
# Get highest priority video to populate backwards compatible field
default_encoded_video = {}
if video_data:
for profile in video_profiles:
default_encoded_video = video_data['profiles'].get(profile, {})
if default_encoded_video:
break
if default_encoded_video:
video_url = default_encoded_video['url']
# Then fall back to VideoDescriptor fields for video URLs
elif video_descriptor.html5_sources:
video_url = video_descriptor.html5_sources[0]
all_sources = video_descriptor.html5_sources
else:
video_url = video_descriptor.source
if video_descriptor.source:
all_sources.append(video_descriptor.source)
# Get duration/size, else default
duration = video_data.get('duration', None)
size = default_encoded_video.get('file_size', 0)
# Transcripts...
transcripts_info = video_descriptor.get_transcripts_info()
transcript_langs = video_descriptor.available_translations(transcripts=transcripts_info)
transcripts = {
lang: reverse(
'video-transcripts-detail',
kwargs={
'course_id': unicode(course_id),
'block_id': video_descriptor.scope_ids.usage_id.block_id,
'lang': lang,
'api_version': api_version
},
request=request,
)
for lang in transcript_langs
}
ret = {
"video_url": video_url,
"video_thumbnail_url": None,
"duration": duration,
"size": size,
"transcripts": transcripts,
"language": video_descriptor.get_default_transcript_language(transcripts_info),
"encoded_videos": video_data.get('profiles'),
"all_sources": all_sources,
}
ret.update(always_available_data)
return ret

View File

@@ -1,1041 +0,0 @@
# -*- 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 mobile_api.models import MobileApiConfig
from mobile_api.testutils import (
MobileAPITestCase,
MobileAuthTestMixin,
MobileCourseAccessTestMixin
)
from mobile_api.utils import API_V05, API_V1
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
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(
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
@ddt.ddt
class TestNonStandardCourseStructure(MobileAPITestCase, TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests /api/mobile/{api_version}/video_outlines/courses/{course_id} with no course set
"""
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id', 'api_version']}
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",
)
@ddt.data(API_V05, API_V1)
def test_structure_course_video(self, api_version):
"""
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(api_version=api_version).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, [])
@ddt.data(API_V05, API_V1)
def test_structure_course_vert_video(self, api_version):
"""
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(api_version=api_version).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'
]
)
@ddt.data(API_V05, API_V1)
def test_structure_course_chap_video(self, api_version):
"""
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(api_version=api_version).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',
]
)
@ddt.data(API_V05, API_V1)
def test_structure_course_section_video(self, api_version):
"""
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(api_version=api_version).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',
]
)
@ddt.data(API_V05, API_V1)
def test_structure_course_chap_section_video(self, api_version):
"""
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(api_version=api_version).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',
]
)
@ddt.data(API_V05, API_V1)
def test_structure_course_section_vert_video(self, api_version):
"""
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(api_version=api_version).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'
]
)
@ddt.ddt
class TestVideoSummaryList(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin,
TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/{api_version}/video_outlines/courses/{course_id}..
"""
REVERSE_INFO = {'name': 'video-summary-list', 'params': ['course_id', 'api_version']}
@ddt.data(API_V05, API_V1)
def test_only_on_web(self, api_version):
self.login_and_enroll()
course_outline = self.api_response(api_version=api_version).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(api_version=api_version).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"])
@ddt.data(API_V05, API_V1)
def test_mobile_api_video_profiles(self, api_version):
"""
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/{api_version}/video_outlines/transcripts/{course_id}/testing_mobile_high_video/en'.format(api_version=api_version, course_id=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(api_version=api_version).data[0]['summary'].get('transcripts'))
# Testing when video_profiles='mobile_low,mobile_high,youtube'
course_outline = self.api_response(api_version=api_version).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(api_version=api_version).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(api_version=api_version).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)
@ddt.data(API_V05, API_V1)
def test_mobile_api_html5_sources(self, api_version):
"""
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/{api_version}/video_outlines/transcripts/{course_id}/testing_html5_sources/en'.format(api_version=api_version, course_id=self.course.id) # pylint: disable=line-too-long
},
'only_on_web': False,
'encoded_videos': None,
'size': 0,
}
course_outline = self.api_response(api_version=api_version).data
self.assertEqual(course_outline[0]['summary'], expected_output)
@ddt.data(API_V05, API_V1)
def test_video_not_in_val(self, api_version):
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(api_version=api_version).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)
@ddt.data(API_V05, API_V1)
def test_course_list(self, api_version):
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(api_version=api_version).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'])
@ddt.data(API_V05, API_V1)
def test_with_nameless_unit(self, api_version):
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(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertEqual(course_outline[0]['path'][2]['name'], self.nameless_unit.location.block_id)
@ddt.data(API_V05, API_V1)
def test_with_video_in_sub_section(self, api_version):
"""
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(api_version=api_version).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"], [API_V05, API_V1])
)
@ddt.unpack
def test_with_split_block(self, is_user_staff, sub_block_category, api_version):
"""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(api_version=api_version).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"])
@ddt.data(API_V05, API_V1)
def test_with_split_vertical(self, api_version):
"""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(api_version=api_version).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", API_V05),
("_create_cohorted_video", API_V1),
("_create_cohorted_vertical_with_video", API_V05),
("_create_cohorted_vertical_with_video", API_V1),
)
@ddt.unpack
def test_with_cohorted_content(self, content_creator_method_name, api_version):
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(api_version=api_version).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(api_version=api_version).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(api_version=api_version).data
self.assertEqual(len(video_outline), 2)
@ddt.data(API_V05, API_V1)
def test_with_hidden_blocks(self, api_version):
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(api_version=api_version).data
self.assertEqual(len(course_outline), 0)
@ddt.data(API_V05, API_V1)
def test_language(self, api_version):
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(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertEqual(course_outline[0]['summary']['language'], case.expected_language)
@ddt.data(API_V05, API_V1)
def test_transcripts(self, api_version):
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(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertSetEqual(
set(course_outline[0]['summary']['transcripts'].keys()),
set(case.expected_transcripts)
)
@ddt.data(
({}, '', [], ['en'], API_V05),
({}, '', [], ['en'], API_V1),
({}, '', ['de'], ['de'], API_V05),
({}, '', ['de'], ['de'], API_V1),
({}, '', ['en', 'de'], ['en', 'de'], API_V05),
({}, '', ['en', 'de'], ['en', 'de'], API_V1),
({}, 'en-subs', ['de'], ['en', 'de'], API_V05),
({}, 'en-subs', ['de'], ['en', 'de'], API_V1),
({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de'], API_V05),
({'uk': 1}, 'en-subs', ['de'], ['en', 'uk', 'de'], API_V1),
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de'], API_V05),
({'uk': 1, 'de': 1}, 'en-subs', ['de', 'en'], ['en', 'uk', 'de'], API_V1),
)
@ddt.unpack
@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, api_version,
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(api_version=api_version).data
self.assertEqual(len(course_outline), 1)
self.assertItemsEqual(course_outline[0]['summary']['transcripts'].keys(), expected_transcripts)
@ddt.ddt
class TestTranscriptsDetail(TestVideoAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin,
TestVideoAPIMixin, MilestonesTestCaseMixin):
"""
Tests for /api/mobile/{api_version}/video_outlines/transcripts/{course_id}..
"""
REVERSE_INFO = {'name': 'video-transcripts-detail', 'params': ['course_id', 'api_version']}
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)
@ddt.data(API_V05, API_V1)
def test_incorrect_language(self, api_version):
self.login_and_enroll()
self.api_response(expected_response_code=404, lang='pl', api_version=api_version)
@ddt.data(API_V05, API_V1)
def test_transcript_with_unicode_file_name(self, api_version):
self.video = self._create_video_with_subs(custom_subid=u'你好')
self.login_and_enroll()
self.api_response(expected_response_code=200, lang='en', api_version=api_version)
@ddt.data(API_V05, API_V1)
@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, api_version, 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', api_version=api_version)
# 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)

View File

@@ -1,21 +0,0 @@
"""
URLs for video outline API
"""
from django.conf import settings
from django.conf.urls import url
from .views import VideoSummaryList, VideoTranscripts
urlpatterns = [
url(
r'^courses/{}$'.format(settings.COURSE_ID_PATTERN),
VideoSummaryList.as_view(),
name='video-summary-list'
),
url(
r'^transcripts/{}/(?P<block_id>[^/]*)/(?P<lang>[^/]*)$'.format(settings.COURSE_ID_PATTERN),
VideoTranscripts.as_view(),
name='video-transcripts-detail'
),
]

View File

@@ -1,127 +0,0 @@
"""
Video Outlines
We only provide the listing view for a video outline, and video outlines are
only displayed at the course level. This is because it makes it a lot easier to
optimize and reason about, and it avoids having to tackle the bigger problem of
general XBlock representation in this rather specialized formatting.
"""
from functools import partial
from django.http import Http404, HttpResponse
from opaque_keys.edx.locator import BlockUsageLocator
from rest_framework import generics
from rest_framework.response import Response
from mobile_api.models import MobileApiConfig
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.video_module.transcripts_utils import get_transcript
from ..decorators import mobile_course_access, mobile_view
from .serializers import BlockOutline, video_summary
@mobile_view()
class VideoSummaryList(generics.ListAPIView):
"""
**Use Case**
Get a list of all videos in the specified course. You can use the
video_url value to access the video file.
**Example Request**
GET /api/mobile/v0.5/video_outlines/courses/{organization}/{course_number}/{course_run}
**Response Values**
If the request is successful, the request returns an HTTP 200 "OK"
response along with an array of videos in the course. The array
includes the following information for each video.
* named_path: An array that consists of the display names of the
courseware objects in the path to the video.
* path: An array that specifies the complete path to the video in
the courseware hierarchy. The array contains the following
values.
* category: The type of division in the course outline.
Possible values are "chapter", "sequential", and "vertical".
* name: The display name for the object.
* id: The The unique identifier for the video.
* section_url: The URL to the first page of the section that
contains the video in the Learning Management System.
* summary: An array of data about the video that includes the
following values.
* category: The type of component. This value will always be "video".
* duration: The length of the video, if available.
* id: The unique identifier for the video.
* language: The language code for the video.
* name: The display name of the video.
* size: The size of the video file.
* transcripts: An array of language codes and URLs to available
video transcripts. Use the URL value to access a transcript
for the video.
* video_thumbnail_url: The URL to the thumbnail image for the
video, if available.
* video_url: The URL to the video file. Use this value to access
the video.
* unit_url: The URL to the unit that contains the video in the Learning
Management System.
"""
@mobile_course_access(depth=None)
def list(self, request, course, *args, **kwargs):
video_profiles = MobileApiConfig.get_video_profiles()
video_outline = list(
BlockOutline(
course.id,
course,
{"video": partial(video_summary, video_profiles)},
request,
video_profiles,
kwargs.get('api_version')
)
)
return Response(video_outline)
@mobile_view()
class VideoTranscripts(generics.RetrieveAPIView):
"""
**Use Case**
Get a transcript for a specified video and language.
**Example request**
GET /api/mobile/v0.5/video_outlines/transcripts/{organization}/{course_number}/{course_run}/{video ID}/{language code}
**Response Values**
If the request is successful, the request returns an HTTP 200 "OK"
response along with an .srt file that you can download.
"""
@mobile_course_access()
def get(self, request, course, *args, **kwargs):
block_id = kwargs['block_id']
lang = kwargs['lang']
usage_key = BlockUsageLocator(course.id, block_type='video', block_id=block_id)
video_descriptor = modulestore().get_item(usage_key)
try:
content, filename, mimetype = get_transcript(video_descriptor, lang=lang)
except NotFoundError:
raise Http404(u'Transcript not found for {}, lang: {}'.format(block_id, lang))
response = HttpResponse(content, content_type=mimetype)
response['Content-Disposition'] = u'attachment; filename="{}"'.format(filename)
return response