diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 01894935bd..02b4982463 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -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.
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index b8287047af..a19d7c2e67 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -11,6 +11,7 @@ VIDEO_BUTTONS = {
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
+ 'handout': '.video-handout.video-download-button a',
}
SELECTORS = {
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video_editor.feature
similarity index 100%
rename from cms/djangoapps/contentstore/features/video-editor.feature
rename to cms/djangoapps/contentstore/features/video_editor.feature
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video_editor.py
similarity index 99%
rename from cms/djangoapps/contentstore/features/video-editor.py
rename to cms/djangoapps/contentstore/features/video_editor.py
index 483172a0b8..31f41b6ae8 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video_editor.py
@@ -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],
diff --git a/cms/djangoapps/contentstore/features/video_handout.feature b/cms/djangoapps/contentstore/features/video_handout.feature
new file mode 100644
index 0000000000..980ae0ed6f
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/video_handout.feature
@@ -0,0 +1,71 @@
+@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"
diff --git a/cms/djangoapps/contentstore/features/video_handout.py b/cms/djangoapps/contentstore/features/video_handout.py
new file mode 100644
index 0000000000..c847a2a5fb
--- /dev/null
+++ b/cms/djangoapps/contentstore/features/video_handout.py
@@ -0,0 +1,40 @@
+# -*- 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))
diff --git a/cms/static/coffee/spec/main_squire.coffee b/cms/static/coffee/spec/main_squire.coffee
index e7e6bef00b..7c85125c7c 100644
--- a/cms/static/coffee/spec/main_squire.coffee
+++ b/cms/static/coffee/spec/main_squire.coffee
@@ -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"
])
diff --git a/cms/static/coffee/spec/models/upload_spec.coffee b/cms/static/coffee/spec/models/upload_spec.coffee
index c91562b6e5..17cd887b36 100644
--- a/cms/static/coffee/spec/models/upload_spec.coffee
+++ b/cms/static/coffee/spec/models/upload_spec.coffee
@@ -13,15 +13,15 @@ define ["js/models/uploads"], (FileUpload) ->
it "is valid by default", ->
expect(@model.isValid()).toBeTruthy()
- it "is invalid 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()).toBeFalsy()
+ expect(@model.isValid()).toBeTruthy()
- it "is invalid 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()).toBeFalsy()
+ expect(@model.isValid()).toBeTruthy()
it "can accept a file type when explicitly set", ->
file = {"type": "image/png", "name": "filename.png"}
diff --git a/cms/static/js/models/uploads.js b/cms/static/js/models/uploads.js
index 41ee4abcdb..7a19b3c4bd 100644
--- a/cms/static/js/models/uploads.js
+++ b/cms/static/js/models/uploads.js
@@ -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
diff --git a/cms/static/js/spec/video/file_uploader_editor_spec.js b/cms/static/js/spec/video/file_uploader_editor_spec.js
new file mode 100644
index 0000000000..95b0913d07
--- /dev/null
+++ b/cms/static/js/spec/video/file_uploader_editor_spec.js
@@ -0,0 +1,176 @@
+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($('
-% 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"]:
diff --git a/cms/templates/widgets/tabs/metadata-edit-tab.html b/cms/templates/widgets/tabs/metadata-edit-tab.html
index 862dfd58d0..2861a9ffd0 100644
--- a/cms/templates/widgets/tabs/metadata-edit-tab.html
+++ b/cms/templates/widgets/tabs/metadata-edit-tab.html
@@ -8,7 +8,7 @@
<%static:include path="js/metadata-editor.underscore" />
-% 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"]:
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index 55b7f78a84..83aa6daa65 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -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;
diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py
index cebb8dc30b..ed5afa2edd 100644
--- a/common/lib/xmodule/xmodule/tests/test_video.py
+++ b/common/lib/xmodule/xmodule/tests/test_video.py
@@ -189,6 +189,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):