diff --git a/cms/djangoapps/contentstore/management/commands/check_course.py b/cms/djangoapps/contentstore/management/commands/check_course.py
index 13ac6af50c21..541f5dee75cd 100644
--- a/cms/djangoapps/contentstore/management/commands/check_course.py
+++ b/cms/djangoapps/contentstore/management/commands/check_course.py
@@ -60,4 +60,3 @@ def _get_discussion_items(module):
for item in queried_discussion_items:
if item.location.url() not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url())
-
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 939286a7656c..772dfd277896 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -124,29 +124,33 @@ def create_new_course(request):
pass
if existing_course is not None:
return JsonResponse({
- 'ErrMsg': _('There is already a course defined with the same '
- 'organization, course number, and course run. Please '
- 'change either organization or course number to be '
- 'unique.'),
- 'OrgErrMsg': _('Please change either the organization or '
- 'course number so that it is unique.'),
- 'CourseErrMsg': _('Please change either the organization or '
- 'course number so that it is unique.'),
+ 'ErrMsg': _('There is already a course defined with the same '
+ 'organization, course number, and course run. Please '
+ 'change either organization or course number to be '
+ 'unique.'),
+ 'OrgErrMsg': _('Please change either the organization or '
+ 'course number so that it is unique.'),
+ 'CourseErrMsg': _('Please change either the organization or '
+ 'course number so that it is unique.'),
})
- course_search_location = ['i4x', dest_location.org, dest_location.course,
- 'course', None
+ course_search_location = [
+ 'i4x',
+ dest_location.org,
+ dest_location.course,
+ 'course',
+ None
]
courses = modulestore().get_items(course_search_location)
if len(courses) > 0:
return JsonResponse({
- 'ErrMsg': _('There is already a course defined with the same '
- 'organization and course number. Please '
- 'change at least one field to be unique.'),
- 'OrgErrMsg': _('Please change either the organization or '
- 'course number so that it is unique.'),
- 'CourseErrMsg': _('Please change either the organization or '
- 'course number so that it is unique.'),
+ 'ErrMsg': _('There is already a course defined with the same '
+ 'organization and course number. Please '
+ 'change at least one field to be unique.'),
+ 'OrgErrMsg': _('Please change either the organization or '
+ 'course number so that it is unique.'),
+ 'CourseErrMsg': _('Please change either the organization or '
+ 'course number so that it is unique.'),
})
# instantiate the CourseDescriptor and then persist it
@@ -156,15 +160,15 @@ def create_new_course(request):
else:
metadata = {'display_name': display_name}
modulestore('direct').create_and_save_xmodule(
- dest_location,
- metadata=metadata
+ dest_location,
+ metadata=metadata
)
new_course = modulestore('direct').get_item(dest_location)
# clone a default 'about' overview module as well
dest_about_location = dest_location.replace(
- category='about',
- name='overview'
+ category='about',
+ name='overview'
)
overview_template = AboutDescriptor.get_template('overview.yaml')
modulestore('direct').create_and_save_xmodule(
@@ -203,13 +207,16 @@ def course_info(request, org, course, name, provided_id=None):
# get current updates
location = Location(['i4x', org, course, 'course_info', "updates"])
- return render_to_response('course_info.html', {
- 'context_course': course_module,
- 'url_base': "/" + org + "/" + course + "/",
- 'course_updates': json.dumps(get_course_updates(location)),
- 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
- 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'})
-
+ return render_to_response(
+ 'course_info.html',
+ {
+ 'context_course': course_module,
+ 'url_base': "/" + org + "/" + course + "/",
+ 'course_updates': json.dumps(get_course_updates(location)),
+ 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
+ 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'
+ }
+ )
@expect_json
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
@@ -245,7 +252,7 @@ def course_info_updates(request, org, course, provided_id=None):
content_type="text/plain"
)
# can be either and sometimes django is rewriting one to the other:
- elif request.method in ('POST', 'PUT'):
+ elif request.method in ('POST', 'PUT'):
try:
return JsonResponse(update_course_updates(location, request.POST, provided_id))
except:
@@ -380,7 +387,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return JsonResponse(CourseGradingModel.fetch_grader(
- Location(location), grader_index
+ Location(location), grader_index
))
elif request.method == "DELETE":
# ??? Should this return anything? Perhaps success fail?
@@ -388,8 +395,8 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return JsonResponse()
else: # post or put, doesn't matter.
return JsonResponse(CourseGradingModel.update_grader_from_json(
- Location(location),
- request.POST
+ Location(location),
+ request.POST
))
@@ -411,8 +418,8 @@ def course_advanced_updates(request, org, course, name):
return JsonResponse(CourseMetadata.fetch(location))
elif request.method == 'DELETE':
return JsonResponse(CourseMetadata.delete_key(
- location,
- json.loads(request.body)
+ location,
+ json.loads(request.body)
))
else:
# NOTE: request.POST is messed up because expect_json
@@ -479,9 +486,9 @@ def course_advanced_updates(request, org, course, name):
filter_tabs = False
try:
return JsonResponse(CourseMetadata.update_from_json(
- location,
- request_body,
- filter_tabs=filter_tabs
+ location,
+ request_body,
+ filter_tabs=filter_tabs
))
except (TypeError, ValueError) as err:
return HttpResponseBadRequest(
@@ -585,8 +592,8 @@ def textbook_index(request, org, course, name):
# MongoKeyValueStore before we update the mongo datastore.
course_module.save()
store.update_metadata(
- course_module.location,
- own_metadata(course_module)
+ course_module.location,
+ own_metadata(course_module)
)
return JsonResponse(course_module.pdf_textbooks)
else:
diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py
index 3b89e2e98848..32bae3c34caf 100644
--- a/cms/envs/acceptance.py
+++ b/cms/envs/acceptance.py
@@ -68,8 +68,8 @@ def seed():
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
- 'TEST_NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
+ 'NAME': TEST_ROOT / "db" / "test_edx.db",
+ 'TEST_NAME': TEST_ROOT / "db" / "test_edx.db"
}
}
diff --git a/cms/envs/acceptance_static.py b/cms/envs/acceptance_static.py
deleted file mode 100644
index f7d69794fb82..000000000000
--- a/cms/envs/acceptance_static.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""
-This config file extends the test environment configuration
-so that we can run the lettuce acceptance tests.
-This is used in the django-admin call as acceptance.py
-contains random seeding, causing django-admin to create a random collection
-"""
-
-# We intentionally define lots of variables that aren't used, and
-# want to import all variables from base settings files
-# pylint: disable=W0401, W0614
-
-from .test import *
-
-# You need to start the server in debug mode,
-# otherwise the browser will not render the pages correctly
-DEBUG = True
-
-# Disable warnings for acceptance tests, to make the logs readable
-import logging
-logging.disable(logging.ERROR)
-import os
-import random
-
-MODULESTORE_OPTIONS = {
- 'default_class': 'xmodule.raw_module.RawDescriptor',
- 'host': 'localhost',
- 'db': 'acceptance_xmodule',
- 'collection': 'acceptance_modulestore',
- 'fs_root': TEST_ROOT / "data",
- 'render_template': 'mitxmako.shortcuts.render_to_string',
-}
-
-MODULESTORE = {
- 'default': {
- 'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
- 'OPTIONS': MODULESTORE_OPTIONS
- },
- 'direct': {
- 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
- 'OPTIONS': MODULESTORE_OPTIONS
- },
- 'draft': {
- 'ENGINE': 'xmodule.modulestore.draft.DraftModuleStore',
- 'OPTIONS': MODULESTORE_OPTIONS
- }
-}
-
-CONTENTSTORE = {
- 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
- 'OPTIONS': {
- 'host': 'localhost',
- 'db': 'acceptance_xcontent',
- },
- # allow for additional options that can be keyed on a name, e.g. 'trashcan'
- 'ADDITIONAL_OPTIONS': {
- 'trashcan': {
- 'bucket': 'trash_fs'
- }
- }
-}
-
-# Set this up so that rake lms[acceptance] and running the
-# harvest command both use the same (test) database
-# which they can flush without messing up your dev db
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_mitx.db",
- 'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
- }
-}
-
-# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
-INSTALLED_APPS += ('lettuce.django',)
-LETTUCE_APPS = ('contentstore',)
-LETTUCE_SERVER_PORT = random.randint(1024, 65535)
-LETTUCE_BROWSER = 'chrome'
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/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index 75c0764b1bb4..498197b59045 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -29,11 +29,13 @@
# to use staticfiles.
try:
import staticfiles
+ import staticfiles.handlers
except ImportError:
pass
else:
import sys
sys.modules['django.contrib.staticfiles'] = staticfiles
+ sys.modules['django.contrib.staticfiles.handlers'] = staticfiles.handlers
LOGGER = getLogger(__name__)
LOGGER.info("Loading the lettuce acceptance testing terrain file...")
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index d27893d44d62..58a3ed8b1265 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -208,10 +208,10 @@ def process_requirements(self):
# end up in a partially-initialized state.
loaded = {}
to_render = set()
- for a in self.get_attributes():
- loaded[a.name] = a.parse_from_xml(self.xml)
- if a.render:
- to_render.add(a.name)
+ for attribute in self.get_attributes():
+ loaded[attribute.name] = attribute.parse_from_xml(self.xml)
+ if attribute.render:
+ to_render.add(attribute.name)
self.loaded_attributes = loaded
self.to_render = to_render
@@ -493,17 +493,17 @@ def get_attributes(cls):
"""
Register the attributes.
"""
- return [Attribute('params', None), # extra iframe params
- Attribute('html_file', None),
- Attribute('gradefn', "gradefn"),
- Attribute('get_statefn', None), # Function to call in iframe
- # to get current state.
- Attribute('set_statefn', None), # Function to call iframe to
- # set state
- Attribute('width', "400"), # iframe width
- Attribute('height', "300")] # iframe height
-
-
+ return [
+ Attribute('params', None), # extra iframe params
+ Attribute('html_file', None),
+ Attribute('gradefn', "gradefn"),
+ Attribute('get_statefn', None), # Function to call in iframe
+ # to get current state.
+ Attribute('set_statefn', None), # Function to call iframe to
+ # set state
+ Attribute('width', "400"), # iframe width
+ Attribute('height', "300") # iframe height
+ ]
def _extra_context(self):
context = {
@@ -514,7 +514,6 @@ def _extra_context(self):
return context
-
registry.register(JSInput)
#-----------------------------------------------------------------------------
@@ -1048,8 +1047,8 @@ def preview_chemcalc(self, data):
try:
result['preview'] = chemcalc.render_to_html(formula)
- except pyparsing.ParseException as p:
- result['error'] = u"Couldn't parse formula: {0}".format(p.msg)
+ except pyparsing.ParseException as err:
+ result['error'] = u"Couldn't parse formula: {0}".format(err.msg)
except Exception:
# this is unexpected, so log
log.warning(
@@ -1189,15 +1188,19 @@ def parse(tag, tag_type):
'can_reuse': smth}.
"""
tag_attrs = dict()
- tag_attrs['draggable'] = {'id': Attribute._sentinel,
- 'label': "", 'icon': "",
- 'can_reuse': ""}
-
- tag_attrs['target'] = {'id': Attribute._sentinel,
- 'x': Attribute._sentinel,
- 'y': Attribute._sentinel,
- 'w': Attribute._sentinel,
- 'h': Attribute._sentinel}
+ tag_attrs['draggable'] = {
+ 'id': Attribute._sentinel,
+ 'label': "", 'icon': "",
+ 'can_reuse': ""
+ }
+
+ tag_attrs['target'] = {
+ 'id': Attribute._sentinel,
+ 'x': Attribute._sentinel,
+ 'y': Attribute._sentinel,
+ 'w': Attribute._sentinel,
+ 'h': Attribute._sentinel
+ }
dic = dict()
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/tabs-edit.html b/common/lib/xmodule/xmodule/js/fixtures/tabs-edit.html
index c83a1456226c..7c0f7be2b78a 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/tabs-edit.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/tabs-edit.html
@@ -1,33 +1,35 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Transcripts
+
+
+ Subtitles
+
-
- Transcripts
+
-
- Subtitles
-
-
-
-
-
+
-
+
+
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 23c248a21aa7..49fb72b16fa1 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="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
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_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
index 67200073108e..7a221a10f71d 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="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
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_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 423177a4b49a..bed85cee4d7e 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/tabs/edit.coffee b/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee
index 734e398c743d..d5502b7a29eb 100644
--- a/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee
@@ -65,7 +65,7 @@ describe "TabsEditingDescriptor", ->
describe "editor/settings header", ->
it "is hidden", ->
- expect(@descriptor.element.find(".component-edit-header").css('display')).toEqual('none')
+ expect(@descriptor.element.closest(".component-editor").find(".component-edit-header")).toBeHidden()
describe "TabsEditingDescriptor special save cases", ->
beforeEach ->
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 41cf6074730d..9f3ab5916a20 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: 'xmodule/include/fixtures/test.mp4',
- webm: 'xmodule/include/fixtures/test.webm',
- ogg: 'xmodule/include/fixtures/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/tabs/tabs-aggregator.coffee b/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
index 7c6e85e34b04..521b19dfbc33 100644
--- a/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
+++ b/common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.coffee
@@ -9,7 +9,7 @@ class @TabsEditingDescriptor
###
# hide editor/settings bar
- $('.component-edit-header').hide()
+ @element.closest('.component-editor').find('.component-edit-header').hide()
@$tabs = $(".tab", @element)
@$content = $(".component-tab", @element)
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 1efb1a1871e1..63bb442aa369 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -147,8 +147,6 @@ function (VideoPlayer) {
if (state.parseYoutubeStreams(state.config.youtubeStreams)) {
state.videoType = 'youtube';
- state.fetchMetadata();
- state.parseSpeed();
return true;
}
return false;
@@ -157,9 +155,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,
@@ -168,20 +164,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) {
@@ -205,7 +220,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;
@@ -231,28 +246,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);
@@ -298,7 +346,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,
@@ -308,7 +362,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;
+ }
}
});
}
@@ -325,7 +386,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;
+ }
});
});
}
@@ -362,12 +425,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/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 8a32f7e8223c..4eadca110c95 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -23,9 +23,11 @@
from xmodule.modulestore import Location
from xmodule.tests import get_test_system, test_util_open_ended
from xmodule.progress import Progress
-from xmodule.tests.test_util_open_ended import (MockQueryDict, DummyModulestore, TEST_STATE_SA_IN,
+from xmodule.tests.test_util_open_ended import (
+ MockQueryDict, DummyModulestore, TEST_STATE_SA_IN,
MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID,
- TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE)
+ TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE
+)
import capa.xqueue_interface as xqueue_interface
@@ -69,7 +71,7 @@ class OpenEndedChildTest(unittest.TestCase):
'peer_grader_count': 1,
'min_to_calibrate': 3,
'max_to_calibrate': 6,
- }
+ }
}
definition = Mock()
descriptor = Mock()
@@ -192,7 +194,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'peer_grader_count': 1,
'min_to_calibrate': 3,
'max_to_calibrate': 6,
- }
+ }
}
oeparam = etree.XML('''
@@ -553,7 +555,7 @@ def test_alternate_orderings(self):
descriptor,
static_data=self.static_data,
metadata=self.metadata,
- instance_state={'task_states' : TEST_STATE_SA})
+ instance_state={'task_states': TEST_STATE_SA})
combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
@@ -561,7 +563,7 @@ def test_alternate_orderings(self):
descriptor,
static_data=self.static_data,
metadata=self.metadata,
- instance_state={'task_states' : TEST_STATE_SA_IN})
+ instance_state={'task_states': TEST_STATE_SA_IN})
def test_get_score_realistic(self):
@@ -605,7 +607,7 @@ def generate_oe_module(self, task_state, task_number, task_xml):
descriptor = Mock(data=definition)
instance_state = {'task_states': task_state, 'graded': True}
if task_number is not None:
- instance_state.update({'current_task_number' : task_number})
+ instance_state.update({'current_task_number': task_number})
combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
definition,
@@ -711,7 +713,7 @@ def test_open_ended_flow_reset(self):
#Simulate a student saving an answer
html = module.handle_ajax("get_html", {})
- module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files" : False, "student_file" : None})
+ module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files": False, "student_file": None})
html = module.handle_ajax("get_html", {})
#Mock a student submitting an assessment
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index d846777360ea..bbdef454f7e2 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -164,6 +164,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(),
@@ -178,7 +184,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..c2c837c5b8a5 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -65,8 +65,8 @@ def seed():
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
- 'TEST_NAME': TEST_ROOT / "db" / "test_mitx_%s.db" % seed(),
+ 'NAME': TEST_ROOT / "db" / "test_edx.db",
+ 'TEST_NAME': TEST_ROOT / "db" / "test_edx.db",
}
}
@@ -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
deleted file mode 100644
index 27efb6160d48..000000000000
--- a/lms/envs/acceptance_static.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""
-This config file extends the test environment configuration
-so that we can run the lettuce acceptance tests.
-"""
-
-# We intentionally define lots of variables that aren't used, and
-# want to import all variables from base settings files
-# pylint: disable=W0401, W0614
-
-from .test import *
-
-# You need to start the server in debug mode,
-# otherwise the browser will not render the pages correctly
-DEBUG = True
-
-# Disable warnings for acceptance tests, to make the logs readable
-import logging
-logging.disable(logging.ERROR)
-import random
-
-# Use the mongo store for acceptance tests
-modulestore_options = {
- 'default_class': 'xmodule.raw_module.RawDescriptor',
- 'host': 'localhost',
- 'db': 'acceptance_xmodule',
- 'collection': 'acceptance_modulestore',
- 'fs_root': TEST_ROOT / "data",
- 'render_template': 'mitxmako.shortcuts.render_to_string',
-}
-
-MODULESTORE = {
- 'default': {
- 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
- 'OPTIONS': modulestore_options
- },
- 'direct': {
- 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
- 'OPTIONS': modulestore_options
- }
-}
-
-CONTENTSTORE = {
- 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
- 'OPTIONS': {
- 'host': 'localhost',
- 'db': 'acceptance_xcontent',
- }
-}
-
-# Set this up so that rake lms[acceptance] and running the
-# harvest command both use the same (test) database
-# which they can flush without messing up your dev db
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': TEST_ROOT / "db" / "test_mitx.db",
- 'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
- }
-}
-
-# Set up XQueue information so that the lms will send
-# requests to a mock XQueue server running locally
-XQUEUE_PORT = random.randint(1024, 65535)
-XQUEUE_INTERFACE = {
- "url": "http://127.0.0.1:%d" % XQUEUE_PORT,
- "django_auth": {
- "username": "lms",
- "password": "***REMOVED***"
- },
- "basic_auth": ('anant', 'agarwal'),
-}
-
-# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
-INSTALLED_APPS += ('lettuce.django',)
-LETTUCE_APPS = ('courseware',)
-LETTUCE_SERVER_PORT = random.randint(1024, 65535)
-LETTUCE_BROWSER = 'chrome'
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 9d886f8dc190..4e70f3ade1ae 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -290,12 +290,14 @@
###
COURSE_DEFAULT = '6.002x_Fall_2012'
-COURSE_SETTINGS = {'6.002x_Fall_2012': {'number': '6.002x',
- 'title': 'Circuits and Electronics',
- 'xmlpath': '6002x/',
- 'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
- }
- }
+COURSE_SETTINGS = {
+ '6.002x_Fall_2012': {
+ 'number': '6.002x',
+ 'title': 'Circuits and Electronics',
+ 'xmlpath': '6002x/',
+ 'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
+ }
+}
# IP addresses that are allowed to reload the course, etc.
# TODO (vshnayder): Will probably need to change as we get real access control in.
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!')}
diff --git a/rakelib/tests.rake b/rakelib/tests.rake
index 1c976b1c05d1..b0c8f4979d1b 100644
--- a/rakelib/tests.rake
+++ b/rakelib/tests.rake
@@ -4,6 +4,8 @@ CLOBBER.include(REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
# Create the directory to hold coverage reports, if it doesn't already exist.
directory REPORT_DIR
+ACCEPTANCE_DB = 'test_root/db/test_edx.db'
+
def test_id_dir(path)
return File.join(".testids", path.to_s)
end
@@ -30,7 +32,7 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true)
test_sh(run_under_coverage(cmd, system))
end
-def run_acceptance_tests(system, report_dir, harvest_args)
+def create_acceptance_db(system)
# HACK: Since now the CMS depends on the existence of some database tables
# that used to be in LMS (Role/Permissions for Forums) we need to make
# sure the acceptance tests create/migrate the database tables
@@ -43,6 +45,22 @@ def run_acceptance_tests(system, report_dir, harvest_args)
end
sh(django_admin(system, 'acceptance', 'syncdb', '--noinput'))
sh(django_admin(system, 'acceptance', 'migrate', '--noinput'))
+end
+
+def setup_acceptance_db(system, fasttest=false)
+ # If running under fasttest mode and the database already
+ # exists, skip the migrations.
+ if File.exists?(ACCEPTANCE_DB)
+ if not fasttest
+ File.delete(ACCEPTANCE_DB)
+ create_acceptance_db(system)
+ end
+ else
+ create_acceptance_db(system)
+ end
+end
+
+def run_acceptance_tests(system, report_dir, harvest_args)
test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args))
end
@@ -58,7 +76,7 @@ end
task :clean_test_files do
desc "Clean fixture files used by tests"
- sh("git clean -fqdx test_root")
+ sh("git clean -fqdx test_root/logs test_root/data test_root/staticfiles test_root/uploads")
end
task :clean_reports_dir => REPORT_DIR do
@@ -91,13 +109,15 @@ TEST_TASK_DIRS = []
# Run acceptance tests
desc "Run acceptance tests"
- #gather_assets uses its own env because acceptance contains seeds to make the information unique
- #acceptance_static is acceptance without the random seeding
- task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance_static", "fasttest_acceptance_#{system}"]
+ task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance"] do |t, args|
+ setup_acceptance_db(system)
+ Rake::Task["fasttest_acceptance_#{system}"].invoke(args.harvest_args)
+ end
- desc "Run acceptance tests without collectstatic"
+ desc "Run acceptance tests without collectstatic or database migrations"
task "fasttest_acceptance_#{system}", [:harvest_args] => [report_dir, :clean_reports_dir, :predjango] do |t, args|
args.with_defaults(:harvest_args => '')
+ setup_acceptance_db(system, fasttest=true)
run_acceptance_tests(system, report_dir, args.harvest_args)
end
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index d5f05083c8e8..58f99d422260 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -1,3 +1,5 @@
numpy==1.6.2
networkx==1.7
-sympy==0.7.1
\ No newline at end of file
+sympy==0.7.1
+pyparsing==1.5.6
+nltk==2.0.4
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 3edce34f5478..8d609e05f192 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -41,6 +41,7 @@ pycrypto>=2.6
pygments==1.5
pygraphviz==1.1
pymongo==2.4.1
+pyparsing==1.5.6
python-memcached==1.48
python-openid==2.2.5
pytz==2012h