Merge pull request #14206 from edx/ammar/tnl-4777-upload-error-message
Improve error handling for Video Uploads
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
"""
|
||||
Unit tests for video-related REST APIs.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import csv
|
||||
import ddt
|
||||
import json
|
||||
import dateutil.parser
|
||||
import re
|
||||
from StringIO import StringIO
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
@@ -16,7 +18,7 @@ from mock import Mock, patch
|
||||
from edxval.api import create_profile, create_video, get_video_info
|
||||
|
||||
from contentstore.models import VideoUploadConfig
|
||||
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings
|
||||
from contentstore.views.videos import KEY_EXPIRATION_IN_SECONDS, StatusDisplayStrings, convert_video_status
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -49,6 +51,7 @@ class VideoUploadTestMixin(object):
|
||||
|
||||
# course ids for videos
|
||||
course_ids = [unicode(self.course.id), unicode(self.course2.id)]
|
||||
created = datetime.now(pytz.utc)
|
||||
|
||||
self.profiles = ["profile1", "profile2"]
|
||||
self.previous_uploads = [
|
||||
@@ -59,6 +62,7 @@ class VideoUploadTestMixin(object):
|
||||
"status": "upload",
|
||||
"courses": course_ids,
|
||||
"encoded_videos": [],
|
||||
"created": created
|
||||
},
|
||||
{
|
||||
"edx_video_id": "test2",
|
||||
@@ -66,6 +70,7 @@ class VideoUploadTestMixin(object):
|
||||
"duration": 128.0,
|
||||
"status": "file_complete",
|
||||
"courses": course_ids,
|
||||
"created": created,
|
||||
"encoded_videos": [
|
||||
{
|
||||
"profile": "profile1",
|
||||
@@ -87,6 +92,7 @@ class VideoUploadTestMixin(object):
|
||||
"duration": 256.0,
|
||||
"status": "transcode_active",
|
||||
"courses": course_ids,
|
||||
"created": created,
|
||||
"encoded_videos": [
|
||||
{
|
||||
"profile": "profile1",
|
||||
@@ -105,6 +111,7 @@ class VideoUploadTestMixin(object):
|
||||
"duration": 3.14,
|
||||
"status": status,
|
||||
"courses": course_ids,
|
||||
"created": created,
|
||||
"encoded_videos": [],
|
||||
}
|
||||
for status in (
|
||||
@@ -184,7 +191,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
self.assertEqual(response_video[field], original_video[field])
|
||||
self.assertEqual(
|
||||
response_video["status"],
|
||||
StatusDisplayStrings.get(original_video["status"])
|
||||
convert_video_status(original_video)
|
||||
)
|
||||
|
||||
def test_get_html(self):
|
||||
@@ -442,6 +449,67 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
self._assert_video_removal(self.url, edx_video_id, 1)
|
||||
self._assert_video_removal(self.get_url_for_course_key(self.course2.id), edx_video_id, 0)
|
||||
|
||||
def test_convert_video_status(self):
|
||||
"""
|
||||
Verifies that convert_video_status works as expected.
|
||||
"""
|
||||
video = self.previous_uploads[0]
|
||||
|
||||
# video status should be failed if it's in upload state for more than 24 hours
|
||||
video['created'] = datetime(2016, 1, 1, 10, 10, 10, 0, pytz.UTC)
|
||||
status = convert_video_status(video)
|
||||
self.assertEqual(status, StatusDisplayStrings.get('upload_failed'))
|
||||
|
||||
# `invalid_token` should be converted to `youtube_duplicate`
|
||||
video['created'] = datetime.now(pytz.UTC)
|
||||
video['status'] = 'invalid_token'
|
||||
status = convert_video_status(video)
|
||||
self.assertEqual(status, StatusDisplayStrings.get('youtube_duplicate'))
|
||||
|
||||
# for all other status, there should not be any conversion
|
||||
statuses = StatusDisplayStrings._STATUS_MAP.keys() # pylint: disable=protected-access
|
||||
statuses.remove('invalid_token')
|
||||
for status in statuses:
|
||||
video['status'] = status
|
||||
new_status = convert_video_status(video)
|
||||
self.assertEqual(new_status, StatusDisplayStrings.get(status))
|
||||
|
||||
def assert_video_status(self, url, edx_video_id, status):
|
||||
"""
|
||||
Verifies that video with `edx_video_id` has `status`
|
||||
"""
|
||||
response = self.client.get_json(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
videos = json.loads(response.content)["videos"]
|
||||
for video in videos:
|
||||
if video['edx_video_id'] == edx_video_id:
|
||||
return self.assertEqual(video['status'], status)
|
||||
|
||||
# Test should fail if video not found
|
||||
self.assertEqual(True, False, 'Invalid edx_video_id')
|
||||
|
||||
def test_video_status_update_request(self):
|
||||
"""
|
||||
Verifies that video status update request works as expected.
|
||||
"""
|
||||
url = self.get_url_for_course_key(self.course.id)
|
||||
edx_video_id = 'test1'
|
||||
|
||||
self.assert_video_status(url, edx_video_id, 'Uploading')
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
json.dumps([{
|
||||
'edxVideoId': edx_video_id,
|
||||
'status': 'upload_failed',
|
||||
'message': 'server down'
|
||||
}]),
|
||||
content_type="application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
self.assert_video_status(url, edx_video_id, 'Failed')
|
||||
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_VIDEO_UPLOAD_PIPELINE": True})
|
||||
@override_settings(VIDEO_UPLOAD_PIPELINE={"BUCKET": "test_bucket", "ROOT_PATH": "test_root"})
|
||||
@@ -486,7 +554,7 @@ class VideoUrlsCsvTestCase(VideoUploadTestMixin, CourseTestCase):
|
||||
self.assertEqual(response_video["Duration"], str(original_video["duration"]))
|
||||
dateutil.parser.parse(response_video["Date Added"])
|
||||
self.assertEqual(response_video["Video ID"], original_video["edx_video_id"])
|
||||
self.assertEqual(response_video["Status"], StatusDisplayStrings.get(original_video["status"]))
|
||||
self.assertEqual(response_video["Status"], convert_video_status(original_video))
|
||||
for profile in expected_profiles:
|
||||
response_profile_url = response_video["{} URL".format(profile)]
|
||||
original_encoded_for_profile = next(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""
|
||||
Views related to the video upload feature
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from boto import s3
|
||||
import csv
|
||||
from uuid import uuid4
|
||||
@@ -12,7 +15,14 @@ from django.utils.translation import ugettext as _, ugettext_noop
|
||||
from django.views.decorators.http import require_GET, require_http_methods
|
||||
import rfc6266
|
||||
|
||||
from edxval.api import create_video, get_videos_for_course, SortDirection, VideoSortField, remove_video_for_course
|
||||
from edxval.api import (
|
||||
create_video,
|
||||
get_videos_for_course,
|
||||
SortDirection,
|
||||
VideoSortField,
|
||||
remove_video_for_course,
|
||||
update_video_status
|
||||
)
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from contentstore.models import VideoUploadConfig
|
||||
@@ -25,6 +35,8 @@ from .course import get_course_and_check_access
|
||||
|
||||
__all__ = ["videos_handler", "video_encodings_download"]
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Default expiration, in seconds, of one-time URLs used for uploading videos.
|
||||
KEY_EXPIRATION_IN_SECONDS = 86400
|
||||
@@ -36,6 +48,9 @@ VIDEO_SUPPORTED_FILE_FORMATS = {
|
||||
|
||||
VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
|
||||
|
||||
# maximum time for video to remain in upload state
|
||||
MAX_UPLOAD_HOURS = 24
|
||||
|
||||
|
||||
class StatusDisplayStrings(object):
|
||||
"""
|
||||
@@ -49,11 +64,17 @@ class StatusDisplayStrings(object):
|
||||
_IN_PROGRESS = ugettext_noop("In Progress")
|
||||
# Translators: This is the status for a video that the servers have successfully processed
|
||||
_COMPLETE = ugettext_noop("Ready")
|
||||
# Translators: This is the status for a video that is uploaded completely
|
||||
_UPLOAD_COMPLETED = ugettext_noop("Uploaded")
|
||||
# Translators: This is the status for a video that the servers have failed to process
|
||||
_FAILED = ugettext_noop("Failed")
|
||||
# Translators: This is the status for a video that is cancelled during upload by user
|
||||
_CANCELLED = ugettext_noop("Cancelled")
|
||||
# Translators: This is the status for a video which has failed
|
||||
# due to being flagged as a duplicate by an external or internal CMS
|
||||
_DUPLICATE = ugettext_noop("Failed Duplicate")
|
||||
# Translators: This is the status for a video which has duplicate token for youtube
|
||||
_YOUTUBE_DUPLICATE = ugettext_noop("YouTube Duplicate")
|
||||
# Translators: This is the status for a video for which an invalid
|
||||
# processing token was provided in the course settings
|
||||
_INVALID_TOKEN = ugettext_noop("Invalid Token")
|
||||
@@ -69,9 +90,14 @@ class StatusDisplayStrings(object):
|
||||
"transcode_active": _IN_PROGRESS,
|
||||
"file_delivered": _COMPLETE,
|
||||
"file_complete": _COMPLETE,
|
||||
"upload_completed": _UPLOAD_COMPLETED,
|
||||
"file_corrupt": _FAILED,
|
||||
"pipeline_error": _FAILED,
|
||||
"upload_failed": _FAILED,
|
||||
"s3_upload_failed": _FAILED,
|
||||
"upload_cancelled": _CANCELLED,
|
||||
"duplicate": _DUPLICATE,
|
||||
"youtube_duplicate": _YOUTUBE_DUPLICATE,
|
||||
"invalid_token": _INVALID_TOKEN,
|
||||
"imported": _IMPORTED,
|
||||
}
|
||||
@@ -115,6 +141,9 @@ def videos_handler(request, course_key_string, edx_video_id=None):
|
||||
remove_video_for_course(course_key_string, edx_video_id)
|
||||
return JsonResponse()
|
||||
else:
|
||||
if is_status_update_request(request.json):
|
||||
return send_video_status_update(request.json)
|
||||
|
||||
return videos_post(course, request)
|
||||
|
||||
|
||||
@@ -226,6 +255,36 @@ def _get_and_validate_course(course_key_string, user):
|
||||
return None
|
||||
|
||||
|
||||
def convert_video_status(video):
|
||||
"""
|
||||
Convert status of a video. Status can be converted to one of the following:
|
||||
|
||||
* FAILED if video is in `upload` state for more than 24 hours
|
||||
* `YouTube Duplicate` if status is `invalid_token`
|
||||
* user-friendly video status
|
||||
"""
|
||||
now = datetime.now(video['created'].tzinfo)
|
||||
if video['status'] == 'upload' and (now - video['created']) > timedelta(hours=MAX_UPLOAD_HOURS):
|
||||
new_status = 'upload_failed'
|
||||
status = StatusDisplayStrings.get(new_status)
|
||||
message = 'Video with id [%s] is still in upload after [%s] hours, setting status to [%s]' % (
|
||||
video['edx_video_id'], MAX_UPLOAD_HOURS, new_status
|
||||
)
|
||||
send_video_status_update([
|
||||
{
|
||||
'edxVideoId': video['edx_video_id'],
|
||||
'status': new_status,
|
||||
'message': message
|
||||
}
|
||||
])
|
||||
elif video['status'] == 'invalid_token':
|
||||
status = StatusDisplayStrings.get('youtube_duplicate')
|
||||
else:
|
||||
status = StatusDisplayStrings.get(video['status'])
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def _get_videos(course):
|
||||
"""
|
||||
Retrieves the list of videos from VAL corresponding to this course.
|
||||
@@ -234,7 +293,7 @@ def _get_videos(course):
|
||||
|
||||
# convert VAL's status to studio's Video Upload feature status.
|
||||
for video in videos:
|
||||
video["status"] = StatusDisplayStrings.get(video["status"])
|
||||
video["status"] = convert_video_status(video)
|
||||
|
||||
return videos
|
||||
|
||||
@@ -386,3 +445,21 @@ def storage_service_key(bucket, file_name):
|
||||
file_name
|
||||
)
|
||||
return s3.key.Key(bucket, key_name)
|
||||
|
||||
|
||||
def send_video_status_update(updates):
|
||||
"""
|
||||
Update video status in edx-val.
|
||||
"""
|
||||
for update in updates:
|
||||
update_video_status(update.get('edxVideoId'), update.get('status'))
|
||||
LOGGER.info(update.get('message'))
|
||||
|
||||
return JsonResponse()
|
||||
|
||||
|
||||
def is_status_update_request(request_data):
|
||||
"""
|
||||
Returns True if `request_data` contains status update else False.
|
||||
"""
|
||||
return any('status' in update for update in request_data)
|
||||
|
||||
@@ -4,6 +4,22 @@
|
||||
(function(requirejs, requireSerial) {
|
||||
'use strict';
|
||||
|
||||
if (window) {
|
||||
define('add-a11y-deps',
|
||||
[
|
||||
'underscore',
|
||||
'underscore.string',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils'
|
||||
], function(_, str, HtmlUtils, StringUtils) {
|
||||
window._ = _;
|
||||
window._.str = str;
|
||||
window.edx = window.edx || {};
|
||||
window.edx.HtmlUtils = HtmlUtils;
|
||||
window.edx.StringUtils = StringUtils;
|
||||
});
|
||||
}
|
||||
|
||||
var i, specHelpers, testFiles;
|
||||
|
||||
requirejs.config({
|
||||
@@ -169,6 +185,10 @@
|
||||
return window.MathJax.Hub.Configured();
|
||||
}
|
||||
},
|
||||
'accessibility': {
|
||||
exports: 'accessibility',
|
||||
deps: ['add-a11y-deps']
|
||||
},
|
||||
'URI': {
|
||||
exports: 'URI'
|
||||
},
|
||||
|
||||
@@ -21,7 +21,13 @@ define(
|
||||
defaults: {
|
||||
videoId: null,
|
||||
status: statusStrings.STATUS_QUEUED,
|
||||
progress: 0
|
||||
progress: 0,
|
||||
failureMessage: null
|
||||
},
|
||||
|
||||
uploading: function() {
|
||||
var status = this.get('status');
|
||||
return (this.get('progress') < 1) && ((status === statusStrings.STATUS_UPLOADING));
|
||||
}
|
||||
},
|
||||
statusStrings
|
||||
|
||||
@@ -4,18 +4,39 @@ define(
|
||||
'jquery',
|
||||
'js/models/active_video_upload',
|
||||
'js/views/active_video_upload_list',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'common/js/spec_helpers/template_helpers',
|
||||
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'accessibility',
|
||||
'mock-ajax'
|
||||
],
|
||||
function($, ActiveVideoUpload, ActiveVideoUploadListView, TemplateHelpers) {
|
||||
function($, ActiveVideoUpload, ActiveVideoUploadListView, StringUtils, TemplateHelpers, AjaxHelpers) {
|
||||
'use strict';
|
||||
var concurrentUploadLimit = 2;
|
||||
var concurrentUploadLimit = 2,
|
||||
POST_URL = '/test/post/url',
|
||||
VIDEO_ID = 'video101',
|
||||
UPLOAD_STATUS = {
|
||||
s3Fail: 's3_upload_failed',
|
||||
fail: 'upload_failed',
|
||||
success: 'upload_completed'
|
||||
},
|
||||
makeUploadUrl,
|
||||
getSentRequests,
|
||||
verifyUploadViewInfo,
|
||||
getStatusUpdateRequest,
|
||||
verifyStatusUpdateRequest,
|
||||
sendUploadPostResponse,
|
||||
verifyA11YMessage,
|
||||
verifyUploadPostRequest;
|
||||
|
||||
describe('ActiveVideoUploadListView', function() {
|
||||
beforeEach(function() {
|
||||
TemplateHelpers.installTemplate('active-video-upload', true);
|
||||
setFixtures(
|
||||
'<div id="page-prompt"></div><div id="page-notification"></div><div id="reader-feedback"></div>'
|
||||
);
|
||||
TemplateHelpers.installTemplate('active-video-upload');
|
||||
TemplateHelpers.installTemplate('active-video-upload-list');
|
||||
this.postUrl = '/test/post/url';
|
||||
this.postUrl = POST_URL;
|
||||
this.uploadButton = $('<button>');
|
||||
this.videoSupportedFileFormats = ['.mp4', '.mov'];
|
||||
this.videoUploadMaxFileSizeInGB = 5;
|
||||
@@ -51,41 +72,235 @@ define(
|
||||
expect(this.view.onBeforeUnload()).toBeUndefined();
|
||||
});
|
||||
|
||||
var makeUploadUrl = function(fileName) {
|
||||
makeUploadUrl = function(fileName) {
|
||||
return 'http://www.example.com/test_url/' + fileName;
|
||||
};
|
||||
|
||||
var getSentRequests = function() {
|
||||
getSentRequests = function() {
|
||||
return jasmine.Ajax.requests.filter(function(request) {
|
||||
return request.readyState > 0;
|
||||
});
|
||||
};
|
||||
|
||||
describe('supported file formats', function() {
|
||||
it('should not show unsupported file format notification for supported files', function() {
|
||||
var supportedFiles = {
|
||||
files: [
|
||||
{name: 'test-1.mp4', size: 0},
|
||||
{name: 'test-1.mov', size: 0}
|
||||
]
|
||||
};
|
||||
this.view.$uploadForm.fileupload('add', supportedFiles);
|
||||
expect(this.view.fileErrorMsg).toBeNull();
|
||||
verifyUploadViewInfo = function(view, expectedTitle, expectedMessage) {
|
||||
expect(view.$('.video-detail-status').text().trim()).toEqual('Upload failed');
|
||||
expect(view.$('.more-details-action').contents().get(0).nodeValue.trim()).toEqual('Read More');
|
||||
expect(view.$('.more-details-action .sr').text().trim()).toEqual('details about the failure');
|
||||
view.$('a.more-details-action').click();
|
||||
expect($('#prompt-warning-title').text().trim()).toEqual(expectedTitle);
|
||||
expect($('#prompt-warning-description').text().trim()).toEqual(expectedMessage);
|
||||
};
|
||||
|
||||
getStatusUpdateRequest = function() {
|
||||
var sentRequests = getSentRequests();
|
||||
return sentRequests.filter(function(request) {
|
||||
return request.method === 'POST' && _.has(
|
||||
JSON.parse(request.params)[0], 'status'
|
||||
);
|
||||
})[0];
|
||||
};
|
||||
|
||||
verifyStatusUpdateRequest = function(videoId, status, message, expectedRequest) {
|
||||
var request = expectedRequest || getStatusUpdateRequest(),
|
||||
expectedData = JSON.stringify({
|
||||
edxVideoId: videoId,
|
||||
status: status,
|
||||
message: message
|
||||
});
|
||||
expect(request.method).toEqual('POST');
|
||||
expect(request.url).toEqual(POST_URL);
|
||||
if (_.has(request, 'requestBody')) {
|
||||
expect(_.isMatch(request.requestBody, expectedData)).toBeTruthy();
|
||||
} else {
|
||||
expect(_.isMatch(request.params, expectedData)).toBeTruthy();
|
||||
}
|
||||
};
|
||||
|
||||
verifyUploadPostRequest = function(requestParams) {
|
||||
// get latest requestParams.length requests
|
||||
var postRequests = getSentRequests().slice(-requestParams.length);
|
||||
_.each(postRequests, function(postRequest, index) {
|
||||
expect(postRequest.method).toEqual('POST');
|
||||
expect(postRequest.url).toEqual(POST_URL);
|
||||
expect(postRequest.params).toEqual(requestParams[index]);
|
||||
});
|
||||
it('should show invalid file format notification for unspoorted files', function() {
|
||||
var unSupportedFiles = {
|
||||
files: [
|
||||
};
|
||||
|
||||
verifyA11YMessage = function(message) {
|
||||
expect($('#reader-feedback').text().trim()).toEqual(message);
|
||||
};
|
||||
|
||||
sendUploadPostResponse = function(request, fileNames, url) {
|
||||
request.respondWith({
|
||||
status: 200,
|
||||
responseText: JSON.stringify({
|
||||
files: _.map(
|
||||
fileNames,
|
||||
function(fileName) {
|
||||
return {
|
||||
edx_video_id: VIDEO_ID,
|
||||
file_name: fileName,
|
||||
upload_url: url || makeUploadUrl(fileName)
|
||||
};
|
||||
}
|
||||
)
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
describe('errors', function() {
|
||||
it('should show error notification for status update request in case of server error', function() {
|
||||
this.view.sendStatusUpdate([
|
||||
{
|
||||
edxVideoId: '101',
|
||||
status: 'upload_completed',
|
||||
message: 'Uploaded completed'
|
||||
}
|
||||
]);
|
||||
getStatusUpdateRequest().respondWith(
|
||||
{
|
||||
status: 500,
|
||||
responseText: JSON.stringify({
|
||||
error: '500 server errror'
|
||||
})
|
||||
}
|
||||
);
|
||||
expect($('#notification-error-title').text().trim()).toEqual(
|
||||
"Studio's having trouble saving your work"
|
||||
);
|
||||
expect($('#notification-error-description').text().trim()).toEqual('500 server errror');
|
||||
});
|
||||
|
||||
it('should correctly parse and show S3 error response xml', function() {
|
||||
var fileInfo = {name: 'video.mp4', size: 10000},
|
||||
videos = {
|
||||
files: [
|
||||
fileInfo
|
||||
]
|
||||
},
|
||||
S3Url = 'http://s3.aws.com/upload/videos/' + fileInfo.name,
|
||||
requests;
|
||||
|
||||
// this is required so that we can use AjaxHelpers ajax mock utils instead of jasmine mock-ajax.js
|
||||
jasmine.Ajax.uninstall();
|
||||
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
this.view.$uploadForm.fileupload('add', videos);
|
||||
AjaxHelpers.respond(requests, {
|
||||
status: 200,
|
||||
body: {
|
||||
files: [{
|
||||
edx_video_id: VIDEO_ID,
|
||||
file_name: fileInfo.name,
|
||||
upload_url: S3Url
|
||||
}]
|
||||
}
|
||||
});
|
||||
expect(requests.length).toEqual(2);
|
||||
AjaxHelpers.respond(
|
||||
requests,
|
||||
{
|
||||
statusCode: 403,
|
||||
contentType: 'application/xml',
|
||||
body: '<Error><Message>Invalid access key.</Message></Error>'
|
||||
}
|
||||
);
|
||||
verifyUploadViewInfo(
|
||||
this.view.itemViews[0],
|
||||
'Your file could not be uploaded',
|
||||
'Invalid access key.'
|
||||
);
|
||||
verifyStatusUpdateRequest(
|
||||
VIDEO_ID,
|
||||
UPLOAD_STATUS.s3Fail,
|
||||
'Invalid access key.',
|
||||
AjaxHelpers.currentRequest(requests)
|
||||
);
|
||||
verifyA11YMessage(
|
||||
StringUtils.interpolate('Upload failed for video {fileName}', {fileName: fileInfo.name})
|
||||
);
|
||||
|
||||
// this is required otherwise mock-ajax will throw an exception when it tries to uninstall Ajax in
|
||||
// outer afterEach
|
||||
jasmine.Ajax.install();
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload cancelled', function() {
|
||||
it('should send correct status update request', function() {
|
||||
var fileInfo = {name: 'video.mp4'},
|
||||
videos = {
|
||||
files: [
|
||||
fileInfo
|
||||
]
|
||||
},
|
||||
sentRequests,
|
||||
uploadCancelledRequest;
|
||||
|
||||
this.view.$uploadForm.fileupload('add', videos);
|
||||
sendUploadPostResponse(getSentRequests()[0], [fileInfo.name]);
|
||||
sentRequests = getSentRequests();
|
||||
|
||||
// no upload cancel request should be sent because `uploading` attribute is not set on model
|
||||
this.view.onUnload();
|
||||
expect(getSentRequests().length).toEqual(sentRequests.length);
|
||||
|
||||
// set `uploading` attribute on each model
|
||||
this.view.collection.each(function(model) {
|
||||
model.set('uploading', true);
|
||||
});
|
||||
|
||||
// upload_cancelled request should be sent
|
||||
this.view.onUnload();
|
||||
uploadCancelledRequest = jasmine.Ajax.requests.mostRecent();
|
||||
expect(uploadCancelledRequest.params).toEqual(
|
||||
JSON.stringify(
|
||||
[{
|
||||
edxVideoId: VIDEO_ID,
|
||||
status: 'upload_cancelled',
|
||||
message: 'User cancelled video upload'
|
||||
}]
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file formats', function() {
|
||||
it('should not fail upload for supported file formats', function() {
|
||||
var supportedFiles = {
|
||||
files: [
|
||||
{name: 'test-1.mp4'},
|
||||
{name: 'test-1.mov'}
|
||||
]
|
||||
},
|
||||
requestParams = _.map(supportedFiles.files, function(file) {
|
||||
return JSON.stringify({files: [{file_name: file.name}]});
|
||||
});
|
||||
this.view.$uploadForm.fileupload('add', supportedFiles);
|
||||
verifyUploadPostRequest(requestParams);
|
||||
});
|
||||
it('should fail upload for unspported file formats', function() {
|
||||
var files = [
|
||||
{name: 'test-3.txt', size: 0},
|
||||
{name: 'test-4.png', size: 0}
|
||||
]
|
||||
};
|
||||
],
|
||||
unSupportedFiles = {
|
||||
files: files
|
||||
},
|
||||
self = this;
|
||||
|
||||
this.view.$uploadForm.fileupload('add', unSupportedFiles);
|
||||
expect(this.view.fileErrorMsg).toBeDefined();
|
||||
expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
|
||||
expect(this.view.fileErrorMsg.options.message).toEqual(
|
||||
'test-3.txt is not in a supported file format. Supported file formats are ' +
|
||||
this.videoSupportedFileFormats.join(' and ') + '.'
|
||||
);
|
||||
_.each(this.view.itemViews, function(uploadView, index) {
|
||||
verifyUploadViewInfo(
|
||||
uploadView,
|
||||
'Your file could not be uploaded',
|
||||
StringUtils.interpolate(
|
||||
'{fileName} is not in a supported file format. Supported file formats are {supportedFormats}.', // eslint-disable-line max-len
|
||||
{fileName: files[index].name, supportedFormats: self.videoSupportedFileFormats.join(' and ')} // eslint-disable-line max-len
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,17 +319,43 @@ define(
|
||||
files: [
|
||||
{name: 'file.mp4', size: fileSize}
|
||||
]
|
||||
};
|
||||
},
|
||||
requestParams = _.map(fileToUpload.files, function(file) {
|
||||
return JSON.stringify({files: [{file_name: file.name}]});
|
||||
}),
|
||||
uploadView;
|
||||
this.view.$uploadForm.fileupload('add', fileToUpload);
|
||||
if (fileSize > maxFileSizeInBytes) {
|
||||
expect(this.view.fileErrorMsg).toBeDefined();
|
||||
expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
|
||||
expect(this.view.fileErrorMsg.options.message).toEqual(
|
||||
uploadView = this.view.itemViews[0];
|
||||
verifyUploadViewInfo(
|
||||
uploadView,
|
||||
'Your file could not be uploaded',
|
||||
'file.mp4 exceeds maximum size of ' + this.videoUploadMaxFileSizeInGB + ' GB.'
|
||||
);
|
||||
verifyA11YMessage(
|
||||
StringUtils.interpolate(
|
||||
'Upload failed for video {fileName}', {fileName: 'file.mp4'}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.view.$uploadForm.fileupload('add', fileToUpload);
|
||||
expect(this.view.fileErrorMsg).toBeNull();
|
||||
verifyUploadPostRequest(requestParams);
|
||||
sendUploadPostResponse(
|
||||
getSentRequests()[0],
|
||||
[fileToUpload.files[0].name]
|
||||
);
|
||||
getSentRequests()[1].respondWith(
|
||||
{status: 200}
|
||||
);
|
||||
verifyStatusUpdateRequest(
|
||||
VIDEO_ID,
|
||||
UPLOAD_STATUS.success,
|
||||
'Uploaded completed'
|
||||
);
|
||||
verifyA11YMessage(
|
||||
StringUtils.interpolate(
|
||||
'Upload completed for video {fileName}', {fileName: fileToUpload.files[0].name}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -140,7 +381,7 @@ define(
|
||||
// that jQuery-File-Upload uses to retrieve it.
|
||||
var realProp = $.prop;
|
||||
spyOn($, 'prop').and.callFake(function(el, propName) {
|
||||
if (arguments.length == 2 && propName == 'files') {
|
||||
if (arguments.length === 2 && propName === 'files') {
|
||||
return _.map(
|
||||
fileNames,
|
||||
function(fileName) { return {name: fileName}; }
|
||||
@@ -169,29 +410,10 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger the notification error handler on server error', function() {
|
||||
this.request.respondWith({status: 500});
|
||||
expect(this.view.fileErrorMsg).toBeDefined();
|
||||
expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
|
||||
});
|
||||
|
||||
describe('and successful server response', function() {
|
||||
beforeEach(function() {
|
||||
jasmine.Ajax.requests.reset();
|
||||
this.request.respondWith({
|
||||
status: 200,
|
||||
responseText: JSON.stringify({
|
||||
files: _.map(
|
||||
fileNames,
|
||||
function(fileName) {
|
||||
return {
|
||||
'file_name': fileName,
|
||||
'upload_url': makeUploadUrl(fileName)
|
||||
};
|
||||
}
|
||||
)
|
||||
})
|
||||
});
|
||||
sendUploadPostResponse(this.request, fileNames);
|
||||
this.$uploadElems = this.view.$('.active-video-upload');
|
||||
});
|
||||
|
||||
@@ -276,6 +498,13 @@ define(
|
||||
getSentRequests()[0].respondWith(
|
||||
{status: subCaseInfo.responseStatus}
|
||||
);
|
||||
// after successful upload, status update request is sent to server
|
||||
// we re-render views after success response is received from server
|
||||
if (subCaseInfo.statusText === ActiveVideoUpload.STATUS_COMPLETED) {
|
||||
getStatusUpdateRequest().respondWith(
|
||||
{status: 200}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should update status and progress', function() {
|
||||
@@ -305,15 +534,17 @@ define(
|
||||
}
|
||||
});
|
||||
|
||||
it('should not trigger the notification error handler', function() {
|
||||
expect(this.view.fileErrorMsg).toBeNull();
|
||||
});
|
||||
|
||||
if (caseInfo.numFiles > concurrentUploadLimit) {
|
||||
it('should start a new upload', function() {
|
||||
var $uploadElem = $(this.$uploadElems[concurrentUploadLimit]);
|
||||
|
||||
// we try to upload 3 files. 2 files(2 requests) will start
|
||||
// uploading immediately and third one will be queued, after
|
||||
// an upload is completed, queued file(3rd request) will start
|
||||
// uploading, 4th request will be sent to server to update
|
||||
// status for completed upload
|
||||
expect(getSentRequests().length).toEqual(
|
||||
concurrentUploadLimit + 1
|
||||
concurrentUploadLimit + 1 + 1
|
||||
);
|
||||
expect(
|
||||
$.trim($uploadElem.find('.video-detail-status').text())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define(
|
||||
['js/models/active_video_upload', 'js/views/baseview'],
|
||||
function(ActiveVideoUpload, BaseView) {
|
||||
['underscore', 'js/models/active_video_upload', 'js/views/baseview', 'common/js/components/views/feedback_prompt'],
|
||||
function(_, ActiveVideoUpload, BaseView, PromptView) {
|
||||
'use strict';
|
||||
|
||||
var STATUS_CLASSES = [
|
||||
@@ -13,15 +13,20 @@ define(
|
||||
tagName: 'li',
|
||||
className: 'active-video-upload',
|
||||
|
||||
events: {
|
||||
'click a.more-details-action': 'showUploadFailureMessage'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = this.loadTemplate('active-video-upload');
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
var $el = this.$el,
|
||||
status;
|
||||
$el.html(this.template(this.model.attributes));
|
||||
var status = this.model.get('status');
|
||||
status = this.model.get('status');
|
||||
_.each(
|
||||
STATUS_CLASSES,
|
||||
function(statusClass) {
|
||||
@@ -29,6 +34,21 @@ define(
|
||||
}
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
showUploadFailureMessage: function() {
|
||||
return new PromptView.Warning({
|
||||
title: gettext('Your file could not be uploaded'),
|
||||
message: this.model.get('failureMessage'),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Close'),
|
||||
click: function(prompt) {
|
||||
return prompt.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'underscore.string',
|
||||
'backbone',
|
||||
'js/models/active_video_upload',
|
||||
'js/views/baseview',
|
||||
'js/views/active_video_upload',
|
||||
'common/js/components/views/feedback_notification',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'text!templates/active-video-upload-list.underscore',
|
||||
'jquery.fileupload'
|
||||
],
|
||||
function($, _, str, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView, NotificationView, HtmlUtils,
|
||||
activeVideoUploadListTemplate) {
|
||||
function($, _, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView,
|
||||
HtmlUtils, StringUtils, activeVideoUploadListTemplate) {
|
||||
'use strict';
|
||||
var ActiveVideoUploadListView,
|
||||
CONVERSION_FACTOR_GBS_TO_BYTES = 1000 * 1000 * 1000;
|
||||
@@ -24,6 +23,8 @@ define([
|
||||
'drop .file-drop-area': 'dragleave'
|
||||
},
|
||||
|
||||
defaultFailureMessage: gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'), // eslint-disable-line max-len
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = HtmlUtils.template(activeVideoUploadListTemplate)({});
|
||||
this.collection = new Backbone.Collection();
|
||||
@@ -37,11 +38,11 @@ define([
|
||||
if (options.uploadButton) {
|
||||
options.uploadButton.click(this.chooseFile.bind(this));
|
||||
}
|
||||
// error message modal for file uploads
|
||||
this.fileErrorMsg = null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var preventDefault;
|
||||
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
this.template
|
||||
@@ -64,12 +65,13 @@ define([
|
||||
|
||||
// Disable default drag and drop behavior for the window (which
|
||||
// is to load the file in place)
|
||||
var preventDefault = function(event) {
|
||||
preventDefault = function(event) {
|
||||
event.preventDefault();
|
||||
};
|
||||
$(window).on('dragover', preventDefault);
|
||||
$(window).on('drop', preventDefault);
|
||||
$(window).on('beforeunload', this.onBeforeUnload.bind(this));
|
||||
$(window).on('unload', this.onUnload.bind(this));
|
||||
|
||||
return this;
|
||||
},
|
||||
@@ -77,17 +79,40 @@ define([
|
||||
onBeforeUnload: function() {
|
||||
// Are there are uploads queued or in progress?
|
||||
var uploading = this.collection.filter(function(model) {
|
||||
var stat = model.get('status');
|
||||
return (model.get('progress') < 1) &&
|
||||
((stat === ActiveVideoUpload.STATUS_QUEUED ||
|
||||
(stat === ActiveVideoUpload.STATUS_UPLOADING)));
|
||||
var isUploading = model.uploading();
|
||||
if (isUploading) {
|
||||
model.set('uploading', true);
|
||||
} else {
|
||||
model.set('uploading', false);
|
||||
}
|
||||
return isUploading;
|
||||
});
|
||||
|
||||
// If so, show a warning message.
|
||||
if (uploading.length) {
|
||||
return gettext('Your video uploads are not complete.');
|
||||
}
|
||||
},
|
||||
|
||||
onUnload: function() {
|
||||
var statusUpdates = [];
|
||||
this.collection.each(function(model) {
|
||||
if (model.get('uploading')) {
|
||||
statusUpdates.push(
|
||||
{
|
||||
edxVideoId: model.get('videoId'),
|
||||
status: 'upload_cancelled',
|
||||
message: 'User cancelled video upload'
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (statusUpdates.length > 0) {
|
||||
this.sendStatusUpdate(statusUpdates);
|
||||
}
|
||||
},
|
||||
|
||||
addUpload: function(model) {
|
||||
var itemView = new ActiveVideoUploadView({model: model});
|
||||
this.itemViews.push(itemView);
|
||||
@@ -100,8 +125,6 @@ define([
|
||||
|
||||
chooseFile: function(event) {
|
||||
event.preventDefault();
|
||||
// hide error message if any present.
|
||||
this.hideErrorMessage();
|
||||
this.$uploadForm.find('.js-file-input').click();
|
||||
},
|
||||
|
||||
@@ -124,70 +147,69 @@ define([
|
||||
fileUploadAdd: function(event, uploadData) {
|
||||
var view = this,
|
||||
model,
|
||||
errors,
|
||||
errorMsg;
|
||||
|
||||
// Validate file
|
||||
errorMsg = view.validateFile(uploadData);
|
||||
if (errorMsg) {
|
||||
view.showErrorMessage(errorMsg);
|
||||
if (uploadData.redirected) {
|
||||
model = new ActiveVideoUpload({
|
||||
fileName: uploadData.files[0].name,
|
||||
videoId: uploadData.videoId
|
||||
});
|
||||
this.collection.add(model);
|
||||
uploadData.cid = model.cid; // eslint-disable-line no-param-reassign
|
||||
uploadData.submit();
|
||||
} else {
|
||||
if (uploadData.redirected) {
|
||||
model = new ActiveVideoUpload({
|
||||
fileName: uploadData.files[0].name,
|
||||
videoId: uploadData.videoId
|
||||
});
|
||||
this.collection.add(model);
|
||||
uploadData.cid = model.cid; // eslint-disable-line no-param-reassign
|
||||
uploadData.submit();
|
||||
} else {
|
||||
_.each(
|
||||
uploadData.files,
|
||||
function(file) {
|
||||
$.ajax({
|
||||
url: view.postUrl,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
files: [{file_name: file.name, content_type: file.type}]
|
||||
}),
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
global: false // Do not trigger global AJAX error handler
|
||||
}).done(function(responseData) {
|
||||
_.each(
|
||||
responseData.files,
|
||||
function(file) { // eslint-disable-line no-shadow
|
||||
view.$uploadForm.fileupload('add', {
|
||||
files: _.filter(uploadData.files, function(fileObj) {
|
||||
return file.file_name === fileObj.name;
|
||||
}),
|
||||
url: file.upload_url,
|
||||
videoId: file.edx_video_id,
|
||||
multipart: false,
|
||||
global: false, // Do not trigger global AJAX error handler
|
||||
redirected: true
|
||||
});
|
||||
}
|
||||
);
|
||||
}).fail(function(response) {
|
||||
if (response.responseText) {
|
||||
try {
|
||||
errorMsg = JSON.parse(response.responseText).error;
|
||||
} catch (error) {
|
||||
errorMsg = str.truncate(response.responseText, 300);
|
||||
}
|
||||
} else {
|
||||
errorMsg = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len
|
||||
}
|
||||
view.showErrorMessage(errorMsg);
|
||||
});
|
||||
}
|
||||
// Validate file and remove the files with errors
|
||||
errors = view.validateFile(uploadData);
|
||||
_.each(errors, function(error) {
|
||||
view.addUploadFailureView(error.fileName, error.message);
|
||||
uploadData.files.splice(
|
||||
_.findIndex(uploadData.files, function(file) { return file.name === error.fileName; }), 1
|
||||
);
|
||||
}
|
||||
});
|
||||
_.each(
|
||||
uploadData.files,
|
||||
function(file) {
|
||||
$.ajax({
|
||||
url: view.postUrl,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
files: [{file_name: file.name, content_type: file.type}]
|
||||
}),
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
global: false // Do not trigger global AJAX error handler
|
||||
}).done(function(responseData) {
|
||||
_.each(
|
||||
responseData.files,
|
||||
function(file) { // eslint-disable-line no-shadow
|
||||
view.$uploadForm.fileupload('add', {
|
||||
files: _.filter(uploadData.files, function(fileObj) {
|
||||
return file.file_name === fileObj.name;
|
||||
}),
|
||||
url: file.upload_url,
|
||||
videoId: file.edx_video_id,
|
||||
multipart: false,
|
||||
global: false, // Do not trigger global AJAX error handler
|
||||
redirected: true
|
||||
});
|
||||
}
|
||||
);
|
||||
}).fail(function(response) {
|
||||
try {
|
||||
errorMsg = JSON.parse(response.responseText).error;
|
||||
} catch (error) {
|
||||
errorMsg = view.defaultFailureMessage;
|
||||
}
|
||||
view.addUploadFailureView(file.name, errorMsg);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: function(cid, status) {
|
||||
this.collection.get(cid).set('status', status);
|
||||
setStatus: function(cid, status, failureMessage) {
|
||||
this.collection.get(cid).set({status: status, failureMessage: failureMessage || null});
|
||||
},
|
||||
|
||||
// progress should be a number between 0 and 1 (inclusive)
|
||||
@@ -204,51 +226,89 @@ define([
|
||||
},
|
||||
|
||||
fileUploadDone: function(event, data) {
|
||||
this.setStatus(data.cid, ActiveVideoUpload.STATUS_COMPLETED);
|
||||
this.setProgress(data.cid, 1);
|
||||
if (this.onFileUploadDone) {
|
||||
this.onFileUploadDone(this.collection);
|
||||
this.clearSuccessful();
|
||||
}
|
||||
var model = this.collection.get(data.cid),
|
||||
self = this;
|
||||
|
||||
this.readMessages([
|
||||
StringUtils.interpolate(
|
||||
gettext('Upload completed for video {fileName}'),
|
||||
{fileName: model.get('fileName')}
|
||||
)
|
||||
]);
|
||||
|
||||
this.sendStatusUpdate([
|
||||
{
|
||||
edxVideoId: model.get('videoId'),
|
||||
status: 'upload_completed',
|
||||
message: 'Uploaded completed'
|
||||
}
|
||||
]).done(function() {
|
||||
self.setStatus(data.cid, ActiveVideoUpload.STATUS_COMPLETED);
|
||||
self.setProgress(data.cid, 1);
|
||||
if (self.onFileUploadDone) {
|
||||
self.onFileUploadDone(self.collection);
|
||||
self.clearSuccessful();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fileUploadFail: function(event, data) {
|
||||
this.setStatus(data.cid, ActiveVideoUpload.STATUS_FAILED);
|
||||
var responseText = data.jqXHR.responseText,
|
||||
message = this.defaultFailureMessage,
|
||||
status = 'upload_failed',
|
||||
model = this.collection.get(data.cid);
|
||||
|
||||
if (responseText && data.jqXHR.getResponseHeader('content-type') === 'application/xml') {
|
||||
message = $(responseText).find('Message').text();
|
||||
status = 's3_upload_failed';
|
||||
}
|
||||
|
||||
this.readMessages([
|
||||
StringUtils.interpolate(
|
||||
gettext('Upload failed for video {fileName}'),
|
||||
{fileName: model.get('fileName')}
|
||||
)
|
||||
]);
|
||||
|
||||
this.sendStatusUpdate([
|
||||
{
|
||||
edxVideoId: model.get('videoId'),
|
||||
status: status,
|
||||
message: message
|
||||
}
|
||||
]);
|
||||
this.setStatus(data.cid, ActiveVideoUpload.STATUS_FAILED, message);
|
||||
},
|
||||
|
||||
addUploadFailureView: function(fileName, failureMessage) {
|
||||
var model = new ActiveVideoUpload({
|
||||
fileName: fileName,
|
||||
status: ActiveVideoUpload.STATUS_FAILED,
|
||||
failureMessage: failureMessage
|
||||
});
|
||||
this.collection.add(model);
|
||||
this.readMessages([
|
||||
StringUtils.interpolate(
|
||||
gettext('Upload failed for video {fileName}'),
|
||||
{fileName: model.get('fileName')}
|
||||
)
|
||||
]);
|
||||
},
|
||||
|
||||
getMaxFileSizeInBytes: function() {
|
||||
return this.videoUploadMaxFileSizeInGB * CONVERSION_FACTOR_GBS_TO_BYTES;
|
||||
},
|
||||
|
||||
hideErrorMessage: function() {
|
||||
if (this.fileErrorMsg) {
|
||||
this.fileErrorMsg.hide();
|
||||
this.fileErrorMsg = null;
|
||||
}
|
||||
},
|
||||
|
||||
readMessages: function(messages) {
|
||||
if ($(window).prop('SR') !== undefined) {
|
||||
$(window).prop('SR').readTexts(messages);
|
||||
}
|
||||
},
|
||||
|
||||
showErrorMessage: function(errorMsg) {
|
||||
var titleMsg;
|
||||
if (!this.fileErrorMsg) {
|
||||
titleMsg = gettext('Your file could not be uploaded');
|
||||
this.fileErrorMsg = new NotificationView.Error({
|
||||
title: titleMsg,
|
||||
message: errorMsg
|
||||
});
|
||||
this.fileErrorMsg.show();
|
||||
this.readMessages([titleMsg, errorMsg]);
|
||||
}
|
||||
},
|
||||
|
||||
validateFile: function(data) {
|
||||
var self = this,
|
||||
error = '',
|
||||
error = null,
|
||||
errors = [],
|
||||
fileName,
|
||||
fileType;
|
||||
|
||||
@@ -263,18 +323,23 @@ define([
|
||||
)
|
||||
.replace('{filename}', fileName)
|
||||
.replace('{supportedFileFormats}', self.videoSupportedFileFormats.join(' and '));
|
||||
return false;
|
||||
}
|
||||
if (file.size > self.getMaxFileSizeInBytes()) {
|
||||
} else if (file.size > self.getMaxFileSizeInBytes()) {
|
||||
error = gettext(
|
||||
'{filename} exceeds maximum size of {maxFileSizeInGB} GB.'
|
||||
)
|
||||
.replace('{filename}', fileName)
|
||||
.replace('{maxFileSizeInGB}', self.videoUploadMaxFileSizeInGB);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
errors.push({
|
||||
fileName: fileName,
|
||||
message: error
|
||||
});
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
return error;
|
||||
return errors;
|
||||
},
|
||||
|
||||
removeViewAt: function(index) {
|
||||
@@ -307,6 +372,16 @@ define([
|
||||
completedMessages.push(gettext('Previous Uploads table has been updated.'));
|
||||
this.readMessages(completedMessages);
|
||||
}
|
||||
},
|
||||
|
||||
sendStatusUpdate: function(statusUpdates) {
|
||||
return $.ajax({
|
||||
url: this.postUrl,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(statusUpdates),
|
||||
dataType: 'json',
|
||||
type: 'POST'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -65,11 +65,15 @@
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.video-detail-status {
|
||||
.video-detail-status, .more-details-action {
|
||||
@include font-size(12);
|
||||
@include line-height(12);
|
||||
}
|
||||
|
||||
.more-details-action, .upload-failure {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-detail-progress {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@@ -105,7 +109,7 @@
|
||||
}
|
||||
|
||||
&.error {
|
||||
.video-detail-status {
|
||||
.video-upload-status {
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
@@ -117,10 +121,20 @@
|
||||
.video-detail-progress::-moz-progress-bar {
|
||||
background-color: $color-error;
|
||||
}
|
||||
|
||||
.more-details-action, .upload-failure {
|
||||
display: inline-block;
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
.more-details-action {
|
||||
margin-top: ($baseline/5);
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
.video-detail-status {
|
||||
.video-upload-status {
|
||||
color: $color-ready;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<h4 class="video-detail-name"><%- fileName %></h4>
|
||||
<progress class="video-detail-progress" value="<%= progress %>"></progress>
|
||||
<p class="video-detail-status"><%- gettext(status) %></p>
|
||||
<div class="video-upload-status">
|
||||
<span class="icon alert-icon fa fa-warning upload-failure" aria-hidden="true"></span>
|
||||
<span class="video-detail-status"><%- gettext(status) %></span>
|
||||
<% if (failureMessage) { %>
|
||||
<a href="#" class="more-details-action">
|
||||
<%- gettext("Read More") %>
|
||||
<span class="sr"><%- gettext("details about the failure") %></span>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ git+https://github.com/edx/edx-ora2.git@1.1.13#egg=ora2==1.1.13
|
||||
-e git+https://github.com/edx/edx-submissions.git@1.1.4#egg=edx-submissions==1.1.4
|
||||
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
|
||||
git+https://github.com/edx/i18n-tools.git@v0.3.2#egg=i18n-tools==v0.3.2
|
||||
git+https://github.com/edx/edx-val.git@0.0.11#egg=edxval==0.0.11
|
||||
git+https://github.com/edx/edx-val.git@0.0.12#egg=edxval==0.0.12
|
||||
git+https://github.com/pmitros/RecommenderXBlock.git@v1.1#egg=recommender-xblock==1.1
|
||||
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
|
||||
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
|
||||
|
||||
Reference in New Issue
Block a user