DEPR-4: Remove Mobile Video Outlines API
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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='_'))
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Video outline API
|
||||
"""
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user