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
127bf7ed
Commit
127bf7ed
authored
Apr 14, 2014
by
polesye
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
BLD-1000: Download handout.
parent
f3842dcc
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
498 additions
and
81 deletions
+498
-81
CHANGELOG.rst
+3
-0
cms/djangoapps/contentstore/features/video.py
+1
-0
cms/djangoapps/contentstore/features/video_editor.feature
+0
-0
cms/djangoapps/contentstore/features/video_editor.py
+1
-0
cms/djangoapps/contentstore/features/video_handout.feature
+71
-0
cms/djangoapps/contentstore/features/video_handout.py
+40
-0
cms/static/coffee/spec/main_squire.coffee
+2
-1
cms/static/coffee/spec/models/upload_spec.coffee
+4
-4
cms/static/js/models/uploads.js
+2
-1
cms/static/js/spec/video/file_uploader_editor_spec.js
+176
-0
cms/static/js/spec/video/translations_editor_spec.js
+19
-51
cms/static/js/views/abstract_editor.js
+15
-6
cms/static/js/views/metadata.js
+65
-5
cms/static/sass/views/_unit.scss
+35
-0
cms/templates/js/metadata-file-uploader-entry.underscore
+9
-0
cms/templates/js/metadata-file-uploader-item.underscore
+3
-0
cms/templates/widgets/metadata-edit.html
+1
-1
cms/templates/widgets/tabs/metadata-edit-tab.html
+1
-1
common/lib/xmodule/xmodule/css/video/display.scss
+1
-2
common/lib/xmodule/xmodule/tests/test_video.py
+13
-2
common/lib/xmodule/xmodule/video_module/video_module.py
+12
-0
common/lib/xmodule/xmodule/video_module/video_xfields.py
+12
-5
lms/djangoapps/courseware/tests/test_video_mongo.py
+5
-0
lms/templates/video.html
+7
-2
No files found.
CHANGELOG.rst
View file @
127bf7ed
...
...
@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Add an upload button for authors to provide students with an option to
download a handout associated with a video (of arbitrary file format). BLD-1000.
Blades: Show the HD button only if there is an HD version available. BLD-937.
Studio: Add edit button to leaf xblocks on the container page. STUD-1306.
...
...
cms/djangoapps/contentstore/features/video.py
View file @
127bf7ed
...
...
@@ -11,6 +11,7 @@ VIDEO_BUTTONS = {
'volume'
:
'.volume'
,
'play'
:
'.video_control.play'
,
'pause'
:
'.video_control.pause'
,
'handout'
:
'.video-handout.video-download-button a'
,
}
SELECTORS
=
{
...
...
cms/djangoapps/contentstore/features/video
-
editor.feature
→
cms/djangoapps/contentstore/features/video
_
editor.feature
View file @
127bf7ed
File moved
cms/djangoapps/contentstore/features/video
-
editor.py
→
cms/djangoapps/contentstore/features/video
_
editor.py
View file @
127bf7ed
...
...
@@ -148,6 +148,7 @@ def correct_video_settings(_step):
[
'Transcript Display'
,
'True'
,
False
],
[
'Transcript Download Allowed'
,
'False'
,
False
],
[
'Transcript Translations'
,
''
,
False
],
[
'Upload Handout'
,
''
,
False
],
[
'Video Download Allowed'
,
'False'
,
False
],
[
'Video Sources'
,
''
,
False
],
[
'Youtube ID'
,
'OEoXaMPEzfM'
,
False
],
...
...
cms/djangoapps/contentstore/features/video_handout.feature
0 → 100644
View file @
127bf7ed
@shard_3
Feature
:
CMS Video Component Handout
As a course author, I want to be able to create video handout
# 1
Scenario
:
Handout uploading works correctly
Given
I have created a Video component with handout file
"textbook.pdf"
And
I save changes
Then
I can see video button
"handout"
And
I can download handout file with mime type
"application/pdf"
# 2
Scenario
:
Handout downloading works correctly w/ preliminary saving
Given
I have created a Video component with handout file
"textbook.pdf"
And
I save changes
And
I edit the component
And
I open tab
"Advanced"
And
I can download handout file in editor with mime type
"application/pdf"
# 3
Scenario
:
Handout downloading works correctly w/o preliminary saving
Given
I have created a Video component with handout file
"textbook.pdf"
And
I can download handout file in editor with mime type
"application/pdf"
# 4
Scenario
:
Handout clearing works correctly w/ preliminary saving
Given
I have created a Video component with handout file
"textbook.pdf"
And
I save changes
And
I can download handout file with mime type
"application/pdf"
And
I edit the component
And
I open tab
"Advanced"
And
I clear handout
And
I save changes
Then
I do not see video button
"handout"
# 5
Scenario
:
Handout clearing works correctly w/o preliminary saving
Given
I have created a Video component with handout file
"asset.html"
And
I clear handout
And
I save changes
Then
I do not see video button
"handout"
# 6
Scenario
:
User can easy replace the handout by another one w/ preliminary saving
Given
I have created a Video component with handout file
"asset.html"
And
I save changes
Then
I can see video button
"handout"
And
I can download handout file with mime type
"text/html"
And
I edit the component
And
I open tab
"Advanced"
And
I replace handout file by
"textbook.pdf"
And
I save changes
Then
I can see video button
"handout"
And
I can download handout file with mime type
"application/pdf"
# 7
Scenario
:
User can easy replace the handout by another one w/o preliminary saving
Given
I have created a Video component with handout file
"asset.html"
And
I replace handout file by
"textbook.pdf"
And
I save changes
Then
I can see video button
"handout"
And
I can download handout file with mime type
"application/pdf"
# 8
Scenario
:
Upload file "A" -> Remove it -> Upload file "B"
Given
I have created a Video component with handout file
"asset.html"
And
I clear handout
And
I upload handout file
"textbook.pdf"
And
I save changes
Then
I can see video button
"handout"
And
I can download handout file with mime type
"application/pdf"
cms/djangoapps/contentstore/features/video_handout.py
0 → 100644
View file @
127bf7ed
# -*- coding: utf-8 -*-
# disable missing docstring
# pylint: disable=C0111
from
lettuce
import
world
,
step
from
nose.tools
import
assert_true
# pylint: disable=E0611
from
video_editor
import
RequestHandlerWithSessionId
,
success_upload_file
@step
(
'I (?:upload|replace) handout file(?: by)? "([^"]*)"$'
)
def
upload_handout
(
step
,
filename
):
world
.
css_click
(
'.wrapper-comp-setting.file-uploader .upload-action'
)
success_upload_file
(
filename
)
@step
(
'I can download handout file( in editor)? with mime type "([^"]*)"$'
)
def
i_can_download_handout_with_mime_type
(
_step
,
is_editor
,
mime_type
):
if
is_editor
:
selector
=
'.wrapper-comp-setting.file-uploader .download-action'
else
:
selector
=
'.video-handout.video-download-button a'
button
=
world
.
css_find
(
selector
)
.
first
url
=
button
[
'href'
]
request
=
RequestHandlerWithSessionId
()
assert_true
(
request
.
get
(
url
)
.
is_success
())
assert_true
(
request
.
check_header
(
'content-type'
,
mime_type
))
@step
(
'I clear handout$'
)
def
clear_handout
(
_step
):
world
.
css_click
(
'.wrapper-comp-setting.file-uploader .setting-clear'
)
@step
(
'I have created a Video component with handout file "([^"]*)"'
)
def
create_video_with_handout
(
_step
,
filename
):
_step
.
given
(
'I have created a Video component'
)
_step
.
given
(
'I edit the component'
)
_step
.
given
(
'I open tab "Advanced"'
)
_step
.
given
(
'I upload handout file "{0}"'
.
format
(
filename
))
cms/static/coffee/spec/main_squire.coffee
View file @
127bf7ed
...
...
@@ -175,5 +175,6 @@ jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
define
([
"coffee/spec/views/assets_spec"
,
"js/spec/video/translations_editor_spec"
"js/spec/video/translations_editor_spec"
,
"js/spec/video/file_uploader_editor_spec"
])
cms/static/coffee/spec/models/upload_spec.coffee
View file @
127bf7ed
...
...
@@ -13,15 +13,15 @@ define ["js/models/uploads"], (FileUpload) ->
it
"is valid by default"
,
->
expect
(
@
model
.
isValid
()).
toBeTruthy
()
it
"is
in
valid for text files by default"
,
->
it
"is valid for text files by default"
,
->
file
=
{
"type"
:
"text/plain"
,
"name"
:
"filename.txt"
}
@
model
.
set
(
"selectedFile"
,
file
);
expect
(
@
model
.
isValid
()).
toBe
Fals
y
()
expect
(
@
model
.
isValid
()).
toBe
Truth
y
()
it
"is
in
valid for PNG files by default"
,
->
it
"is valid for PNG files by default"
,
->
file
=
{
"type"
:
"image/png"
,
"name"
:
"filename.png"
}
@
model
.
set
(
"selectedFile"
,
file
);
expect
(
@
model
.
isValid
()).
toBe
Fals
y
()
expect
(
@
model
.
isValid
()).
toBe
Truth
y
()
it
"can accept a file type when explicitly set"
,
->
file
=
{
"type"
:
"image/png"
,
"name"
:
"filename.png"
}
...
...
cms/static/js/models/uploads.js
View file @
127bf7ed
...
...
@@ -47,7 +47,8 @@ var FileUpload = Backbone.Model.extend({
return
RegExp
((
'(?:.+)
\\
.('
+
formats
.
join
(
'|'
)
+
')$'
),
'i'
);
};
return
_
.
contains
(
attrs
.
mimeTypes
,
file
.
type
)
||
return
(
attrs
.
mimeTypes
.
length
===
0
&&
attrs
.
fileFormats
.
length
===
0
)
||
_
.
contains
(
attrs
.
mimeTypes
,
file
.
type
)
||
getRegExp
(
attrs
.
fileFormats
).
test
(
file
.
name
);
},
// Return strings for the valid file types and extensions this
...
...
cms/static/js/spec/video/file_uploader_editor_spec.js
0 → 100644
View file @
127bf7ed
define
(
[
'jquery'
,
'underscore'
,
'js/spec_helpers/create_sinon'
,
'squire'
],
function
(
$
,
_
,
create_sinon
,
Squire
)
{
'use strict'
;
describe
(
'FileUploader'
,
function
()
{
var
FileUploaderTemplate
=
readFixtures
(
'metadata-file-uploader-entry.underscore'
),
FileUploaderItemTemplate
=
readFixtures
(
'metadata-file-uploader-item.underscore'
),
locator
=
'locator'
,
feedbackTpl
=
readFixtures
(
'system-feedback.underscore'
),
modelStub
=
{
default_value
:
'http://example.org/test_1'
,
display_name
:
'File Upload'
,
explicitly_set
:
false
,
field_name
:
'file_upload'
,
help
:
'Specifies the name for this component.'
,
type
:
'FileUploader'
,
value
:
'http://example.org/test_1'
},
self
,
injector
;
var
setValue
=
function
(
view
,
value
)
{
view
.
setValueInEditor
(
value
);
view
.
updateModel
();
};
var
createPromptSpy
=
function
(
name
)
{
var
spy
=
jasmine
.
createSpyObj
(
name
,
[
'constructor'
,
'show'
,
'hide'
]);
spy
.
constructor
.
andReturn
(
spy
);
spy
.
show
.
andReturn
(
spy
);
spy
.
extend
=
jasmine
.
createSpy
().
andReturn
(
spy
.
constructor
);
return
spy
;
};
beforeEach
(
function
()
{
self
=
this
;
this
.
addMatchers
({
assertValueInView
:
function
(
expected
)
{
var
value
=
this
.
actual
.
getValueFromEditor
();
return
this
.
env
.
equals_
(
value
,
expected
);
},
assertCanUpdateView
:
function
(
expected
)
{
var
view
=
this
.
actual
,
value
;
view
.
setValueInEditor
(
expected
);
value
=
view
.
getValueFromEditor
();
return
this
.
env
.
equals_
(
value
,
expected
);
},
assertClear
:
function
(
modelValue
)
{
var
env
=
this
.
env
,
view
=
this
.
actual
,
model
=
view
.
model
;
return
model
.
getValue
()
===
null
&&
env
.
equals_
(
model
.
getDisplayValue
(),
modelValue
)
&&
env
.
equals_
(
view
.
getValueFromEditor
(),
modelValue
);
},
assertUpdateModel
:
function
(
originalValue
,
newValue
)
{
var
env
=
this
.
env
,
view
=
this
.
actual
,
model
=
view
.
model
,
expectOriginal
;
view
.
setValueInEditor
(
newValue
);
expectOriginal
=
env
.
equals_
(
model
.
getValue
(),
originalValue
);
view
.
updateModel
();
return
expectOriginal
&&
env
.
equals_
(
model
.
getValue
(),
newValue
);
},
verifyButtons
:
function
(
upload
,
download
,
index
)
{
var
view
=
this
.
actual
,
uploadBtn
=
view
.
$
(
'.upload-setting'
),
downloadBtn
=
view
.
$
(
'.download-setting'
);
upload
=
upload
?
uploadBtn
.
length
:
!
uploadBtn
.
length
;
download
=
download
?
downloadBtn
.
length
:
!
downloadBtn
.
length
;
return
upload
&&
download
;
}
});
appendSetFixtures
(
$
(
'<script>'
,
{
id
:
'metadata-file-uploader-entry'
,
type
:
'text/template'
}).
text
(
FileUploaderTemplate
));
appendSetFixtures
(
$
(
'<script>'
,
{
id
:
'metadata-file-uploader-item'
,
type
:
'text/template'
}).
text
(
FileUploaderItemTemplate
));
this
.
uploadSpies
=
createPromptSpy
(
'UploadDialog'
);
injector
=
new
Squire
();
injector
.
mock
(
'js/views/uploads'
,
function
()
{
return
self
.
uploadSpies
.
constructor
;
});
injector
.
mock
(
'js/views/video/transcripts/metadata_videolist'
);
injector
.
mock
(
'js/views/video/translations_editor'
);
runs
(
function
()
{
injector
.
require
([
'js/models/metadata'
,
'js/views/metadata'
],
function
(
MetadataModel
,
MetadataView
)
{
var
model
=
new
MetadataModel
(
$
.
extend
(
true
,
{},
modelStub
));
self
.
view
=
new
MetadataView
.
FileUploader
({
model
:
model
,
locator
:
locator
});
});
});
waitsFor
(
function
()
{
return
self
.
view
;
},
'FileUploader was not created'
,
2000
);
});
afterEach
(
function
()
{
injector
.
clean
();
injector
.
remove
();
});
it
(
'returns the initial value upon initialization'
,
function
()
{
expect
(
this
.
view
).
assertValueInView
(
'http://example.org/test_1'
);
expect
(
this
.
view
).
verifyButtons
(
true
,
true
);
});
it
(
'updates its value correctly'
,
function
()
{
expect
(
this
.
view
).
assertCanUpdateView
(
'http://example.org/test_2'
);
});
it
(
'upload works correctly'
,
function
()
{
var
options
;
setValue
(
this
.
view
,
''
);
expect
(
this
.
view
).
verifyButtons
(
true
,
false
);
this
.
view
.
$el
.
find
(
'.upload-setting'
).
click
();
expect
(
this
.
uploadSpies
.
constructor
).
toHaveBeenCalled
();
expect
(
this
.
uploadSpies
.
show
).
toHaveBeenCalled
();
options
=
this
.
uploadSpies
.
constructor
.
mostRecentCall
.
args
[
0
];
options
.
onSuccess
({
'asset'
:
{
'url'
:
'http://example.org/test_3'
}
});
expect
(
this
.
view
).
verifyButtons
(
true
,
true
);
expect
(
this
.
view
.
getValueFromEditor
()).
toEqual
(
'http://example.org/test_3'
);
});
it
(
'has a clear method to revert to the model default'
,
function
()
{
setValue
(
this
.
view
,
'http://example.org/test_5'
);
this
.
view
.
clear
();
expect
(
this
.
view
).
assertClear
(
'http://example.org/test_1'
);
});
it
(
'has an update model method'
,
function
()
{
expect
(
this
.
view
).
assertUpdateModel
(
null
,
'http://example.org/test_6'
);
});
});
});
cms/static/js/spec/video/translations_editor_spec.js
View file @
127bf7ed
...
...
@@ -212,43 +212,22 @@ function ($, _, create_sinon, Squire) {
});
});
describe
(
'has a clear method to revert to the model default'
,
function
()
{
it
(
'w/ popup, if values were changed'
,
function
(){
var
requests
=
create_sinon
.
requests
(
this
),
options
;
setValue
(
this
.
view
,
{
'fr'
:
'fr.srt'
,
'uk'
:
'uk.srt'
});
this
.
view
.
$el
.
find
(
'.create-setting'
).
click
();
this
.
view
.
clear
();
expect
(
this
.
view
).
assertClear
({
'en'
:
'en.srt'
,
'ru'
:
'ru.srt'
});
expect
(
this
.
view
.
$el
.
find
(
'.create-setting'
)).
not
.
toHaveClass
(
'is-disabled'
);
it
(
'has a clear method to revert to the model default'
,
function
()
{
setValue
(
this
.
view
,
{
'fr'
:
'en.srt'
,
'uk'
:
'ru.srt'
});
it
(
'w/o popup, if just keys were changed'
,
function
(){
setValue
(
this
.
view
,
{
'fr'
:
'en.srt'
,
'uk'
:
'ru.srt'
});
this
.
view
.
$el
.
find
(
'.create-setting'
).
click
();
this
.
view
.
clear
();
this
.
view
.
$el
.
find
(
'.create-setting'
).
click
();
expect
(
this
.
view
).
assertClear
({
'en'
:
'en.srt'
,
'ru'
:
'ru.srt'
});
this
.
view
.
clear
();
expect
(
this
.
view
.
$el
.
find
(
'.create-setting'
)).
not
.
toHaveClass
(
'is-disabled'
);
expect
(
this
.
view
).
assertClear
({
'en'
:
'en.srt'
,
'ru'
:
'ru.srt'
});
expect
(
this
.
view
.
$el
.
find
(
'.create-setting'
)).
not
.
toHaveClass
(
'is-disabled'
);
});
it
(
'has an update model method'
,
function
()
{
...
...
@@ -261,26 +240,15 @@ function ($, _, create_sinon, Squire) {
expect
(
this
.
view
.
$el
.
find
(
'select'
).
length
).
toEqual
(
5
);
});
describe
(
'can remove an entry'
,
function
()
{
it
(
'w/ popup, if values were changed'
,
function
(){
var
requests
=
create_sinon
.
requests
(
this
),
options
;
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
4
);
this
.
view
.
$el
.
find
(
'.remove-setting'
).
last
().
click
();
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
3
);
});
it
(
'w/o popup, if just keys were changed'
,
function
(){
setValue
(
this
.
view
,
{
'en'
:
'en.srt'
,
'ru'
:
'ru.srt'
,
'fr'
:
''
});
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
3
);
this
.
view
.
$el
.
find
(
'.remove-setting'
).
last
().
click
();
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
2
);
it
(
'can remove an entry'
,
function
()
{
setValue
(
this
.
view
,
{
'en'
:
'en.srt'
,
'ru'
:
'ru.srt'
,
'fr'
:
''
});
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
3
);
this
.
view
.
$el
.
find
(
'.remove-setting'
).
last
().
click
();
expect
(
_
.
keys
(
this
.
view
.
model
.
get
(
'value'
)).
length
).
toEqual
(
2
);
});
it
(
'only allows one blank entry at a time'
,
function
()
{
...
...
cms/static/js/views/abstract_editor.js
View file @
127bf7ed
...
...
@@ -8,12 +8,7 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
var
templateName
=
_
.
result
(
this
,
'templateName'
);
// Backbone model cid is only unique within the collection.
this
.
uniqueId
=
_
.
uniqueId
(
templateName
+
"_"
);
var
tpl
=
document
.
getElementById
(
templateName
).
text
;
if
(
!
tpl
)
{
console
.
error
(
"Couldn't load template: "
+
templateName
);
}
this
.
template
=
_
.
template
(
tpl
);
this
.
template
=
this
.
loadTemplate
(
templateName
);
this
.
$el
.
html
(
this
.
template
({
model
:
this
.
model
,
uniqueId
:
this
.
uniqueId
}));
this
.
listenTo
(
this
.
model
,
'change'
,
this
.
render
);
this
.
render
();
...
...
@@ -85,6 +80,20 @@ define(["js/views/baseview", "underscore"], function(BaseView, _) {
}
return
this
;
},
/**
* Loads the named template from the page, or logs an error if it fails.
* @param name The name of the template.
* @returns The loaded template.
*/
loadTemplate
:
function
(
name
)
{
var
templateSelector
=
"#"
+
name
,
templateText
=
$
(
templateSelector
).
text
();
if
(
!
templateText
)
{
console
.
error
(
"Failed to load "
+
name
+
" template"
);
}
return
_
.
template
(
templateText
);
}
});
...
...
cms/static/js/views/metadata.js
View file @
127bf7ed
define
(
[
"js/views/baseview"
,
"underscore"
,
"js/models/metadata"
,
"js/views/abstract_editor"
,
"js/models/uploads"
,
"js/views/uploads"
,
"js/views/video/transcripts/metadata_videolist"
,
"js/views/video/translations_editor"
],
function
(
BaseView
,
_
,
MetadataModel
,
AbstractEditor
,
VideoList
,
VideoTranslations
)
{
function
(
BaseView
,
_
,
MetadataModel
,
AbstractEditor
,
FileUpload
,
UploadDialog
,
VideoList
,
VideoTranslations
)
{
var
Metadata
=
{};
Metadata
.
Editor
=
BaseView
.
extend
({
// Model is CMS.Models.MetadataCollection,
initialize
:
function
()
{
this
.
template
=
this
.
loadTemplate
(
'metadata-editor'
);
var
self
=
this
,
counter
=
0
,
locator
=
self
.
$el
.
closest
(
'[data-locator]'
).
data
(
'locator'
);
this
.
template
=
this
.
loadTemplate
(
'metadata-editor'
);
this
.
$el
.
html
(
this
.
template
({
numEntries
:
this
.
collection
.
length
}));
var
counter
=
0
;
var
self
=
this
;
this
.
collection
.
each
(
function
(
model
)
{
var
data
=
{
el
:
self
.
$el
.
find
(
'.metadata_entry'
)[
counter
++
],
locator
:
locator
,
model
:
model
},
conversions
=
{
...
...
@@ -85,7 +88,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList, VideoTranslation
events
:
{
"change input"
:
"updateModel"
,
"keypress .setting-input"
:
"showClearButton"
,
"keypress .setting-input"
:
"showClearButton"
,
"click .setting-clear"
:
"clear"
},
...
...
@@ -487,5 +490,62 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList, VideoTranslation
}
});
/**
* Provides convenient way to upload/download files in component edit.
* The editor uploads files directly to course assets and stores link
* to uploaded file.
*/
Metadata
.
FileUploader
=
AbstractEditor
.
extend
({
events
:
{
"click .upload-setting"
:
"upload"
,
"click .setting-clear"
:
"clear"
},
templateName
:
"metadata-file-uploader-entry"
,
templateButtonsName
:
"metadata-file-uploader-item"
,
initialize
:
function
()
{
this
.
buttonTemplate
=
this
.
loadTemplate
(
this
.
templateButtonsName
);
AbstractEditor
.
prototype
.
initialize
.
apply
(
this
);
},
getValueFromEditor
:
function
()
{
return
this
.
$
(
'#'
+
this
.
uniqueId
).
val
();
},
setValueInEditor
:
function
(
value
)
{
var
html
=
this
.
buttonTemplate
({
model
:
this
.
model
,
uniqueId
:
this
.
uniqueId
});
this
.
$
(
'#'
+
this
.
uniqueId
).
val
(
value
);
this
.
$
(
'.wrapper-uploader-actions'
).
html
(
html
);
},
upload
:
function
(
event
)
{
var
self
=
this
,
target
=
$
(
event
.
currentTarget
),
url
=
/assets/
+
this
.
options
.
locator
,
model
=
new
FileUpload
({
title
:
gettext
(
'Upload File'
),
}),
view
=
new
UploadDialog
({
model
:
model
,
url
:
url
,
parentElement
:
target
.
closest
(
'.xblock-editor'
),
onSuccess
:
function
(
response
)
{
if
(
response
[
'asset'
]
&&
response
[
'asset'
][
'url'
])
{
self
.
model
.
setValue
(
response
[
'asset'
][
'url'
]);
}
}
}).
show
();
event
.
preventDefault
();
}
});
return
Metadata
;
});
cms/static/sass/views/_unit.scss
View file @
127bf7ed
...
...
@@ -609,6 +609,41 @@ body.course.unit,.view-unit {
display
:
block
;
}
.file-uploader
{
.upload-setting
{
@extend
%ui-btn-flat-outline
;
@extend
%t-action3
;
@include
box-sizing
(
border-box
);
display
:
inline-block
;
padding
:
(
$baseline
/
2
);
font-weight
:
600
;
width
:
49%
;
margin-right
:
2%
;
}
.download-setting
{
@extend
%ui-btn-non
;
@extend
%t-action4
;
@include
box-sizing
(
border-box
);
display
:
inline-block
;
padding
:
(
$baseline
/
2
);
font-weight
:
600
;
width
:
49%
;
text-align
:
center
;
color
:
$blue
;
&
:hover
{
background-color
:
$blue
;
}
}
.wrapper-uploader-actions
{
width
:
45%
;
display
:
inline-block
;
min-width
:
(
$baseline
*
5
);
}
}
//settings-list
.list-input.settings-list
{
margin
:
0
;
...
...
cms/templates/js/metadata-file-uploader-entry.underscore
0 → 100644
View file @
127bf7ed
<div class="wrapper-comp-setting file-uploader">
<label class="label setting-label"><%= model.get('display_name') %></label>
<input type="hidden" id="<%= uniqueId %>" class="input setting-input" value="<%= model.get("value") %>">
<div class="wrapper-uploader-actions"></div>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
cms/templates/js/metadata-file-uploader-item.underscore
0 → 100644
View file @
127bf7ed
<a href="#" class="upload-action upload-setting"><%= model.get('value') ? gettext('Replace') : gettext('Upload') %>
</a><% if (model.get('value')) { %><a href="<%= model.get("value") %>" target="_blank" class="download-action download-setting"><%= gettext("Download") %>
</a><% } %>
cms/templates/widgets/metadata-edit.html
View file @
127bf7ed
...
...
@@ -12,7 +12,7 @@
<script
id=
"metadata-editor-tpl"
type=
"text/template"
>
<%
static
:
include
path
=
"js/metadata-editor.underscore"
/>
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"
, "metadata-file-uploader-entry", "metadata-file-uploader-item"
]:
<script
id=
"${template_name}"
type=
"text/template"
>
<%
static
:
include
path
=
"js/${template_name}.underscore"
/>
</script>
...
...
cms/templates/widgets/tabs/metadata-edit-tab.html
View file @
127bf7ed
...
...
@@ -8,7 +8,7 @@
<%
static
:
include
path
=
"js/metadata-editor.underscore"
/>
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"
, "metadata-file-uploader-entry", "metadata-file-uploader-item"
]:
<script
id=
"${template_name}"
type=
"text/template"
>
<%
static
:
include
path
=
"js/${template_name}.underscore"
/>
</script>
...
...
common/lib/xmodule/xmodule/css/video/display.scss
View file @
127bf7ed
...
...
@@ -42,8 +42,7 @@ div.video {
margin
:
0
;
padding
:
0
;
.video-sources
,
.video-tracks
{
.video-download-button
{
display
:
inline-block
;
vertical-align
:
top
;
margin
:
(
$baseline
*.
75
)
(
$baseline
/
2
)
0
0
;
...
...
common/lib/xmodule/xmodule/tests/test_video.py
View file @
127bf7ed
...
...
@@ -189,6 +189,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="ua" src="ukrainian_translation.srt" />
<transcript language="ge" src="german_translation.srt" />
</video>
...
...
@@ -211,6 +212,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time'
:
datetime
.
timedelta
(
seconds
=
1
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
60
),
'track'
:
'http://www.example.com/track'
,
'handout'
:
'http://www.example.com/handout'
,
'download_track'
:
True
,
'html5_sources'
:
[
'http://www.example.com/source.mp4'
,
'http://www.example.com/source.ogg'
],
'data'
:
''
,
...
...
@@ -229,6 +231,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
end_time="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="uk" src="ukrainian_translation.srt" />
<transcript language="de" src="german_translation.srt" />
</video>
...
...
@@ -243,6 +246,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time'
:
datetime
.
timedelta
(
seconds
=
1
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
60
),
'track'
:
'http://www.example.com/track'
,
'handout'
:
'http://www.example.com/handout'
,
'download_track'
:
False
,
'download_video'
:
False
,
'html5_sources'
:
[
'http://www.example.com/source.mp4'
],
...
...
@@ -273,6 +277,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'track'
:
''
,
'handout'
:
None
,
'download_track'
:
False
,
'download_video'
:
True
,
'html5_sources'
:
[
'http://www.example.com/source.mp4'
],
...
...
@@ -326,6 +331,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'track'
:
''
,
'handout'
:
None
,
'download_track'
:
False
,
'download_video'
:
False
,
'html5_sources'
:
[],
...
...
@@ -345,7 +351,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
show_captions="false"
download_video="true"
sub=""html5_subtitles""
track=""http://download_track""
track=""http://www.example.com/track""
handout=""http://www.example.com/handout""
download_track="true"
youtube_id_0_75=""OEoXaMPEzf65""
youtube_id_1_25=""OEoXaMPEzf125""
...
...
@@ -362,7 +369,8 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'show_captions'
:
False
,
'start_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'track'
:
'http://download_track'
,
'track'
:
'http://www.example.com/track'
,
'handout'
:
'http://www.example.com/handout'
,
'download_track'
:
True
,
'download_video'
:
True
,
'html5_sources'
:
[
"source_1"
,
"source_2"
],
...
...
@@ -386,6 +394,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
'start_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'end_time'
:
datetime
.
timedelta
(
seconds
=
0.0
),
'track'
:
''
,
'handout'
:
None
,
'download_track'
:
False
,
'download_video'
:
False
,
'html5_sources'
:
[],
...
...
@@ -509,6 +518,7 @@ class VideoExportTestCase(unittest.TestCase):
desc
.
start_time
=
datetime
.
timedelta
(
seconds
=
1.0
)
desc
.
end_time
=
datetime
.
timedelta
(
seconds
=
60
)
desc
.
track
=
'http://www.example.com/track'
desc
.
handout
=
'http://www.example.com/handout'
desc
.
download_track
=
True
desc
.
html5_sources
=
[
'http://www.example.com/source.mp4'
,
'http://www.example.com/source.ogg'
]
desc
.
download_video
=
True
...
...
@@ -520,6 +530,7 @@ class VideoExportTestCase(unittest.TestCase):
<source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/>
<track src="http://www.example.com/track"/>
<handout src="http://www.example.com/handout"/>
<transcript language="ge" src="german_translation.srt" />
<transcript language="ua" src="ukrainian_translation.srt" />
</video>
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
127bf7ed
...
...
@@ -143,6 +143,7 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
'data_dir'
:
getattr
(
self
,
'data_dir'
,
None
),
'display_name'
:
self
.
display_name_with_default
,
'end'
:
self
.
end_time
.
total_seconds
(),
'handout'
:
self
.
handout
,
'id'
:
self
.
location
.
html_id
(),
'show_captions'
:
json
.
dumps
(
self
.
show_captions
),
'sources'
:
sources
,
...
...
@@ -248,6 +249,8 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
editable_fields
[
'transcripts'
][
'languages'
]
=
languages
editable_fields
[
'transcripts'
][
'type'
]
=
'VideoTranslations'
editable_fields
[
'transcripts'
][
'urlRoot'
]
=
self
.
runtime
.
handler_url
(
self
,
'studio_transcript'
,
'translation'
)
.
rstrip
(
'/?'
)
editable_fields
[
'handout'
][
'type'
]
=
'FileUploader'
return
editable_fields
@classmethod
...
...
@@ -320,6 +323,11 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
ele
.
set
(
'src'
,
self
.
track
)
xml
.
append
(
ele
)
if
self
.
handout
:
ele
=
etree
.
Element
(
'handout'
)
ele
.
set
(
'src'
,
self
.
handout
)
xml
.
append
(
ele
)
# sorting for easy testing of resulting xml
for
transcript_language
in
sorted
(
self
.
transcripts
.
keys
()):
ele
=
etree
.
Element
(
'transcript'
)
...
...
@@ -422,6 +430,10 @@ class VideoDescriptor(VideoFields, VideoStudioViewHandlers, TabsEditingDescripto
if
track
is
not
None
:
field_data
[
'track'
]
=
track
.
get
(
'src'
)
handout
=
xml
.
find
(
'handout'
)
if
handout
is
not
None
:
field_data
[
'handout'
]
=
handout
.
get
(
'src'
)
transcripts
=
xml
.
findall
(
'transcript'
)
if
transcripts
:
field_data
[
'transcripts'
]
=
{
tr
.
get
(
'language'
):
tr
.
get
(
'src'
)
for
tr
in
transcripts
}
...
...
common/lib/xmodule/xmodule/video_module/video_xfields.py
View file @
127bf7ed
...
...
@@ -15,8 +15,9 @@ class VideoFields(object):
default
=
"Video"
,
scope
=
Scope
.
settings
)
saved_video_position
=
RelativeTime
(
help
=
"Current position in the video"
,
help
=
"Current position in the video
.
"
,
scope
=
Scope
.
user_state
,
default
=
datetime
.
timedelta
(
seconds
=
0
)
)
...
...
@@ -105,13 +106,13 @@ class VideoFields(object):
)
# Data format: {'de': 'german_translation', 'uk': 'ukrainian_translation'}
transcripts
=
Dict
(
help
=
"Add additional transcripts in other languages"
,
help
=
"Add additional transcripts in other languages
.
"
,
display_name
=
"Transcript Translations"
,
scope
=
Scope
.
settings
,
default
=
{}
)
transcript_language
=
String
(
help
=
"Preferred language for transcript"
,
help
=
"Preferred language for transcript
.
"
,
display_name
=
"Preferred language for transcript"
,
scope
=
Scope
.
preferences
,
default
=
"en"
...
...
@@ -130,12 +131,18 @@ class VideoFields(object):
scope
=
Scope
.
user_state
,
)
global_speed
=
Float
(
help
=
"Default speed in cases when speed wasn't explicitly for specific video"
,
help
=
"Default speed in cases when speed wasn't explicitly for specific video
.
"
,
scope
=
Scope
.
preferences
,
default
=
1.0
)
youtube_is_available
=
Boolean
(
help
=
"The availaibility of YouTube API for the user"
,
help
=
"The availaibility of YouTube API for the user
.
"
,
scope
=
Scope
.
user_info
,
default
=
True
)
handout
=
String
(
help
=
"Upload a handout to accompany this video. Students can download the handout by clicking Download Handout under the video."
,
display_name
=
"Upload Handout"
,
scope
=
Scope
.
settings
,
)
lms/djangoapps/courseware/tests/test_video_mongo.py
View file @
127bf7ed
...
...
@@ -41,6 +41,7 @@ class TestVideoYouTube(TestVideo):
'end'
:
3610.0
,
'id'
:
self
.
item_descriptor
.
location
.
html_id
(),
'show_captions'
:
'true'
,
'handout'
:
None
,
'sources'
:
sources
,
'speed'
:
'null'
,
'general_speed'
:
1.0
,
...
...
@@ -104,6 +105,7 @@ class TestVideoNonYouTube(TestVideo):
'ajax_url'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
+
'/save_user_state'
,
'data_dir'
:
getattr
(
self
,
'data_dir'
,
None
),
'show_captions'
:
'true'
,
'handout'
:
None
,
'display_name'
:
u'A Name'
,
'end'
:
3610.0
,
'id'
:
self
.
item_descriptor
.
location
.
html_id
(),
...
...
@@ -203,6 +205,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
expected_context
=
{
'data_dir'
:
getattr
(
self
,
'data_dir'
,
None
),
'show_captions'
:
'true'
,
'handout'
:
None
,
'display_name'
:
u'A Name'
,
'end'
:
3610.0
,
'id'
:
None
,
...
...
@@ -324,6 +327,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
expected_context
=
{
'data_dir'
:
getattr
(
self
,
'data_dir'
,
None
),
'show_captions'
:
'true'
,
'handout'
:
None
,
'display_name'
:
u'A Name'
,
'end'
:
3610.0
,
'id'
:
None
,
...
...
@@ -469,6 +473,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
'options'
:
[],
},
'transcripts'
:
{},
'handout'
:
{},
}
):
metadata
=
{
...
...
lms/templates/video.html
View file @
127bf7ed
...
...
@@ -107,12 +107,12 @@
<div
class=
"focus_grabber last"
></div>
<ul
class=
"wrapper-downloads"
>
% if sources.get('main'):
<li
class=
"video-sources"
>
<li
class=
"video-sources
video-download-button
"
>
${('
<a
href=
"%s"
>
' + _('Download video') + '
</a>
') % sources.get('main')}
</li>
% endif
% if track:
<li
class=
"video-tracks"
>
<li
class=
"video-tracks
video-download-button
"
>
% if transcript_download_format:
${('
<a
href=
"%s"
>
' + _('Download transcript') + '
</a>
') % track
}
...
...
@@ -138,5 +138,10 @@
% endif
</li>
% endif
% if handout:
<li
class=
"video-handout video-download-button"
>
${('
<a
href=
"%s"
target=
"_blank"
>
' + _('Download Handout') + '
</a>
') % handout}
</li>
% endif
</ul>
</div>
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