Commit 3a740c04 by jmclaus

BLD-844: Add possibility to download transcripts in different formats.

parent 2c9585ea
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Blades: Add .txt and .srt options to the "download transcript" button. BLD-844.
Blades: Fix bug when transcript cutting off view in full view mode. BLD-852. Blades: Fix bug when transcript cutting off view in full view mode. BLD-852.
Blades: Show start time or starting position on slider and VCR. BLD-823. Blades: Show start time or starting position on slider and VCR. BLD-823.
......
...@@ -22,6 +22,15 @@ ...@@ -22,6 +22,15 @@
.video-controls .add-fullscreen { .video-controls .add-fullscreen {
display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors display: none !important; // nasty, but needed to override the bad specificity of the xmodule css selectors
} }
.video-tracks {
.a11y-menu-container {
.a11y-menu-list {
bottom: 100%;
top: auto;
}
}
}
} }
} }
......
$gray: rgb(127, 127, 127);
$blue: rgb(0, 159, 230);
$gray-d1: shade($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$blue-s1: saturate($blue,15%);
%use-font-awesome {
font-family: FontAwesome;
-webkit-font-smoothing: antialiased;
display: inline-block;
speak: none;
}
.a11y-menu-container {
position: relative;
&.open {
.a11y-menu-list {
display: block;
}
}
.a11y-menu-list {
top: 100%;
margin: 0;
padding: 0;
display: none;
position: absolute;
z-index: 10;
list-style: none;
background-color: $white;
border: 1px solid #eee;
li {
margin: 0;
padding: 0;
border-bottom: 1px solid #eee;
color: $white;
cursor: pointer;
a {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $gray-l2;
font-size: 14px;
line-height: 23px;
&:hover {
color: $gray-d1;
}
}
&.active{
a {
color: $blue;
}
}
&:last-child {
box-shadow: none;
border-bottom: 0;
margin-top: 0;
}
}
}
}
// Video track button specific styles
.video-tracks {
.a11y-menu-container {
display: inline-block;
vertical-align: top;
border-left: 1px solid #eee;
&.open {
> a {
background-color: $action-primary-active-bg;
color: $very-light-text;
&:after {
color: $very-light-text;
}
}
}
> a {
@include transition(all 0.25s ease-in-out 0s);
@include font-size(12);
display: block;
border-radius: 0 3px 3px 0;
background-color: $very-light-text;
padding: ($baseline*.75 $baseline*1.25 $baseline*.75 $baseline*.75);
color: $gray-l2;
min-width: 1.5em;
line-height: 14px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
&:after {
@extend %use-font-awesome;
content: "\f0d7";
position: absolute;
right: ($baseline*.5);
top: 33%;
color: $lighter-base-font-color;
}
}
.a11y-menu-list {
right: 0;
li {
font-size: em(14);
a {
border: 0;
display: block;
padding: lh(.5);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
...@@ -46,13 +46,15 @@ div.video { ...@@ -46,13 +46,15 @@ div.video {
.video-sources, .video-sources,
.video-tracks { .video-tracks {
display: inline-block; display: inline-block;
vertical-align: top;
margin: ($baseline*.75) ($baseline/2) 0 0; margin: ($baseline*.75) ($baseline/2) 0 0;
a { > a {
@include transition(all 0.25s ease-in-out 0s); @include transition(all 0.25s ease-in-out 0s);
@include font-size(14); @include font-size(14);
display: inline-block; line-height : 14px;
border-radius: 3px 3px 3px 3px; float: left;
border-radius: 3px;
background-color: $very-light-text; background-color: $very-light-text;
padding: ($baseline*.75); padding: ($baseline*.75);
color: $lighter-base-font-color; color: $lighter-base-font-color;
...@@ -62,7 +64,14 @@ div.video { ...@@ -62,7 +64,14 @@ div.video {
color: $very-light-text; color: $very-light-text;
} }
} }
}
.video-tracks {
> a {
border-radius: 3px 0 0 3px;
}
> a.external-track {
border-radius: 3px;
}
} }
} }
......
...@@ -69,6 +69,23 @@ ...@@ -69,6 +69,23 @@
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads">
<li class="video-tracks">
<div class="a11y-menu-container">
<a class="a11y-menu-button" href="#" title=".srt">.srt</a>
<ol class="a11y-menu-list">
<li class="a11y-menu-item">
<a class="a11y-menu-item-link" href="#txt" title="Text (.txt) file" data-value="txt">Text (.txt) file</a>
</li>
<li class="a11y-menu-item active">
<a class="a11y-menu-item-link" href="#srt" title="SubRip (.srt) file" data-value="srt">SubRip (.srt) file</a>
</li>
</ol>
</div>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -42,6 +42,7 @@ require( ...@@ -42,6 +42,7 @@ require(
[ [
'video/01_initialize.js', 'video/01_initialize.js',
'video/025_focus_grabber.js', 'video/025_focus_grabber.js',
'video/035_video_accessible_menu.js',
'video/04_video_control.js', 'video/04_video_control.js',
'video/05_video_quality_control.js', 'video/05_video_quality_control.js',
'video/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
...@@ -52,6 +53,7 @@ require( ...@@ -52,6 +53,7 @@ require(
function ( function (
Initialize, Initialize,
FocusGrabber, FocusGrabber,
VideoAccessibleMenu,
VideoControl, VideoControl,
VideoQualityControl, VideoQualityControl,
VideoProgressSlider, VideoProgressSlider,
...@@ -87,6 +89,7 @@ function ( ...@@ -87,6 +89,7 @@ function (
state.modules = [ state.modules = [
FocusGrabber, FocusGrabber,
VideoAccessibleMenu,
VideoControl, VideoControl,
VideoQualityControl, VideoQualityControl,
VideoProgressSlider, VideoProgressSlider,
......
...@@ -13,6 +13,7 @@ in XML. ...@@ -13,6 +13,7 @@ in XML.
import json import json
import logging import logging
from operator import itemgetter from operator import itemgetter
from HTMLParser import HTMLParser
from lxml import etree from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
...@@ -155,6 +156,15 @@ class VideoFields(object): ...@@ -155,6 +156,15 @@ class VideoFields(object):
scope=Scope.preferences, scope=Scope.preferences,
default="en" default="en"
) )
transcript_download_format = String(
help="Transcript file format to download by user.",
scope=Scope.preferences,
values=[
{"display_name": "SubRip (.srt) file", "value": "srt"},
{"display_name": "Text (.txt) file", "value": "txt"}
],
default='srt',
)
speed = Float( speed = Float(
help="The last speed that was explicitly set by user for the video.", help="The last speed that was explicitly set by user for the video.",
scope=Scope.user_state, scope=Scope.user_state,
...@@ -193,6 +203,7 @@ class VideoModule(VideoFields, XModule): ...@@ -193,6 +203,7 @@ class VideoModule(VideoFields, XModule):
resource_string(module, 'js/src/video/025_focus_grabber.js'), resource_string(module, 'js/src/video/025_focus_grabber.js'),
resource_string(module, 'js/src/video/02_html5_video.js'), resource_string(module, 'js/src/video/02_html5_video.js'),
resource_string(module, 'js/src/video/03_video_player.js'), resource_string(module, 'js/src/video/03_video_player.js'),
resource_string(module, 'js/src/video/035_video_accessible_menu.js'),
resource_string(module, 'js/src/video/04_video_control.js'), resource_string(module, 'js/src/video/04_video_control.js'),
resource_string(module, 'js/src/video/05_video_quality_control.js'), resource_string(module, 'js/src/video/05_video_quality_control.js'),
resource_string(module, 'js/src/video/06_video_progress_slider.js'), resource_string(module, 'js/src/video/06_video_progress_slider.js'),
...@@ -202,20 +213,33 @@ class VideoModule(VideoFields, XModule): ...@@ -202,20 +213,33 @@ class VideoModule(VideoFields, XModule):
resource_string(module, 'js/src/video/10_main.js') resource_string(module, 'js/src/video/10_main.js')
] ]
} }
css = {'scss': [resource_string(module, 'css/video/display.scss')]} css = {'scss': [
resource_string(module, 'css/video/display.scss'),
resource_string(module, 'css/video/accessible_menu.scss'),
]}
js_module_name = "Video" js_module_name = "Video"
def handle_ajax(self, dispatch, data): def handle_ajax(self, dispatch, data):
accepted_keys = ['speed', 'saved_video_position', 'transcript_language'] accepted_keys = [
if dispatch == 'save_user_state': 'speed', 'saved_video_position', 'transcript_language',
'transcript_download_format',
]
conversions = {
'speed': json.loads,
'saved_video_position': lambda v: RelativeTime.isotime_to_timedelta(v),
}
if dispatch == 'save_user_state':
for key in data: for key in data:
if hasattr(self, key) and key in accepted_keys: if hasattr(self, key) and key in accepted_keys:
if key == 'saved_video_position': if key in conversions:
relative_position = RelativeTime.isotime_to_timedelta(data[key]) value = conversions[key](data[key])
self.saved_video_position = relative_position
else: else:
setattr(self, key, json.loads(data[key])) value = data[key]
setattr(self, key, value)
if key == 'speed': if key == 'speed':
self.global_speed = self.speed self.global_speed = self.speed
...@@ -228,6 +252,7 @@ class VideoModule(VideoFields, XModule): ...@@ -228,6 +252,7 @@ class VideoModule(VideoFields, XModule):
def get_html(self): def get_html(self):
track_url = None track_url = None
transcript_download_format = self.transcript_download_format
get_ext = lambda filename: filename.rpartition('.')[-1] get_ext = lambda filename: filename.rpartition('.')[-1]
sources = {get_ext(src): src for src in self.html5_sources} sources = {get_ext(src): src for src in self.html5_sources}
...@@ -241,7 +266,8 @@ class VideoModule(VideoFields, XModule): ...@@ -241,7 +266,8 @@ class VideoModule(VideoFields, XModule):
if self.download_track: if self.download_track:
if self.track: if self.track:
track_url = self.track track_url = self.track
elif self.sub: transcript_download_format = None
elif self.sub or self.transcripts:
track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download' track_url = self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/download'
if not self.transcripts: if not self.transcripts:
...@@ -289,13 +315,15 @@ class VideoModule(VideoFields, XModule): ...@@ -289,13 +315,15 @@ class VideoModule(VideoFields, XModule):
# configuration setting field. # configuration setting field.
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': settings.YOUTUBE_TEST_URL, 'yt_test_url': settings.YOUTUBE_TEST_URL,
'transcript_download_format': transcript_download_format,
'transcript_download_formats_list': self.descriptor.fields['transcript_download_format'].values,
'transcript_language': transcript_language, 'transcript_language': transcript_language,
'transcript_languages': json.dumps(sorted_languages), 'transcript_languages': json.dumps(sorted_languages),
'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation', 'transcript_translation_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/translation',
'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations', 'transcript_available_translations_url': self.runtime.handler_url(self, 'transcript').rstrip('/?') + '/available_translations',
}) })
def get_transcript(self): def get_transcript(self, format='srt'):
""" """
Returns transcript in *.srt format. Returns transcript in *.srt format.
...@@ -307,12 +335,18 @@ class VideoModule(VideoFields, XModule): ...@@ -307,12 +335,18 @@ class VideoModule(VideoFields, XModule):
lang = self.transcript_language lang = self.transcript_language
subs_id = self.sub if lang == 'en' else self.youtube_id_1_0 subs_id = self.sub if lang == 'en' else self.youtube_id_1_0
data = asset(self.location, subs_id, lang).data data = asset(self.location, subs_id, lang).data
str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0) if format == 'txt':
text = json.loads(data)['text']
str_subs = HTMLParser().unescape("\n".join(text))
mime_type = 'text/plain'
else:
str_subs = generate_srt_from_sjson(json.loads(data), speed=1.0)
mime_type = 'application/x-subrip'
if not str_subs: if not str_subs:
log.debug('generate_srt_from_sjson produces no subtitles') log.debug('generate_srt_from_sjson produces no subtitles')
raise ValueError raise ValueError
return str_subs return str_subs, format, mime_type
@XBlock.handler @XBlock.handler
def transcript(self, request, dispatch): def transcript(self, request, dispatch):
...@@ -350,7 +384,7 @@ class VideoModule(VideoFields, XModule): ...@@ -350,7 +384,7 @@ class VideoModule(VideoFields, XModule):
elif dispatch == 'download': elif dispatch == 'download':
try: try:
subs = self.get_transcript() subs, format, mime_type = self.get_transcript(format=self.transcript_download_format)
except (NotFoundError, ValueError, KeyError): except (NotFoundError, ValueError, KeyError):
log.debug("Video@download exception") log.debug("Video@download exception")
response = Response(status=404) response = Response(status=404)
...@@ -358,10 +392,13 @@ class VideoModule(VideoFields, XModule): ...@@ -358,10 +392,13 @@ class VideoModule(VideoFields, XModule):
response = Response( response = Response(
subs, subs,
headerlist=[ headerlist=[
('Content-Disposition', 'attachment; filename="{0}.srt"'.format(self.transcript_language)), ('Content-Disposition', 'attachment; filename="{filename}.{format}"'.format(
filename=self.transcript_language,
format=format,
)),
] ]
) )
response.content_type = "application/x-subrip" response.content_type = mime_type
elif dispatch == 'available_translations': elif dispatch == 'available_translations':
available_translations = [] available_translations = []
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
114220 114220
], ],
"text": [ "text": [
"LILA FISHER: Hi, welcome to Edx.", "Hi, welcome to Edx.",
"I'm Lila Fisher, an Edx fellow helping to put", "I'm Lila Fisher, an Edx fellow helping to put",
"together these courses.", "together these courses.",
"As you know, our courses are entirely online.", "As you know, our courses are entirely online.",
......
...@@ -155,3 +155,24 @@ Feature: LMS Video component ...@@ -155,3 +155,24 @@ Feature: LMS Video component
Then I see video aligned correctly with enabled transcript Then I see video aligned correctly with enabled transcript
And I click video button "CC" And I click video button "CC"
Then I see video aligned correctly without enabled transcript Then I see video aligned correctly without enabled transcript
# 19
Scenario: Download Transcript button works correctly in Video component
Given I am registered for the course "test_course"
And it has a video "A" in "Youtube" mode in position "1" of sequential:
| sub | download_track |
| OEoXaMPEzfM | true |
And a video "B" in "Youtube" mode in position "2" of sequential:
| sub | download_track |
| OEoXaMPEzfM | true |
And a video "C" in "Youtube" mode in position "3" of sequential:
| track | download_track |
| http://example.org/ | true |
And I open the section with videos
And I can download transcript in "srt" format
And I select the transcript format "txt"
And I can download transcript in "txt" format
When I open video "B"
Then I can download transcript in "txt" format
When I open video "C"
Then menu "download_transcript" doesn't exist
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
from lettuce import world, step from lettuce import world, step
import json import json
import os
import requests
from common import i_am_registered_for_the_course, section_location, visit_scenario_item from common import i_am_registered_for_the_course, section_location, visit_scenario_item
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from cache_toolbox.core import del_cached_content from cache_toolbox.core import del_cached_content
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
import os
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
LANGUAGES = settings.ALL_LANGUAGES LANGUAGES = settings.ALL_LANGUAGES
...@@ -32,17 +33,57 @@ VIDEO_BUTTONS = { ...@@ -32,17 +33,57 @@ VIDEO_BUTTONS = {
'play': '.video_control.play', 'play': '.video_control.play',
'pause': '.video_control.pause', 'pause': '.video_control.pause',
'fullscreen': '.add-fullscreen', 'fullscreen': '.add-fullscreen',
'download_transcript': '.video-tracks > a',
} }
VIDEO_MENUS = { VIDEO_MENUS = {
'language': '.lang .menu', 'language': '.lang .menu',
'speed': '.speed .menu', 'speed': '.speed .menu',
'download_transcript': '.video-tracks .a11y-menu-list',
} }
coursenum = 'test_course' coursenum = 'test_course'
sequence = {} sequence = {}
class ReuqestHandlerWithSessionId(object):
def get(self, url):
"""
Sends a request.
"""
kwargs = dict()
session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name']==u'sessionid']
if session_id:
kwargs.update({
'cookies': session_id[0]
})
response = requests.get(url, **kwargs)
self.response = response
self.status_code = response.status_code
self.headers = response.headers
self.content = response.content
return self
def is_success(self):
"""
Returns `True` if the response was succeed, otherwise, returns `False`.
"""
if self.status_code < 400:
return True
return False
def check_header(self, name, value):
"""
Returns `True` if the response header exist and has appropriate value,
otherwise, returns `False`.
"""
if value in self.headers.get(name, ''):
return True
return False
def add_video_to_course(course, player_mode, hashes, display_name='Video'): def add_video_to_course(course, player_mode, hashes, display_name='Video'):
category = 'video' category = 'video'
...@@ -80,15 +121,23 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'): ...@@ -80,15 +121,23 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'):
if hashes: if hashes:
kwargs['metadata'].update(hashes[0]) kwargs['metadata'].update(hashes[0])
course_location = world.scenario_dict['COURSE'].location course_location =world.scenario_dict['COURSE'].location
conversions = {
'transcripts': json.loads,
'download_track': json.loads,
'download_video': json.loads,
}
for key in kwargs['metadata']:
if key in conversions:
kwargs['metadata'][key] = conversions[key](kwargs['metadata'][key])
if 'sub' in kwargs['metadata']: if 'sub' in kwargs['metadata']:
filename = _get_sjson_filename(kwargs['metadata']['sub'], 'en') filename = _get_sjson_filename(kwargs['metadata']['sub'], 'en')
_upload_file(filename, course_location) _upload_file(filename, course_location)
if 'transcripts' in kwargs['metadata']: if 'transcripts' in kwargs['metadata']:
kwargs['metadata']['transcripts'] = json.loads(kwargs['metadata']['transcripts'])
for lang, filename in kwargs['metadata']['transcripts'].items(): for lang, filename in kwargs['metadata']['transcripts'].items():
_upload_file(filename, course_location) _upload_file(filename, course_location)
...@@ -322,6 +371,10 @@ def upload_to_assets(_step, filename): ...@@ -322,6 +371,10 @@ def upload_to_assets(_step, filename):
def is_hidden_button(_step, button): def is_hidden_button(_step, button):
assert not world.css_visible(VIDEO_BUTTONS[button]) assert not world.css_visible(VIDEO_BUTTONS[button])
@step('menu "([^"]*)" doesn\'t exist$')
def is_hidden_menu(_step, menu):
assert world.is_css_not_present(VIDEO_MENUS[menu])
@step('I see video aligned correctly (with(?:out)?) enabled transcript$') @step('I see video aligned correctly (with(?:out)?) enabled transcript$')
def video_alignment(_step, transcript_visibility): def video_alignment(_step, transcript_visibility):
...@@ -345,3 +398,45 @@ def video_alignment(_step, transcript_visibility): ...@@ -345,3 +398,45 @@ def video_alignment(_step, transcript_visibility):
) )
assert all([width, height]) assert all([width, height])
@step('I can download transcript in "([^"]*)" format$')
def i_can_download_transcript(_step, format):
button = world.css_find('.video-tracks .a11y-menu-button').first
assert button.text.strip() == '.' + format
formats = {
'srt': {
'content': '0\n00:00:00,270',
'mime_type': 'application/x-subrip'
},
'txt': {
'content': 'Hi, welcome to Edx.',
'mime_type': 'text/plain'
},
}
url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href']
request = ReuqestHandlerWithSessionId()
assert request.get(url).is_success()
assert request.check_header('content-type', formats[format]['mime_type'])
assert request.content.startswith(formats[format]['content'])
@step('I select the transcript format "([^"]*)"$')
def select_transcript_format(_step, format):
button = world.css_find('.video-tracks .a11y-menu-button').first
button.mouse_over()
assert button.text.strip() == '...'
menu_selector = VIDEO_MENUS['download_transcript']
menu_items = world.css_find(menu_selector + ' a')
for item in menu_items:
if item['data-value'] == format:
item.click()
world.wait_for_ajax_complete()
break
assert world.css_find(menu_selector + ' .active a')[0]['data-value'] == format
assert button.text.strip() == '.' + format
...@@ -110,7 +110,7 @@ class TestVideo(BaseTestXmodule): ...@@ -110,7 +110,7 @@ class TestVideo(BaseTestXmodule):
data = [ data = [
{'speed': 2.0}, {'speed': 2.0},
{'saved_video_position': "00:00:10"}, {'saved_video_position': "00:00:10"},
{'transcript_language': json.dumps('uk')}, {'transcript_language': 'uk'},
] ]
for sample in data: for sample in data:
response = self.clients[self.users[0].username].post( response = self.clients[self.users[0].username].post(
...@@ -129,7 +129,7 @@ class TestVideo(BaseTestXmodule): ...@@ -129,7 +129,7 @@ class TestVideo(BaseTestXmodule):
self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0, 10)) self.assertEqual(self.item_descriptor.saved_video_position, timedelta(0, 10))
self.assertEqual(self.item_descriptor.transcript_language, 'en') self.assertEqual(self.item_descriptor.transcript_language, 'en')
self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': json.dumps("uk")}) self.item_descriptor.handle_ajax('save_user_state', {'transcript_language': "uk"})
self.assertEqual(self.item_descriptor.transcript_language, 'uk') self.assertEqual(self.item_descriptor.transcript_language, 'uk')
def tearDown(self): def tearDown(self):
...@@ -173,11 +173,20 @@ class TestVideoTranscriptTranslation(TestVideo): ...@@ -173,11 +173,20 @@ class TestVideoTranscriptTranslation(TestVideo):
response = self.item.transcript(request=request, dispatch='download') response = self.item.transcript(request=request, dispatch='download')
self.assertEqual(response.status, '404 Not Found') self.assertEqual(response.status, '404 Not Found')
@patch('xmodule.video_module.VideoModule.get_transcript', return_value='Subs!') @patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'srt', 'application/x-subrip'))
def test_download_exist(self, __): def test_download_srt_exist(self, __):
request = Request.blank('/download?language=en') request = Request.blank('/download?language=en')
response = self.item.transcript(request=request, dispatch='download') response = self.item.transcript(request=request, dispatch='download')
self.assertEqual(response.body, 'Subs!') self.assertEqual(response.body, 'Subs!')
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip')
@patch('xmodule.video_module.VideoModule.get_transcript', return_value=('Subs!', 'txt', 'text/plain'))
def test_download_txt_exist(self, __):
self.item.transcript_format = 'txt'
request = Request.blank('/download?language=en')
response = self.item.transcript(request=request, dispatch='download')
self.assertEqual(response.body, 'Subs!')
self.assertEqual(response.headers['Content-Type'], 'text/plain')
def test_download_en_no_sub(self): def test_download_en_no_sub(self):
request = Request.blank('/download?language=en') request = Request.blank('/download?language=en')
...@@ -309,7 +318,7 @@ class TestVideoTranscriptsDownload(TestVideo): ...@@ -309,7 +318,7 @@ class TestVideoTranscriptsDownload(TestVideo):
self.item_descriptor.render('student_view') self.item_descriptor.render('student_view')
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
def test_good_transcript(self): def test_good_srt_transcript(self):
good_sjson = _create_file(content=textwrap.dedent("""\ good_sjson = _create_file(content=textwrap.dedent("""\
{ {
"start": [ "start": [
...@@ -329,7 +338,7 @@ class TestVideoTranscriptsDownload(TestVideo): ...@@ -329,7 +338,7 @@ class TestVideoTranscriptsDownload(TestVideo):
_upload_sjson_file(good_sjson, self.item.location) _upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name) self.item.sub = _get_subs_id(good_sjson.name)
text = self.item.get_transcript() text, format, download = self.item.get_transcript()
expected_text = textwrap.dedent("""\ expected_text = textwrap.dedent("""\
0 0
00:00:00,270 --> 00:00:02,720 00:00:00,270 --> 00:00:02,720
...@@ -343,6 +352,33 @@ class TestVideoTranscriptsDownload(TestVideo): ...@@ -343,6 +352,33 @@ class TestVideoTranscriptsDownload(TestVideo):
self.assertEqual(text, expected_text) self.assertEqual(text, expected_text)
def test_good_txt_transcript(self):
good_sjson = _create_file(content=textwrap.dedent("""\
{
"start": [
270,
2720
],
"end": [
2720,
5430
],
"text": [
"Hi, welcome to Edx.",
"Let&#39;s start with what is on your screen right now."
]
}
"""))
_upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name)
text, format, mime_type = self.item.get_transcript(format="txt")
expected_text = textwrap.dedent("""\
Hi, welcome to Edx.
Let's start with what is on your screen right now.""")
self.assertEqual(text, expected_text)
def test_not_found_error(self): def test_not_found_error(self):
with self.assertRaises(NotFoundError): with self.assertRaises(NotFoundError):
self.item.get_transcript() self.item.get_transcript()
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Video xmodule tests in mongo.""" """Video xmodule tests in mongo."""
from mock import patch, PropertyMock from mock import patch, PropertyMock
import json
from . import BaseTestXmodule from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML from .test_video_xml import SOURCE_XML
...@@ -41,6 +40,8 @@ class TestVideoYouTube(TestVideo): ...@@ -41,6 +40,8 @@ class TestVideoYouTube(TestVideo):
'youtube_streams': create_youtube_string(self.item_descriptor), 'youtube_streams': create_youtube_string(self.item_descriptor),
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': 'en', 'transcript_language': 'en',
'transcript_languages': '{"en": "English", "uk": "Ukrainian"}', 'transcript_languages': '{"en": "English", "uk": "Ukrainian"}',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
...@@ -103,6 +104,8 @@ class TestVideoNonYouTube(TestVideo): ...@@ -103,6 +104,8 @@ class TestVideoNonYouTube(TestVideo):
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': 'en', 'transcript_language': 'en',
'transcript_languages': '{"en": "English"}', 'transcript_languages': '{"en": "English"}',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
...@@ -191,6 +194,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -191,6 +194,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
} }
for data in cases: for data in cases:
...@@ -208,6 +212,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -208,6 +212,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
expected_context.update({ expected_context.update({
'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt',
'transcript_languages': '{"en": "English"}', 'transcript_languages': '{"en": "English"}',
'transcript_language': 'en', 'transcript_language': 'en',
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
...@@ -305,6 +310,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -305,6 +310,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True), 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
'yt_test_timeout': 1500, 'yt_test_timeout': 1500,
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/', 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': 'en', 'transcript_language': 'en',
'transcript_languages': '{"en": "English"}', 'transcript_languages': '{"en": "English"}',
} }
......
...@@ -112,7 +112,29 @@ ...@@ -112,7 +112,29 @@
% endif % endif
% if track: % if track:
<li class="video-tracks"> <li class="video-tracks">
${('<a href="%s">' + _('Download timed transcript') + '</a>') % track} % if transcript_download_format:
${('<a href="%s">' + _('Download transcript') + '</a>') % track
}
<div class="a11y-menu-container">
<a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}">${'.' + transcript_download_format}</a>
<ol class="a11y-menu-list">
% for item in transcript_download_formats_list:
% if item['value'] == transcript_download_format:
<li class="a11y-menu-item active">
% else:
<li class="a11y-menu-item">
% endif
<a class="a11y-menu-item-link" href="#${item['value']}" title="${_('{file_format}'.format(file_format=item['display_name']))}" data-value="${item['value']}">
${_('{file_format}'.format(file_format=item['display_name']))}
</a>
</li>
% endfor
</ol>
</div>
% else:
${('<a href="%s" class="external-track">' + _('Download transcript') + '</a>') % track
}
% endif
</li> </li>
% endif % endif
</ul> </ul>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment