Commit 0ef4390a by Peter Fogg

Merge pull request #132 from edx/peter-fogg/remove-video-xml

Peter fogg/remove video xml
parents fc642264 83d84e2b
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,9 @@ 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.
Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata.
XModule: Only write out assets files if the contents have changed. XModule: Only write out assets files if the contents have changed.
XModule: Don't delete generated xmodule asset files when compiling (for XModule: Don't delete generated xmodule asset files when compiling (for
......
...@@ -171,6 +171,16 @@ def open_new_unit(step): ...@@ -171,6 +171,16 @@ def open_new_unit(step):
world.css_click('a.new-unit-item') world.css_click('a.new-unit-item')
@step('when I view the video it (.*) show the captions')
def shows_captions(step, show_captions):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
if show_captions == 'does not':
assert world.css_find('.video')[0].has_class('closed')
else:
assert world.is_css_not_present('.video.closed')
def type_in_codemirror(index, text): def type_in_codemirror(index, text):
world.css_click(".CodeMirror", index=index) world.css_click(".CodeMirror", index=index)
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
......
...@@ -4,10 +4,20 @@ Feature: Video Component Editor ...@@ -4,10 +4,20 @@ Feature: Video Component Editor
Scenario: User can view metadata Scenario: User can view metadata
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit and select Settings
Then I see only the Video display name setting Then I see the correct settings and default values
Scenario: User can modify display name Scenario: User can modify display name
Given I have created a Video component Given I have created a Video component
And I edit and select Settings And I edit and select Settings
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save And my display name change is persisted on save
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component
And I have set "show captions" to False
Then when I view the video it does not show the captions
Scenario: Captions are shown when "show captions" is true
Given I have created a Video component
And I have set "show captions" to True
Then when I view the video it does show the captions
...@@ -4,6 +4,20 @@ ...@@ -4,6 +4,20 @@
from lettuce import world, step from lettuce import world, step
@step('I see only the video display name setting$') @step('I see the correct settings and default values$')
def i_see_only_the_video_display_name(step): def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Display Name', "default", True]]) world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
['Display Name', 'default', True],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
['Speed: .75x', '', False],
['Speed: 1.25x', '', False],
['Speed: 1.5x', '', False]])
@step('I have set "show captions" to (.*)')
def set_show_captions(step, setting):
world.css_click('a.edit-button')
world.browser.select('Show Captions', setting)
world.css_click('a.save-button')
...@@ -9,7 +9,16 @@ Feature: Video Component ...@@ -9,7 +9,16 @@ Feature: Video Component
Given I have clicked the new unit button Given I have clicked the new unit button
Then creating a video takes a single click Then creating a video takes a single click
Scenario: Captions are shown correctly Scenario: Captions are hidden correctly
Given I have created a Video component Given I have created a Video component
And I have hidden captions And I have hidden captions
Then when I view the video it does not show the captions Then when I view the video it does not show the captions
Scenario: Captions are shown correctly
Given I have created a Video component
Then when I view the video it does show the captions
Scenario: Captions are toggled correctly
Given I have created a Video component
And I have toggled captions
Then when I view the video it does show the captions
...@@ -18,11 +18,16 @@ def video_takes_a_single_click(_step): ...@@ -18,11 +18,16 @@ def video_takes_a_single_click(_step):
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
@step('I have hidden captions') @step('I have (hidden|toggled) captions')
def set_show_captions_false(step): def hide_or_show_captions(step, shown):
world.css_click('a.hide-subtitles') button_css = 'a.hide-subtitles'
if shown == 'hidden':
world.css_click(button_css)
@step('when I view the video it does not show the captions') if shown == 'toggled':
def does_not_show_captions(step): world.css_click(button_css)
assert world.css_find('.video')[0].has_class('closed') # When we click the first time, a tooltip shows up. We want to
# click the button rather than the tooltip, so move the mouse
# away to make it disappear.
button = world.css_find(button_css)
button.mouse_out()
world.css_click(button_css)
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div id="video_example"> <div id="video_example">
<div id="example"> <div id="example">
<div id="video_id" class="video" <div id="video_id" class="video"
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId" data-youtube-id-0-75="slowerSpeedYoutubeId"
data-youtube-id-1-0="normalSpeedYoutubeId"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
...@@ -18,4 +19,4 @@ ...@@ -18,4 +19,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
\ No newline at end of file
...@@ -5,7 +5,6 @@ describe 'Video', -> ...@@ -5,7 +5,6 @@ describe 'Video', ->
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
@videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId' @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
metadata = metadata =
...@@ -30,7 +29,7 @@ describe 'Video', -> ...@@ -30,7 +29,7 @@ describe 'Video', ->
beforeEach -> beforeEach ->
spyOn(window.Video.prototype, 'fetchMetadata').andCallFake -> spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata @metadata = metadata
@video = new Video '#example', @videosDefinition @video = new Video '#example'
it 'reset the current video player', -> it 'reset the current video player', ->
expect(window.player).toBeNull() expect(window.player).toBeNull()
...@@ -60,7 +59,7 @@ describe 'Video', -> ...@@ -60,7 +59,7 @@ describe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = { Player: true } window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example', @videosDefinition @video = new Video '#example'
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -73,7 +72,7 @@ describe 'Video', -> ...@@ -73,7 +72,7 @@ describe 'Video', ->
beforeEach -> beforeEach ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
@video = new Video '#example', @videosDefinition @video = new Video '#example'
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -86,7 +85,7 @@ describe 'Video', -> ...@@ -86,7 +85,7 @@ describe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example', @videosDefinition @video = new Video '#example'
window.onYouTubePlayerAPIReady() window.onYouTubePlayerAPIReady()
afterEach -> afterEach ->
...@@ -99,7 +98,7 @@ describe 'Video', -> ...@@ -99,7 +98,7 @@ describe 'Video', ->
describe 'youtubeId', -> describe 'youtubeId', ->
beforeEach -> beforeEach ->
$.cookie.andReturn '1.0' $.cookie.andReturn '1.0'
@video = new Video '#example', @videosDefinition @video = new Video '#example'
describe 'with speed', -> describe 'with speed', ->
it 'return the video id for given speed', -> it 'return the video id for given speed', ->
...@@ -112,7 +111,7 @@ describe 'Video', -> ...@@ -112,7 +111,7 @@ describe 'Video', ->
describe 'setSpeed', -> describe 'setSpeed', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
describe 'when new speed is available', -> describe 'when new speed is available', ->
beforeEach -> beforeEach ->
...@@ -133,14 +132,14 @@ describe 'Video', -> ...@@ -133,14 +132,14 @@ describe 'Video', ->
describe 'getDuration', -> describe 'getDuration', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
it 'return duration for current video', -> it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200 expect(@video.getDuration()).toEqual 200
describe 'log', -> describe 'log', ->
beforeEach -> beforeEach ->
@video = new Video '#example', @videosDefinition @video = new Video '#example'
@video.setSpeed '1.0' @video.setSpeed '1.0'
spyOn Logger, 'log' spyOn Logger, 'log'
@video.player = { currentTime: 25 } @video.player = { currentTime: 25 }
......
...@@ -8,7 +8,7 @@ class @Video ...@@ -8,7 +8,7 @@ class @Video
@show_captions = @el.data('show-captions') @show_captions = @el.data('show-captions')
window.player = null window.player = null
@el = $("#video_#{@id}") @el = $("#video_#{@id}")
@parseVideos @el.data('streams') @parseVideos()
@fetchMetadata() @fetchMetadata()
@parseSpeed() @parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete') $("#video_#{@id}").data('video', this).addClass('video-load-complete')
...@@ -27,10 +27,14 @@ class @Video ...@@ -27,10 +27,14 @@ class @Video
parseVideos: (videos) -> parseVideos: (videos) ->
@videos = {} @videos = {}
$.each videos.split(/,/), (index, video) => if @el.data('youtube-id-0-75')
video = video.split(/:/) @videos['0.75'] = @el.data('youtube-id-0-75')
speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' if @el.data('youtube-id-1-0')
@videos[speed] = video[1] @videos['1.0'] = @el.data('youtube-id-1-0')
if @el.data('youtube-id-1-25')
@videos['1.25'] = @el.data('youtube-id-1-25')
if @el.data('youtube-id-1-5')
@videos['1.50'] = @el.data('youtube-id-1-5')
parseSpeed: -> parseSpeed: ->
@setSpeed($.cookie('video_speed')) @setSpeed($.cookie('video_speed'))
......
--- ---
metadata: metadata:
display_name: default display_name: default
data_dir: a_made_up_name data: ""
data: |
<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
children: [] children: []
...@@ -336,8 +336,8 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -336,8 +336,8 @@ class ImportTestCase(BaseCourseTestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"]) location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location) toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location) two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8") self.assertEqual(toy_video.youtube_id_1_0, "p2Q6BrNhdh8")
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9") self.assertEqual(two_toy_video.youtube_id_1_0, "p2Q6BrNhdh9")
def test_colon_in_url_name(self): def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly""" """Ensure that colons in url_names convert to file paths properly"""
......
# -*- coding: utf-8 -*-
import unittest
from xmodule.video_module import VideoDescriptor
from .test_import import DummySystem
class VideoDescriptorImportTestCase(unittest.TestCase):
"""
Make sure that VideoDescriptor can import an old XML-based video correctly.
"""
def test_from_xml(self):
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(output.show_captions, False)
self.assertEquals(output.start_time, 1.0)
self.assertEquals(output.end_time, 60)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_missing_attributes(self):
"""
Ensure that attributes have the right values if they aren't
explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
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)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_no_attributes(self):
"""
Make sure settings are correct if none are explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '<video></video>'
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'OEoXaMPEzfM')
self.assertEquals(output.youtube_id_1_25, '')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, '')
self.assertEquals(output.source, '')
...@@ -18,7 +18,7 @@ import unittest ...@@ -18,7 +18,7 @@ import unittest
from mock import Mock from mock import Mock
from lxml import etree from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest from xmodule.tests.test_logic import LogicTest
...@@ -49,7 +49,7 @@ class VideoFactory(object): ...@@ -49,7 +49,7 @@ class VideoFactory(object):
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location} model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location}
descriptor = Mock(weight="1") descriptor = Mock(weight="1", url_name="SampleProblem1")
system = get_test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
...@@ -67,69 +67,57 @@ class VideoModuleLogicTest(LogicTest): ...@@ -67,69 +67,57 @@ class VideoModuleLogicTest(LogicTest):
'data': '<video />' 'data': '<video />'
} }
def test_get_timeframe_no_parameters(self): def test_parse_time(self):
"""Make sure that timeframe() works correctly w/o parameters""" """Ensure that times are parsed correctly into seconds."""
xmltree = etree.fromstring('<video>test</video>') output = _parse_time('00:04:07')
output = self.xmodule.get_timeframe(xmltree) self.assertEqual(output, 247)
self.assertEqual(output, ('', ''))
def test_parse_time_none(self):
def test_get_timeframe_with_one_parameter(self): """Check parsing of None."""
"""Make sure that timeframe() works correctly with one parameter""" output = _parse_time(None)
xmltree = etree.fromstring( self.assertEqual(output, '')
'<video from="00:04:07">test</video>'
) def test_parse_time_empty(self):
output = self.xmodule.get_timeframe(xmltree) """Check parsing of the empty string."""
self.assertEqual(output, (247, '')) output = _parse_time('')
self.assertEqual(output, '')
def test_get_timeframe_with_two_parameters(self):
"""Make sure that timeframe() works correctly with two parameters""" def test_parse_youtube(self):
xmltree = etree.fromstring( """Test parsing old-style Youtube ID strings into a dict."""
'''<video youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
from="00:04:07" output = _parse_youtube(youtube_str)
to="13:04:39" self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
>test</video>''' '1.00': 'ZwkTiUPN0mg',
) '1.25': 'rsq9auxASqI',
output = self.xmodule.get_timeframe(xmltree) '1.50': 'kMyNdzVHHgg'})
self.assertEqual(output, (247, 47079))
def test_parse_youtube_one_video(self):
"""
class VideoModuleUnitTest(unittest.TestCase): Ensure that all keys are present and missing speeds map to the
"""Unit tests for Video Xmodule.""" empty string.
"""
def test_video_constructor(self): youtube_str = '0.75:jNCf2gIqpeE'
"""Make sure that all parameters extracted correclty from xml""" output = _parse_youtube(youtube_str)
module = VideoFactory.create() self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
# `get_html` return only context, cause we '1.25': '',
# overwrite `system.render_template` '1.50': ''})
context = module.get_html()
expected_context = { def test_parse_youtube_key_format(self):
'track': None, """
'show_captions': 'true', Make sure that inconsistent speed keys are parsed correctly.
'display_name': 'SampleProblem1', """
'id': module.location.html_id(), youtube_str = '1.00:p2Q6BrNhdh8'
'end': 3610.0, youtube_str_hack = '1.0:p2Q6BrNhdh8'
'caption_asset_path': '/static/subs/', self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack))
'source': '.../mit-3091x/M-3091X-FA12-L21-3_100.mp4',
'streams': '0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg', def test_parse_youtube_empty(self):
'normal_speed_video_id': 'ZwkTiUPN0mg', """
'position': 0, Some courses have empty youtube attributes, so we should handle
'start': 3603.0 that well.
} """
self.assertDictEqual(context, expected_context) self.assertEqual(_parse_youtube(''),
{'0.75': '',
self.assertEqual( '1.00': '',
module.youtube, '1.25': '',
'0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg') '1.50': ''})
self.assertEqual(
module.video_list(),
module.youtube)
self.assertEqual(
module.position,
0)
self.assertDictEqual(
json.loads(module.get_instance_state()),
{'position': 0})
...@@ -6,23 +6,31 @@ import logging ...@@ -6,23 +6,31 @@ import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string, resource_listdir from pkg_resources import resource_string, resource_listdir
import datetime
import time
from django.http import Http404 from django.http import Http404
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Integer, Scope, String from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import Integer, Scope, String, Float, Boolean
import datetime
import time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`.""" """Fields for `VideoModule` and `VideoDescriptor`."""
data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True)
youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM")
youtube_id_0_75 = String(help="The Youtube ID for the .75x speed video.", display_name="Speed: .75x", scope=Scope.settings, default="")
youtube_id_1_25 = String(help="The Youtube ID for the 1.25x speed video.", display_name="Speed: 1.25x", scope=Scope.settings, default="")
youtube_id_1_5 = String(help="The Youtube ID for the 1.5x speed video.", display_name="Speed: 1.5x", scope=Scope.settings, default="")
start_time = Float(help="Time the video starts", display_name="Start Time", scope=Scope.settings, default=0.0)
end_time = Float(help="Time the video ends", display_name="End Time", scope=Scope.settings, default=0.0)
source = String(help="The external URL to download the video. This appears as a link beneath the video.", display_name="Download Video", scope=Scope.settings, default="")
track = String(help="The external URL to download the subtitle track. This appears as a link beneath the video.", display_name="Download Track", scope=Scope.settings, default="")
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
...@@ -46,54 +54,6 @@ class VideoModule(VideoFields, XModule): ...@@ -46,54 +54,6 @@ class VideoModule(VideoFields, XModule):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs) XModule.__init__(self, *args, **kwargs)
xmltree = etree.fromstring(self.data)
self.youtube = xmltree.get('youtube')
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
self.start_time, self.end_time = self.get_timeframe(xmltree)
def _get_source(self, xmltree):
"""Find the first valid source."""
return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree):
"""Find the first valid track."""
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if src:
result = src
break
return result
def get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds.
If there are no parameters, returns empty string. """
def parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if str_time is None:
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
"""This is not being called right now and we raise 404 error.""" """This is not being called right now and we raise 404 error."""
log.debug(u"GET {0}".format(get)) log.debug(u"GET {0}".format(get))
...@@ -104,38 +64,135 @@ class VideoModule(VideoFields, XModule): ...@@ -104,38 +64,135 @@ class VideoModule(VideoFields, XModule):
"""Return information about state (position).""" """Return information about state (position)."""
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def video_list(self):
"""Return video list."""
return self.youtube
def get_html(self): def get_html(self):
# We normally let JS parse this, but in the case that we need a hacked
# out <object> player because YouTube has broken their <iframe> API for
# the third time in a year, we need to extract it server side.
normal_speed_video_id = None # The 1.0 speed video
# video_list() example:
# "0.75:nugHYNiD3fI,1.0:7m8pab1MfYY,1.25:3CxdPGXShq8,1.50:F-D7bOFCnXA"
for video_id_str in self.video_list().split(","):
if video_id_str.startswith("1.0:"):
normal_speed_video_id = video_id_str.split(":")[1]
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'youtube_id_0_75': self.youtube_id_0_75,
'youtube_id_1_0': self.youtube_id_1_0,
'youtube_id_1_25': self.youtube_id_1_25,
'youtube_id_1_5': self.youtube_id_1_5,
'id': self.location.html_id(), 'id': self.location.html_id(),
'position': self.position, 'position': self.position,
'source': self.source, 'source': self.source,
'track': self.track, 'track': self.track,
'display_name': self.display_name_with_default, 'display_name': self.display_name_with_default,
'caption_asset_path': "/static/subs/", 'caption_asset_path': "/static/subs/",
'show_captions': self.show_captions, 'show_captions': 'true' if self.show_captions else 'false',
'start': self.start_time, 'start': self.start_time,
'end': self.end_time, 'end': self.end_time
'normal_speed_video_id': normal_speed_video_id
}) })
class VideoDescriptor(VideoFields, RawDescriptor): class VideoDescriptor(VideoFields,
"""Descriptor for `VideoModule`.""" MetadataOnlyEditingDescriptor,
RawDescriptor):
module_class = VideoModule module_class = VideoModule
template_dir_name = "video" template_dir_name = "video"
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(MetadataOnlyEditingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([VideoModule.start_time,
VideoModule.end_time])
return non_editable_fields
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: A DescriptorSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
video = super(VideoDescriptor, cls).from_xml(xml_data, system, org, course)
xml = etree.fromstring(xml_data)
display_name = xml.get('display_name')
if display_name:
video.display_name = display_name
youtube = xml.get('youtube')
if youtube:
speeds = _parse_youtube(youtube)
if speeds['0.75']:
video.youtube_id_0_75 = speeds['0.75']
if speeds['1.00']:
video.youtube_id_1_0 = speeds['1.00']
if speeds['1.25']:
video.youtube_id_1_25 = speeds['1.25']
if speeds['1.50']:
video.youtube_id_1_5 = speeds['1.50']
show_captions = xml.get('show_captions')
if show_captions:
video.show_captions = json.loads(show_captions)
source = _get_first_external(xml, 'source')
if source:
video.source = source
track = _get_first_external(xml, 'track')
if track:
video.track = track
start_time = _parse_time(xml.get('from'))
if start_time:
video.start_time = start_time
end_time = _parse_time(xml.get('to'))
if end_time:
video.end_time = end_time
return video
def _get_first_external(xmltree, tag):
"""
Returns the src attribute of the nested `tag` in `xmltree`, if it
exists.
"""
for element in xmltree.findall(tag):
src = element.get('src')
if src:
return src
return None
def _parse_youtube(data):
"""
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
into a dictionary. Necessary for backwards compatibility with
XML-based courses.
"""
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''}
if data == '':
return ret
videos = data.split(',')
for video in videos:
pieces = video.split(':')
# HACK
# To elaborate somewhat: in many LMS tests, the keys for
# Youtube IDs are inconsistent. Sometimes a particular
# speed isn't present, and formatting is also inconsistent
# ('1.0' versus '1.00'). So it's necessary to either do
# something like this or update all the tests to work
# properly.
ret['%.2f' % float(pieces[0])] = pieces[1]
return ret
def _parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if str_time is None or str_time == '':
return ''
else:
obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
hours=obj_time.tm_hour,
minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
...@@ -16,13 +16,16 @@ ...@@ -16,13 +16,16 @@
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" <vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds"
showanswer="attempted" rerandomize="never"> showanswer="attempted" rerandomize="never">
<video <video
youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" youtube_id_0_75="&quot;XNh13VZhThQ&quot;"
slug="What_s_next" name="What's next" /> youtube_id_1_0="&quot;XbDRmF6J0K0&quot;"
youtube_id_1_25="&quot;JDty12WEQWk&quot;"
youtube_id_1_5="&quot;wELKGj-5iyM&quot;"
slug="What_s_next"
name="What's next"/>
<html slug="html_95">Minor correction: Six elements (five resistors)… <html slug="html_95">Minor correction: Six elements (five resistors)…
</html> </html>
<customtag tag="S1" slug="discuss_96" impl="discuss" /> <customtag tag="S1" slug="discuss_96" impl="discuss" />
</vertical> </vertical>
<randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule"> <randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule">
<vertical> <vertical>
<html slug="html_900"> <html slug="html_900">
......
<sequential> <sequential>
<video youtube="1.50:8kARlsUt9lM,1.25:4cLA-IME32w,1.0:pFOrD8k9_p4,0.75:CcgAYu0n0bg" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/> <video youtube_id_1_5="&quot;8kARlsUt9lM&quot;" youtube_id_1_25="&quot;4cLA-IME32w&quot;" youtube_id_1_0="&quot;pFOrD8k9_p4&quot;" youtube_id_0_75="&quot;CcgAYu0n0bg&quot;" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
<customtag tag="S1" slug="discuss_59" impl="discuss"/> <customtag tag="S1" slug="discuss_59" impl="discuss"/>
<customtag page="29" slug="book_60" impl="book"/> <customtag page="29" slug="book_60" impl="book"/>
<customtag lecnum="1" slug="slides_61" impl="slides"/> <customtag lecnum="1" slug="slides_61" impl="slides"/>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<!-- UTF-8 characters are acceptable… HTML entities are not --> <!-- UTF-8 characters are acceptable… HTML entities are not -->
<h1>Inline content…</h1> <h1>Inline content…</h1>
</html> </html>
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/> <video youtube_id_1_5="&quot;vl9xrfxcr38&quot;" youtube_id_1_25="&quot;qxNX4REGqx4&quot;" youtube_id_1_0="&quot;BGU1poJDgOY&quot;" youtube_id_0_75="&quot;8rK9vnpystQ&quot;" slug="S1V14_Summary" name="S1V14: Summary"/>
<customtag tag="S1" slug="discuss_91" impl="discuss"/> <customtag tag="S1" slug="discuss_91" impl="discuss"/>
<customtag page="70" slug="book_92" impl="book"/> <customtag page="70" slug="book_92" impl="book"/>
<customtag lecnum="1" slug="slides_93" impl="slides"/> <customtag lecnum="1" slug="slides_93" impl="slides"/>
......
<sequential> <sequential>
<video youtube="0.75:3NIegrCmA5k,1.0:eLAyO33baQ8,1.25:m1zWi_sh4Aw,1.50:EG-fRTJln_E" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/> <video youtube_id_0_75="&quot;3NIegrCmA5k&quot;" youtube_id_1_0="&quot;eLAyO33baQ8&quot;" youtube_id_1_25="&quot;m1zWi_sh4Aw&quot;" youtube_id_1_5="&quot;EG-fRTJln_E&quot;" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
<customtag tag="S2" slug="discuss_95" impl="discuss"/> <customtag tag="S2" slug="discuss_95" impl="discuss"/>
<customtag page="54" slug="book_96" impl="book"/> <customtag page="54" slug="book_96" impl="book"/>
<customtag lecnum="2" slug="slides_97" impl="slides"/> <customtag lecnum="2" slug="slides_97" impl="slides"/>
......
<sequential> <sequential>
<video youtube="0.75:S_1NaY5te8Q,1.0:G_2F9wivspM,1.25:b-r7dISY-Uc,1.50:jjxHom0oXWk" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/> <video youtube_id_0_75="&quot;S_1NaY5te8Q&quot;" youtube_id_1_0="&quot;G_2F9wivspM&quot;" youtube_id_1_25="&quot;b-r7dISY-Uc&quot;" youtube_id_1_5="&quot;jjxHom0oXWk&quot;" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
<customtag tag="S2" slug="discuss_99" impl="discuss"/> <customtag tag="S2" slug="discuss_99" impl="discuss"/>
<customtag page="56" slug="book_100" impl="book"/> <customtag page="56" slug="book_100" impl="book"/>
<customtag lecnum="2" slug="slides_101" impl="slides"/> <customtag lecnum="2" slug="slides_101" impl="slides"/>
......
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome…"/> <video youtube_id_0_75="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;" format="Video" display_name="Welcome…"/>
<course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall"> <course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
<chapter name="Overview"> <chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/> <video name="Welcome" youtube_id_0_75="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;"/>
<videosequence format="Lecture Sequence" name="A simple sequence"> <videosequence format="Lecture Sequence" name="A simple sequence">
<html name="toylab" filename="toylab"/> <html name="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/> <video name="S0V1: Video Resources" youtube_id_0_75="&quot;EuzkdzfR0i8&quot;" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" youtube_id_1_25="&quot;0v1VzoDVUTM&quot;" youtube_id_1_5="&quot;Bxk_-ZJb240&quot;"/>
</videosequence> </videosequence>
<section name="Lecture 2"> <section name="Lecture 2">
<sequential> <sequential>
<video youtube="1.0:TBvX7HzxexQ"/> <video youtube_id_1_0="&quot;TBvX7HzxexQ&quot;"/>
<problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/> <problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/>
</sequential> </sequential>
</section> </section>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/> <problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/>
</sequential> </sequential>
</section> </section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/> <video name="Lost Video" youtube_id_1_0="&quot;TBvX7HzxexQ&quot;"/>
<sequential format="Lecture Sequence" url_name='test_sequence'> <sequential format="Lecture Sequence" url_name='test_sequence'>
<vertical url_name='test_vertical'> <vertical url_name='test_vertical'>
<html url_name='test_html'> <html url_name='test_html'>
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="toylab"/> <html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="Ch2"> <chapter url_name="Ch2">
<html url_name="test_html"> <html url_name="test_html">
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="toylab"/> <html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="Ch2"> <chapter url_name="Ch2">
<html url_name="test_html"> <html url_name="test_html">
......
<chapter> <chapter>
<video url_name="toyvideo" youtube="blahblah"/> <video url_name="toyvideo" youtube_id_1_0="&quot;OEoXaMPEzfM&quot;"/>
</chapter> </chapter>
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
<chapter url_name="Overview"> <chapter url_name="Overview">
<videosequence url_name="Toy_Videos"> <videosequence url_name="Toy_Videos">
<html url_name="secret:toylab"/> <html url_name="secret:toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/> <video url_name="Video_Resources" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="video_123456789012" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/> <video url_name="video_4f66f493ac8f" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter> </chapter>
<chapter url_name="secret:magic"/> <chapter url_name="secret:magic"/>
</course> </course>
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/> <video youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" display_name="Video Resources"/>
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/> <video youtube_id_1_0="p2Q6BrNhdh9" display_name="Welcome"/>
...@@ -19,16 +19,17 @@ ...@@ -19,16 +19,17 @@
width="640" height="390"></embed> width="640" height="390"></embed>
</object> </object>
%else: %else:
<div id="video_${id}" class="video" <div id="video_${id}"
class="video"
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: data-youtube-id-0-75="${youtube_id_0_75}"
data-streams="${streams}" data-youtube-id-1-0="${youtube_id_1_0}"
% endif data-youtube-id-1-25="${youtube_id_1_25}"
data-youtube-id-1-5="${youtube_id_1_5}"
data-show-captions="${show_captions}" data-show-captions="${show_captions}"
data-start="${start}" data-end="${end}" data-start="${start}"
data-caption-asset-path="${caption_asset_path}" data-end="${end}"
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> data-caption-asset-path="${caption_asset_path}"
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
......
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