Commit de1c6879 by dcadams

Merge branch 'master' of github.com:edx/edx-platform into feature-dcadams-usermanagement

parents bc5d7923 74338450
...@@ -24,6 +24,30 @@ class LMSLinksTestCase(TestCase): ...@@ -24,6 +24,30 @@ class LMSLinksTestCase(TestCase):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about") self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
def about_page_marketing_site_remove_http_test(self):
""" Get URL for about page, marketing root present, remove http://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
def about_page_marketing_site_remove_https_test(self):
""" Get URL for about page, marketing root present, remove https://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
def about_page_marketing_site_https__edge_test(self):
""" Get URL for about page, only remove https:// at the beginning of the string. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={})
def about_page_marketing_urls_not_set_test(self):
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), None)
@override_settings(LMS_BASE=None) @override_settings(LMS_BASE=None)
def about_page_no_lms_base_test(self): def about_page_no_lms_base_test(self):
""" No LMS_BASE, nor is ENABLE_MKTG_SITE True """ """ No LMS_BASE, nor is ENABLE_MKTG_SITE True """
......
...@@ -4,6 +4,10 @@ from xmodule.modulestore.django import modulestore ...@@ -4,6 +4,10 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy import copy
import logging
import re
log = logging.getLogger(__name__)
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
...@@ -108,9 +112,20 @@ def get_lms_link_for_about_page(location): ...@@ -108,9 +112,20 @@ def get_lms_link_for_about_page(location):
Returns the url to the course about page from the location tuple. Returns the url to the course about page from the location tuple.
""" """
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
# Root will be "www.edx.org". The complete URL will still not be exactly correct, if not hasattr(settings, 'MKTG_URLS'):
# but redirects exist from www.edx.org to get to the drupal course about page URL. log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
about_base = settings.MKTG_URLS.get('ROOT') about_base = None
else:
marketing_urls = settings.MKTG_URLS
if marketing_urls.get('ROOT', None) is None:
log.exception('There is no ROOT defined in MKTG_URLS')
about_base = None
else:
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct,
# but redirects exist from www.edx.org to get to the Drupal course about page URL.
about_base = marketing_urls.get('ROOT')
# Strip off https:// (or http://) to be consistent with the formatting of LMS_BASE.
about_base = re.sub(r"^https?://", "", about_base)
elif settings.LMS_BASE is not None: elif settings.LMS_BASE is not None:
about_base = settings.LMS_BASE about_base = settings.LMS_BASE
else: else:
......
...@@ -103,6 +103,7 @@ DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) ...@@ -103,6 +103,7 @@ DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
...@@ -335,3 +335,14 @@ INSTALLED_APPS = ( ...@@ -335,3 +335,14 @@ INSTALLED_APPS = (
################# EDX MARKETING SITE ################################## ################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin' EDXMKTG_COOKIE_NAME = 'edxloggedin'
MKTG_URLS = {}
MKTG_URL_LINK_MAP = {
'ABOUT': 'about_edx',
'CONTACT': 'contact',
'FAQ': 'help_edx',
'COURSES': 'courses',
'ROOT': 'root',
'TOS': 'tos',
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
...@@ -4,13 +4,15 @@ from django.core.urlresolvers import reverse ...@@ -4,13 +4,15 @@ from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from mitxmako.shortcuts import marketing_link from mitxmako.shortcuts import marketing_link
from mock import patch from mock import patch
from nose.plugins.skip import SkipTest
class ShortcutsTests(TestCase): class ShortcutsTests(TestCase):
""" """
Test the mitxmako shortcuts file Test the mitxmako shortcuts file
""" """
# TODO: fix this test. It is causing intermittent test failures on
# subsequent tests due to the way urls are loaded
raise SkipTest()
@override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'}) @override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_marketing_link(self): def test_marketing_link(self):
......
<div class="course-content"> <div class="course-content">
<div id="video_example" class="video"> <div id="video_example">
<div id="example">
<div id="video_id" class="video"
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/">
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
<div id="example"></div> <div id="id"></div>
</section> </section>
<section class="video-controls"></section> <section class="video-controls"></section>
</article> </article>
</div> </div>
</div> </div>
</div>
</div>
</div> </div>
\ No newline at end of file
...@@ -28,7 +28,7 @@ jasmine.stubRequests = -> ...@@ -28,7 +28,7 @@ jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) -> spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
settings.success data: jasmine.stubbedMetadata[match[1]] settings.success data: jasmine.stubbedMetadata[match[1]]
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/ else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption settings.success jasmine.stubbedCaption
else if settings.url.match /.+\/problem_get$/ else if settings.url.match /.+\/problem_get$/
settings.success html: readFixtures('problem_content.html') settings.success html: readFixtures('problem_content.html')
...@@ -47,19 +47,15 @@ jasmine.stubYoutubePlayer = -> ...@@ -47,19 +47,15 @@ jasmine.stubYoutubePlayer = ->
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
enableParts = [enableParts] unless $.isArray(enableParts) enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName enableParts.push currentPartName
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
unless $.inArray(part, enableParts) >= 0
spyOn window, part
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
YT.Player = undefined YT.Player = undefined
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
context.video = new Video '#example', videosDefinition
jasmine.stubYoutubePlayer() jasmine.stubYoutubePlayer()
if createPlayer if createPlayer
return new VideoPlayer(video: context.video) return new VideoPlayer(video: context.video)
......
# TODO: figure out why failing describe 'VideoControl', ->
xdescribe 'VideoControl', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
loadFixtures 'video.html'
$('.video-controls').html '' $('.video-controls').html ''
describe 'constructor', -> describe 'constructor', ->
it 'render the video controls', -> it 'render the video controls', ->
new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
expect($('.video-controls').html()).toContain ''' expect($('.video-controls')).toContain
<div class="slider"></div> ['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
<div> expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
<ul class="vcr">
<li><a class="video_control play" href="#">Play</a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
'''
it 'bind the playback button', -> it 'bind the playback button', ->
control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', control.togglePlayback expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
@control = new window.VideoControl(el: $('.video-controls'))
it 'does not add the play class to video control', -> it 'does not add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).not.toHaveClass 'play' expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play' expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', -> describe 'when on a non-touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false @control = new window.VideoControl(el: $('.video-controls'))
it 'add the play class to video control', -> it 'add the play class to video control', ->
new VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHaveClass 'play' expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play' expect($('.video_control')).toHaveHtml 'Play'
describe 'play', -> describe 'play', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
@control.play() @control.play()
it 'switch playback button to play state', -> it 'switch playback button to play state', ->
...@@ -56,8 +47,9 @@ xdescribe 'VideoControl', -> ...@@ -56,8 +47,9 @@ xdescribe 'VideoControl', ->
expect($('.video_control')).toHaveHtml 'Pause' expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', -> describe 'pause', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
@control.pause() @control.pause()
it 'switch playback button to pause state', -> it 'switch playback button to pause state', ->
...@@ -66,8 +58,9 @@ xdescribe 'VideoControl', -> ...@@ -66,8 +58,9 @@ xdescribe 'VideoControl', ->
expect($('.video_control')).toHaveHtml 'Play' expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', -> describe 'togglePlayback', ->
beforeEach -> beforeEach ->
@control = new VideoControl(el: $('.video-controls')) @control = new window.VideoControl(el: $('.video-controls'))
describe 'when the control does not have play or pause class', -> describe 'when the control does not have play or pause class', ->
beforeEach -> beforeEach ->
......
# TODO: figure out why failing describe 'VideoPlayer', ->
xdescribe 'VideoPlayer', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
# It tries to call methods of VideoProgressSlider on Spy
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider', 'VideoControl']
spyOn(window[part].prototype, 'initialize').andCallThrough()
jasmine.stubVideoPlayer @, [], false jasmine.stubVideoPlayer @, [], false
afterEach -> afterEach ->
...@@ -8,7 +11,6 @@ xdescribe 'VideoPlayer', -> ...@@ -8,7 +11,6 @@ xdescribe 'VideoPlayer', ->
describe 'constructor', -> describe 'constructor', ->
beforeEach -> beforeEach ->
spyOn window, 'VideoControl'
spyOn YT, 'Player' spyOn YT, 'Player'
$.fn.qtip.andCallFake -> $.fn.qtip.andCallFake ->
$(this).data('qtip', true) $(this).data('qtip', true)
...@@ -22,32 +24,47 @@ xdescribe 'VideoPlayer', -> ...@@ -22,32 +24,47 @@ xdescribe 'VideoPlayer', ->
expect(@player.currentTime).toEqual 0 expect(@player.currentTime).toEqual 0
it 'set the element', -> it 'set the element', ->
expect(@player.el).toBe '#video_example' expect(@player.el).toHaveId 'video_id'
it 'create video control', -> it 'create video control', ->
expect(window.VideoControl).toHaveBeenCalledWith el: $('.video-controls', @player.el) expect(window.VideoControl.prototype.initialize).toHaveBeenCalled()
expect(@player.control).toBeDefined()
expect(@player.control.el).toBe $('.video-controls', @player.el)
it 'create video caption', -> it 'create video caption', ->
expect(window.VideoCaption).toHaveBeenCalledWith el: @player.el, youtubeId: 'normalSpeedYoutubeId', currentSpeed: '1.0' expect(window.VideoCaption.prototype.initialize).toHaveBeenCalled()
expect(@player.caption).toBeDefined()
expect(@player.caption.el).toBe @player.el
expect(@player.caption.youtubeId).toEqual 'normalSpeedYoutubeId'
expect(@player.caption.currentSpeed).toEqual '1.0'
expect(@player.caption.captionAssetPath).toEqual '/static/subs/'
it 'create video speed control', -> it 'create video speed control', ->
expect(window.VideoSpeedControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el), speeds: ['0.75', '1.0'], currentSpeed: '1.0' expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.speedControl).toBeDefined()
expect(@player.speedControl.el).toBe $('.secondary-controls', @player.el)
expect(@player.speedControl.speeds).toEqual ['0.75', '1.0']
expect(@player.speedControl.currentSpeed).toEqual '1.0'
it 'create video progress slider', -> it 'create video progress slider', ->
expect(window.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el) expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled()
expect(@player.progressSlider).toBeDefined()
expect(@player.progressSlider.el).toBe $('.slider', @player.el)
it 'create Youtube player', -> it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith('example', { expect(YT.Player).toHaveBeenCalledWith('id', {
playerVars: playerVars:
controls: 0 controls: 0
wmode: 'transparent' wmode: 'transparent'
rel: 0 rel: 0
showinfo: 0 showinfo: 0
enablejsapi: 1 enablejsapi: 1
modestbranding: 1
videoId: 'normalSpeedYoutubeId' videoId: 'normalSpeedYoutubeId'
events: events:
onReady: @player.onReady onReady: @player.onReady
onStateChange: @player.onStateChange onStateChange: @player.onStateChange
onPlaybackQualityChange: @player.onPlaybackQualityChange
}) })
it 'bind to video control play event', -> it 'bind to video control play event', ->
...@@ -69,14 +86,13 @@ xdescribe 'VideoPlayer', -> ...@@ -69,14 +86,13 @@ xdescribe 'VideoPlayer', ->
expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange
it 'bind to key press', -> it 'bind to key press', ->
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to fullscreen switching button', -> it 'bind to fullscreen switching button', ->
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
describe 'when not on a touch based device', -> describe 'when not on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.add-fullscreen, .hide-subtitles').removeData 'qtip' $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video @player = new VideoPlayer video: @video
...@@ -85,11 +101,13 @@ xdescribe 'VideoPlayer', -> ...@@ -85,11 +101,13 @@ xdescribe 'VideoPlayer', ->
expect($('.hide-subtitles')).toHaveData 'qtip' expect($('.hide-subtitles')).toHaveData 'qtip'
it 'create video volume control', -> it 'create video volume control', ->
expect(window.VideoVolumeControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el) expect(window.VideoVolumeControl.prototype.initialize).toHaveBeenCalled()
expect(@player.volumeControl).toBeDefined()
expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el)
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
$('.add-fullscreen, .hide-subtitles').removeData 'qtip' $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer video: @video @player = new VideoPlayer video: @video
...@@ -98,7 +116,8 @@ xdescribe 'VideoPlayer', -> ...@@ -98,7 +116,8 @@ xdescribe 'VideoPlayer', ->
expect($('.hide-subtitles')).not.toHaveData 'qtip' expect($('.hide-subtitles')).not.toHaveData 'qtip'
it 'does not create video volume control', -> it 'does not create video volume control', ->
expect(window.VideoVolumeControl).not.toHaveBeenCalled() expect(window.VideoVolumeControl.prototype.initialize).not.toHaveBeenCalled()
expect(@player.volumeControl).not.toBeDefined()
describe 'onReady', -> describe 'onReady', ->
beforeEach -> beforeEach ->
...@@ -110,7 +129,6 @@ xdescribe 'VideoPlayer', -> ...@@ -110,7 +129,6 @@ xdescribe 'VideoPlayer', ->
describe 'when not on a touch based device', -> describe 'when not on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
...@@ -119,7 +137,7 @@ xdescribe 'VideoPlayer', -> ...@@ -119,7 +137,7 @@ xdescribe 'VideoPlayer', ->
describe 'when on a touch based device', -> describe 'when on a touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
spyOn @player, 'play' spyOn @player, 'play'
@player.onReady() @player.onReady()
...@@ -347,9 +365,6 @@ xdescribe 'VideoPlayer', -> ...@@ -347,9 +365,6 @@ xdescribe 'VideoPlayer', ->
it 'replace the full screen button tooltip', -> it 'replace the full screen button tooltip', ->
expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser' expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
it 'add a new exit from fullscreen button', ->
expect(@player.el).toContain 'a.exit'
it 'add the fullscreen class', -> it 'add the fullscreen class', ->
expect(@player.el).toHaveClass 'fullscreen' expect(@player.el).toHaveClass 'fullscreen'
...@@ -438,7 +453,7 @@ xdescribe 'VideoPlayer', -> ...@@ -438,7 +453,7 @@ xdescribe 'VideoPlayer', ->
describe 'volume', -> describe 'volume', ->
beforeEach -> beforeEach ->
@player = new VideoPlayer @video @player = new VideoPlayer video: @video
@player.player.getVolume.andReturn 42 @player.player.getVolume.andReturn 42
describe 'without value', -> describe 'without value', ->
......
# TODO: figure out why failing describe 'VideoProgressSlider', ->
xdescribe 'VideoProgressSlider', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
describe 'constructor', -> describe 'constructor', ->
describe 'on a non-touch based device', -> describe 'on a non-touch based device', ->
beforeEach -> beforeEach ->
spyOn($.fn, 'slider').andCallThrough() spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn false @player = jasmine.stubVideoPlayer @
@slider = new VideoProgressSlider el: $('.slider') @progressSlider = @player.progressSlider
it 'build the slider', -> it 'build the slider', ->
expect(@slider.slider).toBe '.slider' expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith expect($.fn.slider).toHaveBeenCalledWith
range: 'min' range: 'min'
change: @slider.onChange change: @progressSlider.onChange
slide: @slider.onSlide slide: @progressSlider.onSlide
stop: @slider.onStop stop: @progressSlider.onStop
it 'build the seek handle', -> it 'build the seek handle', ->
expect(@slider.handle).toBe '.slider .ui-slider-handle' expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00" content: "0:00"
position: position:
my: 'bottom center' my: 'bottom center'
at: 'top center' at: 'top center'
container: @slider.handle container: @progressSlider.handle
hide: hide:
delay: 700 delay: 700
style: style:
...@@ -34,47 +33,51 @@ xdescribe 'VideoProgressSlider', -> ...@@ -34,47 +33,51 @@ xdescribe 'VideoProgressSlider', ->
describe 'on a touch-based device', -> describe 'on a touch-based device', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn($.fn, 'slider').andCallThrough() spyOn($.fn, 'slider').andCallThrough()
spyOn(window, 'onTouchBasedDevice').andReturn true @player = jasmine.stubVideoPlayer @
@slider = new VideoProgressSlider el: $('.slider') @progressSlider = @player.progressSlider
it 'does not build the slider', -> it 'does not build the slider', ->
expect(@slider.slider).toBeUndefined expect(@progressSlider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled() expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', -> describe 'play', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') spyOn(VideoProgressSlider.prototype, 'buildSlider').andCallThrough()
spyOn($.fn, 'slider').andCallThrough() @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when the slider was already built', -> describe 'when the slider was already built', ->
beforeEach -> beforeEach ->
@slider.play() @progressSlider.play()
it 'does not build the slider', -> it 'does not build the slider', ->
expect($.fn.slider).not.toHaveBeenCalled expect(@progressSlider.buildSlider.calls.length).toEqual 1
describe 'when the slider was not already built', -> describe 'when the slider was not already built', ->
beforeEach -> beforeEach ->
@slider.slider = null spyOn($.fn, 'slider').andCallThrough()
@slider.play() @progressSlider.slider = null
@progressSlider.play()
it 'build the slider', -> it 'build the slider', ->
expect(@slider.slider).toBe '.slider' expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith expect($.fn.slider).toHaveBeenCalledWith
range: 'min' range: 'min'
change: @slider.onChange change: @progressSlider.onChange
slide: @slider.onSlide slide: @progressSlider.onSlide
stop: @slider.onStop stop: @progressSlider.onStop
it 'build the seek handle', -> it 'build the seek handle', ->
expect(@slider.handle).toBe '.ui-slider-handle' expect(@progressSlider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00" content: "0:00"
position: position:
my: 'bottom center' my: 'bottom center'
at: 'top center' at: 'top center'
container: @slider.handle container: @progressSlider.handle
hide: hide:
delay: 700 delay: 700
style: style:
...@@ -83,21 +86,23 @@ xdescribe 'VideoProgressSlider', -> ...@@ -83,21 +86,23 @@ xdescribe 'VideoProgressSlider', ->
describe 'updatePlayTime', -> describe 'updatePlayTime', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
spyOn($.fn, 'slider').andCallThrough() @progressSlider = @player.progressSlider
describe 'when frozen', -> describe 'when frozen', ->
beforeEach -> beforeEach ->
@slider.frozen = true spyOn($.fn, 'slider').andCallThrough()
@slider.updatePlayTime 20, 120 @progressSlider.frozen = true
@progressSlider.updatePlayTime 20, 120
it 'does not update the slider', -> it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled() expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', -> describe 'when not frozen', ->
beforeEach -> beforeEach ->
@slider.frozen = false spyOn($.fn, 'slider').andCallThrough()
@slider.updatePlayTime 20, 120 @progressSlider.frozen = false
@progressSlider.updatePlayTime 20, 120
it 'update the max value of the slider', -> it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120 expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
...@@ -107,55 +112,58 @@ xdescribe 'VideoProgressSlider', -> ...@@ -107,55 +112,58 @@ xdescribe 'VideoProgressSlider', ->
describe 'onSlide', -> describe 'onSlide', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null @time = null
$(@slider).bind 'seek', (event, time) => @time = time $(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek' spyOnEvent @progressSlider, 'seek'
@slider.onSlide {}, value: 20 @progressSlider.onSlide {}, value: 20
it 'freeze the slider', -> it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy() expect(@progressSlider.frozen).toBeTruthy()
it 'update the tooltip', -> it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled() expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', -> it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20 expect(@time).toEqual 20
describe 'onChange', -> describe 'onChange', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@slider.onChange {}, value: 20 @progressSlider = @player.progressSlider
@progressSlider.onChange {}, value: 20
it 'update the tooltip', -> it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled() expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', -> describe 'onStop', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null @time = null
$(@slider).bind 'seek', (event, time) => @time = time $(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @slider, 'seek' spyOnEvent @progressSlider, 'seek'
spyOn(window, 'setTimeout') @progressSlider.onStop {}, value: 20
@slider.onStop {}, value: 20
it 'freeze the slider', -> it 'freeze the slider', ->
expect(@slider.frozen).toBeTruthy() expect(@progressSlider.frozen).toBeTruthy()
it 'trigger seek event', -> it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @slider expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20 expect(@time).toEqual 20
it 'set timeout to unfreeze the slider', -> it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200 expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]() window.setTimeout.mostRecentCall.args[0]()
expect(@slider.frozen).toBeFalsy() expect(@progressSlider.frozen).toBeFalsy()
describe 'updateTooltip', -> describe 'updateTooltip', ->
beforeEach -> beforeEach ->
@slider = new VideoProgressSlider el: $('.slider') @player = jasmine.stubVideoPlayer @
@slider.updateTooltip 90 @progressSlider = @player.progressSlider
@progressSlider.updateTooltip 90
it 'set the tooltip value', -> it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30' expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
# TODO: figure out why failing describe 'VideoSpeedControl', ->
xdescribe 'VideoSpeedControl', ->
beforeEach -> beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
jasmine.stubVideoPlayer @ jasmine.stubVideoPlayer @
$('.speeds').remove() $('.speeds').remove()
...@@ -10,22 +10,23 @@ xdescribe 'VideoSpeedControl', -> ...@@ -10,22 +10,23 @@ xdescribe 'VideoSpeedControl', ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', -> it 'add the video speed control to player', ->
expect($('.secondary-controls').html()).toContain ''' secondaryControls = $('.secondary-controls')
<div class="speeds"> li = secondaryControls.find('.video_speeds li')
<a href="#"> expect(secondaryControls).toContain '.speeds'
<h3>Speed</h3> expect(secondaryControls).toContain '.video_speeds'
<p class="active">1.0x</p> expect(secondaryControls.find('p.active').text()).toBe '1.0x'
</a> expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
<ol class="video_speeds"><li data-speed="1.0" class="active"><a href="#">1.0x</a></li><li data-speed="0.75"><a href="#">0.75x</a></li></ol> expect(li.length).toBe @speedControl.speeds.length
</div> $.each li.toArray().reverse(), (index, link) =>
''' expect($(link)).toHaveData 'speed', @speedControl.speeds[index]
expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x'
it 'bind to change video speed link', -> it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', -> describe 'when running on touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true window.onTouchBasedDevice.andReturn true
$('.speeds').removeClass 'open' $('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
...@@ -37,7 +38,6 @@ xdescribe 'VideoSpeedControl', -> ...@@ -37,7 +38,6 @@ xdescribe 'VideoSpeedControl', ->
describe 'when running on non-touch based device', -> describe 'when running on non-touch based device', ->
beforeEach -> beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.speeds').removeClass 'open' $('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
......
# TODO: figure out why failing describe 'VideoVolumeControl', ->
xdescribe 'VideoVolumeControl', ->
beforeEach -> beforeEach ->
jasmine.stubVideoPlayer @ jasmine.stubVideoPlayer @
$('.volume').remove() $('.volume').remove()
......
# TODO: figure out why failing describe 'Video', ->
xdescribe 'Video', -> metadata = undefined
beforeEach -> beforeEach ->
loadFixtures 'video.html' loadFixtures 'video.html'
jasmine.stubRequests() jasmine.stubRequests()
@videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' @videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId' @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
metadata =
slowerSpeedYoutubeId:
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
afterEach -> afterEach ->
window.player = undefined window.player = undefined
...@@ -16,17 +24,18 @@ xdescribe 'Video', -> ...@@ -16,17 +24,18 @@ xdescribe 'Video', ->
beforeEach -> beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer') @stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75' $.cookie.andReturn '0.75'
window.player = 100 window.player = undefined
describe 'by default', -> describe 'by default', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata
@video = new Video '#example', @videosDefinition
it 'reset the current video player', -> it 'reset the current video player', ->
expect(window.player).toBeNull() expect(window.player).toBeNull()
it 'set the elements', -> it 'set the elements', ->
expect(@video.el).toBe '#video_example' expect(@video.el).toBe '#video_id'
it 'parse the videos', -> it 'parse the videos', ->
expect(@video.videos).toEqual expect(@video.videos).toEqual
...@@ -34,13 +43,8 @@ xdescribe 'Video', -> ...@@ -34,13 +43,8 @@ xdescribe 'Video', ->
'1.0': @normalSpeedYoutubeId '1.0': @normalSpeedYoutubeId
it 'fetch the video metadata', -> it 'fetch the video metadata', ->
expect(@video.metadata).toEqual expect(@video.fetchMetadata).toHaveBeenCalled
slowerSpeedYoutubeId: expect(@video.metadata).toEqual metadata
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
it 'parse available video speeds', -> it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0'] expect(@video.speeds).toEqual ['0.75', '1.0']
...@@ -56,7 +60,7 @@ xdescribe 'Video', -> ...@@ -56,7 +60,7 @@ xdescribe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = { Player: true } window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -69,7 +73,7 @@ xdescribe 'Video', -> ...@@ -69,7 +73,7 @@ xdescribe 'Video', ->
beforeEach -> beforeEach ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
afterEach -> afterEach ->
window.YT = @originalYT window.YT = @originalYT
...@@ -82,7 +86,7 @@ xdescribe 'Video', -> ...@@ -82,7 +86,7 @@ xdescribe 'Video', ->
@originalYT = window.YT @originalYT = window.YT
window.YT = {} window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
window.onYouTubePlayerAPIReady() window.onYouTubePlayerAPIReady()
afterEach -> afterEach ->
...@@ -95,7 +99,7 @@ xdescribe 'Video', -> ...@@ -95,7 +99,7 @@ xdescribe 'Video', ->
describe 'youtubeId', -> describe 'youtubeId', ->
beforeEach -> beforeEach ->
$.cookie.andReturn '1.0' $.cookie.andReturn '1.0'
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
describe 'with speed', -> describe 'with speed', ->
it 'return the video id for given speed', -> it 'return the video id for given speed', ->
...@@ -108,7 +112,7 @@ xdescribe 'Video', -> ...@@ -108,7 +112,7 @@ xdescribe 'Video', ->
describe 'setSpeed', -> describe 'setSpeed', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
describe 'when new speed is available', -> describe 'when new speed is available', ->
beforeEach -> beforeEach ->
...@@ -129,14 +133,14 @@ xdescribe 'Video', -> ...@@ -129,14 +133,14 @@ xdescribe 'Video', ->
describe 'getDuration', -> describe 'getDuration', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
it 'return duration for current video', -> it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200 expect(@video.getDuration()).toEqual 200
describe 'log', -> describe 'log', ->
beforeEach -> beforeEach ->
@video = new Video 'example', @videosDefinition @video = new Video '#example', @videosDefinition
@video.setSpeed '1.0' @video.setSpeed '1.0'
spyOn Logger, 'log' spyOn Logger, 'log'
@video.player = { currentTime: 25 } @video.player = { currentTime: 25 }
...@@ -144,7 +148,7 @@ xdescribe 'Video', -> ...@@ -144,7 +148,7 @@ xdescribe 'Video', ->
it 'call the logger with valid parameters', -> it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent', expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'example' id: 'id'
code: @normalSpeedYoutubeId code: @normalSpeedYoutubeId
currentTime: 25 currentTime: 25
speed: '1.0' speed: '1.0'
...@@ -37,7 +37,7 @@ class @VideoCaption extends Subview ...@@ -37,7 +37,7 @@ class @VideoCaption extends Subview
@loaded = true @loaded = true
if onTouchBasedDevice() if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video." $('.subtitles').html "<li>Caption will be displayed when you start playing the video.</li>"
else else
@renderCaption() @renderCaption()
......
...@@ -15,7 +15,7 @@ class @VideoPlayer extends Subview ...@@ -15,7 +15,7 @@ class @VideoPlayer extends Subview
$(@progressSlider).bind('seek', @onSeek) $(@progressSlider).bind('seek', @onSeek)
if @volumeControl if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange) $(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document).keyup @bindExitFullScreen $(document.documentElement).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen @$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice() @addToolTip() unless onTouchBasedDevice()
......
...@@ -11,7 +11,7 @@ class @VideoProgressSlider extends Subview ...@@ -11,7 +11,7 @@ class @VideoProgressSlider extends Subview
@buildHandle() @buildHandle()
buildHandle: -> buildHandle: ->
@handle = @$('.slider .ui-slider-handle') @handle = @$('.ui-slider-handle')
@handle.qtip @handle.qtip
content: "#{Time.format(@slider.slider('value'))}" content: "#{Time.format(@slider.slider('value'))}"
position: position:
......
# pylint: disable=W0223
"""Video is ungraded Xmodule for support video content."""
import json import json
import logging import logging
...@@ -8,7 +11,6 @@ from django.http import Http404 ...@@ -8,7 +11,6 @@ from django.http import Http404
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.contentstore.content import StaticContent
from xblock.core import Integer, Scope, String from xblock.core import Integer, Scope, String
import datetime import datetime
...@@ -18,21 +20,26 @@ log = logging.getLogger(__name__) ...@@ -18,21 +20,26 @@ log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`."""
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
class VideoModule(VideoFields, XModule): class VideoModule(VideoFields, XModule):
"""Video Xmodule."""
video_time = 0 video_time = 0
icon_class = 'video' icon_class = 'video'
js = {'coffee': js = {
[resource_string(__name__, 'js/src/time.coffee'), 'coffee': [
resource_string(__name__, 'js/src/video/display.coffee')] + resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/video/display.coffee')
] +
[resource_string(__name__, 'js/src/video/display/' + filename) [resource_string(__name__, 'js/src/video/display/' + filename)
for filename for filename
in sorted(resource_listdir(__name__, 'js/src/video/display')) in sorted(resource_listdir(__name__, 'js/src/video/display'))
if filename.endswith('.coffee')]} if filename.endswith('.coffee')]
}
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
...@@ -44,14 +51,14 @@ class VideoModule(VideoFields, XModule): ...@@ -44,14 +51,14 @@ class VideoModule(VideoFields, XModule):
self.show_captions = xmltree.get('show_captions', 'true') self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree) self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree) self.track = self._get_track(xmltree)
self.start_time, self.end_time = self._get_timeframe(xmltree) self.start_time, self.end_time = self.get_timeframe(xmltree)
def _get_source(self, xmltree): def _get_source(self, xmltree):
# find the first valid source """Find the first valid source."""
return self._get_first_external(xmltree, 'source') return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree): def _get_track(self, xmltree):
# find the first valid track """Find the first valid track."""
return self._get_first_external(xmltree, 'track') return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag): def _get_first_external(self, xmltree, tag):
...@@ -68,52 +75,37 @@ class VideoModule(VideoFields, XModule): ...@@ -68,52 +75,37 @@ class VideoModule(VideoFields, XModule):
break break
return result return result
def _get_timeframe(self, xmltree): def get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds. """ Converts 'from' and 'to' parameters in video tag to seconds.
If there are no parameters, returns empty string. """ If there are no parameters, returns empty string. """
def parse_time(s): def parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is """Converts s in '12:34:45' format to seconds. If s is
None, returns empty string""" None, returns empty string"""
if s is None: if str_time is None:
return '' return ''
else: else:
x = time.strptime(s, '%H:%M:%S') obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(hours=x.tm_hour, return datetime.timedelta(
minutes=x.tm_min, hours=obj_time.tm_hour,
seconds=x.tm_sec).total_seconds() minutes=obj_time.tm_min,
seconds=obj_time.tm_sec
).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' """This is not being called right now and we raise 404 error."""
Handle ajax calls to this video.
TODO (vshnayder): This is not being called right now, so the position
is not being saved.
'''
log.debug(u"GET {0}".format(get)) log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch)) log.debug(u"DISPATCH {0}".format(dispatch))
if dispatch == 'goto_position':
self.position = int(float(get['position']))
log.info(u"NEW POSITION {0}".format(self.position))
return json.dumps({'success': True})
raise Http404() raise Http404()
def get_progress(self):
''' TODO (vshnayder): Get and save duration of youtube video, then return
fraction watched.
(Be careful to notice when video link changes and update)
For now, we have no way of knowing if the video has even been watched, so
just return None.
'''
return None
def get_instance_state(self): def get_instance_state(self):
#log.debug(u"STATE POSITION {0}".format(self.position)) """Return information about state (position)."""
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def video_list(self): def video_list(self):
"""Return video list."""
return self.youtube return self.youtube
def get_html(self): def get_html(self):
...@@ -144,6 +136,7 @@ class VideoModule(VideoFields, XModule): ...@@ -144,6 +136,7 @@ class VideoModule(VideoFields, XModule):
class VideoDescriptor(VideoFields, RawDescriptor): class VideoDescriptor(VideoFields, RawDescriptor):
"""Descriptor for `VideoModule`."""
module_class = VideoModule module_class = VideoModule
stores_state = True stores_state = True
template_dir_name = "video" template_dir_name = "video"
"""
integration tests for xmodule
Contains:
1. BaseTestXmodule class provides course and users
for testing Xmodules with mongo store.
"""
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.tests import test_system
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class BaseTestXmodule(ModuleStoreTestCase):
"""Base class for testing Xmodules with mongo store.
This class prepares course and users for tests:
1. create test course
2. create, enrol and login users for this course
Any xmodule should overwrite only next parameters for test:
1. TEMPLATE_NAME
2. DATA
3. MODEL_DATA
4. COURSE_DATA and USER_COUNT if needed
This class should not contain any tests, because TEMPLATE_NAME
should be defined in child class.
"""
USER_COUNT = 2
COURSE_DATA = {}
# Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml
TEMPLATE_NAME = ""
DATA = ''
MODEL_DATA = {'data': '<some_module></some_module>'}
def setUp(self):
self.course = CourseFactory.create(data=self.COURSE_DATA)
# Turn off cache.
modulestore().request_cache = None
modulestore().metadata_inheritance_cache_subsystem = None
chapter = ItemFactory.create(
parent_location=self.course.location,
template="i4x://edx/templates/sequential/Empty",
)
section = ItemFactory.create(
parent_location=chapter.location,
template="i4x://edx/templates/sequential/Empty"
)
# username = robot{0}, password = 'test'
self.users = [
UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i)
for i in range(self.USER_COUNT)
]
for user in self.users:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
self.item_descriptor = ItemFactory.create(
parent_location=section.location,
template=self.TEMPLATE_NAME,
data=self.DATA
)
location = self.item_descriptor.location
system = test_system()
system.render_template = lambda template, context: context
self.item_module = self.item_descriptor.module_class(
system, location, self.item_descriptor, self.MODEL_DATA
)
self.item_url = Location(location).url()
# login all users for acces to Xmodule
self.clients = {user.username: Client() for user in self.users}
self.login_statuses = [
self.clients[user.username].login(
username=user.username, password='test')
for user in self.users
]
self.assertTrue(all(self.login_statuses))
def get_url(self, dispatch):
"""Return item url with dispatch."""
return reverse(
'modx_dispatch',
args=(self.course.id, self.item_url, dispatch)
)
def tearDown(self):
for user in self.users:
user.delete()
# -*- coding: utf-8 -*-
"""Video xmodule tests in mongo."""
from . import BaseTestXmodule
class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo."""
TEMPLATE_NAME = "i4x://edx/templates/video/default"
DATA = '<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>'
def test_handle_ajax_dispatch(self):
responses = {
user.username: self.clients[user.username].post(
self.get_url('whatever'),
{},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
for user in self.users
}
self.assertEqual(
set([
response.status_code
for _, response in responses.items()
]).pop(),
404)
# -*- coding: utf-8 -*-
"""Test for Video Xmodule functional logic.
These tests data readed from xml, not from mongo.
We have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
You can search for usages of this in the cms and lms tests for examples.
You use this so that it will do things like point the modulestore
setting to mongo, flush the contentstore before and after, load the
templates, etc.
You can then use the CourseFactory and XModuleItemFactory as defined in
common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc.
"""
import json
import unittest
from mock import Mock
from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule
from xmodule.modulestore import Location
from xmodule.tests import test_system
from xmodule.tests.test_logic import LogicTest
class VideoFactory(object):
"""A helper class to create video modules with various parameters
for testing.
"""
# tag that uses youtube videos
sample_problem_xml_youtube = """
<video show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
data_dir=""
caption_asset_path=""
autoplay="true"
from="01:00:03" to="01:00:10"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
</video>
"""
@staticmethod
def create():
"""Method return Video Xmodule instance."""
location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"])
model_data = {'data': VideoFactory.sample_problem_xml_youtube}
descriptor = Mock(weight="1")
system = test_system()
system.render_template = lambda template, context: context
module = VideoModule(system, location, descriptor, model_data)
return module
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
descriptor_class = VideoDescriptor
raw_model_data = {
'data': '<video />'
}
def test_get_timeframe_no_parameters(self):
"""Make sure that timeframe() works correctly w/o parameters"""
xmltree = etree.fromstring('<video>test</video>')
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, ('', ''))
def test_get_timeframe_with_one_parameter(self):
"""Make sure that timeframe() works correctly with one parameter"""
xmltree = etree.fromstring(
'<video from="00:04:07">test</video>'
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, ''))
def test_get_timeframe_with_two_parameters(self):
"""Make sure that timeframe() works correctly with two parameters"""
xmltree = etree.fromstring(
'''<video
from="00:04:07"
to="13:04:39"
>test</video>'''
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for Video Xmodule."""
def test_video_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoFactory.create()
# `get_html` return only context, cause we
# overwrite `system.render_template`
context = module.get_html()
expected_context = {
'track': None,
'show_captions': 'true',
'display_name': 'SampleProblem1',
'id': module.location.html_id(),
'end': 3610.0,
'caption_asset_path': '/static/subs/',
'source': '.../mit-3091x/M-3091X-FA12-L21-3_100.mp4',
'streams': '0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
'normal_speed_video_id': 'ZwkTiUPN0mg',
'position': 0,
'start': 3603.0
}
self.assertDictEqual(context, expected_context)
self.assertEqual(
module.youtube,
'0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg')
self.assertEqual(
module.video_list(),
module.youtube)
self.assertEqual(
module.position,
0)
self.assertDictEqual(
json.loads(module.get_instance_state()),
{'position': 0})
...@@ -88,9 +88,10 @@ $dashboard-profile-header-color: transparent; ...@@ -88,9 +88,10 @@ $dashboard-profile-header-color: transparent;
$dashboard-profile-color: rgb(252,252,252); $dashboard-profile-color: rgb(252,252,252);
$dot-color: $light-gray; $dot-color: $light-gray;
$content-wrapper-bg: shade($body-bg, 2%); $content-wrapper-bg: $white;
$course-bg-color: #d6d6d6; $course-bg-color: #d6d6d6;
$course-bg-image: url(../images/bg-texture.png); $course-bg-image: url(../images/bg-texture.png);
$account-content-wrapper-bg: shade($body-bg, 2%);
$course-profile-bg: rgb(245,245,245); $course-profile-bg: rgb(245,245,245);
$course-header-bg: rgba(255,255,255, 0.93); $course-header-bg: rgba(255,255,255, 0.93);
......
...@@ -35,7 +35,7 @@ a { ...@@ -35,7 +35,7 @@ a {
width: 100%; width: 100%;
border-radius: 3px; border-radius: 3px;
border: 1px solid $outer-border-color; border: 1px solid $outer-border-color;
background: $body-bg; background: $container-bg;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05)); @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
} }
} }
...@@ -50,7 +50,7 @@ textarea, ...@@ -50,7 +50,7 @@ textarea,
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="password"] { input[type="password"] {
background: $body-bg; background: $white;
border: 1px solid $border-color-2; border: 1px solid $border-color-2;
@include border-radius(0); @include border-radius(0);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1)); @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6), inset 0 0 3px 0 rgba(0,0,0, 0.1));
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// page-level // page-level
.view-register, .view-login, .view-passwordreset { .view-register, .view-login, .view-passwordreset {
background: $body-bg; background: $container-bg;
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
// layout // layout
.content-wrapper { .content-wrapper {
background: $content-wrapper-bg; background: $account-content-wrapper-bg;
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -331,7 +331,7 @@ ...@@ -331,7 +331,7 @@
} }
textarea, input { textarea, input {
background: $body-bg; background: $container-bg;
color: rgba(0,0,0,.25); color: rgba(0,0,0,.25);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment