Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
763f0051
Commit
763f0051
authored
Apr 07, 2017
by
muhammad-ammar
Committed by
Mushtaq Ali
Jul 06, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
video thumbnail ui
parent
dca12d65
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
586 additions
and
51 deletions
+586
-51
cms/djangoapps/contentstore/views/tests/test_videos.py
+4
-4
cms/djangoapps/contentstore/views/videos.py
+2
-3
cms/envs/common.py
+1
-1
cms/envs/test.py
+1
-1
cms/static/cms/js/spec/main.js
+1
-0
cms/static/images/video-images/default_video_image.png
+0
-0
cms/static/js/factories/videos_index.js
+6
-0
cms/static/js/spec/views/previous_video_upload_spec.js
+0
-18
cms/static/js/spec/views/video_thumbnail_spec.js
+187
-0
cms/static/js/views/previous_video_upload.js
+10
-15
cms/static/js/views/previous_video_upload_list.js
+2
-0
cms/static/js/views/video_thumbnail.js
+225
-0
cms/static/sass/views/_video-upload.scss
+116
-0
cms/templates/js/previous-video-upload-list.underscore
+6
-6
cms/templates/js/previous-video-upload.underscore
+1
-1
cms/templates/js/video-thumbnail.underscore
+21
-0
cms/templates/videos_index.html
+2
-0
lms/envs/common.py
+1
-1
lms/templates/fields/field_image.underscore
+0
-1
No files found.
cms/djangoapps/contentstore/views/tests/test_videos.py
View file @
763f0051
...
...
@@ -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
})
...
...
cms/djangoapps/contentstore/views/videos.py
View file @
763f0051
...
...
@@ -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
(),
...
...
cms/envs/common.py
View file @
763f0051
...
...
@@ -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'
cms/envs/test.py
View file @
763f0051
...
...
@@ -342,6 +342,6 @@ VIDEO_IMAGE_SETTINGS = dict(
location
=
MEDIA_ROOT
,
base_url
=
MEDIA_URL
,
),
DIRECTORY_PREFIX
=
'video
image
/'
,
DIRECTORY_PREFIX
=
'video
-images
/'
,
)
VIDEO_IMAGE_DEFAULT_FILENAME
=
'default_video_image.png'
cms/static/cms/js/spec/main.js
View file @
763f0051
...
...
@@ -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'
,
...
...
cms/static/images/video-images/default_video_image.png
0 → 100644
View file @
763f0051
3.09 KB
cms/static/js/factories/videos_index.js
View file @
763f0051
...
...
@@ -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
...
...
cms/static/js/spec/views/previous_video_upload_spec.js
View file @
763f0051
...
...
@@ -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
(
...
...
cms/static/js/spec/views/video_thumbnail_spec.js
0 → 100644
View file @
763f0051
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'
]);
});
});
}
);
cms/static/js/views/previous_video_upload.js
View file @
763f0051
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
;
},
...
...
cms/static/js/views/previous_video_upload_list.js
View file @
763f0051
...
...
@@ -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
});
...
...
cms/static/js/views/video_thumbnail.js
0 → 100644
View file @
763f0051
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
;
}
);
cms/static/sass/views/_video-upload.scss
View file @
763f0051
...
...
@@ -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
;
}
}
}
cms/templates/js/previous-video-upload-list.underscore
View file @
763f0051
...
...
@@ -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>
...
...
cms/templates/js/previous-video-upload.underscore
View file @
763f0051
<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>
...
...
cms/templates/js/video-thumbnail.underscore
0 → 100644
View file @
763f0051
<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>
cms/templates/videos_index.html
View file @
763f0051
...
...
@@ -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"),
...
...
lms/envs/common.py
View file @
763f0051
...
...
@@ -2574,7 +2574,7 @@ VIDEO_IMAGE_SETTINGS = dict(
location
=
MEDIA_ROOT
,
base_url
=
MEDIA_URL
,
),
DIRECTORY_PREFIX
=
'video
image
/'
,
DIRECTORY_PREFIX
=
'video
-images
/'
,
)
...
...
lms/templates/fields/field_image.underscore
View file @
763f0051
...
...
@@ -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>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment