diff --git a/.eslintignore b/.eslintignore index 0ed41817..e69de29b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +0,0 @@ -video_xblock/static/js/player_state.js diff --git a/.eslintrc.json b/.eslintrc.json index 8a8c2bc1..697fe280 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,9 @@ "globals": { "videojs": true, "domReady": true, + "getXblockUsageId": true, "getTranscriptUrl": true, + "getDownloadTranscriptUrl": true, "showStatus": true, "gettext": true }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 937ea7f9..6b5c2ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.6.3] - 2017-03-30 + +### Changed + +- Extend Brightcove url regex to include additional set of video urls. + Now it supports both: + - `https://studio.brightcove.com/products/videos/` + - `https://studio.brightcove.com/products/videocloud/media/videos/` +- Restructure JavaScript codebase. + ## [0.6.2] - 2017-03-27 ### Changed @@ -137,4 +147,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/). [0.6.0]: https://github.com/raccoongang/xblock-video/compare/v0.5.0...v0.6.0 [0.6.1]: https://github.com/raccoongang/xblock-video/compare/v0.6.0...v0.6.1 [0.6.2]: https://github.com/raccoongang/xblock-video/compare/v0.6.1...v0.6.2 -[Unreleased]: https://github.com/raccoongang/xblock-video/compare/v0.6.2...HEAD +[0.6.3]: https://github.com/raccoongang/xblock-video/compare/v0.6.2...v0.6.3 +[Unreleased]: https://github.com/raccoongang/xblock-video/compare/v0.6.3...HEAD diff --git a/Makefile b/Makefile index ea8a0e5a..d05992e3 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,10 @@ clean: # Clean working directory test: test-py test-js ## Run tests -test-py: deps-test ## Run Python tests +test-py: ## Run Python tests nosetests video_xblock --with-coverage --cover-package=video_xblock -test-js: tools +test-js: ## Run JavaScript tests karma start video_xblock/static/video_xblock_karma.conf.js quality: quality-py quality-js ## Run code quality checks diff --git a/README.md b/README.md index a7c8fa21..34572652 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ pip install --process-dependency-links -e "git+https://github.com/raccoongang/xb ## Enabling in Studio -You can enable the Wistia xblock in studio through the advanced +You can enable the Video xblock in studio through the advanced settings: 1. From the main page of a specific course, click on *Settings*, @@ -70,7 +70,7 @@ Sample default settings in `/edx/app/edxapp/cms.env.json`: Install dependencies and development tools: ```shell -> make deps deps-test tools +> make tools deps-test ``` Run quality checks: diff --git a/setup.py b/setup.py index 15145683..2b4ab8f7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup -VERSION = '0.6.2' +VERSION = '0.6.3' DESCRIPTION = 'Video XBlock to embed videos hosted on different video platforms into your courseware' @@ -34,6 +34,7 @@ def package_data(pkg, roots): ], dependency_links=[ # At the moment of writing PyPI hosts outdated version of xblock-utils, hence git + # Replace dependency links with numbered versions when it's released on PyPI 'git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils-1.0.2', 'git+https://github.com/edx/xblock-utils.git@v1.0.3#egg=xblock-utils-1.0.3', ], diff --git a/video_xblock/backends/base.py b/video_xblock/backends/base.py index 27225ca2..96ce4f43 100644 --- a/video_xblock/backends/base.py +++ b/video_xblock/backends/base.py @@ -121,7 +121,7 @@ def basic_fields(self): Subclasses can extend or redefine list if needed. Defaults to a tuple defined by VideoXBlock. """ - return ('display_name', 'href') + return ['display_name', 'href'] @property def advanced_fields(self): @@ -130,11 +130,11 @@ def advanced_fields(self): Subclasses can extend or redefine list if needed. Defaults to a tuple defined by VideoXBlock. """ - return ( + return [ 'start_time', 'end_time', 'handout', 'transcripts', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'default_transcripts', 'download_video_allowed', 'download_video_url' - ) + ] @property def fields_help(self): @@ -173,22 +173,22 @@ def get_frag(self, **context): 'static/vendor/js/video.min.js', 'static/vendor/js/videojs-contextmenu.min.js', 'static/vendor/js/videojs-contextmenu-ui.min.js', - 'static/js/video-speed.js', - 'static/js/player_state.js', - 'static/js/videojs-speed-handler.js' + 'static/js/videojs/video-speed.js', + 'static/js/student-view/player-state.js', + 'static/js/videojs/videojs-speed-handler.js' ] if json.loads(context['player_state'])['transcripts']: js_files += [ 'static/vendor/js/videojs-transcript.min.js', - 'static/js/transcript-download.js', - 'static/js/videojs-transcript.js' + 'static/js/student-view/transcript-download.js', + 'static/js/videojs/videojs-transcript.js' ] js_files += [ - 'static/js/videojs-tabindex.js', - 'static/js/toggle-button.js', - 'static/js/videojs_event_plugin.js' + 'static/js/videojs/videojs-tabindex.js', + 'static/js/videojs/toggle-button.js', + 'static/js/videojs/videojs-event-plugin.js' ] for js_file in js_files: @@ -216,7 +216,7 @@ def player_data_setup(context): "offset": { "start": context['start_time'], "end": context['end_time'], - "current_time": context['player_state']['current_time'], + "current_time": context['player_state']['currentTime'], }, "videoJSSpeedHandler": {}, } diff --git a/video_xblock/backends/brightcove.py b/video_xblock/backends/brightcove.py index be5754bd..53484f06 100644 --- a/video_xblock/backends/brightcove.py +++ b/video_xblock/backends/brightcove.py @@ -306,7 +306,7 @@ class BrightcovePlayer(BaseVideoPlayer, BrightcoveHlsMixin): BrightcovePlayer is used for videos hosted on the Brightcove Video Cloud. """ - url_re = re.compile(r'https:\/\/studio.brightcove.com\/products\/videocloud\/media\/videos\/(?P\d+)') + url_re = re.compile(r'https:\/\/studio.brightcove.com\/products(?:\/videocloud\/media)?\/videos\/(?P\d+)') metadata_fields = ['access_token', 'client_id', 'client_secret', ] # Current api for requesting transcripts. @@ -334,7 +334,7 @@ def basic_fields(self): Brightcove videos require Brightcove Account id. """ - return super(BrightcovePlayer, self).basic_fields + ('account_id',) + return super(BrightcovePlayer, self).basic_fields + ['account_id'] @property def advanced_fields(self): @@ -343,7 +343,10 @@ def advanced_fields(self): Brightcove videos require Brightcove Account id. """ - return ('player_id',) + super(BrightcovePlayer, self).advanced_fields + fields_list = ['player_id'] + super(BrightcovePlayer, self).advanced_fields + # Add `token` field before `threeplaymedia_file_id` + fields_list.insert(fields_list.index('threeplaymedia_file_id'), 'token') + return fields_list fields_help = { 'token': 'You can generate a BC token following the guide of ' @@ -387,12 +390,12 @@ def get_frag(self, **context): ) js_files = [ 'static/js/base.js', - 'static/js/toggle-button.js' + 'static/js/videojs/toggle-button.js' ] js_files += [ - 'static/js/videojs-tabindex.js', - 'static/js/videojs_event_plugin.js', - 'static/js/brightcove-videojs-init.js' + 'static/js/videojs/videojs-tabindex.js', + 'static/js/videojs/videojs-event-plugin.js', + 'static/js/videojs/brightcove-videojs-init.js' ] for js_file in js_files: @@ -411,14 +414,14 @@ def get_player_html(self, **context): self.resource_string( 'static/vendor/js/videojs-offset.min.js' ), - self.resource_string('static/js/videojs-speed-handler.js') + self.resource_string('static/js/videojs/videojs-speed-handler.js') ] if context.get('transcripts'): vjs_plugins += [ self.resource_string( 'static/vendor/js/videojs-transcript.min.js' ), - self.resource_string('static/js/videojs-transcript.js') + self.resource_string('static/js/videojs/videojs-transcript.js') ] context['vjs_plugins'] = vjs_plugins return super(BrightcovePlayer, self).get_player_html(**context) diff --git a/video_xblock/backends/html5.py b/video_xblock/backends/html5.py index 88da8de7..3a1bfa88 100644 --- a/video_xblock/backends/html5.py +++ b/video_xblock/backends/html5.py @@ -21,10 +21,10 @@ def advanced_fields(self): Hide `download_video_url` field for Html5Player. """ - return tuple( + return [ field for field in super(Html5Player, self).advanced_fields if field not in self.exclude_advanced_fields - ) + ] exclude_advanced_fields = ('default_transcripts', 'download_video_url') @@ -57,7 +57,7 @@ def get_frag(self, **context): ) js_files = [ 'static/vendor/js/videojs-offset.min.js', - 'static/js/player-context-menu.js' + 'static/js/videojs/player-context-menu.js' ] for js_file in js_files: diff --git a/video_xblock/backends/wistia.py b/video_xblock/backends/wistia.py index 6d1b47dd..0eac8478 100644 --- a/video_xblock/backends/wistia.py +++ b/video_xblock/backends/wistia.py @@ -57,6 +57,18 @@ class WistiaPlayer(BaseVideoPlayer): 'Please ensure appropriate operations scope has been set on the video platform.' } + @property + def advanced_fields(self): + """ + Tuple of VideoXBlock fields to display in Basic tab of edit modal window. + + Brightcove videos require Brightcove Account id. + """ + fields_list = super(WistiaPlayer, self).advanced_fields + # Add `token` field before `threeplaymedia_file_id` + fields_list.insert(fields_list.index('threeplaymedia_file_id'), 'token') + return fields_list + def media_id(self, href): """ Extract Platform's media id from the video url. @@ -78,14 +90,10 @@ def get_frag(self, **context): self.render_resource('static/html/wistiavideo.html', **context) ) - frag.add_javascript( - self.render_resource('static/js/context.js', **context) - ) - js_files = [ 'static/vendor/js/vjs.wistia.js', 'static/vendor/js/videojs-offset.min.js', - 'static/js/player-context-menu.js' + 'static/js/videojs/player-context-menu.js' ] for js_file in js_files: diff --git a/video_xblock/static/js/base.js b/video_xblock/static/js/base.js index 54abc370..f3e13509 100644 --- a/video_xblock/static/js/base.js +++ b/video_xblock/static/js/base.js @@ -7,3 +7,19 @@ var domReady = function(callback) { document.addEventListener('DOMContentLoaded', callback); } }; + +/** Get XblockUsageId from xblock's url. */ +var getXblockUsageId = function() { + 'use strict'; + return window.location.hash.slice(1); +}; + +/** Get transcript url for current caption language */ +var getDownloadTranscriptUrl = function(transcripts, player) { + 'use strict'; + var downloadTranscriptUrl; + if (transcripts[player.captionsLanguage]) { + downloadTranscriptUrl = transcripts[player.captionsLanguage].url; + } + return downloadTranscriptUrl; +}; diff --git a/video_xblock/static/js/player_state.js b/video_xblock/static/js/player_state.js deleted file mode 100644 index bc89e51c..00000000 --- a/video_xblock/static/js/player_state.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * This part is responsible for loading and saving player state. - * State includes: - * - Current time - * - Playback rate - * - Volume - * - Muted - * - * State is loaded after VideoJs player is fully initialized. - * State is saved at certain events. - */ - -var player_state_obj = window.playerStateObj; -var player_state = { - volume: player_state_obj.volume, - currentTime: player_state_obj.current_time, - playbackRate: player_state_obj.playback_rate, - muted: player_state_obj.muted, - transcriptsEnabled: player_state_obj.transcripts_enabled, - captionsEnabled: player_state_obj.captions_enabled, - captionsLanguage: player_state_obj.captions_language -}; -var xblockUsageId = window.location.hash.slice(1); -var transcripts = {}; -player_state_obj.transcripts.forEach(function(transcript) { - transcripts[transcript.lang] = { - 'label': transcript.label, - 'url': transcript.url - }; -}); - -/** Get transcript url for current caption language */ -var getDownloadTranscriptUrl = function(player) { - var downloadTranscriptUrl; - if (transcripts[player.captionsLanguage]) { - downloadTranscriptUrl = transcripts[player.captionsLanguage].url; - } else { - downloadTranscriptUrl = '#'; - }; - return downloadTranscriptUrl; -}; - -/** Restore default or previously saved player state */ -var setInitialState = function(player, state) { - var stateCurrentTime = state.currentTime; - var playbackProgress = localStorage.getItem('playbackProgress'); - if (playbackProgress){ - playbackProgress=JSON.parse(playbackProgress); - if (playbackProgress[window.videoPlayerId]) { - stateCurrentTime = playbackProgress[window.videoPlayerId]; - } - } - if (stateCurrentTime > 0) { - player.currentTime(stateCurrentTime); - } - player - .volume(state.volume) - .muted(state.muted) - .playbackRate(state.playbackRate); - player.transcriptsEnabled = state.transcriptsEnabled; - player.captionsEnabled = state.captionsEnabled; - player.captionsLanguage = state.captionsLanguage; - // To switch off transcripts and captions state if doesn`t have transcripts with current captions language - if (!transcripts[player.captionsLanguage]) { - player.captionsEnabled = player.transcriptsEnabled = false; - }; -}; - -/** - * Save player state by posting it in a message to parent frame. - * Parent frame passes it to a server by calling VideoXBlock.save_state() handler. - */ -var saveState = function() { - var player = this; - var new_state = { - volume: player.volume(), - currentTime: player.ended()? 0 : player.currentTime(), - playbackRate: player.playbackRate(), - muted: player.muted(), - transcriptsEnabled: player.transcriptsEnabled, - captionsEnabled: player.captionsEnabled, - captionsLanguage: player.captionsLanguage - }; - if (JSON.stringify(new_state) !== JSON.stringify(player_state)) { - console.log('Starting saving player state'); - player_state = new_state; - parent.postMessage({ - action: 'saveState', - info: new_state, - xblockUsageId: xblockUsageId, - downloadTranscriptUrl: getDownloadTranscriptUrl(player) - }, - document.location.protocol + '//' + document.location.host - ); - } -}; - -/** - * Save player progress in browser's local storage. - * We need it when user is switching between tabs. - */ -var saveProgressToLocalStore = function saveProgressToLocalStore() { - var player = this; - var playbackProgress = localStorage.getItem('playbackProgress'); - if (!playbackProgress) { - playbackProgress = '{}'; - } - playbackProgress = JSON.parse(playbackProgress); - playbackProgress[window.videoPlayerId] = player.ended() ? 0 : player.currentTime(); - localStorage.setItem('playbackProgress',JSON.stringify(playbackProgress)); -}; - -domReady(function() { - videojs(window.videoPlayerId).ready(function() { - var player = this; - // Restore default or previously saved player state - setInitialState(player, player_state); - player - .on('timeupdate', saveProgressToLocalStore) - .on('volumechange', saveState) - .on('ratechange', saveState) - .on('play', saveState) - .on('pause', saveState) - .on('ended', saveState) - .on('transcriptstatechanged', saveState) - .on('captionstatechanged', saveState) - .on('languagechange', saveState); - }); -}); diff --git a/video_xblock/static/js/spec/base_spec.js b/video_xblock/static/js/spec/base_spec.js new file mode 100644 index 00000000..80cf1096 --- /dev/null +++ b/video_xblock/static/js/spec/base_spec.js @@ -0,0 +1,24 @@ +describe('Base javascript', function() { + 'use strict'; + var playerId = 'test_id'; + var player = { + captionsLanguage: 'en' + }; + var testXblcokUsageId = 'block-v1:test+test+test+type@video_xblock+block@test'; + var transcriptsObject = window.playerStateObj.transcriptsObject; + window.videoPlayerId = playerId; + window.location.hash = '#' + testXblcokUsageId; + beforeEach(function() { + var video = document.createElement('video'); + video.id = playerId; + video.className = 'video-js vjs-default-skin'; + document.body.appendChild(video); + }); + it('return getXblockUsageId', function() { + expect(getXblockUsageId(), testXblcokUsageId); + }); + it('return download transcript url', function() { + // TODO avoid the eslint shutdown for the implicitly received variables + expect(getDownloadTranscriptUrl(transcriptsObject, player)).toBe(transcriptsObject.en.url); // eslint-disable-line + }); +}); diff --git a/video_xblock/static/js/spec/player_state_spec.js b/video_xblock/static/js/spec/player_state_spec.js deleted file mode 100644 index 14a1a984..00000000 --- a/video_xblock/static/js/spec/player_state_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -describe('Player state', function() { - 'use strict'; - var playerId = 'test_id'; - var player = { - captionsLanguage: 'en' - }; - window.videoPlayerId = playerId; - beforeEach(function() { - var video = document.createElement('video'); - video.id = playerId; - video.className = 'video-js vjs-default-skin'; - document.body.appendChild(video); - }); - it('return download transcript url', function() { - // TODO avoid the eslint shutdown for the implicity got variables - expect(getDownloadTranscriptUrl(player)).toBe(transcripts.en.url); // eslint-disable-line - }); -}); diff --git a/video_xblock/static/js/spec/test_context.js b/video_xblock/static/js/spec/test_context.js index 347744ba..a1f92186 100644 --- a/video_xblock/static/js/spec/test_context.js +++ b/video_xblock/static/js/spec/test_context.js @@ -4,16 +4,17 @@ window.videoPlayerId = 'test_id'; window.playerStateObj = { volume: 1, - current_time: 0, - playback_rate: 1, + currentTime: 0, + playbackRate: 1, muted: false, - transcripts: [{ - lang: 'en', - label: 'English', - url: 'http://test.url' - }], - transcripts_enabled: false, - captions_enabled: false, - captions_language: 'en' + transcriptsObject: { + en: { + label: 'English', + url: 'http://test.url' + } + }, + transcriptsEnabled: false, + captionsEnabled: false, + captionsLanguage: 'en' }; diff --git a/video_xblock/static/js/student-view/player-state.js b/video_xblock/static/js/student-view/player-state.js new file mode 100644 index 00000000..c59d1b78 --- /dev/null +++ b/video_xblock/static/js/student-view/player-state.js @@ -0,0 +1,120 @@ +/** + * This part is responsible for loading and saving player state. + * State includes: + * - Current time + * - Playback rate + * - Volume + * - Muted + * + * State is loaded after VideoJs player is fully initialized. + * State is saved at certain events. + */ + +var PlayerState = function(player, playerState) { + 'use strict'; + var xblockUsageId = getXblockUsageId(); + + /** Create hashmap with all transcripts */ + var getTranscipts = function(transcriptsData) { + var result = {}; + transcriptsData.forEach(function(transcript) { + result[transcript.lang] = { + label: transcript.label, + url: transcript.url + }; + }); + return result; + }; + + var transcripts = getTranscipts(playerState.transcripts); + + /** Restore default or previously saved player state */ + var setInitialState = function(state) { + var stateCurrentTime = state.currentTime; + var playbackProgress = localStorage.getItem('playbackProgress'); + if (playbackProgress) { + playbackProgress = JSON.parse(playbackProgress); + if (playbackProgress[window.videoPlayerId]) { + stateCurrentTime = playbackProgress[window.videoPlayerId]; + } + } + if (stateCurrentTime > 0) { + player.currentTime(stateCurrentTime); + } + player + .volume(state.volume) + .muted(state.muted) + .playbackRate(state.playbackRate); + player.captionsLanguage = state.captionsLanguage; // eslint-disable-line no-param-reassign + // To switch off transcripts and captions state if doesn`t have transcripts with current captions language + if (!transcripts[player.captionsLanguage]) { + player.captionsEnabled = player.transcriptsEnabled = false; // eslint-disable-line no-param-reassign + } else { + player.transcriptsEnabled = state.transcriptsEnabled; // eslint-disable-line no-param-reassign + player.captionsEnabled = state.captionsEnabled; // eslint-disable-line no-param-reassign + } + }; + + /** + * Save player state by posting it in a message to parent frame. + * Parent frame passes it to a server by calling VideoXBlock.save_state() handler. + */ + var saveState = function() { + var playerObj = this; + var transcriptUrl = getDownloadTranscriptUrl(transcripts, playerObj); + + var newState = { + volume: playerObj.volume(), + currentTime: playerObj.ended() ? 0 : playerObj.currentTime(), + playbackRate: playerObj.playbackRate(), + muted: playerObj.muted(), + transcriptsEnabled: playerObj.transcriptsEnabled, + captionsEnabled: playerObj.captionsEnabled, + captionsLanguage: playerObj.captionsLanguage + }; + if (JSON.stringify(newState) !== JSON.stringify(playerState)) { + console.log('Starting saving player state'); // eslint-disable-line no-console + playerState = newState; // eslint-disable-line no-param-reassign + parent.postMessage( + { + action: 'saveState', + info: newState, + xblockUsageId: xblockUsageId, + downloadTranscriptUrl: transcriptUrl || '#' + }, + document.location.protocol + '//' + document.location.host + ); + } + }; + + /** + * Save player progress in browser's local storage. + * We need it when user is switching between tabs. + */ + var saveProgressToLocalStore = function() { + var playerObj = this; + var playbackProgress; + playbackProgress = JSON.parse(localStorage.getItem('playbackProgress') || '{}'); + playbackProgress[window.videoPlayerId] = playerObj.ended() ? 0 : playerObj.currentTime(); + localStorage.setItem('playbackProgress', JSON.stringify(playbackProgress)); + }; + + setInitialState(playerState); + player + .on('timeupdate', saveProgressToLocalStore) + .on('volumechange', saveState) + .on('ratechange', saveState) + .on('play', saveState) + .on('pause', saveState) + .on('ended', saveState) + .on('transcriptstatechanged', saveState) + .on('captionstatechanged', saveState) + .on('languagechange', saveState); +}; + +domReady(function() { + 'use strict'; + videojs(window.videoPlayerId).ready(function() { + PlayerState(this, window.playerStateObj); + }); +}); diff --git a/video_xblock/static/js/student-view/transcript-download.js b/video_xblock/static/js/student-view/transcript-download.js new file mode 100644 index 00000000..9a83d95e --- /dev/null +++ b/video_xblock/static/js/student-view/transcript-download.js @@ -0,0 +1,33 @@ +/** + * This part is responsible for downloading of transcripts and captions in LMS and CMS. + */ + +var TranscriptDownload = function(player) { + 'use strict'; + /** Send message of changing transcript to parent window */ + var transcripts = window.playerStateObj.transcriptsObject; + var sendMessage = function() { + var playerObj = this; + parent.postMessage({ + action: 'downloadTranscriptChanged', + downloadTranscriptUrl: getDownloadTranscriptUrl(transcripts, playerObj), + xblockUsageId: getXblockUsageId() + }, document.location.protocol + '//' + document.location.host); + }; + + if (!transcripts[player.captionsLanguage]) { + player.captionsEnabled = player.transcriptsEnabled = false; // eslint-disable-line no-param-reassign + // Need to trigger two events to disable active buttons in control bar + player.trigger('transcriptdisabled'); + player.trigger('captiondisabled'); + } + + player.on('captionstrackchange', sendMessage); +}; + +domReady(function() { + 'use strict'; + videojs(window.videoPlayerId).ready(function() { + TranscriptDownload(this); + }); +}); diff --git a/video_xblock/static/js/video_xblock.js b/video_xblock/static/js/student-view/video-xblock.js similarity index 98% rename from video_xblock/static/js/video_xblock.js rename to video_xblock/static/js/student-view/video-xblock.js index 77d83f53..98be6274 100644 --- a/video_xblock/static/js/video_xblock.js +++ b/video_xblock/static/js/student-view/video-xblock.js @@ -14,7 +14,8 @@ function VideoXBlockStudentViewInit(runtime, element) { var handlers = window.videoXBlockState.handlers = // eslint-disable-line vars-on-top window.videoXBlockState.handlers || { saveState: {}, - analytics: {} + analytics: {}, + downloadTranscriptChanged: {} }; handlers.saveState[usageId] = stateHandlerUrl; handlers.analytics[usageId] = eventHandlerUrl; diff --git a/video_xblock/static/js/studio-edit.js b/video_xblock/static/js/studio-edit/studio-edit.js similarity index 76% rename from video_xblock/static/js/studio-edit.js rename to video_xblock/static/js/studio-edit/studio-edit.js index 313429fe..b1405123 100644 --- a/video_xblock/static/js/studio-edit.js +++ b/video_xblock/static/js/studio-edit/studio-edit.js @@ -1,3 +1,8 @@ +/* global createAvailableTranscriptBlock disableOption removeStandardTranscriptBlock getInitialDefaultTranscriptsData +removeEnabledTranscriptBlock bindUploadListenerAvailableTranscript pushTranscript pushTranscriptsValue +createEnabledTranscriptBlock createTranscriptBlock parseRelativeTime removeAllEnabledTranscripts tinyMCE baseUrl +validateTranscripts fillValues validateTranscriptFile removeTranscriptBlock clickUploader +languageChecker $3playmediaTranscriptsApi getTranscripts3playmediaApiHandlerUrl*/ /** Set up the Video xblock studio editor. This part is responsible for validation and sending of the data to a backend. Reference: @@ -13,18 +18,33 @@ function StudioEditableXBlock(runtime, element) { var $defaultTranscriptsSwitcher = $('input.default-transcripts-switch-input'); var $enabledLabel = $('div.custom-field-section-label.enabled-transcripts'); var $availableLabel = $('div.custom-field-section-label.available-transcripts'); - var noEnabledTranscript; - var noAvailableTranscript; var $modalHeaderTabs = $('.editor-modes.action-list.action-modes'); var currentTabName; var isNotDummy = $('#xb-field-edit-href').val() !== ''; var SUCCESS = 'success'; var ERROR = 'error'; + var transcriptsValue = []; + var disabledLanguages = []; + var $fileUploader = $('.input-file-uploader', element); + var $defaultTranscriptUploader = $('.upload-default-transcript'); + var $defaultTranscriptRemover = $('.remove-default-transcript'); + var $standardTranscriptUploader = $('.add-transcript'); + var $standardTranscriptRemover = $('.remove-action'); + var $langChoiceItem = $('.language-transcript-selector', element); + var $videoApiAuthenticator = $('#video-api-authenticate', element); + var $3playmediaTranscriptsApi = $('#threeplaymedia-api-transcripts', element); + var gotTranscriptsValue = $('input[data-field-name="transcripts"]').val(); + var downloadTranscriptHandlerUrl = runtime.handlerUrl(element, 'download_transcript'); + var authenticateVideoApiHandlerUrl = runtime.handlerUrl(element, 'authenticate_video_api_handler'); + var uploadDefaultTranscriptHandlerUrl = runtime.handlerUrl(element, 'upload_default_transcript_handler'); + var currentLanguageCode; + var initialDefaultTranscriptsData = getInitialDefaultTranscriptsData(); + var initialDefaultTranscripts = initialDefaultTranscriptsData[0]; /** Toggle studio editor's current tab. */ - function toggleEditorTab(tabName) { + function toggleEditorTab(event, tabName) { var $tabDisable; var $tabEnable; var $otherTabName; @@ -57,7 +77,7 @@ function StudioEditableXBlock(runtime, element) { // Bind listeners to the toggle buttons $('.edit-menu-tab').click(function(event) { currentTabName = $(event.currentTarget).attr('data-tab-name'); - toggleEditorTab(currentTabName); + toggleEditorTab(event, currentTabName); }); } }()); @@ -143,6 +163,118 @@ function StudioEditableXBlock(runtime, element) { $('#settings-tab').ready(function() { showBackendSettings(); }); + /** + * Is there a more specific error message we can show? + * @param {String} responseText JSON received from ajax call + * @return {String} Error message extracted from input JSON or a portion of input text + */ + function extractErrorMessage(responseText) { + var message; + try { + message = JSON.parse(responseText).error; + if (typeof message === 'object' && message.messages) { + // e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc. + message = $.map(message.messages, function(msg) { return msg.text; }).join(', '); + } + return message; + } catch (error) { // SyntaxError thrown by JSON.parse + return responseText.substr(0, 300); + } + } + /** + * Bind removal listener to a newly created enabled transcript. + */ + function bindRemovalListenerEnabledTranscript(langCode, langLabel, downloadUrlServer) { + var $removeElement = $( + '.default-transcripts-action-link.remove-default-transcript[data-lang-code=' + langCode + ']'); + $removeElement.click(function(event) { + var defaultTranscript = { + lang: langCode, + label: langLabel, + url: downloadUrlServer + }; + // Affect default transcripts + removeEnabledTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); + createAvailableTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); + bindUploadListenerAvailableTranscript(langCode, langLabel); // eslint-disable-line no-use-before-define + // Affect standard transcripts + removeStandardTranscriptBlock(langCode, transcriptsValue, disabledLanguages); + disableOption($langChoiceItem, disabledLanguages); + event.preventDefault(); + }); + } + /** + * Upload a transcript available on a video platform to video xblock and update displayed default transcripts. + */ + function uploadDefaultTranscriptsToServer(data) { + var message, status; + + $.ajax({ + type: 'POST', + url: uploadDefaultTranscriptHandlerUrl, + data: JSON.stringify(data), + dataType: 'json' + }) + .done(function(response) { + var newLang = response.lang; + var newLabel = response.label; + var newUrl = response.url; + // Add a default transcript to the list of enabled ones + var downloadUrl = downloadTranscriptHandlerUrl + '?' + newUrl; + var defaultTranscript = { + lang: newLang, + label: newLabel, + url: downloadUrl + }; + // Create a standard transcript + pushTranscript(newLang, newLabel, newUrl, '', transcriptsValue); + pushTranscriptsValue(transcriptsValue); + createEnabledTranscriptBlock(defaultTranscript, downloadUrl); + bindRemovalListenerEnabledTranscript(newLang, newLabel, newUrl); + message = response.success_message; + status = SUCCESS; + }) + .fail(function(jqXHR) { + message = gettext('This may be happening because of an error with our server or your ' + + 'internet connection. Try refreshing the page or making sure you are online.'); + if (jqXHR.responseText) { // Is there a more specific error message we can show? + message += extractErrorMessage(jqXHR.responseText); + } + status = ERROR; + }) + .always(function() { + showStatus( + $('.api-response.upload-default-transcript.' + currentLanguageCode + '.status'), + status, + message + ); + }); + } + /** + * Bind upload listener to a newly created available transcript. + */ + function bindUploadListenerAvailableTranscript(langCode, langLabel) { + var $uploadElement = $( + '.default-transcripts-action-link.upload-default-transcript[data-lang-code=' + langCode + ']'); + $uploadElement.click(function() { + // Get url for a transcript fetching from the API + var downloadUrlApi = getTranscriptUrl(initialDefaultTranscripts, langCode); + var defaultTranscript = { + lang: langCode, + label: langLabel, + url: downloadUrlApi + }; + uploadDefaultTranscriptsToServer(defaultTranscript); + // Affect standard transcripts + createTranscriptBlock(langCode, langLabel, transcriptsValue, downloadTranscriptHandlerUrl); + }); + } + /** Field Changed event */ + function fieldChanged($wrapper, $resetButton) { + // Field value has been modified: + $wrapper.addClass('is-set'); + $resetButton.removeClass('inactive').addClass('active'); + } $(element).find('.field-data-control').each(function() { var $field = $(this); @@ -157,16 +289,13 @@ function StudioEditableXBlock(runtime, element) { val: function() { var val = $field.val(); // Cast values to the appropriate type so that we send nice clean JSON over the wire: - if (type == 'boolean') { // eslint-disable-line - return (val == 'true' || val == '1'); // eslint-disable-line - } - if (type == 'integer') { // eslint-disable-line + if (type === 'boolean') { // eslint-disable-line + return (val === 'true' || val === '1'); // eslint-disable-line + } else if (type === 'integer') { // eslint-disable-line return parseInt(val, 10); - } - if (type == 'float') { // eslint-disable-line + } else if (type === 'float') { // eslint-disable-line return parseFloat(val); - } - if (type == 'generic' || type == 'list' || type == 'set') { // eslint-disable-line + } else if (['generic', 'list', 'set'].indexOf(type) !== -1) { val = val.trim(); if (val === '') { val = null; @@ -174,55 +303,50 @@ function StudioEditableXBlock(runtime, element) { val = JSON.parse(val); // TODO: handle parse errors } return val; - } - /* eslint-disable */ - if (type == 'string' && ( - contextId == 'xb-field-edit-start_time' - || contextId == 'xb-field-edit-end_time')) { + } else if (type === 'string' && ( + contextId === 'xb-field-edit-start_time' + || contextId === 'xb-field-edit-end_time')) { return parseRelativeTime(val); + } else { + return val; } - /* eslint-disable */ - return val; }, removeEditor: function() { $field.tinymce().remove(); } }); - var fieldChanged = function() { - // Field value has been modified: - $wrapper.addClass('is-set'); - $resetButton.removeClass('inactive').addClass('active'); - }; - $field.bind('change input paste', fieldChanged); + $field.bind('change input paste', fieldChanged($wrapper, $resetButton)); $resetButton.click(function() { - $field.val($wrapper.attr('data-default')); // Use attr instead of data to force treating the default value as a string + // Use attr instead of data to force treating the default value as a string + $field.val($wrapper.attr('data-default')); $wrapper.removeClass('is-set'); $resetButton.removeClass('active').addClass('inactive'); // Remove all enabled default transcripts removeAllEnabledTranscripts(initialDefaultTranscriptsData, bindUploadListenerAvailableTranscript); }); - if (type == 'html' && tinyMceAvailable) { + if (type === 'html' && tinyMceAvailable) { tinyMCE.baseURL = baseUrl + '/js/vendor/tinymce/js/tinymce'; $field.tinymce({ theme: 'modern', skin: 'studio-tmce4', height: '200px', - formats: { code: { inline: 'code' } }, - codemirror: { path: '' + baseUrl + '/js/vendor' }, + formats: {code: {inline: 'code'}}, + codemirror: {path: '' + baseUrl + '/js/vendor'}, convert_urls: false, plugins: 'link codemirror', menubar: false, statusbar: false, toolbar_items_size: 'small', - toolbar: 'formatselect | styleselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink | code', + toolbar: 'formatselect | styleselect | bold italic underline forecolor wrapAsCode | bullist numlist' + + ' outdent indent blockquote | link unlink | code', resize: 'both', - setup : function(ed) { - ed.on('change', fieldChanged); + setup: function(ed) { + ed.on('change', fieldChanged($wrapper, $resetButton)); } }); } - if (type == 'datepicker' && datepickerAvailable) { + if (type === 'datepicker' && datepickerAvailable) { $field.datepicker('destroy'); $field.datepicker({dateFormat: 'm/d/yy'}); } @@ -230,15 +354,9 @@ function StudioEditableXBlock(runtime, element) { $(element).find('.wrapper-list-settings .list-set').each(function() { var $optionList = $(this); - var $checkboxes = $(this).find('input'); + var $checkboxes = $optionList.find('input'); var $wrapper = $optionList.closest('li'); var $resetButton = $wrapper.find('button.setting-clear'); - var fieldChanged = function() { - // Field value has been modified: - $wrapper.addClass('is-set'); - $resetButton.removeClass('inactive').addClass('active'); - }; - fields.push({ name: $wrapper.data('field-name'), isSet: function() { return $wrapper.hasClass('is-set'); }, @@ -247,13 +365,13 @@ function StudioEditableXBlock(runtime, element) { var val = []; $checkboxes.each(function() { if ($(this).is(':checked')) { - val.push(JSON.parse($(this).val())); + val.push(JSON.parse($optionList.val())); } }); return val; } }); - $checkboxes.bind('change input', fieldChanged); + $checkboxes.bind('change input', fieldChanged($wrapper, $resetButton)); $resetButton.click(function() { var defaults = JSON.parse($wrapper.attr('data-default')); @@ -268,37 +386,23 @@ function StudioEditableXBlock(runtime, element) { }); }); - /** - * Is there a more specific error message we can show? - * @param {String} responseText JSON received from ajax call - * @return {String} Error message extracted from input JSON or a portion of input text - */ - function extractErrorMessage(responseText) { - try { - message = JSON.parse(responseText).error; - if (typeof message === 'object' && message.messages) { - // e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc. - message = $.map(message.messages, function(msg) { return msg.text; }).join(', '); - } - return message; - } catch (error) { // SyntaxError thrown by JSON.parse - return responseText.substr(0, 300); - } - } /** Submit studio editor settings. */ - function studio_submit(data) { + function studioSubmit(data) { var handlerUrl = runtime.handlerUrl(element, 'submit_studio_edits'); + var message; runtime.notify('save', {state: 'start', message: gettext('Saving')}); $.ajax({ type: 'POST', url: handlerUrl, data: JSON.stringify(data), dataType: 'json', - global: false, // Disable Studio's error handling that conflicts with studio's notify('save') and notify('cancel') :-/ - success: function(response) { runtime.notify('save', {state: 'end'}); } + // Disable Studio's error handling that conflicts with studio's notify('save') and notify('cancel') :-/ + global: false, + success: function() { runtime.notify('save', {state: 'end'}); } }).fail(function(jqXHR) { - var message = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); + message = gettext('This may be happening because of an error with our server or your internet' + + ' connection. Try refreshing the page or making sure you are online.'); if (jqXHR.responseText) { // Is there a more specific error message we can show? message = extractErrorMessage(jqXHR.responseText); } @@ -308,54 +412,30 @@ function StudioEditableXBlock(runtime, element) { // Raccoongang changes $('.save-button', element).bind('click', function(event) { - var isValidated = validateTranscripts(event, $langChoiceItem); - if (isValidated) { - var values = fillValues(fields); - studio_submit(values); + if (validateTranscripts(event, $langChoiceItem)) { + studioSubmit(fillValues(fields)); } }); $(element).find('.cancel-button').bind('click', function(event) { // Remove TinyMCE instances to make sure jQuery does not try to access stale instances // when loading editor for another block: - for (var i in fields) { - var field = fields[i]; + fields.forEach(function(field) { if (field.hasEditor()) { field.removeEditor(); } - } + }); event.preventDefault(); runtime.notify('cancel', {}); }); // End of Raccoongang changes - // Raccoongang addons - var transcriptsValue = []; - var disabledLanguages = []; - var $fileUploader = $('.input-file-uploader', element); - var $defaultTranscriptUploader = $('.upload-default-transcript'); - var $defaultTranscriptRemover = $('.remove-default-transcript'); - var $standardTranscriptUploader = $('.add-transcript'); - var $standardTranscriptRemover = $('.remove-action'); - var $langChoiceItem = $('.language-transcript-selector', element); - var $videoApiAuthenticator = $('#video-api-authenticate', element); - var $3playmediaTranscriptsApi = $('#threeplaymedia-api-transcripts', element); - var gotTranscriptsValue = $('input[data-field-name="transcripts"]').val(); - var downloadTranscriptHandlerUrl = runtime.handlerUrl(element, 'download_transcript'); - var authenticateVideoApiHandlerUrl = runtime.handlerUrl(element, 'authenticate_video_api_handler'); - var uploadDefaultTranscriptHandlerUrl = runtime.handlerUrl(element, 'upload_default_transcript_handler'); - var getTranscripts3playmediaApiHandlerUrl = runtime.handlerUrl(element, 'get_transcripts_3playmedia_api_handler'); - var currentLanguageCode; - var currentLanguageLabel; - var initialDefaultTranscriptsData = getInitialDefaultTranscriptsData(); - var initialDefaultTranscripts = initialDefaultTranscriptsData[0]; - if (gotTranscriptsValue) { transcriptsValue = JSON.parse(gotTranscriptsValue); } transcriptsValue.forEach(function(transcriptValue) { - disabledLanguages.push(transcriptValue.lang) + disabledLanguages.push(transcriptValue.lang); }); /** @@ -367,17 +447,17 @@ function StudioEditableXBlock(runtime, element) { type: 'POST', url: getTranscripts3playmediaApiHandlerUrl, dataType: 'json', - data: JSON.stringify(data), + data: JSON.stringify(data) }; $.ajax(options) .done(function(response) { - var success_message = response['success_message']; - var error_message = response['error_message']; - if (success_message && response.transcripts) { - response.transcripts.forEach(function (item) { - includeLang = transcriptsValue.find(function (element) { - return element.lang == item.lang; + var errorMessage = response.error_message; + var successMessage = response.success_message; + if (successMessage && response.transcripts) { + response.transcripts.forEach(function(item) { + includeLang = transcriptsValue.find(function(element) { // eslint-disable-line no-shadow + return element.lang === item.lang; }); // Add a transcript from the 3playmedia only for non exists language if (!includeLang) { @@ -388,11 +468,11 @@ function StudioEditableXBlock(runtime, element) { }); } - if (success_message) { - message = success_message; + if (successMessage) { + message = successMessage; status = SUCCESS; } else { - message = error_message; + message = errorMessage; status = ERROR; } }) @@ -424,15 +504,14 @@ function StudioEditableXBlock(runtime, element) { dataType: 'json' }) .done(function(response) { - var error_message = response['error_message']; - var success_message = response['success_message']; - if (success_message) { - message = success_message; + var errorMessage = response.error_message; + var successMessage = response.success_message; + if (successMessage) { + message = successMessage; status = SUCCESS; showBackendSettings(); - } - else if (error_message) { - message = error_message; + } else if (errorMessage) { + message = errorMessage; status = ERROR; } }) @@ -455,52 +534,6 @@ function StudioEditableXBlock(runtime, element) { }); } - /** - * Upload a transcript available on a video platform to video xblock and update displayed default transcripts. - */ - function uploadDefaultTranscriptsToServer(data) { - var message, status; - - $.ajax({ - type: 'POST', - url: uploadDefaultTranscriptHandlerUrl, - data: JSON.stringify(data), - dataType: 'json' - }) - .done(function(response) { - var newLang = response['lang']; - var newLabel = response['label']; - var newUrl = response['url']; - // Create a standard transcript - pushTranscript(newLang, newLabel, newUrl, '', transcriptsValue); - pushTranscriptsValue(transcriptsValue); - // Add a default transcript to the list of enabled ones - var downloadUrl = downloadTranscriptHandlerUrl + '?' + newUrl; - var defaultTranscript= {'lang': newLang, 'label': newLabel, 'url': downloadUrl}; - createEnabledTranscriptBlock(defaultTranscript, downloadUrl); - bindRemovalListenerEnabledTranscript(newLang, newLabel, newUrl); - // Display status messages - // var error_message = response['error_message']; - message = response['success_message']; - status = SUCCESS; - }) - .fail(function(jqXHR) { - message = gettext('This may be happening because of an error with our server or your ' + - 'internet connection. Try refreshing the page or making sure you are online.'); - if (jqXHR.responseText) { // Is there a more specific error message we can show? - message += extractErrorMessage(jqXHR.responseText); - } - status = ERROR; - }) - .always(function() { - showStatus( - $('.api-response.upload-default-transcript.' + currentLanguageCode + '.status'), - status, - message - ); - }); - } - /** * Create a new transcript, containing valid data, after successful form submit. * @@ -515,22 +548,22 @@ function StudioEditableXBlock(runtime, element) { * currentLiTag (Object): DOM element containing information on an uploaded subtitle. */ function successHandler(event, response, statusText, xhr, fieldName, lang, label, currentLiTag) { - var url = '/' + response['asset']['id']; + var url = '/' + response.asset.id; // User can upload a file without extension var filename = $fileUploader[0].files[0].name; var downloadUrl = downloadTranscriptHandlerUrl + '?' + url; - var successMessage = 'File "' + filename + '" uploaded successfully'; + var successMessage = gettext('File "{filename}" uploaded successfully').replace('{filename}', filename); var $parentDiv; var downloadUrlServer; var defaultTranscript; var isValidated = validateTranscriptFile(event, fieldName, filename, $fileUploader); - if (fieldName == 'handout' && isValidated) { + if (fieldName === 'handout' && isValidated) { $parentDiv = $('.file-uploader'); $('.download-setting', $parentDiv).attr('href', downloadUrl).removeClass('is-hidden'); $('a[data-change-field-name=' + fieldName + ']').text('Replace'); - displayStatusCaptions(SUCCESS, successMessage, $parentDiv); + showStatus($('.status', $parentDiv), SUCCESS, successMessage); $('input[data-field-name=' + fieldName + ']').val(url).change(); - } else if (fieldName == 'transcripts' && isValidated) { + } else if (fieldName === 'transcripts' && isValidated) { pushTranscript(lang, label, url, '', transcriptsValue); $('.add-transcript').removeClass('is-disabled'); $('input[data-field-name=' + fieldName + ']').val(JSON.stringify(transcriptsValue)).change(); @@ -538,13 +571,21 @@ function StudioEditableXBlock(runtime, element) { $(currentLiTag).find('.download-transcript') .removeClass('is-hidden') .attr('href', downloadUrl); - displayStatusTranscripts(SUCCESS, successMessage, currentLiTag); + showStatus( + $('.status', $(currentLiTag)), + SUCCESS, + successMessage + ); // Affect default transcripts: update a respective enabled transcript with an external url // of a newly created standard transcript downloadUrlServer = $('.list-settings-buttons .upload-setting.upload-transcript[data-lang-code=' + lang + ']') .siblings('a.download-transcript.download-setting').attr('href'); - defaultTranscript = {'lang': lang, 'label': label, 'url': downloadUrlServer}; + defaultTranscript = { + lang: lang, + label: label, + url: downloadUrlServer + }; createEnabledTranscriptBlock(defaultTranscript, downloadUrl); bindRemovalListenerEnabledTranscript(lang, label, downloadUrl); } @@ -556,71 +597,43 @@ function StudioEditableXBlock(runtime, element) { }); } - /** - * Bind upload listener to a newly created available transcript. - */ - function bindUploadListenerAvailableTranscript(langCode, langLabel) { - var $uploadElement = $('.default-transcripts-action-link.upload-default-transcript[data-lang-code=' + langCode + ']'); - $uploadElement.click(function () { - // Get url for a transcript fetching from the API - var downloadUrlApi = getTranscriptUrl(initialDefaultTranscripts, langCode); - var defaultTranscript = {'lang': langCode, 'label': langLabel, 'url': downloadUrlApi}; - uploadDefaultTranscriptsToServer(defaultTranscript); - // Affect standard transcripts - createTranscriptBlock(langCode, langLabel, transcriptsValue, downloadTranscriptHandlerUrl); - }); - } - - /** - * Bind removal listener to a newly created enabled transcript. - */ - function bindRemovalListenerEnabledTranscript(langCode, langLabel, downloadUrlServer) { - var $removeElement = $('.default-transcripts-action-link.remove-default-transcript[data-lang-code=' + langCode + ']'); - $removeElement.click(function(event) { - var defaultTranscript = {'lang' : langCode, 'label' : langLabel, 'url': downloadUrlServer}; - // Affect default transcripts - removeEnabledTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); - createAvailableTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); - bindUploadListenerAvailableTranscript(langCode, langLabel); - // Affect standard transcripts - removeStandardTranscriptBlock(langCode, transcriptsValue, disabledLanguages); - disableOption($langChoiceItem, disabledLanguages); - event.preventDefault(); - }); - } - /** * Wrap standard transcript removal sequence for it to be re-used. */ function standardTranscriptRemovalWrapper(event) { - // Affect standard transcripts - removeTranscriptBlock(event, transcriptsValue, disabledLanguages); - disableOption($langChoiceItem, disabledLanguages); // Affect default transcripts var $currentBlock = $(event.currentTarget).closest('li'); var lang = $currentBlock.find('option:selected').val(); var label = $currentBlock.find('option:selected').attr('data-lang-label'); - var defaultTranscript = {'lang' : lang, 'label' : label, 'url': ''}; + var defaultTranscript = { + lang: lang, + label: label, + url: '' + }; + // Affect standard transcripts + removeTranscriptBlock(event, transcriptsValue, disabledLanguages); + disableOption($langChoiceItem, disabledLanguages); removeEnabledTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); createAvailableTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); bindUploadListenerAvailableTranscript(lang, label); } $fileUploader.on('change', function(event) { + var $currentTarget = $(event.currentTarget); + var fieldName = $currentTarget.attr('data-change-field-name'); + var lang = $currentTarget.attr('data-lang-code'); + var label = $currentTarget.attr('data-lang-label'); + var currentLiTag = $('.language-transcript-selector') + .children()[parseInt($currentTarget.attr('data-li-index'), 10)]; if (!$fileUploader.val()) { return; } - var fieldName = $(event.currentTarget).attr('data-change-field-name'); - var lang = $(event.currentTarget).attr('data-lang-code'); - var label = $(event.currentTarget).attr('data-lang-label'); - var currentLiIndex = $(event.currentTarget).attr('data-li-index'); - var currentLiTag = $('.language-transcript-selector').children()[parseInt(currentLiIndex)]; $('.upload-setting', element).addClass('is-disabled'); $('.file-uploader-form', element).ajaxSubmit({ success: function(response, statusText, xhr) { - successHandler(event, response, statusText, xhr, fieldName, lang, label, currentLiTag) + successHandler(event, response, statusText, xhr, fieldName, lang, label, currentLiTag); }, - error: function(jqXHR, textStatus, errorThrown) { + error: function(jqXHR, textStatus) { runtime.notify('error', {title: gettext('Unable to update settings'), message: textStatus}); } }); @@ -628,18 +641,18 @@ function StudioEditableXBlock(runtime, element) { }); $videoApiAuthenticator.on('click', function(event) { + var $data = $('.token', element).val(); event.preventDefault(); event.stopPropagation(); - var $data = $('.token', element).val(); authenticateVideoApi($data); }); $3playmediaTranscriptsApi.on('click', function(event) { + var $apiKey = $('.threeplaymedia-api-key', element).val(); + var $fileId = $('#xb-field-edit-threeplaymedia_file_id', element).val(); event.preventDefault(); event.stopPropagation(); - var $api_key = $('.threeplaymedia-api-key', element).val(); - var $file_id = $('#xb-field-edit-threeplaymedia_file_id', element).val(); - getTranscripts3playmediaApi({api_key: $api_key, file_id: $file_id}); + getTranscripts3playmediaApi({api_key: $apiKey, file_id: $fileId}); }); $('.lang-select').on('change', function(event) { @@ -652,7 +665,7 @@ function StudioEditableXBlock(runtime, element) { clickUploader(event, $fileUploader); }); - $('.setting-clear').on('click', function (event) { + $('.setting-clear').on('click', function(event) { var $currentBlock = $(event.currentTarget).closest('li'); if ($('.file-uploader', $currentBlock).length > 0) { $('.upload-setting', $currentBlock).text('Upload'); @@ -666,44 +679,53 @@ function StudioEditableXBlock(runtime, element) { event.preventDefault(); $(event.currentTarget).addClass('is-disabled'); $templateItem.removeClass('is-hidden').appendTo($langChoiceItem); - $('.upload-transcript', $templateItem).on('click', function(event) { + $('.upload-transcript', $templateItem).on('click', function(event) { // eslint-disable-line no-shadow clickUploader(event, $fileUploader); }); - $('.lang-select', $templateItem).on('change', function(event) { + $('.lang-select', $templateItem).on('change', function(event) { // eslint-disable-line no-shadow languageChecker(event, transcriptsValue, disabledLanguages); disableOption($langChoiceItem, disabledLanguages); pushTranscriptsValue(transcriptsValue); }); // Bind a listener - $('.remove-action').on('click', function(event) { + $('.remove-action').on('click', function(event) { // eslint-disable-line no-shadow standardTranscriptRemovalWrapper(event); }); - }); + }); $standardTranscriptRemover.click(function(event) { standardTranscriptRemovalWrapper(event); }); $defaultTranscriptUploader.click(function(event) { + var $currentTarget = $(event.currentTarget); + var langCode = $currentTarget.attr('data-lang-code'); + var label = $currentTarget.attr('data-lang-label'); + var url = $currentTarget.attr('data-download-url'); + var defaultTranscript = { + lang: langCode, + label: label, + url: url + }; event.preventDefault(); event.stopPropagation(); - var langCode = $(event.currentTarget).attr('data-lang-code'); - var label = $(event.currentTarget).attr('data-lang-label'); - var url = $(event.currentTarget).attr('data-download-url'); currentLanguageCode = langCode; - currentLanguageLabel = label; - var defaultTranscript = {'lang': langCode, 'label' : label, 'url' : url}; // Affect default transcripts uploadDefaultTranscriptsToServer(defaultTranscript); // Affect standard transcripts - createTranscriptBlock(langCode, label, transcriptsValue, downloadTranscriptHandlerUrl) + createTranscriptBlock(langCode, label, transcriptsValue, downloadTranscriptHandlerUrl); }); $defaultTranscriptRemover.click(function(event) { - var langCode = $(event.currentTarget).attr('data-lang-code'); - var langLabel = $(event.currentTarget).attr('data-lang-label'); - var downloadUrl = $(event.currentTarget).attr('data-download-url'); - var defaultTranscript = {'lang' : langCode, 'label' : langLabel, 'url': downloadUrl}; + var $currentTarget = $(event.currentTarget); + var langCode = $currentTarget.attr('data-lang-code'); + var langLabel = $currentTarget.attr('data-lang-label'); + var downloadUrl = $currentTarget.attr('data-download-url'); + var defaultTranscript = { + lang: langCode, + label: langLabel, + url: downloadUrl + }; // Affect default transcripts removeEnabledTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); createAvailableTranscriptBlock(defaultTranscript, initialDefaultTranscriptsData); @@ -714,13 +736,9 @@ function StudioEditableXBlock(runtime, element) { event.preventDefault(); }); - $defaultTranscriptsSwitcher.change(function(){ - noEnabledTranscript = !$('.enabled-default-transcripts-section:visible').length; - noAvailableTranscript = !$('.available-default-transcripts-section:visible').length; - // Hide label of enabled default transcripts block if no transcript is enabled on video xblock, and vice versa - setDisplayDefaultTranscriptsLabel(noEnabledTranscript, $enabledLabel); - // Hide label of available default transcripts block if no transcript is available on a platform, and vice versa - setDisplayDefaultTranscriptsLabel(noAvailableTranscript, $availableLabel); + $defaultTranscriptsSwitcher.change(function() { + $enabledLabel.toggleClass('is-hidden', $('.enabled-default-transcripts-section:visible').length); + $availableLabel.toggleClass('is-hidden', $('.available-default-transcripts-section:visible').length); }); // End of Raccoongang addons } diff --git a/video_xblock/static/js/studio-edit-transcripts-autoload.js b/video_xblock/static/js/studio-edit/transcripts-autoload.js similarity index 63% rename from video_xblock/static/js/studio-edit-transcripts-autoload.js rename to video_xblock/static/js/studio-edit/transcripts-autoload.js index 42a61134..ffbb8a05 100644 --- a/video_xblock/static/js/studio-edit-transcripts-autoload.js +++ b/video_xblock/static/js/studio-edit/transcripts-autoload.js @@ -10,29 +10,14 @@ function createStatusMessageElement(langCode, actionSelector) { var parentSelector = ''; var messageSelector = '.api-response.' + actionSelector + '.' + langCode + '.status'; - var $messageUpload; if (actionSelector === 'upload-default-transcript') { - parentSelector = 'available-default-transcripts-section'; + parentSelector = '.available-default-transcripts-section:visible'; } else if (actionSelector === 'remove-default-transcript') { - parentSelector = 'enabled-default-transcripts-section'; + parentSelector = '.enabled-default-transcripts-section:visible'; } if ($(messageSelector).length === 0) { - $messageUpload = $('
', - {class: messageSelector}); - $messageUpload.appendTo($('.' + parentSelector + ':visible').last()); - } -} - -/** - * Manage default transcripts labels display depending on enabled/available subs presence. - */ -function setDisplayDefaultTranscriptsLabel(isNotDisplayedDefaultSub, labelElement) { - 'use strict'; - if (isNotDisplayedDefaultSub) { - labelElement.addClass('is-hidden'); - } else { - labelElement.removeClass('is-hidden'); + $('
', {class: messageSelector}).appendTo($(parentSelector).last()); } } @@ -41,19 +26,20 @@ function setDisplayDefaultTranscriptsLabel(isNotDisplayedDefaultSub, labelElemen */ function getInitialDefaultTranscriptsData() { 'use strict'; - var $defaultSubs = $('.initial-default-transcript'); var initialDefaultTranscripts = []; var langCodes = []; var langCode; var langLabel; var downloadUrl; - var newSub; - $defaultSubs.each(function() { + $('.initial-default-transcript').each(function() { langCode = $(this).attr('data-lang-code'); langLabel = $(this).attr('data-lang-label'); downloadUrl = $(this).attr('data-download-url'); - newSub = {lang: langCode, label: langLabel, url: downloadUrl}; - initialDefaultTranscripts.push(newSub); + initialDefaultTranscripts.push({ + lang: langCode, + label: langLabel, + url: downloadUrl + }); langCodes.push(langCode); }); return [initialDefaultTranscripts, langCodes]; @@ -63,10 +49,8 @@ function getInitialDefaultTranscriptsData() { function getDefaultTranscriptsArray(defaultTranscriptType) { 'use strict'; var defaultTranscriptsArray = []; - var code; $('.' + defaultTranscriptType + '-default-transcripts-section .default-transcripts-label:visible').each(function() { - code = $(this).attr('value'); - defaultTranscriptsArray.push(code); + defaultTranscriptsArray.push($(this).attr('value')); }); return defaultTranscriptsArray; } @@ -78,31 +62,26 @@ function createAvailableTranscriptBlock(defaultTranscript, initialDefaultTranscr var langLabel = defaultTranscript.label; var initialDefaultTranscripts = initialDefaultTranscriptsData[0]; var initialDefaultTranscriptsLangCodes = initialDefaultTranscriptsData[1]; - // Get all the currently available transcripts - var allAvailableTranscripts = getDefaultTranscriptsArray('available'); // Create a new available transcript if stored on a platform and doesn't already exist on video xblock - var isNotDisplayedAvailableTranscript = $.inArray(langCode, allAvailableTranscripts) === -1; + var isNotDisplayedAvailableTranscript = $.inArray(langCode, getDefaultTranscriptsArray('available')) === -1; var isStoredVideoPlatform = $.inArray(langCode, initialDefaultTranscriptsLangCodes) !== -1; - var $availableLabel; - var isHiddenAvailableLabel; - var $newAvailableTranscriptBlock; - var downloadUrlApi; if (isNotDisplayedAvailableTranscript && isStoredVideoPlatform) { - // Show label of available transcripts if no such label is displayed - $availableLabel = $('div.custom-field-section-label.available-transcripts'); - isHiddenAvailableLabel = !$('div.custom-field-section-label.available-transcripts:visible').length; - if (isHiddenAvailableLabel) { $availableLabel.removeClass('is-hidden'); } + $('div.custom-field-section-label.available-transcripts').removeClass('is-hidden'); // Create a default (available) transcript block - $newAvailableTranscriptBlock = $('.available-default-transcripts-section:hidden').clone(); - $newAvailableTranscriptBlock.removeClass('is-hidden').appendTo($('.default-transcripts-wrapper')); + $('.available-default-transcripts-section:hidden') + .clone() + .removeClass('is-hidden') + .appendTo($('.default-transcripts-wrapper')); $('.default-transcripts-label:visible').last() .attr('value', langCode) .text(langLabel); - // Get url for a transcript fetching from the API - downloadUrlApi = getTranscriptUrl(initialDefaultTranscripts, langCode); // External url for API call // Update attributes $('.default-transcripts-action-link.upload-default-transcript').last() - .attr({'data-lang-code': langCode, 'data-lang-label': langLabel, 'data-download-url': downloadUrlApi}); + .attr({ + 'data-lang-code': langCode, + 'data-lang-label': langLabel, + 'data-download-url': getTranscriptUrl(initialDefaultTranscripts, langCode) + }); // Create elements to display status messages on available transcript upload createStatusMessageElement(langCode, 'upload-default-transcript'); } @@ -120,12 +99,7 @@ function createEnabledTranscriptBlock(defaultTranscript, downloadUrlServer) { 'use strict'; var langCode = defaultTranscript.lang; var langLabel = defaultTranscript.label; - var $availableTranscriptBlock = $('div[value=' + langCode + ']') - .closest('div.available-default-transcripts-section:visible'); var $enabledLabel = $('div.custom-field-section-label.enabled-transcripts'); - var $availableLabel = $('div.custom-field-section-label.available-transcripts'); - var allEnabledTranscripts; - var isNotDisplayedEnabledTranscript; var isHiddenEnabledLabel; var $newEnabledTranscriptBlock; var $lastEnabledTranscriptBlock; @@ -133,25 +107,18 @@ function createEnabledTranscriptBlock(defaultTranscript, downloadUrlServer) { var $parentElement; var $insertedEnabledTranscriptBlock; var $insertedEnabledTranscriptLabel; - var $downloadElement; - var $removeElement; - var areNotVisibleAvailableTranscripts; // Remove a transcript of choice from the list of available ones - $availableTranscriptBlock.remove(); + $('div[value=' + langCode + ']').closest('div.available-default-transcripts-section:visible').remove(); // Hide label of available transcripts if no such items left and if default transcripts are shown - areNotVisibleAvailableTranscripts = !$('div.available-default-transcripts-section:visible').length; - if (areNotVisibleAvailableTranscripts) { - $availableLabel.addClass('is-hidden'); - } - // Get all the currently enabled transcripts - allEnabledTranscripts = getDefaultTranscriptsArray('enabled'); + $('div.custom-field-section-label.available-transcripts').addClass('is-hidden'); // Create a new enabled transcript if it doesn't already exist in a video xblock - isNotDisplayedEnabledTranscript = $.inArray(langCode, allEnabledTranscripts) === -1; - if (isNotDisplayedEnabledTranscript) { + if ($.inArray(langCode, getDefaultTranscriptsArray('enabled')) === -1) { // Display label of enabled transcripts if hidden isHiddenEnabledLabel = $('div.custom-field-section-label.enabled-transcripts').hasClass('is-hidden'); - if (isHiddenEnabledLabel) { $enabledLabel.removeClass('is-hidden'); } + if (isHiddenEnabledLabel) { + $enabledLabel.removeClass('is-hidden'); + } // Create a default (enabled) transcript block $newEnabledTranscriptBlock = $('.enabled-default-transcripts-section:hidden').clone(); // Insert a new default transcript block @@ -169,14 +136,11 @@ function createEnabledTranscriptBlock(defaultTranscript, downloadUrlServer) { $insertedEnabledTranscriptLabel = $insertedEnabledTranscriptBlock.find('.default-transcripts-label'); $insertedEnabledTranscriptLabel.attr('value', langCode).text(langLabel); - $downloadElement = $insertedEnabledTranscriptBlock - .find('.default-transcripts-action-link.download-transcript.download-setting'); - $downloadElement.attr( - {'data-lang-code': langCode, 'data-lang-label': langLabel, href: downloadUrlServer} + $insertedEnabledTranscriptBlock.find('.default-transcripts-action-link.download-transcript.download-setting') + .attr({'data-lang-code': langCode, 'data-lang-label': langLabel, href: downloadUrlServer} ); - $removeElement = $insertedEnabledTranscriptBlock - .find('.default-transcripts-action-link.remove-default-transcript'); - $removeElement.attr({'data-lang-code': langCode, 'data-lang-label': langLabel}); + $insertedEnabledTranscriptBlock.find('.default-transcripts-action-link.remove-default-transcript') + .attr({'data-lang-code': langCode, 'data-lang-label': langLabel}); } } @@ -186,27 +150,20 @@ function removeEnabledTranscriptBlock(enabledTranscript, initialDefaultTranscrip var langCode = enabledTranscript.lang; var langLabel = enabledTranscript.label; var initialDefaultTranscriptsLangCodes = initialDefaultTranscriptsData[1]; - // Remove enabled transcript of choice - var $enabledTranscriptBlock = $('div[value=' + langCode + ']').closest('div.enabled-default-transcripts-section'); - var $enabledLabel = $('div.custom-field-section-label.enabled-transcripts'); - var allEnabledTranscripts; var isSuccessfulRemoval; var isStoredVideoPlatform; - var isNotPresentEnabledTranscripts; - var message, status; + var message; + var status; var SUCCESS = 'success'; var ERROR = 'error'; - $enabledTranscriptBlock.remove(); - isNotPresentEnabledTranscripts = !$('div.enabled-default-transcripts-section:visible').length; + $('div[value=' + langCode + ']').closest('div.enabled-default-transcripts-section').remove(); // Hide label of enabled transcripts if no such items left - if (isNotPresentEnabledTranscripts) { - $enabledLabel.addClass('is-hidden'); + if (!$('div.enabled-default-transcripts-section:visible').length) { + $('div.custom-field-section-label.enabled-transcripts').addClass('is-hidden'); } // Create elements to display status messages on enabled transcript removal createStatusMessageElement(langCode, 'remove-default-transcript'); - // Get all the currently enabled transcripts - allEnabledTranscripts = getDefaultTranscriptsArray('enabled'); - isSuccessfulRemoval = $.inArray(langCode, allEnabledTranscripts) === -1; // Is not in array + isSuccessfulRemoval = $.inArray(langCode, getDefaultTranscriptsArray('enabled')) === -1; // Is not in array isStoredVideoPlatform = $.inArray(langCode, initialDefaultTranscriptsLangCodes) !== -1; // Is in array // Display message with results of removal if (isSuccessfulRemoval && isStoredVideoPlatform) { diff --git a/video_xblock/static/js/studio-edit-transcripts-manual-upload.js b/video_xblock/static/js/studio-edit/transcripts-manual-upload.js similarity index 88% rename from video_xblock/static/js/studio-edit-transcripts-manual-upload.js rename to video_xblock/static/js/studio-edit/transcripts-manual-upload.js index 86e09087..e7d5bc82 100644 --- a/video_xblock/static/js/studio-edit-transcripts-manual-upload.js +++ b/video_xblock/static/js/studio-edit/transcripts-manual-upload.js @@ -3,30 +3,6 @@ * Transcripts and captions often share the same logic. */ -/** - * Display message with results of a performed action with captions. - */ -function displayStatusCaptions(statusType, statusMessage, $parentDiv) { - 'use strict'; - showStatus( - $('.status', $parentDiv), - statusType, - statusMessage - ); -} - -/** - * Display message with results of a performed action with transcripts. - */ -function displayStatusTranscripts(statusType, statusMessage, currentLiTag) { - 'use strict'; - showStatus( - $('.status', $(currentLiTag)), - statusType, - statusMessage - ); -} - /** * Ensure transcript text's timing has two-digits. * By default max value of RelativeTime field on Backend is 23:59:59, that is 86399 seconds. @@ -108,14 +84,12 @@ function validateTranscriptFile(event, fieldName, filename, $fileUploader) { var fileExtension = filename.split('.').pop(); var fileSize = $fileUploader[0].files[0].size; var acceptedFormats = $fileUploader[0].accept || '.vtt .srt'; - var isEmptyExtension = fileExtension === ''; var isNotAcceptedExtension = acceptedFormats.indexOf(fileExtension) === -1; - var isNotAcceptedFormat = isEmptyExtension || isNotAcceptedExtension; + var isNotAcceptedFormat = fileExtension === '' || isNotAcceptedExtension; // The maximum file size allowed is 300 KB. Tripple size of LoTR subtitles var maxFileSize = 307200; var isNotAcceptedSize = fileSize > maxFileSize; var errorMessage = 'Couldn\'t upload "' + filename + '". '; - var $parentDiv; var currentLiIndex; var currentLiTag; var isValid = true; @@ -132,12 +106,15 @@ function validateTranscriptFile(event, fieldName, filename, $fileUploader) { // Display validation error message if a transcript/caption file may not be not accepted if (!isValid) { if (fieldName === 'handout') { - $parentDiv = $('.file-uploader'); - displayStatusCaptions('error', errorMessage, $parentDiv); + showStatus($('.file-uploader .status'), 'error', errorMessage); } else { currentLiIndex = $(event.currentTarget).attr('data-li-index'); currentLiTag = $('.language-transcript-selector').children()[parseInt(currentLiIndex, 10)]; - displayStatusTranscripts('error', errorMessage, currentLiTag); + showStatus( + $(currentLiTag).find($('.status')), + 'error', + errorMessage + ); } } @@ -209,11 +186,7 @@ function removeTranscript(lang, transcriptsValue) { function disableOption($langChoiceItem, disabledLanguages) { 'use strict'; $langChoiceItem.find('option').each(function() { - if (disabledLanguages.indexOf($(this).val()) > -1) { - $(this).attr('disabled', true); - } else { - $(this).attr('disabled', false); - } + $(this).attr('disabled', disabledLanguages.indexOf($(this).val()) > -1); }); } @@ -223,7 +196,7 @@ function disableOption($langChoiceItem, disabledLanguages) { function pushTranscriptsValue(transcriptsValue) { 'use strict'; transcriptsValue.forEach(function(transcriptValue, index) { - if (transcriptValue.lang === '' || transcriptValue.label === '' || transcriptValue.url === '') { + if ([transcriptValue.lang, transcriptValue.label, transcriptValue.url].indexOf('') !== -1) { transcriptsValue.splice(index, 1); } }); @@ -235,24 +208,20 @@ function pushTranscriptsValue(transcriptsValue) { */ function createTranscriptBlock(langCode, langLabel, transcriptsValue, downloadTranscriptHandlerUrl) { 'use strict'; - var $createdOption; var $createdLi; - var $createdUploadReplace; var $createdDownload; var externalResourceUrl; var externalDownloadUrl; // Create a transcript block if not already displayed $('.add-transcript').trigger('click'); // Select language option - $createdOption = $('li.list-settings-item:visible select').last(); - $createdOption.val(langCode); + $('li.list-settings-item:visible select').last().val(langCode); $createdLi = $('li.list-settings-item:visible').last(); // Update language label $createdLi.val(langLabel); - $createdUploadReplace = $createdLi.find('a.upload-setting.upload-transcript:hidden'); - $createdUploadReplace + $createdLi.find('a.upload-setting.upload-transcript:hidden') .removeClass('is-hidden') - .html('Replace') + .html(gettext('Replace')) .attr({'data-lang-code': langCode, 'data-lang-label': langLabel}); $createdDownload = $createdLi.find('a.download-transcript.download-setting:hidden'); $createdDownload.removeClass('is-hidden'); diff --git a/video_xblock/static/js/studio-edit-utils.js b/video_xblock/static/js/studio-edit/utils.js similarity index 100% rename from video_xblock/static/js/studio-edit-utils.js rename to video_xblock/static/js/studio-edit/utils.js diff --git a/video_xblock/static/js/transcript-download.js b/video_xblock/static/js/transcript-download.js deleted file mode 100644 index d034a6e4..00000000 --- a/video_xblock/static/js/transcript-download.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * This part is responsible for downloading of transcripts and captions in LMS and CMS. - */ - -domReady(function() { - 'use strict'; - videojs(window.videoPlayerId).ready(function() { - var player = this; - var transcripts = window.playerStateObj.transcripts_object; - var xblockUsageId = window.location.hash.slice(1); - /** Get transcript url for current caption language */ - var getDownloadTranscriptUrl = function() { - var downloadTranscriptUrl; - if (transcripts[player.captionsLanguage]) { - downloadTranscriptUrl = transcripts[player.captionsLanguage].url; - } - return downloadTranscriptUrl; - }; - var sendMessage = function() { - parent.postMessage({ - action: 'downloadTranscriptChanged', - downloadTranscriptUrl: getDownloadTranscriptUrl(), - xblockUsageId: xblockUsageId - }, document.location.protocol + '//' + document.location.host); - }; - if (!transcripts[player.captionsLanguage]) { - player.captionsEnabled = player.transcriptsEnabled = false; - // Need to trigger two events to disable active buttons in control bar - player.trigger('transcriptdisabled'); - player.trigger('captiondisabled'); - } - player.on('captionstrackchange', sendMessage); - }); -}); diff --git a/video_xblock/static/js/brightcove-videojs-init.js b/video_xblock/static/js/videojs/brightcove-videojs-init.js similarity index 100% rename from video_xblock/static/js/brightcove-videojs-init.js rename to video_xblock/static/js/videojs/brightcove-videojs-init.js diff --git a/video_xblock/static/js/player-context-menu.js b/video_xblock/static/js/videojs/player-context-menu.js similarity index 100% rename from video_xblock/static/js/player-context-menu.js rename to video_xblock/static/js/videojs/player-context-menu.js diff --git a/video_xblock/static/js/toggle-button.js b/video_xblock/static/js/videojs/toggle-button.js similarity index 100% rename from video_xblock/static/js/toggle-button.js rename to video_xblock/static/js/videojs/toggle-button.js diff --git a/video_xblock/static/js/video-speed.js b/video_xblock/static/js/videojs/video-speed.js similarity index 100% rename from video_xblock/static/js/video-speed.js rename to video_xblock/static/js/videojs/video-speed.js diff --git a/video_xblock/static/js/videojs_event_plugin.js b/video_xblock/static/js/videojs/videojs-event-plugin.js similarity index 98% rename from video_xblock/static/js/videojs_event_plugin.js rename to video_xblock/static/js/videojs/videojs-event-plugin.js index 0de5fec0..8f3c2f5a 100644 --- a/video_xblock/static/js/videojs_event_plugin.js +++ b/video_xblock/static/js/videojs/videojs-event-plugin.js @@ -128,7 +128,7 @@ onShowLanguageMenu, onHideLanguageMenu, onShowTranscript, onHideTranscript, onShowCaptions, onHideCaptions */ this.log = function(eventName, data) { - var xblockUsageId = window.location.hash.slice(1); + var xblockUsageId = getXblockUsageId(); data = data || {}; // eslint-disable-line no-param-reassign data.eventType = 'xblock-video.' + eventName; // eslint-disable-line no-param-reassign parent.postMessage({ diff --git a/video_xblock/static/js/videojs-speed-handler.js b/video_xblock/static/js/videojs/videojs-speed-handler.js similarity index 100% rename from video_xblock/static/js/videojs-speed-handler.js rename to video_xblock/static/js/videojs/videojs-speed-handler.js diff --git a/video_xblock/static/js/videojs-tabindex.js b/video_xblock/static/js/videojs/videojs-tabindex.js similarity index 100% rename from video_xblock/static/js/videojs-tabindex.js rename to video_xblock/static/js/videojs/videojs-tabindex.js diff --git a/video_xblock/static/js/videojs-transcript.js b/video_xblock/static/js/videojs/videojs-transcript.js similarity index 100% rename from video_xblock/static/js/videojs-transcript.js rename to video_xblock/static/js/videojs/videojs-transcript.js diff --git a/video_xblock/static/video_xblock_karma.conf.js b/video_xblock/static/video_xblock_karma.conf.js index bb228cca..8054c647 100644 --- a/video_xblock/static/video_xblock_karma.conf.js +++ b/video_xblock/static/video_xblock_karma.conf.js @@ -18,7 +18,6 @@ module.exports = function (config) { 'js/base.js', 'vendor/js/video.min.js', 'js/spec/test_context.js', - 'js/player_state.js', 'js/spec/*_spec.js' ], plugins: [ diff --git a/video_xblock/tests/test_backends.py b/video_xblock/tests/test_backends.py index 0320f4d4..99c49a9d 100644 --- a/video_xblock/tests/test_backends.py +++ b/video_xblock/tests/test_backends.py @@ -77,7 +77,7 @@ def test_get_player_html(self): 'label': 'English', 'url': 'http://test.url' }], - 'current_time': '' + 'currentTime': '' }, 'url': 'https://example.com/video.mp4', 'start_time': '', @@ -89,39 +89,39 @@ def test_get_player_html(self): self.assertIn('window.videojs', res.body) expected_basic_fields = [ - ('display_name', 'href'), - ('display_name', 'href', 'account_id'), - ('display_name', 'href'), - ('display_name', 'href'), - ('display_name', 'href'), + ['display_name', 'href'], + ['display_name', 'href', 'account_id'], + ['display_name', 'href'], + ['display_name', 'href'], + ['display_name', 'href'], ] expected_advanced_fields = [ - ( + [ # Youtube 'start_time', 'end_time', 'handout', 'transcripts', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'default_transcripts', 'download_video_allowed', 'download_video_url' - ), - ( - 'player_id', 'start_time', 'end_time', 'handout', 'transcripts', + ], + [ # Brightcove + 'player_id', 'start_time', 'end_time', 'handout', 'transcripts', 'token', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'default_transcripts', 'download_video_allowed', 'download_video_url' - ), - ( - 'start_time', 'end_time', 'handout', 'transcripts', + ], + [ # Wistia + 'start_time', 'end_time', 'handout', 'transcripts', 'token', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'default_transcripts', 'download_video_allowed', 'download_video_url' - ), - ( + ], + [ # Vimeo 'start_time', 'end_time', 'handout', 'transcripts', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'default_transcripts', 'download_video_allowed', 'download_video_url' - ), - ( + ], + [ # Html5 'start_time', 'end_time', 'handout', 'transcripts', 'threeplaymedia_file_id', 'threeplaymedia_apikey', 'download_transcript_allowed', 'download_video_allowed', - ), + ], ] @data(*zip(backends, expected_basic_fields, expected_advanced_fields)) @@ -131,8 +131,8 @@ def test_basic_advanced_fields(self, backend, expected_basic_fields, expected_ad Test basic_fields & advanced_fields for {0} backend """ player = self.player[backend](self.xblock) - self.assertTupleEqual(player.basic_fields, expected_basic_fields) - self.assertTupleEqual(player.advanced_fields, expected_advanced_fields) + self.assertListEqual(player.basic_fields, expected_basic_fields) + self.assertListEqual(player.advanced_fields, expected_advanced_fields) @data( ([{'lang': 'ru'}], [{'lang': 'en'}, {'lang': 'uk'}]), @@ -173,36 +173,49 @@ def test_get_transcript_language_parameters(self, lng_abbr, lng_name): 'https://example.com/sample.mp4' ] media_urls = [ - 'https://www.youtube.com/watch?v=44zaxzFsthY', - 'https://studio.brightcove.com/products/videocloud/media/videos/45263567468485', - 'https://wi.st/medias/HRrr784kH8932Z', - 'https://vimeo.com/202889234', - 'https://example.com/sample.mp4', + [ # Youtube + 'https://www.youtube.com/watch?v=44zaxzFsthY' + ], + [ # Brightcove + 'https://studio.brightcove.com/products/videocloud/media/videos/45263567468485', + 'https://studio.brightcove.com/products/videos/45263567468485', + ], + [ # Wistia + 'https://wi.st/medias/HRrr784kH8932Z' + ], + [ # Vimeo + 'https://vimeo.com/202889234' + ], + [ # Html5 + 'https://example.com/sample.mp4' + ], ] @data(*zip(backends, media_urls, media_ids)) @unpack - def test_media_id(self, backend, url, expected_media_id): + def test_media_id(self, backend, urls, expected_media_id): """ Check that media id is extracted from the video url for {0} backend """ - player = self.player[backend](self.xblock) - res = player.media_id(url) - self.assertEqual(res, expected_media_id) + for url in urls: + player = self.player[backend](self.xblock) + res = player.media_id(url) + self.assertEqual(res, expected_media_id) @data(*zip(backends, media_urls)) @unpack - def test_match(self, backend, url): + def test_match(self, backend, urls): """ Check if provided video `href` validates in right way for {0} backend """ - player = self.player[backend] - res = player.match(url) - self.assertTrue(bool(res)) + for url in urls: + player = self.player[backend] + res = player.match(url) + self.assertTrue(bool(res)) - # test wrong data - res = player.match('http://wrong.url') - self.assertFalse(bool(res)) + # test wrong data + res = player.match('http://wrong.url') + self.assertFalse(bool(res)) @data(*zip(backends, ['some_token'] * len(backends), auth_mocks)) @unpack diff --git a/video_xblock/tests/test_utils.py b/video_xblock/tests/test_utils.py new file mode 100644 index 00000000..f79ffde1 --- /dev/null +++ b/video_xblock/tests/test_utils.py @@ -0,0 +1,25 @@ +""" +Test utils. +""" + +import unittest +from ddt import ddt, data + +from video_xblock.utils import underscore_to_mixedcase + + +@ddt +class UtilsTest(unittest.TestCase): + """ + Test Utils. + """ + + @data({ + 'test': 'test', + 'test_variable': 'testVariable', + 'long_test_variable': 'longTestVariable' + }) + def test_underscore_to_mixedcase(self, test_data): + """Test string conversion from underscore to mixedcase""" + for string, expected_result in test_data.items(): + self.assertEqual(underscore_to_mixedcase(string), expected_result) diff --git a/video_xblock/tests/test_video_xblock.py b/video_xblock/tests/test_video_xblock.py index 565ad00f..32f82d5d 100644 --- a/video_xblock/tests/test_video_xblock.py +++ b/video_xblock/tests/test_video_xblock.py @@ -59,15 +59,15 @@ def test_player_state(self): self.assertDictEqual( self.xblock.player_state, { - 'current_time': self.xblock.current_time, + 'currentTime': self.xblock.current_time, 'muted': self.xblock.muted, - 'playback_rate': self.xblock.playback_rate, + 'playbackRate': self.xblock.playback_rate, 'volume': self.xblock.volume, 'transcripts': [], - 'transcripts_enabled': self.xblock.transcripts_enabled, - 'captions_enabled': self.xblock.captions_enabled, - 'captions_language': 'en', - 'transcripts_object': {} + 'transcriptsEnabled': self.xblock.transcripts_enabled, + 'captionsEnabled': self.xblock.captions_enabled, + 'captionsLanguage': 'en', + 'transcriptsObject': {} } ) @@ -98,20 +98,20 @@ def test_save_player_state(self): 'transcriptsEnabled': True, 'captionsEnabled': True, 'captionsLanguage': 'ru', - 'transcripts_object': {} + 'transcriptsObject': {} } factory = RequestFactory() request = factory.post('', json.dumps(data), content_type='application/json') response = self.xblock.save_player_state(request) self.assertEqual('{"success": true}', response.body) # pylint: disable=no-member self.assertDictEqual(self.xblock.player_state, { - 'current_time': data['currentTime'], + 'currentTime': data['currentTime'], 'muted': data['muted'], - 'playback_rate': data['playbackRate'], + 'playbackRate': data['playbackRate'], 'volume': data['volume'], 'transcripts': data['transcripts'], - 'transcripts_enabled': data['transcriptsEnabled'], - 'captions_enabled': data['captionsEnabled'], - 'captions_language': data['captionsLanguage'], - 'transcripts_object': {} + 'transcriptsEnabled': data['transcriptsEnabled'], + 'captionsEnabled': data['captionsEnabled'], + 'captionsLanguage': data['captionsLanguage'], + 'transcriptsObject': {} }) diff --git a/video_xblock/utils.py b/video_xblock/utils.py index 30fb8016..270b7bd2 100644 --- a/video_xblock/utils.py +++ b/video_xblock/utils.py @@ -52,3 +52,17 @@ def ugettext(text): Dummy ugettext method that doesn't do anything. """ return text + + +def underscore_to_mixedcase(value): + """ + Convert variables with under_score to mixedCase style. + """ + def mixedcase(): + """Mixedcase generator.""" + yield str.lower + while True: + yield str.capitalize + + mix = mixedcase() + return "".join(mix.next()(x) if x else '_' for x in value.split("_")) diff --git a/video_xblock/video_xblock.py b/video_xblock/video_xblock.py index fd9fe250..e8324657 100644 --- a/video_xblock/video_xblock.py +++ b/video_xblock/video_xblock.py @@ -29,8 +29,7 @@ from .mixins import SettingsMixin from .settings import ALL_LANGUAGES from .fields import RelativeTime -from .utils import render_template, render_resource, resource_string, ugettext as _ - +from .utils import render_template, render_resource, resource_string, underscore_to_mixedcase, ugettext as _ log = logging.getLogger(__name__) @@ -355,6 +354,11 @@ class PlaybackStateMixin(XBlock): help="Captions are enabled or not" ) + player_state_fields = ( + 'current_time', 'muted', 'playback_rate', 'volume', 'transcripts_enabled', + 'captions_enabled', 'captions_language', 'transcripts' + ) + @property def player_state(self): """ @@ -366,17 +370,16 @@ def player_state(self): trans['lang']: {'url': trans['url'], 'label': trans['label']} for trans in transcripts } - return { - 'current_time': self.current_time, - 'muted': self.muted, - 'playback_rate': self.playback_rate, - 'volume': self.volume, - 'transcripts': transcripts, - 'transcripts_enabled': self.transcripts_enabled, - 'captions_enabled': self.captions_enabled, - 'captions_language': self.captions_language or course.language, - 'transcripts_object': transcripts_object - } + result = dict() + result['captionsLanguage'] = self.captions_language or course.language + result['transcriptsObject'] = transcripts_object + result['transcripts'] = transcripts + for field_name in self.player_state_fields: + if field_name not in result: + mixedcase_field_name = underscore_to_mixedcase(field_name) + if mixedcase_field_name not in result: + result[mixedcase_field_name] = getattr(self, field_name) + return result @player_state.setter def player_state(self, state): @@ -386,14 +389,8 @@ def player_state(self, state): Arguments: state (dict): Video player state key-value pairs. """ - self.current_time = state.get('current_time', self.current_time) - self.muted = state.get('muted', self.muted) - self.playback_rate = state.get('playback_rate', self.playback_rate) - self.volume = state.get('volume', self.volume) - self.transcripts = state.get('transcripts', self.transcripts) - self.transcripts_enabled = state.get('transcripts_enabled', self.transcripts_enabled) - self.captions_enabled = state.get('captions_enabled', self.captions_enabled) - self.captions_language = state.get('captions_language', self.captions_language) + for field_name in self.player_state_fields: + setattr(self, field_name, state.get(field_name, getattr(self, field_name))) class VideoXBlock( @@ -686,7 +683,7 @@ def student_view(self, context=None): # pylint: disable=unused-argument transcript_download_link=full_transcript_download_link ) ) - frag.add_javascript(resource_string("static/js/video_xblock.js")) + frag.add_javascript(resource_string("static/js/student-view/video-xblock.js")) frag.add_css(resource_string("static/css/student-view.css")) frag.initialize_js('VideoXBlockStudentViewInit') return frag @@ -752,10 +749,10 @@ def studio_view(self, context): # pylint: disable=unused-argument fragment.add_css(resource_string("static/css/student-view.css")) fragment.add_css(resource_string("static/css/transcripts-upload.css")) fragment.add_css(resource_string("static/css/studio-edit.css")) - fragment.add_javascript(resource_string("static/js/studio-edit-utils.js")) - fragment.add_javascript(resource_string("static/js/studio-edit.js")) - fragment.add_javascript(resource_string("static/js/studio-edit-transcripts-autoload.js")) - fragment.add_javascript(resource_string("static/js/studio-edit-transcripts-manual-upload.js")) + fragment.add_javascript(resource_string("static/js/studio-edit/utils.js")) + fragment.add_javascript(resource_string("static/js/studio-edit/studio-edit.js")) + fragment.add_javascript(resource_string("static/js/studio-edit/transcripts-autoload.js")) + fragment.add_javascript(resource_string("static/js/studio-edit/transcripts-manual-upload.js")) fragment.initialize_js('StudioEditableXBlock') return fragment @@ -800,15 +797,13 @@ def save_player_state(self, request, suffix=''): # pylint: disable=unused-argum Data on success (dict). """ player_state = { - 'current_time': request['currentTime'], - 'playback_rate': request['playbackRate'], - 'volume': request['volume'], - 'muted': request['muted'], - 'transcripts': self.transcripts, - 'transcripts_enabled': request['transcriptsEnabled'], - 'captions_enabled': request['captionsEnabled'], - 'captions_language': request['captionsLanguage'] + 'transcripts': self.transcripts } + + for field_name in self.player_state_fields: + if field_name not in player_state: + player_state[field_name] = request[underscore_to_mixedcase(field_name)] + self.player_state = player_state return {'success': True} @@ -938,8 +933,7 @@ def prepare_studio_editor_fields(self, fields): made_fields (list): XBlock fields prepared to be rendered in a studio edit modal. """ made_fields = [ - self._make_field_info(key, self.fields[key]) # pylint: disable=unsubscriptable-object - for key in fields + self._make_field_info(key, self.fields[key]) for key in fields # pylint: disable=unsubscriptable-object ] return made_fields @@ -1102,9 +1096,9 @@ def update_metadata_authentication(self, auth_data, player): # If the last authentication effort was not successful, metadata should be updated as well. # Since video xblock metadata may store various information, this is to update the auth data only. if not auth_data: - self.metadata['token'] = '' # Wistia API - self.metadata['access_token'] = '' # Brightcove API - self.metadata['client_id'] = '' # Brightcove API + self.metadata['token'] = '' # Wistia API + self.metadata['access_token'] = '' # Brightcove API + self.metadata['client_id'] = '' # Brightcove API self.metadata['client_secret'] = '' # Brightcove API @XBlock.json_handler