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

@@ -25,7 +25,6 @@ from .test_import import DummySystem
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from textwrap import dedent
from xmodule.tests import get_test_descriptor_system
@@ -187,6 +186,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
download_track="true"
start_time="00:00:01"
end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/>
@@ -211,6 +211,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'download_track': True,
'html5_sources': ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'],
'data': ''
})
@@ -221,6 +222,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
download_track="false"
start_time="00:00:01"
end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/>
@@ -237,6 +239,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'download_track': False,
'source': 'http://www.example.com/source.mp4',
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
@@ -253,7 +256,6 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
show_captions="true">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system, Mock())
@@ -265,7 +267,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'show_captions': True,
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': 'http://www.example.com/track',
'track': '',
'download_track': False,
'source': 'http://www.example.com/source.mp4',
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
@@ -287,6 +290,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': '',
'download_track': False,
'source': '',
'html5_sources': [],
'data': ''
@@ -305,6 +309,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
source="&quot;http://download_video&quot;"
sub="&quot;html5_subtitles&quot;"
track="&quot;http://download_track&quot;"
download_track="true"
youtube_id_0_75="&quot;OEoXaMPEzf65&quot;"
youtube_id_1_25="&quot;OEoXaMPEzf125&quot;"
youtube_id_1_5="&quot;OEoXaMPEzf15&quot;"
@@ -321,6 +326,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': 'http://download_track',
'download_track': True,
'source': 'http://download_video',
'html5_sources': ["source_1", "source_2"],
'data': ''
@@ -343,6 +349,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=0.0),
'end_time': datetime.timedelta(seconds=0.0),
'track': '',
'download_track': False,
'source': '',
'html5_sources': [],
'data': ''
@@ -373,6 +380,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'download_track': True,
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
})
@@ -402,6 +410,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'download_track': True,
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
})
@@ -431,6 +440,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time': datetime.timedelta(seconds=1),
'end_time': datetime.timedelta(seconds=60),
'track': 'http://www.example.com/track',
'download_track': True,
'html5_sources': ['http://www.example.com/source.mp4'],
'data': ''
})
@@ -461,11 +471,12 @@ class VideoExportTestCase(unittest.TestCase):
desc.start_time = datetime.timedelta(seconds=1.0)
desc.end_time = datetime.timedelta(seconds=60)
desc.track = 'http://www.example.com/track'
desc.download_track = True
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = etree.fromstring('''\
<video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00">
<video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00" download_track="true">
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
@@ -488,11 +499,12 @@ class VideoExportTestCase(unittest.TestCase):
desc.start_time = datetime.timedelta(seconds=5.0)
desc.end_time = datetime.timedelta(seconds=0.0)
desc.track = 'http://www.example.com/track'
desc.download_track = True
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = etree.fromstring('''\
<video url_name="SampleProblem1" start_time="0:00:05" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false">
<video url_name="SampleProblem1" start_time="0:00:05" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" download_track="true">
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>

View File

@@ -13,18 +13,24 @@ in XML.
import json
import logging
from HTMLParser import HTMLParser
from lxml import etree
from pkg_resources import resource_string
import datetime
import copy
from webob import Response
from django.http import Http404
from django.conf import settings
from xmodule.x_module import XModule
from xmodule.x_module import XModule, module_attr
from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xblock.core import XBlock
from xblock.fields import Scope, String, Boolean, List, Integer, ScopeIds
from xmodule.fields import RelativeTime
@@ -103,11 +109,19 @@ class VideoFields(object):
display_name="Video Sources",
scope=Scope.settings,
)
# `track` is deprecated field and should not be used in future.
# `download_track` is used instead.
track = String(
help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
help="The external URL to download the timed transcript track.",
display_name="Download Transcript",
scope=Scope.settings,
default=""
default=''
)
download_track = Boolean(
help="Show a link beneath the video to allow students to download the transcript. Note: You must add a link to the HTML5 Transcript field above.",
display_name="Transcript Download Allowed",
scope=Scope.settings,
default=False
)
sub = String(
help="The name of the timed transcript track (for non-Youtube videos).",
@@ -162,18 +176,25 @@ class VideoModule(VideoFields, XModule):
raise Http404()
def get_html(self):
track_url = None
caption_asset_path = "/static/subs/"
get_ext = lambda filename: filename.rpartition('.')[-1]
sources = {get_ext(src): src for src in self.html5_sources}
sources['main'] = self.source
if self.download_track:
if self.track:
track_url = self.track
elif self.sub:
track_url = self.runtime.handler_url(self, 'download_transcript')
return self.system.render_template('video.html', {
'youtube_streams': _create_youtube_string(self),
'id': self.location.html_id(),
'sub': self.sub,
'sources': sources,
'track': self.track,
'track': track_url,
'display_name': self.display_name_with_default,
# This won't work when we move to data that
# isn't on the filesystem
@@ -189,10 +210,58 @@ class VideoModule(VideoFields, XModule):
'yt_test_url': settings.YOUTUBE_TEST_URL
})
def get_transcript(self, subs_id):
'''
Returns transcript without timecodes.
Args:
`subs_id`: str, subtitles id
Raises:
- NotFoundError if cannot find transcript file in storage.
- ValueError if transcript file is incorrect JSON.
- KeyError if transcript file has incorrect format.
'''
filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location(
self.location.org, self.location.course, filename
)
data = contentstore().find(content_location).data
text = json.loads(data)['text']
return HTMLParser().unescape("\n".join(text))
@XBlock.handler
def download_transcript(self, __, ___):
"""
This is called to get transcript file without timecodes to student.
"""
try:
subs = self.get_transcript(self.sub)
except (NotFoundError):
log.debug("Can't find content in storage for %s transcript", self.sub)
return Response(status=404)
except (ValueError, KeyError):
log.debug("Invalid transcript JSON.")
return Response(status=400)
response = Response(
subs,
headerlist=[
('Content-Disposition', 'attachment; filename="{0}.txt"'.format(self.sub)),
])
response.content_type="text/plain; charset=utf-8"
return response
class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor):
"""Descriptor for `VideoModule`."""
module_class = VideoModule
download_transcript = module_attr('download_transcript')
tabs = [
{
@@ -207,6 +276,12 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
]
def __init__(self, *args, **kwargs):
'''
`track` is deprecated field.
If `track` field exists show `track` field on front-end as not-editable
but clearable. Dropdown `download_track` is a new field and it has value
True.
'''
super(VideoDescriptor, self).__init__(*args, **kwargs)
# For backwards compatibility -- if we've got XML data, parse
# it out and set the metadata fields
@@ -215,6 +290,24 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
self._field_data.set_many(self, field_data)
del self.data
self.track_visible = False
if self.track:
self.track_visible = True
download_track = self.editable_metadata_fields['download_track']
if not download_track['explicitly_set']:
self.download_track = True
@property
def editable_metadata_fields(self):
editable_fields = super(VideoDescriptor, self).editable_metadata_fields
if self.track_visible:
editable_fields['track']['non_editable'] = True
else:
editable_fields.pop('track')
return editable_fields
@classmethod
def from_xml(cls, xml_data, system, id_generator):
"""
@@ -265,6 +358,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
'start_time': self.start_time,
'end_time': self.end_time,
'sub': self.sub,
'download_track': json.dumps(self.download_track),
}
for key, value in attrs.items():
# Mild workaround to ensure that tests pass -- if a field
@@ -282,6 +376,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
ele = etree.Element('track')
ele.set('src', self.track)
xml.append(ele)
return xml
def get_context(self):