video thumbnail ui
This commit is contained in:
committed by
Mushtaq Ali
parent
dca12d65c2
commit
763f0051bd
@@ -210,6 +210,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertRegexpMatches(response["Content-Type"], "^text/html(;.*)?$")
|
||||
self.assertIn(_get_default_video_image_url(), response.content)
|
||||
# Crude check for presence of data in returned HTML
|
||||
for video in self.previous_uploads:
|
||||
self.assertIn(video["edx_video_id"], response.content)
|
||||
@@ -586,12 +587,11 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
response = json.loads(response.content)
|
||||
self.assertEqual(response['error'], 'No file provided for video image')
|
||||
|
||||
def test_default_video_image(self):
|
||||
def test_no_video_image(self):
|
||||
"""
|
||||
Test default video image.
|
||||
Test image url is set to None if no video image.
|
||||
"""
|
||||
edx_video_id = 'test1'
|
||||
default_video_image_url = _get_default_video_image_url()
|
||||
get_videos_url = reverse_course_url('videos_handler', self.course.id)
|
||||
video_image_upload_url = self.get_url_for_course_key(self.course.id, {'edx_video_id': edx_video_id})
|
||||
with make_image_file() as image_file:
|
||||
@@ -606,7 +606,7 @@ class VideoImageTestCase(VideoUploadTestBase, CourseTestCase):
|
||||
if response_video['edx_video_id'] == edx_video_id:
|
||||
self.assertEqual(response_video['course_video_image_url'], val_image_url)
|
||||
else:
|
||||
self.assertEqual(response_video['course_video_image_url'], default_video_image_url)
|
||||
self.assertEqual(response_video['course_video_image_url'], None)
|
||||
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True})
|
||||
|
||||
@@ -336,7 +336,6 @@ def _get_index_videos(course):
|
||||
Returns the information about each video upload required for the video list
|
||||
"""
|
||||
course_id = unicode(course.id)
|
||||
default_video_image_url = _get_default_video_image_url()
|
||||
attrs = ['edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'courses']
|
||||
|
||||
def _get_values(video):
|
||||
@@ -347,8 +346,7 @@ def _get_index_videos(course):
|
||||
for attr in attrs:
|
||||
if attr == 'courses':
|
||||
course = filter(lambda c: course_id in c, video['courses'])
|
||||
(__, image_url), = course[0].items()
|
||||
values['course_video_image_url'] = image_url or default_video_image_url
|
||||
(__, values['course_video_image_url']), = course[0].items()
|
||||
else:
|
||||
values[attr] = video[attr]
|
||||
|
||||
@@ -370,6 +368,7 @@ def videos_index_html(course):
|
||||
'image_upload_url': reverse_course_url('video_images_handler', unicode(course.id)),
|
||||
'video_handler_url': reverse_course_url('videos_handler', unicode(course.id)),
|
||||
'encodings_download_url': reverse_course_url('video_encodings_download', unicode(course.id)),
|
||||
'default_video_image_url': _get_default_video_image_url(),
|
||||
'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(),
|
||||
|
||||
@@ -1349,4 +1349,4 @@ PROFILE_IMAGE_SIZES_MAP = {
|
||||
|
||||
###################### VIDEO IMAGE STORAGE ######################
|
||||
|
||||
VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png'
|
||||
VIDEO_IMAGE_DEFAULT_FILENAME = 'images/video-images/default_video_image.png'
|
||||
|
||||
@@ -342,6 +342,6 @@ VIDEO_IMAGE_SETTINGS = dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
DIRECTORY_PREFIX='videoimage/',
|
||||
DIRECTORY_PREFIX='video-images/',
|
||||
)
|
||||
VIDEO_IMAGE_DEFAULT_FILENAME = 'default_video_image.png'
|
||||
|
||||
@@ -258,6 +258,7 @@
|
||||
'js/spec/utils/module_spec',
|
||||
'js/spec/views/active_video_upload_list_spec',
|
||||
'js/spec/views/previous_video_upload_spec',
|
||||
'js/spec/views/video_thumbnail_spec',
|
||||
'js/spec/views/previous_video_upload_list_spec',
|
||||
'js/spec/views/assets_spec',
|
||||
'js/spec/views/baseview_spec',
|
||||
|
||||
BIN
cms/static/images/video-images/default_video_image.png
Normal file
BIN
cms/static/images/video-images/default_video_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -5,8 +5,10 @@ define([
|
||||
'use strict';
|
||||
var VideosIndexFactory = function(
|
||||
$contentWrapper,
|
||||
videoImageUploadURL,
|
||||
videoHandlerUrl,
|
||||
encodingsDownloadUrl,
|
||||
defaultVideoImageURL,
|
||||
concurrentUploadLimit,
|
||||
uploadButton,
|
||||
previousUploads,
|
||||
@@ -34,6 +36,8 @@ define([
|
||||
isActive[0].get('status') === ActiveVideoUpload.STATUS_COMPLETE;
|
||||
}),
|
||||
updatedView = new PreviousVideoUploadListView({
|
||||
videoImageUploadURL: videoImageUploadURL,
|
||||
defaultVideoImageURL: defaultVideoImageURL,
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
collection: updatedCollection,
|
||||
encodingsDownloadUrl: encodingsDownloadUrl
|
||||
@@ -43,6 +47,8 @@ define([
|
||||
}
|
||||
}),
|
||||
previousView = new PreviousVideoUploadListView({
|
||||
videoImageUploadURL: videoImageUploadURL,
|
||||
defaultVideoImageURL: defaultVideoImageURL,
|
||||
videoHandlerUrl: videoHandlerUrl,
|
||||
collection: new Backbone.Collection(previousUploads),
|
||||
encodingsDownloadUrl: encodingsDownloadUrl
|
||||
|
||||
@@ -30,24 +30,6 @@ define(
|
||||
expect($el.find('.name-col').text()).toEqual(testName);
|
||||
});
|
||||
|
||||
_.each(
|
||||
[
|
||||
{desc: 'zero as pending', seconds: 0, expected: 'Pending'},
|
||||
{desc: 'less than one second as zero', seconds: 0.75, expected: '0:00'},
|
||||
{desc: 'with minutes and without seconds', seconds: 900, expected: '15:00'},
|
||||
{desc: 'with seconds and without minutes', seconds: 15, expected: '0:15'},
|
||||
{desc: 'with minutes and seconds', seconds: 915, expected: '15:15'},
|
||||
{desc: 'with seconds padded', seconds: 5, expected: '0:05'},
|
||||
{desc: 'longer than an hour as many minutes', seconds: 7425, expected: '123:45'}
|
||||
],
|
||||
function(caseInfo) {
|
||||
it('should render duration ' + caseInfo.desc, function() {
|
||||
var $el = render({duration: caseInfo.seconds});
|
||||
expect($el.find('.duration-col').text()).toEqual(caseInfo.expected);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('should render created timestamp correctly', function() {
|
||||
var fakeDate = 'fake formatted date';
|
||||
spyOn(Date.prototype, 'toLocaleString').and.callFake(
|
||||
|
||||
187
cms/static/js/spec/views/video_thumbnail_spec.js
Normal file
187
cms/static/js/spec/views/video_thumbnail_spec.js
Normal file
@@ -0,0 +1,187 @@
|
||||
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) {
|
||||
'use strict';
|
||||
describe('VideoThumbnailView', function() {
|
||||
var IMAGE_UPLOAD_URL = '/videos/upload/image',
|
||||
UPLOADED_IMAGE_URL = 'images/upload_success.jpg',
|
||||
videoThumbnailView,
|
||||
createFakeImageFile,
|
||||
verifyStateInfo,
|
||||
render = function(modelData) {
|
||||
var 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'}
|
||||
);
|
||||
};
|
||||
|
||||
verifyStateInfo = function($thumbnail, state, onHover, additionalSRText) {
|
||||
var beforeIcon,
|
||||
beforeText;
|
||||
|
||||
// Verify hover message, save the text before hover to verify later
|
||||
if (onHover) {
|
||||
beforeIcon = $thumbnail.find('.action-icon').html().trim();
|
||||
beforeText = $thumbnail.find('.action-text').html().trim();
|
||||
$thumbnail.trigger('mouseover');
|
||||
}
|
||||
|
||||
if (additionalSRText) {
|
||||
expect(
|
||||
$thumbnail.find('.thumbnail-action .action-text-sr').text().trim()
|
||||
).toEqual(additionalSRText);
|
||||
}
|
||||
|
||||
expect($thumbnail.find('.action-icon').html().trim()).toEqual(
|
||||
videoThumbnailView.actionsInfo[state].icon
|
||||
);
|
||||
expect($thumbnail.find('.action-text').html().trim()).toEqual(
|
||||
videoThumbnailView.actionsInfo[state].text
|
||||
);
|
||||
|
||||
// Verify if messages are restored after focus moved away
|
||||
if (onHover) {
|
||||
$thumbnail.trigger('mouseout');
|
||||
expect($thumbnail.find('.action-icon').html().trim()).toEqual(beforeIcon);
|
||||
expect($thumbnail.find('.action-text').html().trim()).toEqual(beforeText);
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div id="page-prompt"></div><div id="page-notification"></div>');
|
||||
TemplateHelpers.installTemplate('video-thumbnail');
|
||||
});
|
||||
|
||||
it('renders as expected', function() {
|
||||
var $el = render({});
|
||||
expect($el.find('.thumbnail-wrapper')).toExist();
|
||||
expect($el.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();
|
||||
});
|
||||
|
||||
it('shows the duration if available', function() {
|
||||
var $el = render({}),
|
||||
$duration = $el.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');
|
||||
});
|
||||
|
||||
it('calculates duration correctly', function() {
|
||||
var durations = [
|
||||
{duration: -1},
|
||||
{duration: 0},
|
||||
{duration: 0.75, machine: '0:00', humanize: ''},
|
||||
{duration: 5, machine: '0:05', humanize: 'Video duration is 5 seconds'},
|
||||
{duration: 103, machine: '1:43', humanize: 'Video duration is 1 minute and 43 seconds'},
|
||||
{duration: 120, machine: '2:00', humanize: 'Video duration is 2 minutes'},
|
||||
{duration: 500, machine: '8:20', humanize: 'Video duration is 8 minutes and 20 seconds'},
|
||||
{duration: 7425, machine: '123:45', humanize: 'Video duration is 123 minutes and 45 seconds'}
|
||||
],
|
||||
expectedDuration;
|
||||
|
||||
durations.forEach(function(item) {
|
||||
expectedDuration = videoThumbnailView.getDuration(item.duration);
|
||||
if (item.duration <= 0) {
|
||||
expect(expectedDuration).toEqual(null);
|
||||
} else {
|
||||
expect(expectedDuration.machine).toEqual(item.machine);
|
||||
expect(expectedDuration.humanize).toEqual(item.humanize);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('can upload image', function() {
|
||||
var $el = render({}),
|
||||
$thumbnail = $el.find('.thumbnail-wrapper'),
|
||||
requests = AjaxHelpers.requests(this),
|
||||
additionalSRText = videoThumbnailView.getSRText();
|
||||
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
verifyStateInfo($thumbnail, 'upload');
|
||||
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)]});
|
||||
|
||||
verifyStateInfo($thumbnail, 'progress');
|
||||
|
||||
// Verify if POST request received for image upload
|
||||
AjaxHelpers.expectRequest(requests, 'POST', IMAGE_UPLOAD_URL + '/dummy_id', new FormData());
|
||||
|
||||
// Send successful upload response
|
||||
AjaxHelpers.respondWithJson(requests, {image_url: UPLOADED_IMAGE_URL});
|
||||
|
||||
verifyStateInfo($thumbnail, 'edit', true);
|
||||
|
||||
// Verify uploaded image src
|
||||
expect($thumbnail.find('img').attr('src')).toEqual(UPLOADED_IMAGE_URL);
|
||||
});
|
||||
|
||||
it('shows error state correctly', function() {
|
||||
var $el = render({}),
|
||||
$thumbnail = $el.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)]});
|
||||
|
||||
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);
|
||||
|
||||
videoThumbnailView.chooseFile();
|
||||
|
||||
// Add image to upload queue and send POST request to upload image
|
||||
$el.find('.upload-image-input').fileupload('add', {files: [createFakeImageFile(60)]});
|
||||
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
expect($('#notification-error-title').text().trim()).toEqual(
|
||||
"Studio's having trouble saving your work"
|
||||
);
|
||||
});
|
||||
|
||||
it('calls readMessage with correct message', function() {
|
||||
spyOn(videoThumbnailView, 'readMessages');
|
||||
|
||||
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']);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,8 +1,9 @@
|
||||
define(
|
||||
['underscore', 'gettext', 'js/utils/date_utils', 'js/views/baseview', 'common/js/components/views/feedback_prompt',
|
||||
'common/js/components/views/feedback_notification', 'common/js/components/utils/view_utils',
|
||||
'edx-ui-toolkit/js/utils/html-utils', 'text!templates/previous-video-upload.underscore'],
|
||||
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, ViewUtils, HtmlUtils,
|
||||
'common/js/components/views/feedback_notification', 'js/views/video_thumbnail',
|
||||
'common/js/components/utils/view_utils', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'text!templates/previous-video-upload.underscore'],
|
||||
function(_, gettext, DateUtils, BaseView, PromptView, NotificationView, VideoThumbnailView, ViewUtils, HtmlUtils,
|
||||
previousVideoUploadTemplate) {
|
||||
'use strict';
|
||||
|
||||
@@ -16,22 +17,15 @@ define(
|
||||
initialize: function(options) {
|
||||
this.template = HtmlUtils.template(previousVideoUploadTemplate);
|
||||
this.videoHandlerUrl = options.videoHandlerUrl;
|
||||
},
|
||||
|
||||
renderDuration: function(seconds) {
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var seconds = Math.floor(seconds - minutes * 60);
|
||||
|
||||
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
this.videoThumbnailView = new VideoThumbnailView({
|
||||
model: this.model,
|
||||
imageUploadURL: options.videoImageUploadURL,
|
||||
defaultVideoImageURL: options.defaultVideoImageURL
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var duration = this.model.get('duration');
|
||||
var renderedAttributes = {
|
||||
// Translators: This is listed as the duration for a video
|
||||
// that has not yet reached the point in its processing by
|
||||
// the servers where its duration is determined.
|
||||
duration: duration > 0 ? this.renderDuration(duration) : gettext('Pending'),
|
||||
created: DateUtils.renderDate(this.model.get('created')),
|
||||
status: this.model.get('status')
|
||||
};
|
||||
@@ -41,6 +35,7 @@ define(
|
||||
_.extend({}, this.model.attributes, renderedAttributes)
|
||||
)
|
||||
);
|
||||
this.videoThumbnailView.setElement(this.$('.thumbnail-col')).render();
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ define(
|
||||
this.encodingsDownloadUrl = options.encodingsDownloadUrl;
|
||||
this.itemViews = this.collection.map(function(model) {
|
||||
return new PreviousVideoUploadView({
|
||||
videoImageUploadURL: options.videoImageUploadURL,
|
||||
defaultVideoImageURL: options.defaultVideoImageURL,
|
||||
videoHandlerUrl: options.videoHandlerUrl,
|
||||
model: model
|
||||
});
|
||||
|
||||
225
cms/static/js/views/video_thumbnail.js
Normal file
225
cms/static/js/views/video_thumbnail.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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) {
|
||||
'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 | .gif'),
|
||||
{
|
||||
lineBreak: HtmlUtils.HTML('<br>')
|
||||
}
|
||||
).toString()
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .thumbnail-wrapper': 'chooseFile',
|
||||
'mouseover .thumbnail-wrapper': 'showHoverState',
|
||||
'mouseout .thumbnail-wrapper': 'hideHoverState',
|
||||
'focus .thumbnail-wrapper': 'showHoverState',
|
||||
'blur .thumbnail-wrapper': 'hideHoverState'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = HtmlUtils.template(VideoThumbnailTemplate);
|
||||
this.imageUploadURL = options.imageUploadURL;
|
||||
this.defaultVideoImageURL = options.defaultVideoImageURL;
|
||||
this.action = this.model.get('course_video_image_url') ? 'edit' : 'upload';
|
||||
_.bindAll(
|
||||
this, 'render', 'chooseFile', 'imageSelected', 'imageUploadSucceeded', 'imageUploadFailed',
|
||||
'showHoverState', 'hideHoverState'
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
this.template({
|
||||
action: this.action,
|
||||
imageAltText: this.getImageAltText(),
|
||||
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'))
|
||||
})
|
||||
);
|
||||
this.hideHoverState();
|
||||
return this;
|
||||
},
|
||||
|
||||
getImageAltText: function() {
|
||||
return StringUtils.interpolate(
|
||||
// Translators: message will be like Thumbnail for Arrow.mp4
|
||||
gettext('Thumbnail for {videoName}'),
|
||||
{videoName: this.model.get('client_video_id')}
|
||||
);
|
||||
},
|
||||
|
||||
getSRText: function() {
|
||||
return StringUtils.interpolate(
|
||||
// Translators: message will be like Add Thumbnail - Arrow.mp4
|
||||
gettext('Add Thumbnail - {videoName}'),
|
||||
{videoName: this.model.get('client_video_id')}
|
||||
);
|
||||
},
|
||||
|
||||
getDuration: function(durationSeconds) {
|
||||
if (durationSeconds <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
humanize: this.getDurationTextHuman(durationSeconds),
|
||||
machine: this.getDurationTextMachine(durationSeconds)
|
||||
};
|
||||
},
|
||||
|
||||
getDurationTextHuman: function(durationSeconds) {
|
||||
var humanize = this.getHumanizeDuration(durationSeconds);
|
||||
|
||||
// This case is specifically to handle values between 0 and 1 seconds excluding upper bound
|
||||
if (humanize.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return StringUtils.interpolate(
|
||||
// Translators: humanizeDuration will be like 10 minutes, an hour and 20 minutes etc
|
||||
gettext('Video duration is {humanizeDuration}'),
|
||||
{
|
||||
humanizeDuration: humanize
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getHumanizeDuration: function(durationSeconds) {
|
||||
var minutes,
|
||||
seconds,
|
||||
minutesText = null,
|
||||
secondsText = null;
|
||||
|
||||
minutes = Math.trunc(moment.duration(durationSeconds, 'seconds').asMinutes());
|
||||
seconds = moment.duration(durationSeconds, 'seconds').seconds();
|
||||
|
||||
if (minutes) {
|
||||
minutesText = minutes > 1 ? gettext('minutes') : gettext('minute');
|
||||
minutesText = StringUtils.interpolate(
|
||||
// Translators: message will be like 15 minutes, 1 minute
|
||||
gettext('{minutes} {unit}'),
|
||||
{minutes: minutes, unit: minutesText}
|
||||
);
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
secondsText = seconds > 1 ? gettext('seconds') : gettext('second');
|
||||
secondsText = StringUtils.interpolate(
|
||||
// Translators: message will be like 20 seconds, 1 second
|
||||
gettext('{seconds} {unit}'),
|
||||
{seconds: seconds, unit: secondsText}
|
||||
);
|
||||
}
|
||||
|
||||
// Translators: `and` will be used to combine both miuntes and seconds like `13 minutes and 45 seconds`
|
||||
return _.filter([minutesText, secondsText]).join(gettext(' and '));
|
||||
},
|
||||
|
||||
getDurationTextMachine: function(durationSeconds) {
|
||||
var minutes = Math.floor(durationSeconds / 60),
|
||||
seconds = Math.floor(durationSeconds - minutes * 60);
|
||||
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
||||
},
|
||||
|
||||
chooseFile: function() {
|
||||
this.$('.upload-image-input').fileupload({
|
||||
url: this.imageUploadURL + '/' + encodeURIComponent(this.model.get('edx_video_id')),
|
||||
add: this.imageSelected,
|
||||
done: this.imageUploadSucceeded,
|
||||
fail: this.imageUploadFailed
|
||||
});
|
||||
},
|
||||
|
||||
imageSelected: function(event, data) {
|
||||
this.readMessages([gettext('Video image upload started')]);
|
||||
this.showUploadInProgressMessage();
|
||||
data.submit();
|
||||
},
|
||||
|
||||
imageUploadSucceeded: function(event, data) {
|
||||
this.action = 'edit';
|
||||
this.setActionInfo(this.action, false);
|
||||
this.$('img').attr('src', data.result.image_url);
|
||||
this.readMessages([gettext('Video image upload completed')]);
|
||||
},
|
||||
|
||||
imageUploadFailed: function() {
|
||||
this.action = 'error';
|
||||
this.setActionInfo(this.action, true);
|
||||
this.readMessages([gettext('Video image upload failed')]);
|
||||
},
|
||||
|
||||
showUploadInProgressMessage: function() {
|
||||
this.action = 'progress';
|
||||
this.setActionInfo(this.action, true);
|
||||
},
|
||||
|
||||
showHoverState: function() {
|
||||
if (this.action === 'upload') {
|
||||
this.setActionInfo('requirements', true, this.getSRText());
|
||||
} else if (this.action === 'edit') {
|
||||
this.setActionInfo(this.action, true);
|
||||
}
|
||||
this.$('.thumbnail-wrapper').addClass('focused');
|
||||
},
|
||||
|
||||
hideHoverState: function() {
|
||||
if (this.action === 'upload') {
|
||||
this.setActionInfo(this.action, true);
|
||||
} else if (this.action === 'edit') {
|
||||
this.setActionInfo(this.action, false);
|
||||
}
|
||||
},
|
||||
|
||||
setActionInfo: function(action, showText, additionalSRText) {
|
||||
this.$('.thumbnail-action').toggle(showText);
|
||||
HtmlUtils.setHtml(
|
||||
this.$('.thumbnail-action .action-icon'),
|
||||
HtmlUtils.HTML(this.actionsInfo[action].icon)
|
||||
);
|
||||
this.$('.thumbnail-action .action-text').html(this.actionsInfo[action].text);
|
||||
this.$('.thumbnail-action .action-text-sr').text(additionalSRText || '');
|
||||
this.$('.thumbnail-wrapper').attr('class', 'thumbnail-wrapper {action}'.replace('{action}', action));
|
||||
},
|
||||
|
||||
readMessages: function(messages) {
|
||||
if ($(window).prop('SR') !== undefined) {
|
||||
$(window).prop('SR').readTexts(messages);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return VideoThumbnailView;
|
||||
}
|
||||
);
|
||||
@@ -163,4 +163,120 @@
|
||||
@extend %actions-list;
|
||||
}
|
||||
}
|
||||
|
||||
$thumbnail-width: ($baseline*7.5);
|
||||
$thumbnail-height: ($baseline*5);
|
||||
|
||||
.thumbnail-wrapper {
|
||||
position: relative;
|
||||
max-width: $thumbnail-width;
|
||||
max-height: $thumbnail-height;
|
||||
|
||||
img {
|
||||
width: $thumbnail-width;
|
||||
height: $thumbnail-height;
|
||||
}
|
||||
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.upload,
|
||||
&.requirements {
|
||||
border: 1px dashed $gray-l3;
|
||||
}
|
||||
|
||||
&.requirements {
|
||||
.video-duration {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.edit {
|
||||
background: black;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.focused {
|
||||
img, .video-duration {
|
||||
@include transition(all 0.3s linear);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.progress {
|
||||
background: white;
|
||||
|
||||
img {
|
||||
@include transition(all 0.5s linear);
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.error .thumbnail-action {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.upload .thumbnail-action {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&.progress .thumbnail-action {
|
||||
.action-icon {
|
||||
@include font-size(20);
|
||||
}
|
||||
}
|
||||
|
||||
&.edit .thumbnail-action {
|
||||
background-color: white;
|
||||
padding: ($baseline/4);
|
||||
border-radius: ($baseline/5);
|
||||
}
|
||||
|
||||
.thumbnail-action {
|
||||
@include font-size(14);
|
||||
}
|
||||
|
||||
.thumbnail-overlay > :not(.upload-image-input) {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 5px;;
|
||||
right: 5px;
|
||||
@include transform(translateY(-50%));
|
||||
}
|
||||
|
||||
.upload-image-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
z-index: 6;
|
||||
width: $thumbnail-width;
|
||||
height: $thumbnail-height;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
bottom: 1px;
|
||||
@include right(1px);
|
||||
width: auto;
|
||||
min-width: 25%;
|
||||
color: white;
|
||||
padding: ($baseline/10) ($baseline/5);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
box-shadow: 0 0 ($baseline/5) 1px $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<table class="assets-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%- gettext("Name") %></th>
|
||||
<th><%- gettext("Duration") %></th>
|
||||
<th><%- gettext("Date Added") %></th>
|
||||
<th><%- gettext("Video ID") %></th>
|
||||
<th><%- gettext("Status") %></th>
|
||||
<th><%- gettext("Action") %></th>
|
||||
<th scope="col"><%- gettext("Thumbnail") %></th>
|
||||
<th scope="col"><%- gettext("Name") %></th>
|
||||
<th scope="col"><%- gettext("Date Added") %></th>
|
||||
<th scope="col"><%- gettext("Video ID") %></th>
|
||||
<th scope="col"><%- gettext("Status") %></th>
|
||||
<th scope="col"><%- gettext("Action") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="js-table-body"></tbody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<td class="thumbnail-col"></td>
|
||||
<td class="name-col"><%- client_video_id %></td>
|
||||
<td class="duration-col"><%- duration %></td>
|
||||
<td class="date-col"><%- created %></td>
|
||||
<td class="video-id-col"><%- edx_video_id %></td>
|
||||
<td class="status-col"><%- status %></td>
|
||||
|
||||
21
cms/templates/js/video-thumbnail.underscore
Normal file
21
cms/templates/js/video-thumbnail.underscore
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="thumbnail-wrapper <%- action === 'upload' ? 'upload' : '' %>" tabindex="-1">
|
||||
<img src="<%- thumbnailURL %>" alt="<%- imageAltText %>">
|
||||
<div class="thumbnail-overlay">
|
||||
<input id="thumb-<%- videoId %>" class="upload-image-input" type="file" name="file" accept=".bmp, .jpg, .jpeg, .png, .gif"/>
|
||||
<label for="thumb-<%- videoId %>" class="thumbnail-action">
|
||||
<span class="action-icon" aria-hidden="true"><%- actionInfo.icon %></span>
|
||||
<span class="action-text-sr sr"></span>
|
||||
<span class="action-text"><%- actionInfo.text %></span>
|
||||
</label>
|
||||
|
||||
<span class="requirements-text-sr sr">
|
||||
<%- gettext('Recommended image resolution is 1280x720 pixels and format must be one of .jpg, .png or .gif') %>
|
||||
</span>
|
||||
</div>
|
||||
<% if(duration) { %>
|
||||
<div class="video-duration">
|
||||
<span class="duration-text-human sr"><%- duration.humanize %></span>
|
||||
<span class="duration-text-machine" aria-hidden="true"><%- duration.machine %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -29,8 +29,10 @@
|
||||
var $contentWrapper = $(".content-primary");
|
||||
VideosIndexFactory(
|
||||
$contentWrapper,
|
||||
"${image_upload_url | n, js_escaped_string}",
|
||||
"${video_handler_url | n, js_escaped_string}",
|
||||
"${encodings_download_url | n, js_escaped_string}",
|
||||
"${default_video_image_url | n, js_escaped_string}",
|
||||
${concurrent_upload_limit | n, dump_js_escaped_json},
|
||||
$(".nav-actions .upload-button"),
|
||||
$contentWrapper.data("previous-uploads"),
|
||||
|
||||
@@ -2574,7 +2574,7 @@ VIDEO_IMAGE_SETTINGS = dict(
|
||||
location=MEDIA_ROOT,
|
||||
base_url=MEDIA_URL,
|
||||
),
|
||||
DIRECTORY_PREFIX='videoimage/',
|
||||
DIRECTORY_PREFIX='video-images/',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<input class="upload-button-input" type="file" name="<%= inputName %>"/>
|
||||
</label>
|
||||
<button class="upload-submit" type="button" hidden="true"><%= uploadButtonTitle %></button>
|
||||
|
||||
<button class="u-field-remove-button" type="button">
|
||||
<span class="remove-button-icon" aria-hidden="true"><%= removeButtonIcon %></span>
|
||||
<span class="remove-button-title" aria-live="polite"><%= removeButtonTitle %></span>
|
||||
|
||||
Reference in New Issue
Block a user