Commit cf6200cb by Mushtaq Ali

Add frontend video image validations - EDUCATOR-447

parent e8fee2e0
......@@ -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;
};
......
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;
};
createFakeImageFile = function(size) {
var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf';
return new Blob(
[fileFakeData.substr(0, size)],
{type: 'image/jpg'}
);
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, 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
});
});
......
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;
}
......
<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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment