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:
Anton Stupak
2014-01-22 12:03:41 -08:00
10 changed files with 519 additions and 55 deletions

View File

@@ -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(

View File

@@ -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&#39;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)

View File

@@ -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/'