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}

+
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