Merge pull request #2149 from edx/anton/video-download-transcript
Allows students to download the transcript of the video without timecodes
This commit is contained in:
@@ -15,7 +15,6 @@ from edxmako.shortcuts import render_to_string
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import Scope
|
||||
from xmodule.tests import get_test_system, get_test_descriptor_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -35,7 +34,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
|
||||
Any xmodule should overwrite only next parameters for test:
|
||||
1. CATEGORY
|
||||
2. DATA
|
||||
2. DATA or METADATA
|
||||
3. MODEL_DATA
|
||||
4. COURSE_DATA and USER_COUNT if needed
|
||||
|
||||
@@ -48,6 +47,10 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
|
||||
CATEGORY = "vertical"
|
||||
DATA = ''
|
||||
# METADATA must be overwritten for every instance that uses it. Otherwise,
|
||||
# if we'll change it in the tests, it will be changed for all other instances
|
||||
# of parent class.
|
||||
METADATA = {}
|
||||
MODEL_DATA = {'data': '<some_module></some_module>'}
|
||||
|
||||
def new_module_runtime(self):
|
||||
@@ -71,37 +74,13 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
runtime.get_block = modulestore().get_item
|
||||
return runtime
|
||||
|
||||
def setUp(self):
|
||||
def initialize_module(self, **kwargs):
|
||||
kwargs.update({
|
||||
'parent_location': self.section.location,
|
||||
'category': self.CATEGORY
|
||||
})
|
||||
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
|
||||
# Turn off cache.
|
||||
modulestore().request_cache = None
|
||||
modulestore().metadata_inheritance_cache_subsystem = None
|
||||
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="sequential",
|
||||
)
|
||||
section = ItemFactory.create(
|
||||
parent_location=chapter.location,
|
||||
category="sequential"
|
||||
)
|
||||
|
||||
# username = robot{0}, password = 'test'
|
||||
self.users = [
|
||||
UserFactory.create()
|
||||
for i in range(self.USER_COUNT)
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
self.item_descriptor = ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
category=self.CATEGORY,
|
||||
data=self.DATA
|
||||
)
|
||||
self.item_descriptor = ItemFactory.create(**kwargs)
|
||||
|
||||
self.runtime = self.new_descriptor_runtime()
|
||||
|
||||
@@ -115,6 +94,31 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
|
||||
self.item_url = Location(self.item_module.location).url()
|
||||
|
||||
def setup_course(self):
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
|
||||
# Turn off cache.
|
||||
modulestore().request_cache = None
|
||||
modulestore().metadata_inheritance_cache_subsystem = None
|
||||
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="sequential",
|
||||
)
|
||||
self.section = ItemFactory.create(
|
||||
parent_location=chapter.location,
|
||||
category="sequential"
|
||||
)
|
||||
|
||||
# username = robot{0}, password = 'test'
|
||||
self.users = [
|
||||
UserFactory.create()
|
||||
for i in range(self.USER_COUNT)
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
# login all users for acces to Xmodule
|
||||
self.clients = {user.username: Client() for user in self.users}
|
||||
self.login_statuses = [
|
||||
@@ -125,6 +129,10 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
|
||||
self.assertTrue(all(self.login_statuses))
|
||||
|
||||
def setUp(self):
|
||||
self.setup_course();
|
||||
self.initialize_module(metadata=self.METADATA, data=self.DATA)
|
||||
|
||||
def get_url(self, dispatch):
|
||||
"""Return item url with dispatch."""
|
||||
return reverse(
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Video xmodule tests in mongo."""
|
||||
|
||||
from mock import patch, PropertyMock
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
from functools import partial
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from . import BaseTestXmodule
|
||||
from .test_video_xml import SOURCE_XML
|
||||
from django.conf import settings
|
||||
from xmodule.video_module import _create_youtube_string
|
||||
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.exceptions import NotFoundError
|
||||
|
||||
class TestVideo(BaseTestXmodule):
|
||||
"""Integration tests: web client + mongo."""
|
||||
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
|
||||
def test_handle_ajax_dispatch(self):
|
||||
responses = {
|
||||
@@ -29,16 +39,21 @@ class TestVideo(BaseTestXmodule):
|
||||
]).pop(),
|
||||
404)
|
||||
|
||||
def tearDown(self):
|
||||
_clear_assets(self.item_module.location)
|
||||
|
||||
|
||||
class TestVideoYouTube(TestVideo):
|
||||
METADATA = {}
|
||||
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that all parameters extracted correclty from xml"""
|
||||
|
||||
context = self.item_module.render('student_view').content
|
||||
|
||||
sources = {
|
||||
'main': u'example.mp4',
|
||||
u'mp4': u'example.mp4',
|
||||
u'webm': u'example.webm',
|
||||
u'ogv': u'example.ogv'
|
||||
}
|
||||
|
||||
expected_context = {
|
||||
@@ -51,7 +66,7 @@ class TestVideo(BaseTestXmodule):
|
||||
'sources': sources,
|
||||
'start': 3603.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'track': '',
|
||||
'track': None,
|
||||
'youtube_streams': _create_youtube_string(self.item_module),
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'yt_test_timeout': 1500,
|
||||
@@ -75,12 +90,12 @@ class TestVideoNonYouTube(TestVideo):
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<source src="example.ogv"/>
|
||||
</video>
|
||||
"""
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
METADATA = {}
|
||||
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that if the 'youtube' attribute is omitted in XML, then
|
||||
@@ -90,7 +105,6 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'main': u'example.mp4',
|
||||
u'mp4': u'example.mp4',
|
||||
u'webm': u'example.webm',
|
||||
u'ogv': u'example.ogv'
|
||||
}
|
||||
|
||||
context = self.item_module.render('student_view').content
|
||||
@@ -105,7 +119,7 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'sources': sources,
|
||||
'start': 3603.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'track': '',
|
||||
'track': None,
|
||||
'youtube_streams': '1.00:OEoXaMPEzfM',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
@@ -116,3 +130,319 @@ class TestVideoNonYouTube(TestVideo):
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
|
||||
|
||||
class TestVideoGetTranscriptsMethod(TestVideo):
|
||||
"""
|
||||
Make sure that `get_transcript` method works correctly
|
||||
"""
|
||||
|
||||
DATA = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
</video>
|
||||
"""
|
||||
MODEL_DATA = {
|
||||
'data': DATA
|
||||
}
|
||||
METADATA = {}
|
||||
|
||||
def test_good_transcript(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content="""
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
],
|
||||
"text": [
|
||||
"Hi, welcome to Edx.",
|
||||
"Let's start with what is on your screen right now."
|
||||
]
|
||||
}
|
||||
""")
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
text = item.get_transcript(subs_id)
|
||||
expected_text = "Hi, welcome to Edx.\nLet's start with what is on your screen right now."
|
||||
|
||||
self.assertEqual(
|
||||
text, expected_text
|
||||
)
|
||||
|
||||
def test_not_found_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
with self.assertRaises(NotFoundError):
|
||||
item.get_transcript('wrong')
|
||||
|
||||
def test_value_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content="""
|
||||
bad content
|
||||
""")
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
item.get_transcript(subs_id)
|
||||
|
||||
def test_key_error(self):
|
||||
self.item_module.render('student_view')
|
||||
item = self.item_descriptor.xmodule_runtime.xmodule_instance
|
||||
|
||||
good_sjson = _create_file(content="""
|
||||
{
|
||||
"start": [
|
||||
270,
|
||||
2720
|
||||
],
|
||||
"end": [
|
||||
2720,
|
||||
5430
|
||||
]
|
||||
}
|
||||
""")
|
||||
|
||||
_upload_file(good_sjson, self.item_module.location)
|
||||
subs_id = _get_subs_id(good_sjson.name)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
item.get_transcript(subs_id)
|
||||
|
||||
|
||||
class TestGetHtmlMethod(BaseTestXmodule):
|
||||
"""
|
||||
Make sure that `get_html` works correctly.
|
||||
"""
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
|
||||
def setUp(self):
|
||||
self.setup_course();
|
||||
|
||||
def test_get_html_track(self):
|
||||
SOURCE_XML = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
sub="{sub}" download_track="{download_track}"
|
||||
start_time="01:00:03" end_time="01:00:10"
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
{track}
|
||||
</video>
|
||||
"""
|
||||
|
||||
cases = [
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'<track src="http://www.example.com/track"/>',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': u'http://www.example.com/track',
|
||||
},
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': u'a_sub_file.srt.sjson',
|
||||
},
|
||||
{
|
||||
'download_track': u'true',
|
||||
'track': u'',
|
||||
'sub': u'',
|
||||
'expected_track_url': None
|
||||
},
|
||||
{
|
||||
'download_track': u'false',
|
||||
'track': u'<track src="http://www.example.com/track"/>',
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'expected_track_url': None,
|
||||
}
|
||||
]
|
||||
|
||||
expected_context = {
|
||||
'data_dir': getattr(self, 'data_dir', None),
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'show_captions': 'true',
|
||||
'display_name': u'A Name',
|
||||
'end': 3610.0,
|
||||
'id': None,
|
||||
'sources': {
|
||||
'main': u'example.mp4',
|
||||
u'mp4': u'example.mp4',
|
||||
u'webm': u'example.webm'
|
||||
},
|
||||
'start': 3603.0,
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'track': '',
|
||||
'youtube_streams': '1.00:OEoXaMPEzfM',
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
}
|
||||
|
||||
for data in cases:
|
||||
DATA = SOURCE_XML.format(
|
||||
download_track=data['download_track'],
|
||||
track=data['track'],
|
||||
sub=data['sub'],
|
||||
)
|
||||
|
||||
self.initialize_module(data=DATA)
|
||||
track_url = self.item_descriptor.xmodule_runtime.handler_url(self.item_module, 'download_transcript')
|
||||
|
||||
expected_context.update({
|
||||
'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
|
||||
'sub': data['sub'],
|
||||
'id': self.item_module.location.html_id(),
|
||||
})
|
||||
|
||||
context = self.item_module.render('student_view').content
|
||||
self.assertEqual(
|
||||
context,
|
||||
self.item_module.xmodule_runtime.render_template('video.html', expected_context)
|
||||
)
|
||||
|
||||
|
||||
class TestVideoDescriptorInitialization(BaseTestXmodule):
|
||||
"""
|
||||
Make sure that module initialization works correctly.
|
||||
"""
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
|
||||
def setUp(self):
|
||||
self.setup_course();
|
||||
|
||||
def test_track_is_not_empty(self):
|
||||
metatdata = {
|
||||
'track': 'http://example.org/track',
|
||||
}
|
||||
|
||||
self.initialize_module(metadata=metatdata)
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertIn('track', fields)
|
||||
self.assertEqual(self.item_module.track, 'http://example.org/track')
|
||||
self.assertTrue(self.item_module.download_track)
|
||||
self.assertTrue(self.item_module.track_visible)
|
||||
|
||||
@patch('xmodule.x_module.XModuleDescriptor.editable_metadata_fields', new_callable=PropertyMock)
|
||||
def test_download_track_is_explicitly_set(self, mock_editable_fields):
|
||||
mock_editable_fields.return_value = {
|
||||
'download_track': {
|
||||
'default_value': False,
|
||||
'explicitly_set': True,
|
||||
'display_name': 'Transcript Download Allowed',
|
||||
'help': 'Show a link beneath the video to allow students to download the transcript.',
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
'field_name': 'download_track',
|
||||
'options': [
|
||||
{'display_name': "True", "value": True},
|
||||
{'display_name': "False", "value": False}
|
||||
]
|
||||
},
|
||||
'track': {
|
||||
'default_value': '',
|
||||
'explicitly_set': False,
|
||||
'display_name': 'Download Transcript',
|
||||
'help': 'The external URL to download the timed transcript track.',
|
||||
'type': 'Generic',
|
||||
'value': u'http://example.org/track',
|
||||
'field_name': 'track',
|
||||
'options': []
|
||||
},
|
||||
}
|
||||
metadata = {
|
||||
'track': 'http://example.org/track',
|
||||
}
|
||||
|
||||
self.initialize_module(metadata=metadata)
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertIn('track', fields)
|
||||
self.assertEqual(self.item_module.track, 'http://example.org/track')
|
||||
self.assertFalse(self.item_module.download_track)
|
||||
self.assertTrue(self.item_module.track_visible)
|
||||
|
||||
|
||||
def test_track_is_empty(self):
|
||||
metatdata = {
|
||||
'track': '',
|
||||
}
|
||||
|
||||
self.initialize_module(metadata=metatdata)
|
||||
fields = self.item_descriptor.editable_metadata_fields
|
||||
|
||||
self.assertNotIn('track', fields)
|
||||
self.assertEqual(self.item_module.track, '')
|
||||
self.assertFalse(self.item_module.download_track)
|
||||
self.assertFalse(self.item_module.track_visible)
|
||||
|
||||
|
||||
def _clear_assets(location):
|
||||
store = contentstore()
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, location.name
|
||||
)
|
||||
|
||||
assets, __ = store.get_all_content_for_course(content_location)
|
||||
for asset in assets:
|
||||
asset_location = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_location)
|
||||
store.delete(id)
|
||||
|
||||
def _get_subs_id(filename):
|
||||
basename = os.path.splitext(os.path.basename(filename))[0]
|
||||
return basename.replace('subs_', '').replace('.srt', '')
|
||||
|
||||
def _create_file(content=''):
|
||||
sjson_file = tempfile.NamedTemporaryFile(prefix="subs_", suffix=".srt.sjson")
|
||||
sjson_file.content_type = 'application/json'
|
||||
sjson_file.write(textwrap.dedent(content))
|
||||
sjson_file.seek(0)
|
||||
|
||||
return sjson_file
|
||||
|
||||
def _upload_file(file, location):
|
||||
filename = 'subs_{}.srt.sjson'.format(_get_subs_id(file.name))
|
||||
mime_type = file.content_type
|
||||
|
||||
content_location = StaticContent.compute_location(
|
||||
location.org, location.course, filename
|
||||
)
|
||||
|
||||
sc_partial = partial(StaticContent, content_location, filename, mime_type)
|
||||
content = sc_partial(file.read())
|
||||
|
||||
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(
|
||||
content,
|
||||
tempfile_path=None
|
||||
)
|
||||
del_cached_content(thumbnail_location)
|
||||
|
||||
if thumbnail_content is not None:
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
@@ -35,7 +35,6 @@ SOURCE_XML = """
|
||||
>
|
||||
<source src="example.mp4"/>
|
||||
<source src="example.webm"/>
|
||||
<source src="example.ogv"/>
|
||||
</video>
|
||||
"""
|
||||
|
||||
@@ -68,12 +67,10 @@ class VideoModuleUnitTest(unittest.TestCase):
|
||||
def test_video_get_html(self):
|
||||
"""Make sure that all parameters extracted correclty from xml"""
|
||||
module = VideoFactory.create()
|
||||
|
||||
sources = {
|
||||
'main': 'example.mp4',
|
||||
'mp4': 'example.mp4',
|
||||
'webm': 'example.webm',
|
||||
'ogv': 'example.ogv'
|
||||
}
|
||||
|
||||
expected_context = {
|
||||
@@ -87,7 +84,7 @@ class VideoModuleUnitTest(unittest.TestCase):
|
||||
'show_captions': 'true',
|
||||
'sources': sources,
|
||||
'youtube_streams': _create_youtube_string(module),
|
||||
'track': '',
|
||||
'track': None,
|
||||
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
Reference in New Issue
Block a user