diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index 0b3ea1214f..d02835a7a2 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -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}) diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py index 64e1d254fa..81b8f4e73c 100644 --- a/cms/djangoapps/contentstore/views/videos.py +++ b/cms/djangoapps/contentstore/views/videos.py @@ -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(), diff --git a/cms/envs/common.py b/cms/envs/common.py index 484d216db9..fa74242274 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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' diff --git a/cms/envs/test.py b/cms/envs/test.py index 332a369891..0e6b5ad465 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -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' diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 93298c1470..0085de004e 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -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', diff --git a/cms/static/images/video-images/default_video_image.png b/cms/static/images/video-images/default_video_image.png new file mode 100644 index 0000000000..f331805aec Binary files /dev/null and b/cms/static/images/video-images/default_video_image.png differ diff --git a/cms/static/js/factories/videos_index.js b/cms/static/js/factories/videos_index.js index 3db118037f..07a193e7bb 100644 --- a/cms/static/js/factories/videos_index.js +++ b/cms/static/js/factories/videos_index.js @@ -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 diff --git a/cms/static/js/spec/views/previous_video_upload_spec.js b/cms/static/js/spec/views/previous_video_upload_spec.js index e6155b7572..2b0c164863 100644 --- a/cms/static/js/spec/views/previous_video_upload_spec.js +++ b/cms/static/js/spec/views/previous_video_upload_spec.js @@ -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( diff --git a/cms/static/js/spec/views/video_thumbnail_spec.js b/cms/static/js/spec/views/video_thumbnail_spec.js new file mode 100644 index 0000000000..c62eae63de --- /dev/null +++ b/cms/static/js/spec/views/video_thumbnail_spec.js @@ -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('
'); + 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']); + }); + }); + } +); diff --git a/cms/static/js/views/previous_video_upload.js b/cms/static/js/views/previous_video_upload.js index 430d7e680c..bdea422af1 100644 --- a/cms/static/js/views/previous_video_upload.js +++ b/cms/static/js/views/previous_video_upload.js @@ -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; }, diff --git a/cms/static/js/views/previous_video_upload_list.js b/cms/static/js/views/previous_video_upload_list.js index 819e66695c..31e9b543f6 100644 --- a/cms/static/js/views/previous_video_upload_list.js +++ b/cms/static/js/views/previous_video_upload_list.js @@ -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 }); diff --git a/cms/static/js/views/video_thumbnail.js b/cms/static/js/views/video_thumbnail.js new file mode 100644 index 0000000000..ec757a891e --- /dev/null +++ b/cms/static/js/views/video_thumbnail.js @@ -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: '', + text: gettext('Edit Thumbnail') + }, + error: { + icon: '', + text: gettext('Image upload failed') + }, + progress: { + icon: '', + 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('
') + } + ).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; + } +); diff --git a/cms/static/sass/views/_video-upload.scss b/cms/static/sass/views/_video-upload.scss index bc9f13aa3b..4d2a1bd1b7 100644 --- a/cms/static/sass/views/_video-upload.scss +++ b/cms/static/sass/views/_video-upload.scss @@ -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; + } + } } diff --git a/cms/templates/js/previous-video-upload-list.underscore b/cms/templates/js/previous-video-upload-list.underscore index c7a7ef0988..b0cc3cb806 100644 --- a/cms/templates/js/previous-video-upload-list.underscore +++ b/cms/templates/js/previous-video-upload-list.underscore @@ -8,12 +8,12 @@ - - - - - - + + + + + + diff --git a/cms/templates/js/previous-video-upload.underscore b/cms/templates/js/previous-video-upload.underscore index 0db4d2e227..e9154bcb3c 100644 --- a/cms/templates/js/previous-video-upload.underscore +++ b/cms/templates/js/previous-video-upload.underscore @@ -1,5 +1,5 @@ + - diff --git a/cms/templates/js/video-thumbnail.underscore b/cms/templates/js/video-thumbnail.underscore new file mode 100644 index 0000000000..dae21fe398 --- /dev/null +++ b/cms/templates/js/video-thumbnail.underscore @@ -0,0 +1,21 @@ +
+ <%- imageAltText %> +
+ + + + + <%- gettext('Recommended image resolution is 1280x720 pixels and format must be one of .jpg, .png or .gif') %> + +
+ <% if(duration) { %> +
+ <%- duration.humanize %> + +
+ <% } %> +
diff --git a/cms/templates/videos_index.html b/cms/templates/videos_index.html index dceaa32daf..0a3d54d480 100644 --- a/cms/templates/videos_index.html +++ b/cms/templates/videos_index.html @@ -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"), diff --git a/lms/envs/common.py b/lms/envs/common.py index eeb71ada91..419bd3822b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2574,7 +2574,7 @@ VIDEO_IMAGE_SETTINGS = dict( location=MEDIA_ROOT, base_url=MEDIA_URL, ), - DIRECTORY_PREFIX='videoimage/', + DIRECTORY_PREFIX='video-images/', ) diff --git a/lms/templates/fields/field_image.underscore b/lms/templates/fields/field_image.underscore index d4c677ea6d..79c422ed30 100644 --- a/lms/templates/fields/field_image.underscore +++ b/lms/templates/fields/field_image.underscore @@ -7,7 +7,6 @@ -
<%- gettext("Name") %><%- gettext("Duration") %><%- gettext("Date Added") %><%- gettext("Video ID") %><%- gettext("Status") %><%- gettext("Action") %><%- gettext("Thumbnail") %><%- gettext("Name") %><%- gettext("Date Added") %><%- gettext("Video ID") %><%- gettext("Status") %><%- gettext("Action") %>
<%- client_video_id %><%- duration %> <%- created %> <%- edx_video_id %> <%- status %>