diff --git a/cms/templates/import.html b/cms/templates/import.html
index a5c6b9f41261..27337bf235d3 100644
--- a/cms/templates/import.html
+++ b/cms/templates/import.html
@@ -72,7 +72,7 @@
${_("Course to import:")}
add: function(e, data) {
submitBtn.unbind('click');
var file = data.files[0];
- if (file.type == "application/x-gzip") {
+ if (file.name.match(/tar\.gz$/)) {
submitBtn.click(function(e){
e.preventDefault();
submitBtn.hide();
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index c2bdeadc2143..08a223f609b8 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -555,6 +555,13 @@ def _extract_html(self, problemtree): # private
Used by get_html.
'''
+ if not isinstance(problemtree.tag, basestring):
+ # Comment and ProcessingInstruction nodes are not Elements,
+ # and we're ok leaving those behind.
+ # BTW: etree gives us no good way to distinguish these things
+ # other than to examine .tag to see if it's a string. :(
+ return
+
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py
index 9bc326d7b924..8e343ee1cf8a 100644
--- a/common/lib/capa/capa/tests/test_html_render.py
+++ b/common/lib/capa/capa/tests/test_html_render.py
@@ -226,6 +226,26 @@ def test_substitute_python_vars(self):
span_element = rendered_html.find('span')
self.assertEqual(span_element.get('attr'), "TEST")
+ def test_xml_comments_and_other_odd_things(self):
+ # Comments and processing instructions should be skipped.
+ xml_str = textwrap.dedent("""\
+
+
+ ]>
+
+
+
+
+ """)
+
+ # Create the problem
+ problem = new_loncapa_problem(xml_str)
+
+ # Render the HTML
+ the_html = problem.get_html()
+ self.assertRegexpMatches(the_html, r"\s+
")
+
def _create_test_file(self, path, content_str):
test_fp = self.system.filestore.open(path, "w")
test_fp.write(content_str)
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index 533ab2aec0dc..dc801be0f99a 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -40,6 +40,12 @@ div.video {
padding-bottom: 56.25%;
position: relative;
+ div {
+ &.hidden {
+ display: none;
+ }
+ }
+
object, iframe {
border: none;
height: 100%;
@@ -48,6 +54,15 @@ div.video {
top: 0;
width: 100%;
}
+
+ h3 {
+ text-align: center;
+ color: white;
+
+ &.hidden {
+ display: none;
+ }
+ }
}
section.video-controls {
@@ -516,6 +531,12 @@ div.video {
height: 0px;
}
+ article.video-wrapper section.video-player {
+ h3 {
+ color: black;
+ }
+ }
+
ol.subtitles {
width: 0;
height: 0;
@@ -563,6 +584,12 @@ div.video {
position: static;
}
+ article.video-wrapper section.video-player {
+ h3 {
+ color: white;
+ }
+ }
+
div.tc-wrapper {
@include clearfix;
display: table;
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html
index 341e18ae9d66..6e4df9ec9c77 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video.html
@@ -10,6 +10,8 @@
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
+ data-yt-test-timeout="1500"
+ data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
index 25a3c2c0ab4f..85fd004976b9 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
@@ -13,6 +13,8 @@
data-webm-source="test_files/test.webm"
data-ogg-source="test_files/test.ogv"
data-autoplay="False"
+ data-yt-test-timeout="1500"
+ data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
-
\ No newline at end of file
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
index 677ab9b24738..f2c749ef2746 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
@@ -13,6 +13,8 @@
data-webm-source="test_files/test.webm"
data-ogg-source="test_files/test.ogv"
data-autoplay="False"
+ data-yt-test-timeout="1500"
+ data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
-
\ No newline at end of file
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
index c611acfffdd9..69207230fa5d 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
@@ -10,6 +10,8 @@
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
+ data-yt-test-timeout="1500"
+ data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
>
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee
index f3cecf71cbe6..7b5d3156e909 100644
--- a/common/lib/xmodule/xmodule/js/spec/helper.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee
@@ -90,12 +90,24 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
- if settings.success
+ status = match[1].split('_')
+ if status and status[0] is 'status'
+ {
+ always: (callback) ->
+ callback.call(window, {}, status[1])
+ error: (callback) ->
+ callback.call(window, {}, status[1])
+ done: (callback) ->
+ callback.call(window, {}, status[1])
+ }
+ else if settings.success
# match[1] - it's video ID
settings.success data: jasmine.stubbedMetadata[match[1]]
else {
always: (callback) ->
- callback.call(window, {}, 'success');
+ callback.call(window, {}, 'success')
+ done: (callback) ->
+ callback.call(window, {}, 'success')
}
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption
diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
index 9194106fff12..54f952bffbcd 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
@@ -55,46 +55,6 @@
expect(this.state.speed).toEqual('0.75');
});
});
-
- describe('Check Youtube link existence', function () {
- var statusList = {
- error: 'html5',
- timeout: 'html5',
- abort: 'html5',
- parsererror: 'html5',
- success: 'youtube',
- notmodified: 'youtube'
- };
-
- function stubDeffered(data, status) {
- return {
- always: function(callback) {
- callback.call(window, data, status);
- }
- }
- }
-
- function checkPlayer(videoType, data, status) {
- this.state = new window.Video('#example');
- spyOn(this.state , 'getVideoMetadata')
- .andReturn(stubDeffered(data, status));
- this.state.initialize('#example');
-
- expect(this.state.videoType).toEqual(videoType);
- }
-
- it('if video id is incorrect', function () {
- checkPlayer('html5', { error: {} }, 'success');
- });
-
- $.each(statusList, function(status, mode){
- it('Status:' + status + ', mode:' + mode, function () {
- checkPlayer(mode, {}, status);
- });
- });
-
- });
-
});
describe('HTML5', function () {
@@ -154,10 +114,22 @@
it('parse Html5 sources', function () {
var html5Sources = {
- mp4: 'test_files/test.mp4',
- webm: 'test_files/test.webm',
- ogg: 'test_files/test.ogv'
- };
+ mp4: null,
+ webm: null,
+ ogg: null
+ }, v = document.createElement('video');
+
+ if (!!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''))) {
+ html5Sources['webm'] = 'xmodule/include/fixtures/test.webm';
+ }
+
+ if (!!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''))) {
+ html5Sources['mp4'] = 'xmodule/include/fixtures/test.mp4';
+ }
+
+ if (!!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''))) {
+ html5Sources['ogg'] = 'xmodule/include/fixtures/test.ogv';
+ }
expect(state.html5Sources).toEqual(html5Sources);
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
index 79bc16dbdad7..b41bdd6f1cad 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -143,8 +143,6 @@ function (VideoPlayer) {
if (state.parseYoutubeStreams(state.config.youtubeStreams)) {
state.videoType = 'youtube';
- state.fetchMetadata();
- state.parseSpeed();
return true;
}
return false;
@@ -153,9 +151,7 @@ function (VideoPlayer) {
// function _prepareHTML5Video(state)
// The function prepare HTML5 video, parse HTML5
// video sources etc.
- function _prepareHTML5Video(state) {
- state.videoType = 'html5';
-
+ function _prepareHTML5Video(state, html5Mode) {
state.parseVideoSources(
{
mp4: state.config.mp4Source,
@@ -164,20 +160,39 @@ function (VideoPlayer) {
}
);
+ if (html5Mode) {
+ state.speeds = ['0.75', '1.0', '1.25', '1.50'];
+ state.videos = {
+ '0.75': state.config.sub,
+ '1.0': state.config.sub,
+ '1.25': state.config.sub,
+ '1.5': state.config.sub
+ };
+ }
+
+ // We must have at least one non-YouTube video source available.
+ // Otherwise, return a negative.
+ if (
+ state.html5Sources.webm === null &&
+ state.html5Sources.mp4 === null &&
+ state.html5Sources.ogg === null
+ ) {
+ state.el.find('.video-player div').addClass('hidden');
+ state.el.find('.video-player h3').removeClass('hidden');
+
+ return false;
+ }
+
+ state.videoType = 'html5';
+
if (!state.config.sub || !state.config.sub.length) {
state.config.sub = '';
state.config.show_captions = false;
}
- state.speeds = ['0.75', '1.0', '1.25', '1.50'];
- state.videos = {
- '0.75': state.config.sub,
- '1.0': state.config.sub,
- '1.25': state.config.sub,
- '1.5': state.config.sub
- };
-
state.setSpeed($.cookie('video_speed'));
+
+ return true;
}
function _setConfigurations(state) {
@@ -201,7 +216,7 @@ function (VideoPlayer) {
// The function set initial configuration and preparation.
function initialize(element) {
- var _this = this;
+ var _this = this, tempYtTestTimeout;
// This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'.
this.isFullScreen = false;
@@ -227,28 +242,61 @@ function (VideoPlayer) {
webmSource: this.el.data('webm-source'),
oggSource: this.el.data('ogg-source'),
+ ytTestUrl: this.el.data('yt-test-url'),
+
fadeOutTimeout: 1400,
availableQualities: ['hd720', 'hd1080', 'highres']
};
+ // Check if the YT test timeout has been set. If not, or it is in
+ // improper format, then set to default value.
+ tempYtTestTimeout = parseInt(this.el.data('yt-test-timeout'), 10);
+ if (!isFinite(tempYtTestTimeout)) {
+ tempYtTestTimeout = 1500;
+ }
+ this.config.ytTestTimeout = tempYtTestTimeout;
+
if (!(_parseYouTubeIDs(this))) {
// If we do not have YouTube ID's, try parsing HTML5 video sources.
- _prepareHTML5Video(this);
+ if (!_prepareHTML5Video(this, true)) {
+ // Non-YouTube sources were not found either.
+ return;
+ }
+
_setConfigurations(this);
_renderElements(this);
} else {
- this.getVideoMetadata()
+ if (!this.youtubeXhr) {
+ this.youtubeXhr = this.getVideoMetadata();
+ }
+
+ this.youtubeXhr
.always(function(json, status) {
var err = $.isPlainObject(json.error) ||
- (status !== "success" && status !== "notmodified");
-
- if (err){
+ (status !== 'success' && status !== 'notmodified');
+ if (err) {
// When the youtube link doesn't work for any reason
// (for example, the great firewall in china) any
// alternate sources should automatically play.
- _prepareHTML5Video(_this);
- _this.el.find('a.quality_control').hide();
+ if (!_prepareHTML5Video(_this)) {
+ // Non-YouTube sources were not found either.
+
+ _this.el.find('.video-player div').removeClass('hidden');
+ _this.el.find('.video-player h3').addClass('hidden');
+
+ // If in reality the timeout was to short, try to
+ // continue loading the YouTube video anyways.
+ _this.fetchMetadata();
+ _this.parseSpeed();
+ } else {
+ // In-browser HTML5 player does not support quality
+ // control.
+ _this.el.find('a.quality_control').hide();
+ }
+ } else {
+ _this.fetchMetadata();
+ _this.parseSpeed();
}
_setConfigurations(_this);
@@ -294,7 +342,13 @@ function (VideoPlayer) {
// Take the HTML5 sources (URLs of videos), and make them available explictly for each type
// of video format (mp4, webm, ogg).
function parseVideoSources(sources) {
- var _this = this;
+ var _this = this,
+ v = document.createElement('video'),
+ sourceCodecs = {
+ mp4: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ webm: 'video/webm; codecs="vp8, vorbis"',
+ ogg: 'video/ogg; codecs="theora"'
+ };
this.html5Sources = {
mp4: null,
@@ -304,7 +358,14 @@ function (VideoPlayer) {
$.each(sources, function (name, source) {
if (source && source.length) {
- _this.html5Sources[name] = source;
+ if (
+ Boolean(
+ v.canPlayType &&
+ v.canPlayType(sourceCodecs[name]).replace(/no/, '')
+ )
+ ) {
+ _this.html5Sources[name] = source;
+ }
}
});
}
@@ -321,7 +382,9 @@ function (VideoPlayer) {
$.each(this.videos, function (speed, url) {
_this.getVideoMetadata(url, function(data) {
- _this.metadata[data.data.id] = data.data;
+ if (data.data) {
+ _this.metadata[data.data.id] = data.data;
+ }
});
});
}
@@ -358,12 +421,11 @@ function (VideoPlayer) {
if (typeof url !== 'string') {
url = this.videos['1.0'] || '';
}
-
successHandler = ($.isFunction(callback)) ? callback : null;
xhr = $.ajax({
- url: 'https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc',
- timeout: 500,
+ url: this.config.ytTestUrl + url + '?v=2&alt=jsonc',
dataType: 'jsonp',
+ timeout: this.config.ytTestTimeout,
success: successHandler
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
index c315e4afbced..91d2ba6fba75 100644
--- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
+++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js
@@ -10,21 +10,31 @@ function () {
return function (state) {
state.videoSpeedControl = {};
+ if (state.videoType === 'html5') {
+ _initialize(state);
+ } else if (state.videoType === 'youtube' && state.youtubeXhr) {
+ state.youtubeXhr.done(function () {
+ _initialize(state);
+ });
+ }
+
if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
_hideSpeedControl(state);
return;
}
-
- _makeFunctionsPublic(state);
- _renderElements(state);
- _bindHandlers(state);
};
// ***************************************************************
// Private functions start here.
// ***************************************************************
+ function _initialize(state) {
+ _makeFunctionsPublic(state);
+ _renderElements(state);
+ _bindHandlers(state);
+ }
+
// function _makeFunctionsPublic(state)
//
// Functions which will be accessible via 'state' object. When called,
diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js
index 70fdbc580d2b..457433592a8c 100644
--- a/common/lib/xmodule/xmodule/js/src/video/10_main.js
+++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js
@@ -20,7 +20,8 @@ function (
VideoSpeedControl,
VideoCaption
) {
- var previousState;
+ var previousState,
+ youtubeXhr = null;
// Because this constructor can be called multiple times on a single page (when
// the user switches verticals, the page doesn't reload, but the content changes), we must
@@ -53,7 +54,11 @@ function (
state = {};
previousState = state;
+ state.youtubeXhr = youtubeXhr;
Initialize(state, element);
+ if (!youtubeXhr) {
+ youtubeXhr = state.youtubeXhr;
+ }
VideoControl(state);
VideoQualityControl(state);
@@ -67,6 +72,10 @@ function (
// Video with Jasmine.
return state;
};
+
+ window.Video.clearYoutubeXhr = function () {
+ youtubeXhr = null;
+ };
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index be77cd268426..8ea87b2d41b8 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -167,6 +167,12 @@ def get_html(self):
sources = {get_ext(src): src for src in self.html5_sources}
sources['main'] = self.source
+ # for testing Youtube timeout in acceptance tests
+ if getattr(settings, 'VIDEO_PORT', None):
+ yt_test_url = "http://127.0.0.1:" + str(settings.VIDEO_PORT) + '/test_youtube/'
+ else:
+ yt_test_url = 'https://gdata.youtube.com/feeds/api/videos/'
+
return self.system.render_template('video.html', {
'youtube_streams': _create_youtube_string(self),
'id': self.location.html_id(),
@@ -181,7 +187,11 @@ def get_html(self):
'show_captions': json.dumps(self.show_captions),
'start': self.start_time,
'end': self.end_time,
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
+ # TODO: Later on the value 1500 should be taken from some global
+ # configuration setting field.
+ 'yt_test_timeout': 1500,
+ 'yt_test_url': yt_test_url
})
diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature
index 6c8299f2c531..b741c8bee11c 100644
--- a/lms/djangoapps/courseware/features/video.feature
+++ b/lms/djangoapps/courseware/features/video.feature
@@ -1,18 +1,39 @@
Feature: Video component
As a student, I want to view course videos in LMS.
-
Scenario: Video component is fully rendered in the LMS in HTML5 mode
Given the course has a Video component in HTML5 mode
Then when I view the video it has rendered in HTML5 mode
And all sources are correct
- Scenario: Video component is fully rendered in the LMS in Youtube mode
- Given the course has a Video component in Youtube mode
- Then when I view the video it has rendered in Youtube mode
-
- # Firefox doesn't have HTML5
+ # Firefox doesn't have HTML5 (only mp4 - fix here)
@skip_firefox
Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component in HTML5 mode
Then when I view the video it has autoplay enabled
+
+# Youtube testing
+Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
+Given youtube server is up and response time is 0.4 seconds
+And the course has a Video component in Youtube_HTML5 mode
+Then when I view the video it has rendered in Youtube mode
+
+Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources
+Given youtube server is up and response time is 2 seconds
+And the course has a Video component in Youtube_HTML5 mode
+Then when I view the video it has rendered in HTML5 mode
+
+Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
+Given youtube server is up and response time is 2 seconds
+And the course has a Video component in Youtube mode
+Then when I view the video it has rendered in Youtube mode
+
+Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser
+Given youtube server is up and response time is 2 seconds
+And the course has a Video component in Youtube_HTML5_Unsupported_Video mode
+Then when I view the video it has rendered in Youtube mode
+
+Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser
+Given the course has a Video component in HTML5_Unsupported_Video mode
+Then error message is shown
+And error message has correct text
diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py
index f5977920197d..e0a1461aea5e 100644
--- a/lms/djangoapps/courseware/features/video.py
+++ b/lms/djangoapps/courseware/features/video.py
@@ -3,6 +3,7 @@
from lettuce import world, step
from lettuce.django import django_url
from common import i_am_registered_for_the_course, section_location
+from django.utils.translation import ugettext as _
############### ACTIONS ####################
@@ -11,6 +12,9 @@
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv'
]
+HTML5_SOURCES_INCORRECT = [
+ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99'
+]
@step('when I view the (.*) it has autoplay enabled$')
def does_autoplay_video(_step, video_type):
@@ -51,10 +55,37 @@ def add_video_to_course(course, player_mode):
'html5_sources': HTML5_SOURCES
}
})
+ if player_mode == 'youtube_html5':
+ kwargs.update({
+ 'metadata': {
+ 'html5_sources': HTML5_SOURCES
+ }
+ })
+ if player_mode == 'youtube_html5_unsupported_video':
+ kwargs.update({
+ 'metadata': {
+ 'html5_sources': HTML5_SOURCES_INCORRECT
+ }
+ })
+ if player_mode == 'html5_unsupported_video':
+ kwargs.update({
+ 'metadata': {
+ 'youtube_id_1_0': '',
+ 'youtube_id_0_75': '',
+ 'youtube_id_1_25': '',
+ 'youtube_id_1_5': '',
+ 'html5_sources': HTML5_SOURCES_INCORRECT
+ }
+ })
world.ItemFactory.create(**kwargs)
+@step('youtube server is up and response time is (.*) seconds$')
+def set_youtube_response_timeout(_step, time):
+ world.youtube_server.time_to_response = time
+
+
@step('when I view the video it has rendered in (.*) mode$')
def video_is_rendered(_step, mode):
modes = {
@@ -64,9 +95,23 @@ def video_is_rendered(_step, mode):
html_tag = modes[mode.lower()]
assert world.css_find('.video {0}'.format(html_tag)).first
+
@step('all sources are correct$')
def all_sources_are_correct(_step):
sources = world.css_find('.video video source')
assert set(source['src'] for source in sources) == set(HTML5_SOURCES)
+@step('error message is shown$')
+def error_message_is_shown(_step):
+ selector = '.video .video-player h3'
+ assert world.css_visible(selector)
+
+
+@step('error message has correct text$')
+def error_message_has_correct_text(_step):
+ selector = '.video .video-player h3'
+ text = _('ERROR: No playable video sources found!')
+ assert world.css_has_text(selector, text)
+
+
diff --git a/lms/djangoapps/courseware/features/youtube_setup.py b/lms/djangoapps/courseware/features/youtube_setup.py
new file mode 100644
index 000000000000..8233d1f4586e
--- /dev/null
+++ b/lms/djangoapps/courseware/features/youtube_setup.py
@@ -0,0 +1,45 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from courseware.mock_youtube_server.mock_youtube_server import MockYoutubeServer
+from lettuce import before, after, world
+from django.conf import settings
+import threading
+
+from logging import getLogger
+logger = getLogger(__name__)
+
+
+@before.all
+def setup_mock_youtube_server():
+ # import ipdb; ipdb.set_trace()
+ server_host = '127.0.0.1'
+
+ server_port = settings.VIDEO_PORT
+
+ address = (server_host, server_port)
+
+ # Create the mock server instance
+ server = MockYoutubeServer(address)
+ logger.debug("Youtube server started at {} port".format(str(server_port)))
+
+ server.time_to_response = 1 # seconds
+
+ # Start the server running in a separate daemon thread
+ # Because the thread is a daemon, it will terminate
+ # when the main thread terminates.
+ server_thread = threading.Thread(target=server.serve_forever)
+ server_thread.daemon = True
+ server_thread.start()
+
+ # Store the server instance in lettuce's world
+ # so that other steps can access it
+ # (and we can shut it down later)
+ world.youtube_server = server
+
+
+@after.all
+def teardown_mock_youtube_server(total):
+
+ # Stop the LTI server and free up the port
+ world.youtube_server.shutdown()
diff --git a/lms/djangoapps/courseware/mock_youtube_server/__init__.py b/lms/djangoapps/courseware/mock_youtube_server/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py b/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py
new file mode 100644
index 000000000000..46b269dda683
--- /dev/null
+++ b/lms/djangoapps/courseware/mock_youtube_server/mock_youtube_server.py
@@ -0,0 +1,81 @@
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+import urlparse
+from requests.packages.oauthlib.oauth1.rfc5849 import signature
+import mock
+import threading
+import json
+from logging import getLogger
+logger = getLogger(__name__)
+import time
+
+class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
+ '''
+ A handler for Youtube GET requests.
+ '''
+
+ protocol = "HTTP/1.0"
+
+ def do_HEAD(self):
+ self._send_head()
+
+ def do_GET(self):
+ '''
+ Handle a GET request from the client and sends response back.
+ '''
+ self._send_head()
+
+ logger.debug("Youtube provider received GET request to path {}".format(
+ self.path)
+ ) # Log the request
+
+ status_message = "I'm youtube."
+ response_timeout = float(self.server.time_to_response)
+
+ # threading timer produces TypeError: 'NoneType' object is not callable here
+ # so we use time.sleep, as we already in separate thread.
+ time.sleep(response_timeout)
+ self._send_response(status_message)
+
+ def _send_head(self):
+ '''
+ Send the response code and MIME headers
+ '''
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def _send_response(self, message):
+ '''
+ Send message back to the client
+ '''
+ callback = urlparse.parse_qs(self.path)['callback'][0]
+ response = callback + '({})'.format(json.dumps({'message': message}))
+ # Log the response
+ logger.debug("Youtube: sent response {}".format(message))
+
+ self.wfile.write(response)
+
+
+class MockYoutubeServer(HTTPServer):
+ '''
+ A mock Youtube provider server that responds
+ to GET requests to localhost.
+ '''
+
+ def __init__(self, address):
+ '''
+ Initialize the mock XQueue server instance.
+
+ *address* is the (host, host's port to listen to) tuple.
+ '''
+ handler = MockYoutubeRequestHandler
+ HTTPServer.__init__(self, address, handler)
+
+ def shutdown(self):
+ '''
+ Stop the server and free up the port
+ '''
+ # First call superclass shutdown()
+ HTTPServer.shutdown(self)
+ # We also need to manually close the socket
+ self.socket.close()
diff --git a/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py b/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py
new file mode 100644
index 000000000000..4ccd7cdc58db
--- /dev/null
+++ b/lms/djangoapps/courseware/mock_youtube_server/test_mock_youtube_server.py
@@ -0,0 +1,53 @@
+"""
+Test for Mock_Youtube_Server
+"""
+import unittest
+import threading
+import urllib
+from mock_youtube_server import MockYoutubeServer
+
+from nose.plugins.skip import SkipTest
+
+
+class MockYoutubeServerTest(unittest.TestCase):
+ '''
+ A mock version of the Youtube provider server that listens on a local
+ port and responds with jsonp.
+
+ Used for lettuce BDD tests in lms/courseware/features/video.feature
+ '''
+
+ def setUp(self):
+
+ # This is a test of the test setup,
+ # so it does not need to run as part of the unit test suite
+ # You can re-enable it by commenting out the line below
+ raise SkipTest
+
+ # Create the server
+ server_port = 8034
+ server_host = '127.0.0.1'
+ address = (server_host, server_port)
+ self.server = MockYoutubeServer(address, )
+ self.server.time_to_response = 0.5
+ # Start the server in a separate daemon thread
+ server_thread = threading.Thread(target=self.server.serve_forever)
+ server_thread.daemon = True
+ server_thread.start()
+
+ def tearDown(self):
+
+ # Stop the server, freeing up the port
+ self.server.shutdown()
+
+ def test_request(self):
+ """
+ Tests that Youtube server processes request with right program
+ path, and responses with incorrect signature.
+ """
+ # GET request
+ response_handle = urllib.urlopen(
+ 'http://127.0.0.1:8034/feeds/api/videos/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
+ )
+ response = response_handle.read()
+ self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response)
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index 3436938cc09b..b393b33da857 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -64,7 +64,9 @@ def test_video_constructor(self):
'sub': u'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': _create_youtube_string(self.item_module),
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
+ 'yt_test_timeout': 1500,
+ 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.maxDiff = None
@@ -114,7 +116,9 @@ def test_video_constructor(self):
'sub': 'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': '1.00:OEoXaMPEzfM',
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
+ 'yt_test_timeout': 1500,
+ 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(context, expected_context)
diff --git a/lms/djangoapps/courseware/tests/test_video_xml.py b/lms/djangoapps/courseware/tests/test_video_xml.py
index 33df1432c029..d79017346854 100644
--- a/lms/djangoapps/courseware/tests/test_video_xml.py
+++ b/lms/djangoapps/courseware/tests/test_video_xml.py
@@ -92,7 +92,9 @@ def test_video_get_html(self):
'sources': sources,
'youtube_streams': _create_youtube_string(module),
'track': '',
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
+ 'yt_test_timeout': 1500,
+ 'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(module.get_html(), expected_context)
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
index e866a250d91b..7924780f3abc 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -82,6 +82,11 @@ def seed():
"basic_auth": ('anant', 'agarwal'),
}
+
+# Set up Video information so that the lms will send
+# requests to a mock Youtube server running locally
+VIDEO_PORT = XQUEUE_PORT + 2
+
# Forums are disabled in test.py to speed up unit tests, but we do not have
# per-test control for acceptance tests
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
diff --git a/lms/envs/acceptance_static.py b/lms/envs/acceptance_static.py
index 27efb6160d48..c09c9e29e816 100644
--- a/lms/envs/acceptance_static.py
+++ b/lms/envs/acceptance_static.py
@@ -70,6 +70,10 @@
"basic_auth": ('anant', 'agarwal'),
}
+# Set up Video information so that the lms will send
+# requests to a mock Youtube server running locally
+VIDEO_PORT = XQUEUE_PORT + 2
+
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('courseware',)
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 43f36915a0f7..3f06f0051181 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -23,6 +23,8 @@ ${display_name}
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}"
+ data-yt-test-timeout="${yt_test_timeout}"
+ data-yt-test-url="${yt_test_url}"
>
@@ -30,6 +32,7 @@ ${display_name}
+ ${_('ERROR: No playable video sources found!')}