diff --git a/cms/static/js/spec/views/video_transcripts_spec.js b/cms/static/js/spec/views/video_transcripts_spec.js index fe1e20118f..b820e5bacc 100644 --- a/cms/static/js/spec/views/video_transcripts_spec.js +++ b/cms/static/js/spec/views/video_transcripts_spec.js @@ -1,12 +1,15 @@ define( ['jquery', 'underscore', 'backbone', 'js/views/video_transcripts', 'js/views/previous_video_upload_list', - 'common/js/spec_helpers/template_helpers'], - function($, _, Backbone, VideoTranscriptsView, PreviousVideoUploadListView, TemplateHelpers) { + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers'], + function($, _, Backbone, VideoTranscriptsView, PreviousVideoUploadListView, AjaxHelpers, TemplateHelpers) { 'use strict'; describe('VideoTranscriptsView', function() { var videoTranscriptsView, renderView, - verifyTranscriptActions, + verifyTranscriptStateInfo, + verifyMessage, + verifyDetailedErrorMessage, + createFakeTranscriptFile, transcripts = { en: 'English', es: 'Spanish', @@ -23,17 +26,28 @@ define( }, TRANSCRIPT_DOWNLOAD_FILE_FORMAT = 'srt', TRANSCRIPT_DOWNLOAD_URL = 'abc.com/transcript_download/course_id', + TRANSCRIPT_UPLOAD_URL = 'abc.com/transcript_upload/course_id', videoSupportedFileFormats = ['.mov', '.mp4'], videoTranscriptSettings = { trancript_download_file_format: TRANSCRIPT_DOWNLOAD_FILE_FORMAT, - transcript_download_handler_url: TRANSCRIPT_DOWNLOAD_URL + transcript_download_handler_url: TRANSCRIPT_DOWNLOAD_URL, + transcript_upload_handler_url: TRANSCRIPT_UPLOAD_URL }, videoListView; - verifyTranscriptActions = function($transcriptActionsEl, transcriptLanguage) { - var downloadTranscriptActionEl = $transcriptActionsEl.find('.download-transcript-button'), + verifyTranscriptStateInfo = function($transcriptEl, transcriptLanguage) { + var $transcriptActionsEl = $transcriptEl.find('.transcript-actions'), + downloadTranscriptActionEl = $transcriptActionsEl.find('.download-transcript-button'), uploadTranscriptActionEl = $transcriptActionsEl.find('.upload-transcript-button'); + // Verify transcript data attributes + expect($transcriptEl.data('edx-video-id')).toEqual(edxVideoID); + expect($transcriptEl.data('language-code')).toEqual(transcriptLanguage); + + // Verify transcript language dropdown has correct value set. + expect($transcriptEl.find('.transcript-language-menu').val(), transcriptLanguage); + + // Verify transcript actions expect(downloadTranscriptActionEl.html().trim(), 'Download'); expect( downloadTranscriptActionEl.attr('href'), @@ -41,8 +55,33 @@ define( ); expect(uploadTranscriptActionEl.html().trim(), 'Upload'); - expect(uploadTranscriptActionEl.data('edx-video-id'), edxVideoID); - expect(uploadTranscriptActionEl.data('language-code'), transcriptLanguage); + }; + + verifyMessage = function($transcriptEl, status) { + var $transcriptStatusEl = $transcriptEl.find('.transcript-upload-status-container'), + statusData = videoTranscriptsView.transcriptUploadStatuses[status]; + + expect($transcriptStatusEl.hasClass(statusData.statusClass)).toEqual(true); + expect($transcriptStatusEl.find('span.fa').hasClass(statusData.iconClasses)).toEqual(true); + expect( + $transcriptStatusEl.find('.more-details-action').hasClass('hidden') + ).toEqual(statusData.hiddenClass === 'hidden'); + expect( + $transcriptStatusEl.find('.transcript-detail-status').html().trim() + ).toEqual(statusData.shortMessage); + }; + + verifyDetailedErrorMessage = function($transcriptEl, expectedTitle, expectedMessage) { + $transcriptEl.find('.more-details-action').click(); + expect($('#prompt-warning-title').text().trim()).toEqual(expectedTitle); + expect($('#prompt-warning-description').text().trim()).toEqual(expectedMessage); + }; + + createFakeTranscriptFile = function(transcriptFileName) { + var transcriptFileName = transcriptFileName || 'test-transcript.srt', // eslint-disable-line no-redeclare, max-len + size = 100, + type = ''; + return new File([new Blob([Array(size).join('i')], {type: type})], transcriptFileName); }; renderView = function(availableTranscripts, isVideoTranscriptEnabled) { @@ -71,7 +110,10 @@ define( }; beforeEach(function() { - setFixtures('
'); + setFixtures( + '
' + + '
' + ); TemplateHelpers.installTemplate('previous-video-upload-list'); renderView(transcripts); }); @@ -177,18 +219,133 @@ define( ); _.each(transcripts, function(langaugeText, languageCode) { - $transcriptEl = $(videoTranscriptsView.$el.find('#show-video-transcript-content-' + languageCode)); + $transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len // Verify correct transcript title is set. expect($transcriptEl.find('.transcript-title').html()).toEqual( 'Video client title n_' + languageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT ); - // Verify transcript language dropdown has correct value set. - expect($transcriptEl.find('.transcript-language-menu').val(), languageCode); - - // Verify transcript actions are rendered correctly. - verifyTranscriptActions($transcriptEl.find('.transcript-actions'), languageCode); + // Verify transcript is rendered with correct info. + verifyTranscriptStateInfo($transcriptEl, languageCode); }); }); + + it('can upload transcript', function() { + var languageCode = 'en', + newLanguageCode = 'ar', + requests = AjaxHelpers.requests(this), + $transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + // Verify correct transcript title is set. + expect($transcriptEl.find('.transcript-title').html()).toEqual( + 'Video client title n_' + languageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT + ); + + // Select a language + $transcriptEl.find('.transcript-language-menu').val(newLanguageCode); + + $transcriptEl.find('.upload-transcript-button').click(); + + // Add transcript to upload queue and send POST request to upload transcript. + $transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]}); + + // Verify if POST request received for image upload + AjaxHelpers.expectRequest( + requests, + 'POST', + TRANSCRIPT_UPLOAD_URL + ); + + // Send successful upload response + AjaxHelpers.respondWithJson(requests, {}); + + // Verify correct transcript title is set. + expect($transcriptEl.find('.transcript-title').html()).toEqual( + 'Video client title n_' + newLanguageCode + '.' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT + ); + + verifyMessage($transcriptEl, 'uploaded'); + + // Verify transcript is rendered with correct info. + verifyTranscriptStateInfo($transcriptEl, newLanguageCode); + }); + + it('shows error state correctly', function() { + var languageCode = 'en', + requests = AjaxHelpers.requests(this), + errorMessage = 'Transcript failed error message', + $transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + $transcriptEl.find('.upload-transcript-button').click(); + + // Add transcript to upload queue and send POST request to upload transcript. + $transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]}); + + // Server response with bad request. + AjaxHelpers.respondWithError(requests, 400, {error: errorMessage}); + + verifyMessage($transcriptEl, 'failed'); + + // verify detailed error message + verifyDetailedErrorMessage( + $transcriptEl, + videoTranscriptsView.defaultFailureTitle, + errorMessage + ); + + // Verify transcript is rendered with correct info. + verifyTranscriptStateInfo($transcriptEl, languageCode); + }); + + it('should show error message in case of server error', function() { + var languageCode = 'en', + requests = AjaxHelpers.requests(this), + $transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + $transcriptEl.find('.upload-transcript-button').click(); + + // Add transcript to upload queue and send POST request to upload transcript. + $transcriptEl.find('.upload-transcript-input').fileupload('add', {files: [createFakeTranscriptFile()]}); + + AjaxHelpers.respondWithError(requests, 500); + + verifyMessage($transcriptEl, 'failed'); + + // verify detailed error message + verifyDetailedErrorMessage( + $transcriptEl, + videoTranscriptsView.defaultFailureTitle, + videoTranscriptsView.defaultFailureMessage + ); + + // Verify transcript is rendered with correct info. + verifyTranscriptStateInfo($transcriptEl, languageCode); + }); + + it('should show error message in case of unsupported transcript file format', function() { + var languageCode = 'en', + transcriptFileName = 'unsupported-transcript-file-format.txt', + errorMessage = transcriptFileName + ' is not in a supported file format. Supported file format is ' + TRANSCRIPT_DOWNLOAD_FILE_FORMAT + '.', // eslint-disable-line max-len + $transcriptEl = videoTranscriptsView.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + $transcriptEl.find('.upload-transcript-button').click(); + + // Add transcript to upload queue and send POST request to upload transcript. + $transcriptEl.find('.upload-transcript-input').fileupload('add', { + files: [createFakeTranscriptFile(transcriptFileName)] + }); + + verifyMessage($transcriptEl, 'validationFailed'); + + // verify detailed error message + verifyDetailedErrorMessage( + $transcriptEl, + videoTranscriptsView.defaultFailureTitle, + errorMessage + ); + + // Verify transcript is rendered with correct info. + verifyTranscriptStateInfo($transcriptEl, languageCode); + }); }); } ); diff --git a/cms/static/js/views/video_transcripts.js b/cms/static/js/views/video_transcripts.js index fb78f2526a..b755cfa2f9 100644 --- a/cms/static/js/views/video_transcripts.js +++ b/cms/static/js/views/video_transcripts.js @@ -1,14 +1,18 @@ define( - ['underscore', 'gettext', 'js/views/baseview', 'edx-ui-toolkit/js/utils/html-utils', - 'edx-ui-toolkit/js/utils/string-utils', 'text!templates/video-transcripts.underscore'], - function(_, gettext, BaseView, HtmlUtils, StringUtils, videoTranscriptsTemplate) { + ['underscore', 'gettext', 'js/views/baseview', 'common/js/components/views/feedback_prompt', + 'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils', + 'text!templates/video-transcripts.underscore', 'text!templates/video-transcript-upload-status.underscore'], + function(_, gettext, BaseView, PromptView, HtmlUtils, StringUtils, videoTranscriptsTemplate, + videoTranscriptUploadStatusTemplate) { 'use strict'; var VideoTranscriptsView = BaseView.extend({ tagName: 'div', events: { - 'click .toggle-show-transcripts-button': 'toggleShowTranscripts' + 'click .toggle-show-transcripts-button': 'toggleShowTranscripts', + 'click .upload-transcript-button': 'chooseFile', + 'click .more-details-action': 'showUploadFailureMessage' }, initialize: function(options) { @@ -19,6 +23,40 @@ define( this.videoSupportedFileFormats = options.videoSupportedFileFormats; this.videoTranscriptSettings = options.videoTranscriptSettings; this.template = HtmlUtils.template(videoTranscriptsTemplate); + this.transcriptUploadStatusTemplate = HtmlUtils.template(videoTranscriptUploadStatusTemplate); + this.defaultFailureTitle = gettext('Your file could not be uploaded'); + this.defaultFailureMessage = 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.'); // eslint-disable-line max-len + this.transcriptUploadStatuses = { + uploaded: { + statusClass: 'success', + iconClasses: 'fa-check', + shortMessage: 'Transcript uploaded.', + hiddenClass: 'hidden' + }, + uploading: { + statusClass: '', + iconClasses: 'fa-spinner fa-pulse', + shortMessage: 'Uploading transcript', + hiddenClass: 'hidden' + }, + failed: { + statusClass: 'error', + iconClasses: 'fa-warning', + shortMessage: 'Upload failed.', + hiddenClass: '' + }, + validationFailed: { + statusClass: 'error', + iconClasses: 'fa-warning', + shortMessage: 'Validation failed.', + hiddenClass: '' + } + }; + // This is needed to attach transcript methods to this object while uploading. + _.bindAll( + this, 'render', 'chooseFile', 'transcriptSelected', 'transcriptUploadSucceeded', + 'transcriptUploadFailed' + ); }, /* @@ -40,7 +78,7 @@ define( var clientTitle = this.clientVideoID; // Remove video file extension for transcript title. _.each(this.videoSupportedFileFormats, function(videoFormat) { - clientTitle.replace(videoFormat, ''); + clientTitle = clientTitle.replace(videoFormat, ''); }); return clientTitle.substring(0, 20); }, @@ -74,6 +112,140 @@ define( } }, + validateTranscriptUpload: function(file) { + var errorMessage = '', + fileName = file.name, + fileType = fileName.substr(fileName.lastIndexOf('.') + 1); + + if (fileType !== this.videoTranscriptSettings.trancript_download_file_format) { + errorMessage = gettext( + '{filename} is not in a supported file format. ' + + 'Supported file format is {supportedFileFormat}.' + ) + .replace('{filename}', fileName) + .replace('{supportedFileFormat}', this.videoTranscriptSettings.trancript_download_file_format); + } + + return errorMessage; + }, + + chooseFile: function(event) { + var $transcriptContainer = $(event.target).parents('.show-video-transcript-content'), + $transcriptUploadEl = $transcriptContainer.find('.upload-transcript-input'); + + $transcriptUploadEl.fileupload({ + url: this.videoTranscriptSettings.transcript_upload_handler_url, + add: this.transcriptSelected, + done: this.transcriptUploadSucceeded, + fail: this.transcriptUploadFailed, + formData: { + edx_video_id: this.edxVideoID, + language_code: $transcriptContainer.attr('data-language-code'), + new_language_code: $transcriptContainer.find('.transcript-language-menu').val() + } + }); + + $transcriptUploadEl.click(); + }, + + transcriptSelected: function(event, data) { + var errorMessage, + $transcriptContainer = $(event.target).parents('.show-video-transcript-content'); + + errorMessage = this.validateTranscriptUpload(data.files[0]); + if (!errorMessage) { + // Do not trigger global AJAX error handler + data.global = false; // eslint-disable-line no-param-reassign + data.submit(); + this.renderMessage($transcriptContainer, 'uploading'); + } else { + // Reset transcript language back to original. + $transcriptContainer.find('.transcript-language-menu').val($transcriptContainer.attr('data-language-code')); // eslint-disable-line max-len + this.renderMessage($transcriptContainer, 'validationFailed', errorMessage); + } + }, + + transcriptUploadSucceeded: function(event, data) { + var languageCode = data.formData.language_code, + newLanguageCode = data.formData.new_language_code, + $transcriptContainer = this.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + $transcriptContainer.attr('data-language-code', newLanguageCode); + HtmlUtils.setHtml( + $transcriptContainer.find('.transcript-title'), + StringUtils.interpolate(gettext('{transcriptClientTitle}_{transcriptLanguageCode}.{fileExtension}'), + { + transcriptClientTitle: this.getTranscriptClientTitle(), + transcriptLanguageCode: newLanguageCode, + fileExtension: this.videoTranscriptSettings.trancript_download_file_format + } + ) + ); + + this.renderMessage($transcriptContainer, 'uploaded'); + }, + + transcriptUploadFailed: function(event, data) { + var errorMessage, + languageCode = data.formData.language_code, + $transcriptContainer = this.$el.find('.show-video-transcript-content[data-language-code="' + languageCode + '"]'); // eslint-disable-line max-len + + try { + errorMessage = JSON.parse(data.jqXHR.responseText).error; + errorMessage = errorMessage || this.defaultFailureMessage; + } catch (error) { + errorMessage = this.defaultFailureMessage; + } + // Reset transcript language back to original. + $transcriptContainer.find('.transcript-language-menu').val(languageCode); + + this.renderMessage($transcriptContainer, 'failed', errorMessage); + }, + + clearMessage: function() { + var $transcriptStatusesEl = this.$el.find('.transcript-upload-status-container'); + // Clear all message containers + HtmlUtils.setHtml($transcriptStatusesEl, ''); + $transcriptStatusesEl.removeClass('success error'); + }, + + renderMessage: function($transcriptContainer, status, errorMessage) { + var statusData = this.transcriptUploadStatuses[status], + $transcriptStatusEl = $transcriptContainer.find('.transcript-upload-status-container'); + + // If a messge is already present above the video transcript element, remove it. + this.clearMessage(); + + HtmlUtils.setHtml( + $transcriptStatusEl, + this.transcriptUploadStatusTemplate({ + status: statusData.statusClass, + iconClasses: statusData.iconClasses, + shortMessage: gettext(statusData.shortMessage), + errorMessage: errorMessage || '', + hiddenClass: statusData.hiddenClass + }) + ); + + $transcriptStatusEl.addClass(statusData.statusClass); + }, + + showUploadFailureMessage: function(event) { + var errorMessage = $(event.target).data('error-message'); + return new PromptView.Warning({ + title: this.defaultFailureTitle, + message: errorMessage, + actions: { + primary: { + text: gettext('Close'), + click: function(prompt) { + return prompt.hide(); + } + } + } + }).show(); + }, + /* Renders transcripts view. */ @@ -85,7 +257,7 @@ define( transcriptAvailableLanguages: this.sortByValue(this.transcriptAvailableLanguages), edxVideoID: this.edxVideoID, transcriptClientTitle: this.getTranscriptClientTitle(), - transcriptDownloadFileFormat: this.videoTranscriptSettings.trancript_download_file_format, + transcriptFileFormat: this.videoTranscriptSettings.trancript_download_file_format, transcriptDownloadHandlerUrl: this.videoTranscriptSettings.transcript_download_handler_url }) ); diff --git a/cms/static/sass/views/_video-upload.scss b/cms/static/sass/views/_video-upload.scss index 20eb128229..88f6259929 100644 --- a/cms/static/sass/views/_video-upload.scss +++ b/cms/static/sass/views/_video-upload.scss @@ -8,7 +8,7 @@ @extend %t-copy; vertical-align: bottom; - margin-right: ($baseline/5); + @include margin-right($baseline/45); } } @@ -33,12 +33,30 @@ } } - .show-video-transcripts-wrapper.hidden { + .hidden { display: none; } .show-video-transcript-content { margin-top: ($baseline/2); + + .transcript-upload-status-container { + + .video-transcript-detail-status, .more-details-action { + @include font-size(12); + @include line-height(12); + @include margin-left($baseline/4); + } + } + + .transcript-upload-status-container.error { + color: $color-error; + } + + .transcript-upload-status-container.success { + color: $color-ready; + } + .transcript-language-menu { display: block; width: 200px; diff --git a/cms/templates/js/video-transcript-upload-status.underscore b/cms/templates/js/video-transcript-upload-status.underscore new file mode 100644 index 0000000000..a296813fe1 --- /dev/null +++ b/cms/templates/js/video-transcript-upload-status.underscore @@ -0,0 +1,5 @@ + +<%- shortMessage %> + diff --git a/cms/templates/js/video-transcripts.underscore b/cms/templates/js/video-transcripts.underscore index 355c2dcbbf..5f63ed0c2e 100644 --- a/cms/templates/js/video-transcripts.underscore +++ b/cms/templates/js/video-transcripts.underscore @@ -13,14 +13,19 @@ <% }%>