Add frontend video image validations - EDUCATOR-447
This commit is contained in:
@@ -423,7 +423,14 @@ def videos_index_html(course):
|
||||
'previous_uploads': _get_index_videos(course),
|
||||
'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0),
|
||||
'video_supported_file_formats': VIDEO_SUPPORTED_FILE_FORMATS.keys(),
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB
|
||||
'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB,
|
||||
'video_image_settings': {
|
||||
'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'],
|
||||
'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'],
|
||||
'max_width': settings.VIDEO_IMAGE_MAX_WIDTH,
|
||||
'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT,
|
||||
'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ define([
|
||||
uploadButton,
|
||||
previousUploads,
|
||||
videoSupportedFileFormats,
|
||||
videoUploadMaxFileSizeInGB
|
||||
videoUploadMaxFileSizeInGB,
|
||||
videoImageSettings
|
||||
) {
|
||||
var activeView = new ActiveVideoUploadListView({
|
||||
postUrl: videoHandlerUrl,
|
||||
@@ -21,6 +22,7 @@ define([
|
||||
uploadButton: uploadButton,
|
||||
videoSupportedFileFormats: videoSupportedFileFormats,
|
||||
videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB,
|
||||
videoImageSettings: videoImageSettings,
|
||||
onFileUploadDone: function(activeVideos) {
|
||||
$.ajax({
|
||||
url: videoHandlerUrl,
|
||||
@@ -40,7 +42,8 @@ define([
|
||||
defaultVideoImageURL: defaultVideoImageURL,
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
collection: updatedCollection,
|
||||
encodingsDownloadUrl: encodingsDownloadUrl
|
||||
encodingsDownloadUrl: encodingsDownloadUrl,
|
||||
videoImageSettings: videoImageSettings
|
||||
});
|
||||
$contentWrapper.find('.wrapper-assets').replaceWith(updatedView.render().$el);
|
||||
});
|
||||
@@ -51,7 +54,8 @@ define([
|
||||
defaultVideoImageURL: defaultVideoImageURL,
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
collection: new Backbone.Collection(previousUploads),
|
||||
encodingsDownloadUrl: encodingsDownloadUrl
|
||||
encodingsDownloadUrl: encodingsDownloadUrl,
|
||||
videoImageSettings: videoImageSettings
|
||||
});
|
||||
$contentWrapper.append(activeView.render().$el);
|
||||
$contentWrapper.append(previousView.render().$el);
|
||||
|
||||
@@ -25,7 +25,8 @@ define(
|
||||
);
|
||||
var view = new PreviousVideoUploadListView({
|
||||
collection: collection,
|
||||
videoHandlerUrl: videoHandlerUrl
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
videoImageSettings: {}
|
||||
});
|
||||
return view.render().$el;
|
||||
},
|
||||
|
||||
@@ -14,7 +14,8 @@ define(
|
||||
},
|
||||
view = new PreviousVideoUploadView({
|
||||
model: new Backbone.Model($.extend({}, defaultData, modelData)),
|
||||
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0'
|
||||
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0',
|
||||
videoImageSettings: {}
|
||||
});
|
||||
return view.render().$el;
|
||||
};
|
||||
|
||||
@@ -1,36 +1,77 @@
|
||||
define(
|
||||
['jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'js/views/video_thumbnail', 'common/js/spec_helpers/template_helpers'],
|
||||
function($, _, Backbone, AjaxHelpers, VideoThumbnailView, TemplateHelpers) {
|
||||
'js/views/video_thumbnail', 'js/views/previous_video_upload_list', 'common/js/spec_helpers/template_helpers'],
|
||||
function($, _, Backbone, AjaxHelpers, VideoThumbnailView, PreviousVideoUploadListView, TemplateHelpers) {
|
||||
'use strict';
|
||||
describe('VideoThumbnailView', function() {
|
||||
var IMAGE_UPLOAD_URL = '/videos/upload/image',
|
||||
UPLOADED_IMAGE_URL = 'images/upload_success.jpg',
|
||||
VIDEO_IMAGE_MAX_BYTES = 2 * 1024 * 1024,
|
||||
VIDEO_IMAGE_MIN_BYTES = 2 * 1024,
|
||||
VIDEO_IMAGE_MAX_WIDTH = 1280,
|
||||
VIDEO_IMAGE_MAX_HEIGHT = 720,
|
||||
VIDEO_IMAGE_SUPPORTED_FILE_FORMATS = {
|
||||
'.bmp': 'image/bmp',
|
||||
'.bmp2': 'image/x-ms-bmp', // PIL gives x-ms-bmp format
|
||||
'.gif': 'image/gif',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png'
|
||||
},
|
||||
videoListView,
|
||||
videoThumbnailView,
|
||||
$videoListEl,
|
||||
$videoThumbnailEl,
|
||||
createVideoListView,
|
||||
createFakeImageFile,
|
||||
verifyStateInfo,
|
||||
render = function(modelData) {
|
||||
var defaultData = {
|
||||
verifyStateInfo;
|
||||
|
||||
/**
|
||||
* Creates a list of video records.
|
||||
*
|
||||
* @param {Object} modelData Model data for video records.
|
||||
* @param {Integer} numVideos Number of video elements to create.
|
||||
* @param {Integer} videoViewIndex Index of video on which videoThumbnailView would be based.
|
||||
*/
|
||||
createVideoListView = function(modelData, numVideos, videoViewIndex) {
|
||||
var modelData = modelData || {}, // eslint-disable-line no-redeclare
|
||||
numVideos = numVideos || 1, // eslint-disable-line no-redeclare
|
||||
videoViewIndex = videoViewIndex || 0, // eslint-disable-line no-redeclare,
|
||||
defaultData = {
|
||||
client_video_id: 'foo.mp4',
|
||||
duration: 42,
|
||||
created: '2014-11-25T23:13:05',
|
||||
edx_video_id: 'dummy_id',
|
||||
status: 'uploading',
|
||||
thumbnail_url: null
|
||||
};
|
||||
videoThumbnailView = new VideoThumbnailView({
|
||||
model: new Backbone.Model($.extend({}, defaultData, modelData)),
|
||||
imageUploadURL: IMAGE_UPLOAD_URL
|
||||
});
|
||||
return videoThumbnailView.render().$el;
|
||||
};
|
||||
status: 'uploading'
|
||||
},
|
||||
collection = new Backbone.Collection(_.map(_.range(numVideos), function(num, index) {
|
||||
return new Backbone.Model(
|
||||
_.extend({}, defaultData, {edx_video_id: 'dummy_id_' + index}, modelData)
|
||||
);
|
||||
}));
|
||||
videoListView = new PreviousVideoUploadListView({
|
||||
collection: collection,
|
||||
videoHandlerUrl: '/videos/course-v1:org.0+course_0+Run_0',
|
||||
videoImageUploadURL: IMAGE_UPLOAD_URL,
|
||||
videoImageSettings: {
|
||||
max_size: VIDEO_IMAGE_MAX_BYTES,
|
||||
min_size: VIDEO_IMAGE_MIN_BYTES,
|
||||
max_width: VIDEO_IMAGE_MAX_WIDTH,
|
||||
max_height: VIDEO_IMAGE_MAX_HEIGHT,
|
||||
supported_file_formats: VIDEO_IMAGE_SUPPORTED_FILE_FORMATS
|
||||
}
|
||||
});
|
||||
$videoListEl = videoListView.render().$el;
|
||||
videoThumbnailView = videoListView.itemViews[videoViewIndex].videoThumbnailView;
|
||||
$videoThumbnailEl = videoThumbnailView.render().$el;
|
||||
return videoListView;
|
||||
};
|
||||
|
||||
createFakeImageFile = function(size) {
|
||||
var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf';
|
||||
return new Blob(
|
||||
[fileFakeData.substr(0, size)],
|
||||
{type: 'image/jpg'}
|
||||
);
|
||||
|
||||
createFakeImageFile = function(size, type) {
|
||||
var size = size || VIDEO_IMAGE_MIN_BYTES, // eslint-disable-line no-redeclare
|
||||
type = type || 'image/jpeg'; // eslint-disable-line no-redeclare
|
||||
return new Blob([Array(size + 1).join('i')], {type: type});
|
||||
};
|
||||
|
||||
verifyStateInfo = function($thumbnail, state, onHover, additionalSRText) {
|
||||
@@ -50,9 +91,12 @@ define(
|
||||
).toEqual(additionalSRText);
|
||||
}
|
||||
|
||||
expect($thumbnail.find('.action-icon').html().trim()).toEqual(
|
||||
videoThumbnailView.actionsInfo[state].icon
|
||||
);
|
||||
if (state !== 'error') {
|
||||
expect($thumbnail.find('.action-icon').html().trim()).toEqual(
|
||||
videoThumbnailView.actionsInfo[state].icon
|
||||
);
|
||||
}
|
||||
|
||||
expect($thumbnail.find('.action-text').html().trim()).toEqual(
|
||||
videoThumbnailView.actionsInfo[state].text
|
||||
);
|
||||
@@ -68,22 +112,22 @@ define(
|
||||
beforeEach(function() {
|
||||
setFixtures('<div id="page-prompt"></div><div id="page-notification"></div>');
|
||||
TemplateHelpers.installTemplate('video-thumbnail');
|
||||
TemplateHelpers.installTemplate('previous-video-upload-list');
|
||||
createVideoListView();
|
||||
});
|
||||
|
||||
it('renders as expected', function() {
|
||||
var $el = render({});
|
||||
expect($el.find('.thumbnail-wrapper')).toExist();
|
||||
expect($el.find('.upload-image-input')).toExist();
|
||||
expect($videoThumbnailEl.find('.thumbnail-wrapper')).toExist();
|
||||
expect($videoThumbnailEl.find('.upload-image-input')).toExist();
|
||||
});
|
||||
|
||||
it('does not show duration if not available', function() {
|
||||
var $el = render({duration: 0});
|
||||
expect($el.find('.thumbnail-wrapper .video-duration')).not.toExist();
|
||||
createVideoListView({duration: 0});
|
||||
expect($videoThumbnailEl.find('.thumbnail-wrapper .video-duration')).not.toExist();
|
||||
});
|
||||
|
||||
it('shows the duration if available', function() {
|
||||
var $el = render({}),
|
||||
$duration = $el.find('.thumbnail-wrapper .video-duration');
|
||||
var $duration = $videoThumbnailEl.find('.thumbnail-wrapper .video-duration');
|
||||
expect($duration).toExist();
|
||||
expect($duration.find('.duration-text-machine').text().trim()).toEqual('0:42');
|
||||
expect($duration.find('.duration-text-human').text().trim()).toEqual('Video duration is 42 seconds');
|
||||
@@ -114,8 +158,8 @@ define(
|
||||
});
|
||||
|
||||
it('can upload image', function() {
|
||||
var $el = render({}),
|
||||
$thumbnail = $el.find('.thumbnail-wrapper'),
|
||||
var videoViewIndex = 0,
|
||||
$thumbnail = $videoThumbnailEl.find('.thumbnail-wrapper'),
|
||||
requests = AjaxHelpers.requests(this),
|
||||
additionalSRText = videoThumbnailView.getSRText();
|
||||
|
||||
@@ -125,12 +169,17 @@ define(
|
||||
verifyStateInfo($thumbnail, 'requirements', true, additionalSRText);
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$el.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
$videoThumbnailEl.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile()]});
|
||||
|
||||
verifyStateInfo($thumbnail, 'progress');
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', IMAGE_UPLOAD_URL + '/dummy_id', new FormData());
|
||||
AjaxHelpers.expectRequest(
|
||||
requests,
|
||||
'POST',
|
||||
IMAGE_UPLOAD_URL + '/dummy_id_' + videoViewIndex,
|
||||
new FormData()
|
||||
);
|
||||
|
||||
// Send successful upload response
|
||||
AjaxHelpers.respondWithJson(requests, {image_url: UPLOADED_IMAGE_URL});
|
||||
@@ -142,45 +191,136 @@ define(
|
||||
});
|
||||
|
||||
it('shows error state correctly', function() {
|
||||
var $el = render({}),
|
||||
$thumbnail = $el.find('.thumbnail-wrapper'),
|
||||
var $thumbnail = $videoThumbnailEl.find('.thumbnail-wrapper'),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$el.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
$videoThumbnailEl.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile()]});
|
||||
|
||||
AjaxHelpers.respondWithError(requests, 400);
|
||||
|
||||
verifyStateInfo($thumbnail, 'error');
|
||||
});
|
||||
|
||||
it('should show error notification in case of server error', function() {
|
||||
var $el = render({}),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
it('calls readMessage with correct message', function() {
|
||||
var errorMessage = 'This image file type is not supported. Supported file types are ' +
|
||||
videoThumbnailView.getVideoImageSupportedFileFormats().humanize + '.',
|
||||
successData = {
|
||||
files: [createFakeImageFile()],
|
||||
submit: function() {}
|
||||
},
|
||||
failureData = {
|
||||
jqXHR: {
|
||||
responseText: JSON.stringify({
|
||||
error: errorMessage
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(videoThumbnailView, 'readMessages');
|
||||
|
||||
videoThumbnailView.imageSelected({}, successData);
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(['Video image upload started']);
|
||||
videoThumbnailView.imageUploadSucceeded({}, {result: {image_url: UPLOADED_IMAGE_URL}});
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(['Video image upload completed']);
|
||||
videoThumbnailView.imageUploadFailed({}, failureData);
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(
|
||||
['Could not upload the video image file', errorMessage]
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error message in case of server error', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$el.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
$videoThumbnailEl.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile()]});
|
||||
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
expect($('#notification-error-title').text().trim()).toEqual(
|
||||
"Studio's having trouble saving your work"
|
||||
// Verify error message is present
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper')).toExist();
|
||||
});
|
||||
|
||||
it('should show error message when file is smaller than minimum size', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MIN_BYTES - 10)]});
|
||||
|
||||
// Verify error message
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper').find('.action-text').html()
|
||||
.trim()).toEqual(
|
||||
'The selected image must be larger than ' +
|
||||
videoThumbnailView.getVideoImageMinSize().humanize + '.'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls readMessage with correct message', function() {
|
||||
spyOn(videoThumbnailView, 'readMessages');
|
||||
it('should show error message when file is larger than maximum size', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
videoThumbnailView.imageSelected({}, {submit: function() {}});
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(['Video image upload started']);
|
||||
videoThumbnailView.imageUploadSucceeded({}, {result: {image_url: UPLOADED_IMAGE_URL}});
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(['Video image upload completed']);
|
||||
videoThumbnailView.imageUploadFailed();
|
||||
expect(videoThumbnailView.readMessages).toHaveBeenCalledWith(['Video image upload failed']);
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MAX_BYTES + 10)]});
|
||||
|
||||
// Verify error message
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper').find('.action-text').html()
|
||||
.trim()).toEqual(
|
||||
'The selected image must be smaller than ' +
|
||||
videoThumbnailView.getVideoImageMaxSize().humanize + '.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show error message when file size is equals to minimum file size', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MIN_BYTES)]});
|
||||
|
||||
// Verify error not present.
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper')).not.toExist();
|
||||
});
|
||||
|
||||
it('should not show error message when file size is equals to maximum file size', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MAX_BYTES)]});
|
||||
|
||||
// Verify error not present.
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper')).not.toExist();
|
||||
});
|
||||
|
||||
it('should show error message when file has unsupported content type', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MIN_BYTES, 'mov/mp4')]});
|
||||
|
||||
// Verify error message
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper').find('.action-text').html()
|
||||
.trim()).toEqual(
|
||||
'This image file type is not supported. Supported file types are ' +
|
||||
videoThumbnailView.getVideoImageSupportedFileFormats().humanize + '.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show error message when file has supported content type', function() {
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$videoThumbnailEl.find('.upload-image-input')
|
||||
.fileupload('add', {files: [createFakeImageFile(VIDEO_IMAGE_MIN_BYTES)]});
|
||||
|
||||
// Verify error message is not present
|
||||
expect($videoListEl.find('.thumbnail-error-wrapper')).not.toExist();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ define(
|
||||
this.videoThumbnailView = new VideoThumbnailView({
|
||||
model: this.model,
|
||||
imageUploadURL: options.videoImageUploadURL,
|
||||
defaultVideoImageURL: options.defaultVideoImageURL
|
||||
defaultVideoImageURL: options.defaultVideoImageURL,
|
||||
videoImageSettings: options.videoImageSettings
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ define(
|
||||
videoImageUploadURL: options.videoImageUploadURL,
|
||||
defaultVideoImageURL: options.defaultVideoImageURL,
|
||||
videoHandlerUrl: options.videoHandlerUrl,
|
||||
videoImageSettings: options.videoImageSettings,
|
||||
model: model
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +1,14 @@
|
||||
define(
|
||||
['underscore', 'gettext', 'moment', 'js/utils/date_utils', 'js/views/baseview',
|
||||
'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils', 'text!templates/video-thumbnail.underscore'],
|
||||
function(_, gettext, moment, DateUtils, BaseView, ViewUtils, HtmlUtils, StringUtils, VideoThumbnailTemplate) {
|
||||
'edx-ui-toolkit/js/utils/string-utils', 'text!templates/video-thumbnail.underscore',
|
||||
'text!templates/video-thumbnail-error.underscore'],
|
||||
function(_, gettext, moment, DateUtils, BaseView, ViewUtils, HtmlUtils, StringUtils, VideoThumbnailTemplate,
|
||||
VideoThumbnailErrorTemplate) {
|
||||
'use strict';
|
||||
|
||||
var VideoThumbnailView = BaseView.extend({
|
||||
|
||||
actionsInfo: {
|
||||
upload: {
|
||||
icon: '',
|
||||
text: gettext('Add Thumbnail')
|
||||
},
|
||||
edit: {
|
||||
icon: '<span class="icon fa fa-pencil" aria-hidden="true"></span>',
|
||||
text: gettext('Edit Thumbnail')
|
||||
},
|
||||
error: {
|
||||
icon: '<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span>',
|
||||
text: gettext('Image upload failed')
|
||||
},
|
||||
progress: {
|
||||
icon: '<span class="icon fa fa-spinner fa-pulse fa-spin" aria-hidden="true"></span>',
|
||||
text: gettext('Uploading')
|
||||
},
|
||||
requirements: {
|
||||
icon: '',
|
||||
text: HtmlUtils.interpolateHtml(
|
||||
// Translators: This is a 3 part text which tells the image requirements.
|
||||
gettext('Image requirements: {lineBreak} 1280px by 720px {lineBreak} .jpg, .png, or .gif'),
|
||||
{
|
||||
lineBreak: HtmlUtils.HTML('<br>')
|
||||
}
|
||||
).toString()
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .thumbnail-wrapper': 'chooseFile',
|
||||
'mouseover .thumbnail-wrapper': 'showHoverState',
|
||||
@@ -46,9 +19,45 @@ define(
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = HtmlUtils.template(VideoThumbnailTemplate);
|
||||
this.thumbnailErrorTemplate = HtmlUtils.template(VideoThumbnailErrorTemplate);
|
||||
this.imageUploadURL = options.imageUploadURL;
|
||||
this.defaultVideoImageURL = options.defaultVideoImageURL;
|
||||
this.action = this.model.get('course_video_image_url') ? 'edit' : 'upload';
|
||||
this.videoImageSettings = options.videoImageSettings;
|
||||
this.actionsInfo = {
|
||||
upload: {
|
||||
icon: '',
|
||||
text: gettext('Add Thumbnail')
|
||||
},
|
||||
edit: {
|
||||
icon: '<span class="icon fa fa-pencil" aria-hidden="true"></span>',
|
||||
text: gettext('Edit Thumbnail')
|
||||
},
|
||||
error: {
|
||||
icon: '',
|
||||
text: gettext('Image upload failed')
|
||||
},
|
||||
progress: {
|
||||
icon: '<span class="icon fa fa-spinner fa-pulse fa-spin" aria-hidden="true"></span>',
|
||||
text: gettext('Uploading')
|
||||
},
|
||||
requirements: {
|
||||
icon: '',
|
||||
text: HtmlUtils.interpolateHtml(
|
||||
// Translators: This is a 3 part text which tells the image requirements.
|
||||
gettext('{ReqTextSpanStart}Image requirements{spanEnd}{lineBreak}{InstructionsSpanStart}{videoImageResoultion}{lineBreak} {videoImageSupportedFileFormats}{spanEnd}'), // eslint-disable-line max-len
|
||||
{
|
||||
videoImageResoultion: this.getVideoImageResolution(),
|
||||
videoImageSupportedFileFormats: this.getVideoImageSupportedFileFormats().humanize,
|
||||
lineBreak: HtmlUtils.HTML('<br>'),
|
||||
ReqTextSpanStart: HtmlUtils.HTML('<span class="requirements-text">'),
|
||||
InstructionsSpanStart: HtmlUtils.HTML('<span class="requirements-instructions">'),
|
||||
spanEnd: HtmlUtils.HTML('</span>')
|
||||
}
|
||||
).toString()
|
||||
}
|
||||
};
|
||||
|
||||
_.bindAll(
|
||||
this, 'render', 'chooseFile', 'imageSelected', 'imageUploadSucceeded', 'imageUploadFailed',
|
||||
'showHoverState', 'hideHoverState'
|
||||
@@ -64,13 +73,49 @@ define(
|
||||
videoId: this.model.get('edx_video_id'),
|
||||
actionInfo: this.actionsInfo[this.action],
|
||||
thumbnailURL: this.model.get('course_video_image_url') || this.defaultVideoImageURL,
|
||||
duration: this.getDuration(this.model.get('duration'))
|
||||
duration: this.getDuration(this.model.get('duration')),
|
||||
videoImageSupportedFileFormats: this.getVideoImageSupportedFileFormats(),
|
||||
videoImageMaxSize: this.getVideoImageMaxSize(),
|
||||
videoImageResolution: this.getVideoImageResolution()
|
||||
})
|
||||
);
|
||||
this.hideHoverState();
|
||||
return this;
|
||||
},
|
||||
|
||||
getVideoImageSupportedFileFormats: function() {
|
||||
var supportedFormats = _.reject(_.keys(this.videoImageSettings.supported_file_formats), function(item) {
|
||||
// Don't show redundant extensions to end user.
|
||||
return item === '.bmp2' || item === '.jpeg';
|
||||
}).sort();
|
||||
return {
|
||||
humanize: supportedFormats.slice(0, -1).join(', ') + ' or ' + supportedFormats.slice(-1),
|
||||
machine: _.values(this.videoImageSettings.supported_file_formats)
|
||||
};
|
||||
},
|
||||
|
||||
getVideoImageMaxSize: function() {
|
||||
return {
|
||||
humanize: this.videoImageSettings.max_size / (1024 * 1024) + ' MB',
|
||||
machine: this.videoImageSettings.max_size
|
||||
};
|
||||
},
|
||||
|
||||
getVideoImageMinSize: function() {
|
||||
return {
|
||||
humanize: this.videoImageSettings.min_size / 1024 + ' KB',
|
||||
machine: this.videoImageSettings.min_size
|
||||
};
|
||||
},
|
||||
|
||||
getVideoImageResolution: function() {
|
||||
return StringUtils.interpolate(
|
||||
// Translators: message will be like 1280x720 pixels
|
||||
gettext('{maxWidth}x{maxHeight} pixels'),
|
||||
{maxWidth: this.videoImageSettings.max_width, maxHeight: this.videoImageSettings.max_height}
|
||||
);
|
||||
},
|
||||
|
||||
getImageAltText: function() {
|
||||
return StringUtils.interpolate(
|
||||
// Translators: message will be like Thumbnail for Arrow.mp4
|
||||
@@ -162,9 +207,20 @@ define(
|
||||
},
|
||||
|
||||
imageSelected: function(event, data) {
|
||||
this.readMessages([gettext('Video image upload started')]);
|
||||
this.showUploadInProgressMessage();
|
||||
data.submit();
|
||||
var errorMessage;
|
||||
// If an error is already present above the video element, remove it.
|
||||
this.clearErrorMessage(this.model.get('edx_video_id'));
|
||||
|
||||
errorMessage = this.validateImageFile(data.files[0]);
|
||||
if (!errorMessage) {
|
||||
// Do not trigger global AJAX error handler
|
||||
data.global = false; // eslint-disable-line no-param-reassign
|
||||
this.readMessages([gettext('Video image upload started')]);
|
||||
this.showUploadInProgressMessage();
|
||||
data.submit();
|
||||
} else {
|
||||
this.showErrorMessage(errorMessage);
|
||||
}
|
||||
},
|
||||
|
||||
imageUploadSucceeded: function(event, data) {
|
||||
@@ -174,10 +230,9 @@ define(
|
||||
this.readMessages([gettext('Video image upload completed')]);
|
||||
},
|
||||
|
||||
imageUploadFailed: function() {
|
||||
this.action = 'error';
|
||||
this.setActionInfo(this.action, true);
|
||||
this.readMessages([gettext('Video image upload failed')]);
|
||||
imageUploadFailed: function(event, data) {
|
||||
var errorText = JSON.parse(data.jqXHR.responseText).error;
|
||||
this.showErrorMessage(errorText);
|
||||
},
|
||||
|
||||
showUploadInProgressMessage: function() {
|
||||
@@ -191,7 +246,12 @@ define(
|
||||
} else if (this.action === 'edit') {
|
||||
this.setActionInfo(this.action, true);
|
||||
}
|
||||
this.$('.thumbnail-wrapper').addClass('focused');
|
||||
|
||||
// When we had error, focused effect was not wearing off after hover out.
|
||||
// Add focused class to all rows except rows having error.
|
||||
if (!$(this.$el.parent()).hasClass('has-thumbnail-error')) {
|
||||
this.$('.thumbnail-wrapper').addClass('focused');
|
||||
}
|
||||
},
|
||||
|
||||
hideHoverState: function() {
|
||||
@@ -204,10 +264,19 @@ define(
|
||||
|
||||
setActionInfo: function(action, showText, additionalSRText) {
|
||||
this.$('.thumbnail-action').toggle(showText);
|
||||
HtmlUtils.setHtml(
|
||||
this.$('.thumbnail-action .action-icon'),
|
||||
HtmlUtils.HTML(this.actionsInfo[action].icon)
|
||||
);
|
||||
|
||||
// In case of error, we don't want to show any icon on the image.
|
||||
if (action === 'error') {
|
||||
HtmlUtils.setHtml(
|
||||
this.$('.thumbnail-action .action-icon'),
|
||||
HtmlUtils.HTML('')
|
||||
);
|
||||
} else {
|
||||
HtmlUtils.setHtml(
|
||||
this.$('.thumbnail-action .action-icon'),
|
||||
HtmlUtils.HTML(this.actionsInfo[action].icon)
|
||||
);
|
||||
}
|
||||
HtmlUtils.setHtml(
|
||||
this.$('.thumbnail-action .action-text'),
|
||||
HtmlUtils.HTML(this.actionsInfo[action].text)
|
||||
@@ -216,6 +285,137 @@ define(
|
||||
this.$('.thumbnail-wrapper').attr('class', 'thumbnail-wrapper {action}'.replace('{action}', action));
|
||||
},
|
||||
|
||||
validateImageFile: function(imageFile) {
|
||||
var errorMessage = '';
|
||||
|
||||
if (!_.contains(this.getVideoImageSupportedFileFormats().machine, imageFile.type)) {
|
||||
errorMessage = StringUtils.interpolate(
|
||||
// Translators: supportedFileFormats will be like .bmp, gif, .jpg or .png.
|
||||
gettext(
|
||||
'This image file type is not supported. Supported file types are {supportedFileFormats}.'
|
||||
),
|
||||
{
|
||||
supportedFileFormats: this.getVideoImageSupportedFileFormats().humanize
|
||||
}
|
||||
);
|
||||
} else if (imageFile.size > this.getVideoImageMaxSize().machine) {
|
||||
errorMessage = StringUtils.interpolate(
|
||||
// Translators: maxFileSizeInMB will be like 2 MB.
|
||||
gettext('The selected image must be smaller than {maxFileSizeInMB}.'),
|
||||
{
|
||||
maxFileSizeInMB: this.getVideoImageMaxSize().humanize
|
||||
}
|
||||
);
|
||||
} else if (imageFile.size < this.getVideoImageMinSize().machine) {
|
||||
errorMessage = StringUtils.interpolate(
|
||||
// Translators: minFileSizeInKB will be like 2 KB.
|
||||
gettext('The selected image must be larger than {minFileSizeInKB}.'),
|
||||
{
|
||||
minFileSizeInKB: this.getVideoImageMinSize().humanize
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
},
|
||||
|
||||
clearErrorMessage: function(videoId) {
|
||||
var $thumbnailWrapperEl = $('.thumbnail-error-wrapper[data-video-id="' + videoId + '"]');
|
||||
if ($thumbnailWrapperEl.length) {
|
||||
$thumbnailWrapperEl.remove();
|
||||
}
|
||||
},
|
||||
|
||||
showErrorMessage: function(errorText) {
|
||||
var videoId = this.model.get('edx_video_id'),
|
||||
$parentRowEl = $(this.$el.parent());
|
||||
|
||||
this.action = 'error';
|
||||
this.setActionInfo(this.action, true);
|
||||
this.readMessages([gettext('Could not upload the video image file'), errorText]);
|
||||
|
||||
// Add css classes so as to distinguish.
|
||||
$parentRowEl.addClass('has-thumbnail-error thumbnail-error');
|
||||
|
||||
// We need to update data attr in DOM too so as to find our element on hover.
|
||||
$parentRowEl.attr('data-video-id', videoId);
|
||||
|
||||
// Add error wrapper html before current video element row.
|
||||
$parentRowEl.before( // safe-lint: disable=javascript-jquery-insertion
|
||||
HtmlUtils.ensureHtml(
|
||||
this.thumbnailErrorTemplate({videoId: videoId, errorText: errorText})
|
||||
).toString()
|
||||
);
|
||||
|
||||
// We need to treat error and error throwing row as one.
|
||||
// Refresh table rows to reflect error row.
|
||||
this.refreshVideoTableRowClasses();
|
||||
|
||||
// To treat current row and it's error row as one on hover,
|
||||
// we add hover effect to both rows, even if it is hovered on only one row, thus, giving us
|
||||
// the combined one row feel.
|
||||
$('.thumbnail-error[data-video-id="' + videoId + '"]').hover(function() {
|
||||
$('.thumbnail-error[data-video-id="' + videoId + '"]').toggleClass('blue-l5');
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
Refresh video table classes.
|
||||
|
||||
This method treats row and their corresponsing error rows as one, for that to achieve we need to reset
|
||||
table row even odd colors.
|
||||
*/
|
||||
refreshVideoTableRowClasses: function() {
|
||||
var savedClass, // this class will be applied to the row corresponding to error row.
|
||||
oddRowClass = 'white',
|
||||
evenRowClass = 'gray-l6';
|
||||
|
||||
$('.view-video-uploads .assets-table .js-table-body tr').each(function(index) {
|
||||
var currentRowClass;
|
||||
|
||||
// Decide current iterated row is even or odd.
|
||||
if (index % 2 === 0) {
|
||||
currentRowClass = evenRowClass;
|
||||
} else {
|
||||
currentRowClass = oddRowClass;
|
||||
}
|
||||
|
||||
// If the row is error row, save it's class so that it can be applied to it's corresponding row.
|
||||
if ($(this).hasClass('thumbnail-error-wrapper')) {
|
||||
savedClass = currentRowClass;
|
||||
}
|
||||
|
||||
// If current iterated row is the row which generated error
|
||||
// Apply the class same as it's corresponding error row. The class was saved.
|
||||
if ($(this).hasClass('has-thumbnail-error')) {
|
||||
// First remove previously added classes.
|
||||
$(this).removeClass(evenRowClass);
|
||||
$(this).removeClass(oddRowClass);
|
||||
|
||||
// Apply new class now.
|
||||
$(this).addClass(savedClass);
|
||||
|
||||
// Now after the saved class, swap even odd row classes.
|
||||
if (currentRowClass === oddRowClass) {
|
||||
oddRowClass = evenRowClass;
|
||||
evenRowClass = currentRowClass;
|
||||
} else {
|
||||
evenRowClass = oddRowClass;
|
||||
oddRowClass = currentRowClass;
|
||||
}
|
||||
// Reset the saved class after it has been applied.
|
||||
savedClass = '';
|
||||
} else {
|
||||
// For all simple rows, first remove classes if added
|
||||
$(this).removeClass(evenRowClass);
|
||||
$(this).removeClass(oddRowClass);
|
||||
|
||||
// then add the class based on it's rows.
|
||||
$(this).addClass(currentRowClass);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
readMessages: function(messages) {
|
||||
if ($(window).prop('SR') !== undefined) {
|
||||
$(window).prop('SR').readTexts(messages);
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-l5;
|
||||
background-color: $blue-l5 !important;
|
||||
|
||||
.date-col,
|
||||
.embed-col,
|
||||
|
||||
@@ -169,6 +169,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gray-l6 {
|
||||
background: $gray-l6 !important;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.blue-l5 {
|
||||
background: $blue-l5 !important;
|
||||
}
|
||||
|
||||
.thumbnail-error-wrapper {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
|
||||
.thumbnail-error-text {
|
||||
color: $red;
|
||||
.action-text {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr.has-thumbnail-error {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
$thumbnail-width: ($baseline*7.5);
|
||||
$thumbnail-height: ($baseline*5);
|
||||
|
||||
@@ -192,6 +220,16 @@
|
||||
}
|
||||
|
||||
&.requirements {
|
||||
.requirements-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
.requirements-instructions {
|
||||
font-size: 15px;
|
||||
font-family: "Open Sans";
|
||||
text-align: left;
|
||||
color: #4c4c4c;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.video-duration {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
7
cms/templates/js/video-thumbnail-error.underscore
Normal file
7
cms/templates/js/video-thumbnail-error.underscore
Normal file
@@ -0,0 +1,7 @@
|
||||
<tr class="thumbnail-error-wrapper thumbnail-error" data-video-id="<%- videoId %>">
|
||||
<td colspan="6" class="thumbnail-error-text" colspan="6">
|
||||
<span class="action-icon" aria-hidden="true">
|
||||
<span class="icon fa fa-exclamation-triangle" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="action-text"><%- errorText %></span></td>
|
||||
</tr>
|
||||
@@ -9,7 +9,10 @@
|
||||
</label>
|
||||
|
||||
<span class="requirements-text-sr sr">
|
||||
<%- gettext('Recommended image resolution is 1280x720 pixels and format must be one of .jpg, .png or .gif') %>
|
||||
<%- edx.StringUtils.interpolate(
|
||||
gettext("Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}."),
|
||||
{imageResolution: videoImageResolution, maxFileSize: videoImageMaxSize.humanize, supportedImageFormats: videoImageSupportedFileFormats.humanize}
|
||||
) %>
|
||||
</span>
|
||||
</div>
|
||||
<% if(duration) { %>
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
$(".nav-actions .upload-button"),
|
||||
$contentWrapper.data("previous-uploads"),
|
||||
${video_supported_file_formats | n, dump_js_escaped_json},
|
||||
${video_upload_max_file_size | n, dump_js_escaped_json}
|
||||
${video_upload_max_file_size | n, dump_js_escaped_json},
|
||||
${video_image_settings | n, dump_js_escaped_json}
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
@@ -77,7 +77,7 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
|
||||
git+https://github.com/edx/edx-ora2.git@1.4.3#egg=ora2==1.4.3
|
||||
-e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0
|
||||
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
|
||||
git+https://github.com/edx/edx-val.git@0.0.14#egg=edxval==0.0.14
|
||||
git+https://github.com/edx/edx-val.git@0.0.15#egg=edxval==0.0.15
|
||||
git+https://github.com/pmitros/RecommenderXBlock.git@v1.2#egg=recommender-xblock==1.2
|
||||
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
|
||||
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
|
||||
|
||||
Reference in New Issue
Block a user