Commit 61cb5159 by Greg Price

Add front end for Studio video upload feature

Co-authored-by: Chris <crodriguez@edx.org>
Co-authored-by: Mark Hoeber <hoeber@edx.org>
parent 0687a62a
......@@ -115,6 +115,14 @@ class VideoUploadTestCase(CourseTestCase):
for field in ["edx_video_id", "client_video_id", "duration", "status"]:
self.assertEqual(response_video[field], original_video[field])
def test_get_html(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertRegexpMatches(response["Content-Type"], "^text/html(;.*)?$")
# Crude check for presence of data in returned HTML
for video in self.previous_uploads:
self.assertIn(video["edx_video_id"], response.content)
def test_post_non_json(self):
response = self.client.post(self.url, {"files": []})
self.assertEqual(response.status_code, 400)
......
......@@ -12,6 +12,8 @@ from django.views.decorators.http import require_http_methods
from edxval.api import create_video, get_videos_for_ids
from opaque_keys.edx.keys import CourseKey
from contentstore.utils import reverse_course_url
from edxmako.shortcuts import render_to_response
from util.json_request import expect_json, JsonResponse
from xmodule.assetstore import AssetMetadata
from xmodule.modulestore.django import modulestore
......@@ -37,6 +39,8 @@ def videos_handler(request, course_key_string):
The restful handler for video uploads.
GET
html: return an HTML page to display previous video uploads and allow
new ones
json: return json representing the videos that have been uploaded and
their statuses
POST
......@@ -58,8 +62,11 @@ def videos_handler(request, course_key_string):
):
return HttpResponseNotFound()
if request.method == 'GET':
return videos_index_json(course)
if request.method == "GET":
if "application/json" in request.META.get("HTTP_ACCEPT", ""):
return videos_index_json(course)
else:
return videos_index_html(course)
else:
return videos_post(course, request)
......@@ -82,6 +89,21 @@ def _get_videos(course):
)
def videos_index_html(course):
"""
Returns an HTML page to display previous video uploads and allow new ones
"""
return render_to_response(
"videos_index.html",
{
"context_course": course,
"post_url": reverse_course_url("videos_handler", unicode(course.id)),
"previous_uploads": _get_videos(course),
"concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0),
}
)
def videos_index_json(course):
"""
Returns JSON in the following format:
......
......@@ -46,6 +46,7 @@
'js/factories/settings_advanced',
'js/factories/settings_graders',
'js/factories/textbooks',
'js/factories/videos_index',
'js/factories/xblock_validation'
]),
/**
......
......@@ -45,6 +45,7 @@ requirejs.config({
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady",
"URI": "xmodule_js/common_static/js/vendor/URI.min",
"mock-ajax": "xmodule_js/common_static/js/vendor/mock-ajax",
"mathjax": "//cdn.mathjax.org/mathjax/2.2-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
"youtube": "//www.youtube.com/player_api?noext",
......@@ -190,6 +191,9 @@ requirejs.config({
exports: "XBlock",
deps: ["xblock/core"]
},
"mock-ajax": {
deps: ["jasmine", "jquery"]
}
"coffee/src/main": {
deps: ["coffee/src/ajax_prefix"]
......@@ -228,6 +232,9 @@ define([
"js/spec/utils/handle_iframe_binding_spec",
"js/spec/utils/module_spec",
"js/spec/views/active_video_upload_list_spec",
"js/spec/views/previous_video_upload_spec",
"js/spec/views/previous_video_upload_list_spec",
"js/spec/views/paging_spec",
"js/spec/views/assets_spec",
"js/spec/views/baseview_spec",
......
define(
["jquery", "backbone", "js/views/active_video_upload_list", "js/views/previous_video_upload_list"],
function ($, Backbone, ActiveVideoUploadListView, PreviousVideoUploadListView) {
"use strict";
var VideosIndexFactory = function($contentWrapper, postUrl, concurrentUploadLimit, uploadButton, previousUploads) {
var activeView = new ActiveVideoUploadListView({
postUrl: postUrl,
concurrentUploadLimit: concurrentUploadLimit,
uploadButton: uploadButton
});
$contentWrapper.append(activeView.render().$el);
var previousCollection = new Backbone.Collection(previousUploads);
var previousView = new PreviousVideoUploadListView({collection: previousCollection});
$contentWrapper.append(previousView.render().$el);
};
return VideosIndexFactory;
}
);
define(
["backbone", "gettext"],
function(Backbone, gettext) {
"use strict";
var statusStrings = {
// Translators: This is the status of a video upload that is queued
// waiting for other uploads to complete
STATUS_QUEUED: gettext("Queued"),
// Translators: This is the status of an active video upload
STATUS_UPLOADING: gettext("Uploading"),
// Translators: This is the status of a video upload that has
// completed successfully
STATUS_COMPLETED: gettext("Upload completed"),
// Translators: This is the status of a video upload that has failed
STATUS_FAILED: gettext("Upload failed")
};
var ActiveVideoUpload = Backbone.Model.extend(
{
defaults: {
status: statusStrings.STATUS_QUEUED
}
},
statusStrings
);
return ActiveVideoUpload;
}
);
define(
["jquery", "js/models/active_video_upload", "js/views/active_video_upload_list", "js/common_helpers/template_helpers", "mock-ajax", "jasmine-jquery"],
function($, ActiveVideoUpload, ActiveVideoUploadListView, TemplateHelpers) {
"use strict";
var concurrentUploadLimit = 2;
describe("ActiveVideoUploadListView", function() {
beforeEach(function() {
TemplateHelpers.installTemplate("active-video-upload", true);
TemplateHelpers.installTemplate("active-video-upload-list");
this.postUrl = "/test/post/url";
this.uploadButton = $("<button>");
this.view = new ActiveVideoUploadListView({
concurrentUploadLimit: concurrentUploadLimit,
postUrl: this.postUrl,
uploadButton: this.uploadButton
});
this.view.render();
jasmine.Ajax.useMock();
clearAjaxRequests();
this.globalAjaxError = jasmine.createSpy();
$(document).ajaxError(this.globalAjaxError);
});
it("should trigger file selection when either the upload button or the drop zone is clicked", function() {
var clickSpy = jasmine.createSpy();
clickSpy.andCallFake(function(event) { event.preventDefault(); });
this.view.$(".js-file-input").on("click", clickSpy);
this.view.$(".file-drop-area").click();
expect(clickSpy).toHaveBeenCalled();
clickSpy.reset();
this.uploadButton.click();
expect(clickSpy).toHaveBeenCalled();
});
var makeUploadUrl = function(fileName) {
return "http://www.example.com/test_url/" + fileName;
}
var getSentRequests = function() {
return _.filter(
ajaxRequests,
function(request) { return request.readyState > 0; }
);
}
_.each(
[
{desc: "a single file", numFiles: 1},
{desc: "multiple files", numFiles: concurrentUploadLimit},
{desc: "more files than upload limit", numFiles: concurrentUploadLimit + 1},
],
function(caseInfo) {
var fileNames = _.map(
_.range(caseInfo.numFiles),
function(i) { return "test" + i + ".mp4";}
);
describe("on selection of " + caseInfo.desc, function() {
beforeEach(function() {
// The files property cannot be set on a file input for
// security reasons, so we must mock the access mechanism
// that jQuery-File-Upload uses to retrieve it.
var realProp = $.prop;
spyOn($, "prop").andCallFake(function(el, propName) {
if (arguments.length == 2 && propName == "files") {
return _.map(
fileNames,
function(fileName) { return {name: fileName}; }
);
} else {
realProp.apply(this, arguments);
}
});
this.view.$(".js-file-input").change();
this.request = mostRecentAjaxRequest();
});
it("should trigger the correct request", function() {
expect(this.request.url).toEqual(this.postUrl);
expect(this.request.method).toEqual("POST");
expect(this.request.requestHeaders["Content-Type"]).toEqual("application/json");
expect(this.request.requestHeaders["Accept"]).toContain("application/json");
expect(JSON.parse(this.request.params)).toEqual({
"files": _.map(
fileNames,
function(fileName) { return {"file_name": fileName}; }
)
});
});
it("should trigger the global AJAX error handler on server error", function() {
this.request.response({status: 500});
expect(this.globalAjaxError).toHaveBeenCalled();
});
describe("and successful server response", function() {
beforeEach(function() {
clearAjaxRequests();
this.request.response({
status: 200,
responseText: JSON.stringify({
files: _.map(
fileNames,
function(fileName) {
return {
"file_name": fileName,
"upload_url": makeUploadUrl(fileName)
};
}
)
})
});
this.$uploadElems = this.view.$(".active-video-upload");
});
it("should start uploads", function() {
var spec = this;
var sentRequests = getSentRequests();
expect(sentRequests.length).toEqual(
_.min([concurrentUploadLimit, caseInfo.numFiles])
);
_.each(
sentRequests,
function(uploadRequest, i) {
expect(uploadRequest.url).toEqual(
makeUploadUrl(fileNames[i])
);
expect(uploadRequest.method).toEqual("PUT");
}
);
});
it("should display status", function() {
var spec = this;
expect(this.$uploadElems.length).toEqual(caseInfo.numFiles);
this.$uploadElems.each(function(i, uploadElem) {
var $uploadElem = $(uploadElem);
expect($.trim($uploadElem.find(".video-detail-name").text())).toEqual(
fileNames[i]
);
expect($.trim($uploadElem.find(".video-detail-status").text())).toEqual(
i >= concurrentUploadLimit ?
ActiveVideoUpload.STATUS_QUEUED :
ActiveVideoUpload.STATUS_UPLOADING
);
expect($uploadElem.find(".success").length).toEqual(0);
expect($uploadElem.find(".error").length).toEqual(0);
});
});
_.each(
[
{
desc: "completion",
responseStatus: 204,
statusText: ActiveVideoUpload.STATUS_COMPLETED,
presentSelector: ".success",
absentSelector: ".error"
},
{
desc: "failure",
responseStatus: 500,
statusText: ActiveVideoUpload.STATUS_FAILED,
presentSelector: ".error",
absentSelector: ".success"
},
],
function(subCaseInfo) {
describe("and upload " + subCaseInfo.desc, function() {
beforeEach(function() {
getSentRequests()[0].response({status: subCaseInfo.responseStatus});
});
it("should update status", function() {
var $uploadElem = this.view.$(".active-video-upload:first");
expect($uploadElem.length).toEqual(1);
expect($.trim($uploadElem.find(".video-detail-status").text())).toEqual(
subCaseInfo.statusText
);
expect($uploadElem.find(subCaseInfo.presentSelector).length).toEqual(1);
expect($uploadElem.find(subCaseInfo.absentSelector).length).toEqual(0);
});
it("should not trigger the global AJAX error handler", function() {
expect(this.globalAjaxError).not.toHaveBeenCalled();
});
if (caseInfo.numFiles > concurrentUploadLimit) {
it("should start a new upload", function() {
expect(getSentRequests().length).toEqual(
concurrentUploadLimit + 1
);
var $uploadElem = $(this.$uploadElems[concurrentUploadLimit]);
expect($.trim($uploadElem.find(".video-detail-status").text())).toEqual(
ActiveVideoUpload.STATUS_UPLOADING
);
});
}
});
}
);
});
});
}
);
});
}
);
......@@ -20,6 +20,8 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
appendSetFixtures(uploadModalTpl);
appendSetFixtures(sandbox({ id: "asset_table_body" }));
spyOn($.fn, "fileupload").andReturn("");
var collection = new AssetCollection();
collection.url = "assets-url";
assetsView = new AssetsView({
......@@ -57,10 +59,6 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
files: [{name: 'largefile', size: 0}]
};
$.fn.fileupload = function() {
return '';
};
var event = {}
event.target = {"value": "dummy.jpg"};
......
define(
["jquery", "underscore", "backbone", "js/views/previous_video_upload_list", "js/common_helpers/template_helpers"],
function($, _, Backbone, PreviousVideoUploadListView, TemplateHelpers) {
"use strict";
describe("PreviousVideoUploadListView", function() {
beforeEach(function() {
TemplateHelpers.installTemplate("previous-video-upload", true);
TemplateHelpers.installTemplate("previous-video-upload-list");
});
var render = function(numModels) {
var modelData = {
client_video_id: "foo.mp4",
duration: 42,
created: "2014-11-25T23:13:05",
edx_video_id: "dummy_id",
status: "uploading"
};
var collection = new Backbone.Collection(
_.map(
_.range(numModels),
function() { return new Backbone.Model(modelData); }
)
);
var view = new PreviousVideoUploadListView({collection: collection});
return view.render().$el;
};
it("should render an empty collection", function() {
var $el = render(0);
expect($el.find(".js-table-body").length).toEqual(1);
expect($el.find(".js-table-body tr").length).toEqual(0);
});
it("should render a non-empty collection", function() {
var $el = render(5);
expect($el.find(".js-table-body").length).toEqual(1);
expect($el.find(".js-table-body tr").length).toEqual(5);
});
});
}
);
define(
["jquery", "backbone", "js/views/previous_video_upload", "js/common_helpers/template_helpers"],
function($, Backbone, PreviousVideoUploadView, TemplateHelpers) {
"use strict";
describe("PreviousVideoUploadView", function() {
beforeEach(function() {
TemplateHelpers.installTemplate("previous-video-upload", true);
});
var 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"
};
var view = new PreviousVideoUploadView(
{model: new Backbone.Model($.extend({}, defaultData, modelData))}
);
return view.render().$el;
};
it("should render video name correctly", function() {
var testName = "test name";
var $el = render({client_video_id: testName});
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").andCallFake(
function(locales, options) {
expect(locales).toEqual([]);
expect(options.timeZone).toEqual("UTC");
expect(options.timeZoneName).toEqual("short");
return fakeDate;
}
);
var $el = render({});
expect($el.find(".date-col").text()).toEqual(fakeDate);
});
it("should render video id correctly", function() {
var testId = "test_id";
var $el = render({edx_video_id: testId});
expect($el.find(".video-id-col").text()).toEqual(testId);
});
_.each(
[
{status: "upload", expected: "Uploading"},
{status: "ingest", expected: "In Progress"},
{status: "transcode_queue", expected: "In Progress"},
{status: "transcode_active", expected: "In Progress"},
{status: "file_delivered", expected: "Complete"},
{status: "file_complete", expected: "Complete"},
{status: "file_corrupt", expected: "Failed"},
{status: "pipeline_error", expected: "Failed"},
{status: "invalid_token", expected: "Invalid Token"},
{status: "unexpected_status_string", expected: "Unknown"}
],
function(caseInfo) {
it("should render " + caseInfo.status + " status correctly", function() {
var $el = render({status: caseInfo.status});
expect($el.find(".status-col").text()).toEqual(caseInfo.expected);
});
}
);
});
}
);
......@@ -25,8 +25,19 @@ define(["jquery", "date", "jquery.ui", "jquery.timepicker"], function($, date) {
}
};
var renderDate = function(dateArg) {
// Render a localized date from an argument that can be passed to
// the Date constructor (e.g. another Date or an ISO 8601 string)
var date = new Date(dateArg);
return date.toLocaleString(
[],
{timeZone: "UTC", timeZoneName: "short"}
);
};
return {
getDate: getDate,
setDate: setDate
setDate: setDate,
renderDate: renderDate
};
});
define(
["js/models/active_video_upload", "js/views/baseview"],
function(ActiveVideoUpload, BaseView) {
"use strict";
var ActiveVideoUploadView = BaseView.extend({
tagName: "li",
className: "active-video-upload",
initialize: function() {
this.template = this.loadTemplate("active-video-upload");
this.listenTo(this.model, "change", this.render);
},
render: function() {
this.$el.html(this.template(this.model.attributes));
var $statusEl = this.$el.find(".video-detail-status");
var status = this.model.get("status");
$statusEl.toggleClass("success", status == ActiveVideoUpload.STATUS_COMPLETED);
$statusEl.toggleClass("error", status == ActiveVideoUpload.STATUS_FAILED);
return this;
},
});
return ActiveVideoUploadView;
}
);
define(
["jquery", "underscore", "backbone", "js/models/active_video_upload", "js/views/baseview", "js/views/active_video_upload", "jquery.fileupload"],
function($, _, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView) {
"use strict";
var ActiveVideoUploadListView = BaseView.extend({
tagName: "div",
events: {
"click .file-drop-area": "chooseFile",
"dragleave .file-drop-area": "dragleave",
"drop .file-drop-area": "dragleave"
},
initialize: function(options) {
this.template = this.loadTemplate("active-video-upload-list");
this.collection = new Backbone.Collection();
this.itemViews = [];
this.listenTo(this.collection, "add", this.addUpload);
this.concurrentUploadLimit = options.concurrentUploadLimit || 0;
this.postUrl = options.postUrl;
if (options.uploadButton) {
options.uploadButton.click(this.chooseFile.bind(this));
}
},
render: function() {
this.$el.html(this.template());
_.each(this.itemViews, this.renderUploadView.bind(this));
this.$uploadForm = this.$(".file-upload-form");
this.$dropZone = this.$uploadForm.find(".file-drop-area");
this.$uploadForm.fileupload({
type: "PUT",
singleFileUploads: false,
limitConcurrentUploads: this.concurrentUploadLimit,
dropZone: this.$dropZone,
dragover: this.dragover.bind(this),
add: this.fileUploadAdd.bind(this),
send: this.fileUploadSend.bind(this),
done: this.fileUploadDone.bind(this),
fail: this.fileUploadFail.bind(this)
});
// Disable default drag and drop behavior for the window (which
// is to load the file in place)
var preventDefault = function(event) {
event.preventDefault();
};
$(window).on("dragover", preventDefault);
$(window).on("drop", preventDefault);
return this;
},
addUpload: function(model) {
var itemView = new ActiveVideoUploadView({model: model});
this.itemViews.push(itemView);
this.renderUploadView(itemView);
},
renderUploadView: function(view) {
this.$(".active-video-upload-list").append(view.render().$el);
},
chooseFile: function(event) {
event.preventDefault();
this.$uploadForm.find(".js-file-input").click();
},
dragover: function(event) {
event.preventDefault();
this.$dropZone.addClass("is-dragged");
},
dragleave: function(event) {
event.preventDefault();
this.$dropZone.removeClass("is-dragged");
},
// Each file is ultimately sent to a separate URL, but we want to make a
// single API call to get the URLs for all videos that the user wants to
// upload at one time. The file upload plugin only allows for this one
// callback, so this makes the API call and then breaks apart the
// individual file uploads, using the extra `redirected` field to
// indicate that the correct upload url has already been retrieved
fileUploadAdd: function(event, uploadData) {
var view = this;
if (uploadData.redirected) {
var model = new ActiveVideoUpload({fileName: uploadData.files[0].name});
this.collection.add(model);
uploadData.cid = model.cid;
uploadData.submit();
} else {
$.ajax({
url: this.postUrl,
contentType: "application/json",
data: JSON.stringify({
files: _.map(
uploadData.files,
function(file) {
return {"file_name": file.name, "content_type": file.type};
}
)
}),
dataType: "json",
type: "POST"
}).done(function(responseData) {
_.each(
responseData["files"],
function(file, index) {
view.$uploadForm.fileupload("add", {
files: [uploadData.files[index]],
url: file["upload_url"],
multipart: false,
global: false, // Do not trigger global AJAX error handler
redirected: true
});
}
);
});
}
},
setStatus: function(cid, status) {
this.collection.get(cid).set("status", status);
},
fileUploadSend: function(event, data) {
this.setStatus(data.cid, ActiveVideoUpload.STATUS_UPLOADING);
},
fileUploadDone: function(event, data) {
this.setStatus(data.cid, ActiveVideoUpload.STATUS_COMPLETED);
},
fileUploadFail: function(event, data) {
this.setStatus(data.cid, ActiveVideoUpload.STATUS_FAILED);
}
});
return ActiveVideoUploadListView;
}
);
define(
["gettext", "js/utils/date_utils", "js/views/baseview"],
function(gettext, DateUtils, BaseView) {
"use strict";
var statusDisplayStrings = {
// Translators: This is the status of an active video upload
UPLOADING: gettext("Uploading"),
// Translators: This is the status for a video that the servers
// are currently processing
IN_PROGRESS: gettext("In Progress"),
// Translators: This is the status for a video that the servers
// have successfully processed
COMPLETE: gettext("Complete"),
// Translators: This is the status for a video that the servers
// have failed to process
FAILED: gettext("Failed"),
// Translators: This is the status for a video for which an invalid
// processing token was provided in the course settings
INVALID_TOKEN: gettext("Invalid Token"),
// Translators: This is the status for a video that is in an unknown
// state
UNKNOWN: gettext("Unknown")
};
var statusMap = {
"upload": statusDisplayStrings.UPLOADING,
"ingest": statusDisplayStrings.IN_PROGRESS,
"transcode_queue": statusDisplayStrings.IN_PROGRESS,
"transcode_active": statusDisplayStrings.IN_PROGRESS,
"file_delivered": statusDisplayStrings.COMPLETE,
"file_complete": statusDisplayStrings.COMPLETE,
"file_corrupt": statusDisplayStrings.FAILED,
"pipeline_error": statusDisplayStrings.FAILED,
"invalid_token": statusDisplayStrings.INVALID_TOKEN
};
var PreviousVideoUploadView = BaseView.extend({
tagName: "tr",
initialize: function() {
this.template = this.loadTemplate("previous-video-upload");
},
renderDuration: function(seconds) {
var minutes = Math.floor(seconds/ 60);
var seconds = Math.floor(seconds - minutes * 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
},
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: statusMap[this.model.get("status")] || statusDisplayStrings.UNKNOWN
};
this.$el.html(
this.template(_.extend({}, this.model.attributes, renderedAttributes))
);
return this;
}
});
return PreviousVideoUploadView;
}
);
define(
["jquery", "underscore", "backbone", "js/views/baseview", "js/views/previous_video_upload"],
function($, _, Backbone, BaseView, PreviousVideoUploadView) {
"use strict";
var PreviousVideoUploadListView = BaseView.extend({
tagName: "section",
className: "wrapper-assets",
initialize: function() {
this.template = this.loadTemplate("previous-video-upload-list");
this.itemViews = this.collection.map(function(model) {
return new PreviousVideoUploadView({model: model});
});
},
render: function() {
var $el = this.$el;
$el.html(this.template());
var $tabBody = $el.find(".js-table-body");
_.each(this.itemViews, function(view) {
$tabBody.append(view.render().$el);
});
return this;
},
});
return PreviousVideoUploadListView;
}
);
......@@ -52,6 +52,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
- xmodule_js/common_static/js/vendor/jasmine.async.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js
- xmodule_js/src/xmodule.js
- xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/js/vendor/draggabilly.pkgd.js
......@@ -67,6 +68,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate.js
- xmodule_js/common_static/js/vendor/mock-ajax.js
# Paths to source JavaScript files
src_paths:
......
<form class="file-upload-form">
<div class="file-drop-area">
<%- gettext("Drag and drop or click here to upload video files.") %>
</div>
<input type="file" class="sr js-file-input" name="file" multiple>
</form>
<section class="active-video-upload-container">
<h3 class="sr">Active Uploads</h3>
<ul class="active-video-upload-list"></ul>
</section>
<h4 class="video-detail-name"><%- fileName %></h4>
<p class="video-detail-status"><%- gettext(status) %></p>
<div class="assets-library">
<h3 class="assets-title"><%- gettext("Previous Uploads") %></h3>
<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>
</tr>
</thead>
<tbody class="js-table-body"></tbody>
</table>
</div>
<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>
<%inherit file="base.html" />
<%!
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Video Uploads")}</%block>
<%block name="bodyclass">is-signedin course view-uploads view-video-uploads</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in ["active-video-upload-list", "active-video-upload", "previous-video-upload-list", "previous-video-upload"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="requirejs">
require(["js/factories/videos_index"], function (VideosIndexFactory) {
"use strict";
var $contentWrapper = $(".content-primary");
VideosIndexFactory(
$contentWrapper,
"${post_url}",
${concurrent_upload_limit},
$(".nav-actions .upload-button"),
$contentWrapper.data("previous-uploads")
);
});
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Content")}</small>
<span class="sr">&gt; </span>${_("Video Uploads")}
</h1>
<nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="icon-plus"></i> ${_("Upload New File")}</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main" data-previous-uploads="${json.dumps(previous_uploads, cls=DjangoJSONEncoder) | h}"></article>
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("Why upload video files?")}</h3>
<p>${_("For a video to play on different devices, it needs to be available in multiple formats. After you upload an original video file in .mp4 or .mov format on this page, an automated process creates those additional formats and stores them for you.")}</p>
<h3 class="title-3">${_("Monitoring files as they upload")}</h3>
<p>${_("Each video file that you upload needs to reach the video processing servers successfully before additional work can begin. You can monitor the progress of files as they upload, and try again if the upload fails.")}</p>
<h3 class="title-3">${_("Managing uploaded files")}</h3>
<p>${_("After a file uploads successfully, automated processing begins. After automated processing begins for a file it is listed under Previous Uploads as {em_start}In Progress{em_end}. When the status is {em_start}Complete{em_end}, edX assigns a unique video ID to the video file and you can add it to your course. If something goes wrong, the {em_start}Failed{em_end} status message appears. Check for problems in the file and upload a replacement.").format(em_start='<strong>', em_end="</strong>")}</p>
<h3 class="title-3">${_("How do I get the videos into my course?")}</h3>
<p>${_("After processing is complete for the video file, you copy its unique video ID. On the Course Outline page, you create or locate a video component to play this video. Edit the video component to paste the ID into the Advanced {em_start}EdX Video ID{em_end} field.").format(em_start='<strong>', em_end="</strong>")}</p>
</div>
</aside>
</section>
</div>
</%block>
......@@ -22,6 +22,7 @@
course_team_url = reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(course_key)})
assets_url = reverse('contentstore.views.assets_handler', kwargs={'course_key_string': unicode(course_key)})
textbooks_url = reverse('contentstore.views.textbooks_list_handler', kwargs={'course_key_string': unicode(course_key)})
videos_url = reverse('contentstore.views.videos_handler', kwargs={'course_key_string': unicode(course_key)})
import_url = reverse('contentstore.views.import_handler', kwargs={'course_key_string': unicode(course_key)})
course_info_url = reverse('contentstore.views.course_info_handler', kwargs={'course_key_string': unicode(course_key)})
export_url = reverse('contentstore.views.export_handler', kwargs={'course_key_string': unicode(course_key)})
......@@ -62,6 +63,11 @@
<li class="nav-item nav-course-courseware-textbooks">
<a href="${textbooks_url}">${_("Textbooks")}</a>
</li>
% if context_course.video_pipeline_configured:
<li class="nav-item nav-course-courseware-videos">
<a href="${videos_url}">${_("Video Uploads")}</a>
</li>
% endif
</ul>
</div>
</div>
......
......@@ -111,7 +111,6 @@ urlpatterns += patterns(
url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict),
)
if settings.FEATURES.get('ENABLE_EXPORT_GIT'):
urlpatterns += (url(
r'^export_git/{}$'.format(
......
......@@ -151,7 +151,7 @@ class VideoFields(object):
scope=Scope.settings,
)
edx_video_id = String(
help=_('Optional. Use this for videos where download and streaming URLs for the videos are completely managed by edX. This will override the settings for "Default Video URL", "Video File URLs", and all YouTube IDs. If you do not know what this setting is, you can leave it blank and continue to use these other settings.'),
help=_("If you were assigned a Video ID by edX for the video to play in this component, enter the ID here. In this case, do not enter values in the Default Video URL, the Video File URLs, and the YouTube ID fields. If you were not assigned an edX Video ID, enter values in those other fields and ignore this field."),
display_name=_("EdX Video ID"),
scope=Scope.settings,
default="",
......
/*
Jasmine-Ajax : a set of helpers for testing AJAX requests under the Jasmine
BDD framework for JavaScript.
Supports jQuery.
http://github.com/pivotal/jasmine-ajax
Jasmine Home page: http://pivotal.github.com/jasmine
Copyright (c) 2008-2013 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Jasmine-Ajax interface
var ajaxRequests = [];
function mostRecentAjaxRequest() {
if (ajaxRequests.length > 0) {
return ajaxRequests[ajaxRequests.length - 1];
} else {
return null;
}
}
function clearAjaxRequests() {
ajaxRequests = [];
}
// Fake XHR for mocking Ajax Requests & Responses
function FakeXMLHttpRequest() {
var extend = Object.extend || jQuery.extend;
extend(this, {
requestHeaders: {},
open: function() {
this.method = arguments[0];
this.url = arguments[1];
this.username = arguments[3];
this.password = arguments[4];
this.readyState = 1;
},
setRequestHeader: function(header, value) {
this.requestHeaders[header] = value;
},
abort: function() {
this.readyState = 0;
},
readyState: 0,
onload: function() {
},
onreadystatechange: function(isTimeout) {
},
status: null,
send: function(data) {
this.params = data;
this.readyState = 2;
},
data: function() {
var data = {};
if (typeof this.params !== 'string') return data;
var params = this.params.split('&');
for (var i = 0; i < params.length; ++i) {
var kv = params[i].replace(/\+/g, ' ').split('=');
var key = decodeURIComponent(kv[0]);
data[key] = data[key] || [];
data[key].push(decodeURIComponent(kv[1]));
data[key].sort();
}
return data;
},
getResponseHeader: function(name) {
return this.responseHeaders[name];
},
getAllResponseHeaders: function() {
var responseHeaders = [];
for (var i in this.responseHeaders) {
if (this.responseHeaders.hasOwnProperty(i)) {
responseHeaders.push(i + ': ' + this.responseHeaders[i]);
}
}
return responseHeaders.join('\r\n');
},
responseText: null,
response: function(response) {
this.status = response.status;
this.responseText = response.responseText || "";
this.readyState = 4;
this.responseHeaders = response.responseHeaders ||
{"Content-type": response.contentType || "application/json" };
// uncomment for jquery 1.3.x support
// jasmine.Clock.tick(20);
this.onload();
this.onreadystatechange();
},
responseTimeout: function() {
this.readyState = 4;
jasmine.Clock.tick(jQuery.ajaxSettings.timeout || 30000);
this.onreadystatechange('timeout');
}
});
return this;
}
jasmine.Ajax = {
isInstalled: function() {
return jasmine.Ajax.installed === true;
},
assertInstalled: function() {
if (!jasmine.Ajax.isInstalled()) {
throw new Error("Mock ajax is not installed, use jasmine.Ajax.useMock()");
}
},
useMock: function() {
if (!jasmine.Ajax.isInstalled()) {
var spec = jasmine.getEnv().currentSpec;
spec.after(jasmine.Ajax.uninstallMock);
jasmine.Ajax.installMock();
}
},
installMock: function() {
if (typeof jQuery != 'undefined') {
jasmine.Ajax.installJquery();
} else {
throw new Error("jasmine.Ajax currently only supports jQuery");
}
jasmine.Ajax.installed = true;
},
installJquery: function() {
jasmine.Ajax.mode = 'jQuery';
jasmine.Ajax.real = jQuery.ajaxSettings.xhr;
jQuery.ajaxSettings.xhr = jasmine.Ajax.jQueryMock;
},
uninstallMock: function() {
jasmine.Ajax.assertInstalled();
if (jasmine.Ajax.mode == 'jQuery') {
jQuery.ajaxSettings.xhr = jasmine.Ajax.real;
}
jasmine.Ajax.reset();
},
reset: function() {
jasmine.Ajax.installed = false;
jasmine.Ajax.mode = null;
jasmine.Ajax.real = null;
},
jQueryMock: function() {
var newXhr = new FakeXMLHttpRequest();
ajaxRequests.push(newXhr);
return newXhr;
},
installed: false,
mode: null
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment