diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py
index 6db155fbf8..c49c4c5a3c 100644
--- a/cms/djangoapps/contentstore/views/tests/test_videos.py
+++ b/cms/djangoapps/contentstore/views/tests/test_videos.py
@@ -286,6 +286,27 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
self.assertIn('error', response)
self.assertEqual(response['error'], "Request 'files' entry contain unsupported content_type")
+ @override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret')
+ @patch('boto.s3.connection.S3Connection')
+ def test_upload_with_non_ascii_charaters(self, mock_conn):
+ """
+ Test that video uploads throws error message when file name contains special characters.
+ """
+ file_name = u'test\u2019_file.mp4'
+ files = [{'file_name': file_name, 'content_type': 'video/mp4'}]
+
+ bucket = Mock()
+ mock_conn.return_value = Mock(get_bucket=Mock(return_value=bucket))
+
+ response = self.client.post(
+ self.url,
+ json.dumps({'files': files}),
+ content_type='application/json'
+ )
+ self.assertEqual(response.status_code, 400)
+ response = json.loads(response.content)
+ self.assertEqual(response['error'], 'The file name for %s must contain only ASCII characters.' % file_name)
+
@override_settings(AWS_ACCESS_KEY_ID="test_key_id", AWS_SECRET_ACCESS_KEY="test_secret")
@patch("boto.s3.key.Key")
@patch("boto.s3.connection.S3Connection")
diff --git a/cms/djangoapps/contentstore/views/videos.py b/cms/djangoapps/contentstore/views/videos.py
index ada8081449..df268c9796 100644
--- a/cms/djangoapps/contentstore/views/videos.py
+++ b/cms/djangoapps/contentstore/views/videos.py
@@ -34,6 +34,8 @@ VIDEO_SUPPORTED_FILE_FORMATS = {
'.mov': 'video/quicktime',
}
+VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
+
class StatusDisplayStrings(object):
"""
@@ -262,7 +264,8 @@ def videos_index_html(course):
"encodings_download_url": reverse_course_url("video_encodings_download", unicode(course.id)),
"previous_uploads": _get_index_videos(course),
"concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0),
- "video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys()
+ "video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys(),
+ "video_upload_max_file_size": VIDEO_UPLOAD_MAX_FILE_SIZE_GB
}
)
@@ -328,6 +331,12 @@ def videos_post(course, request):
for req_file in req_files:
file_name = req_file["file_name"]
+ try:
+ file_name.encode('ascii')
+ except UnicodeEncodeError:
+ error_msg = 'The file name for %s must contain only ASCII characters.' % file_name
+ return JsonResponse({'error': error_msg}, status=400)
+
edx_video_id = unicode(uuid4())
key = storage_service_key(bucket, file_name=edx_video_id)
for metadata_name, value in [
diff --git a/cms/static/js/factories/videos_index.js b/cms/static/js/factories/videos_index.js
index 47cd356912..3db118037f 100644
--- a/cms/static/js/factories/videos_index.js
+++ b/cms/static/js/factories/videos_index.js
@@ -10,13 +10,15 @@ define([
concurrentUploadLimit,
uploadButton,
previousUploads,
- videoSupportedFileFormats
+ videoSupportedFileFormats,
+ videoUploadMaxFileSizeInGB
) {
var activeView = new ActiveVideoUploadListView({
postUrl: videoHandlerUrl,
concurrentUploadLimit: concurrentUploadLimit,
uploadButton: uploadButton,
videoSupportedFileFormats: videoSupportedFileFormats,
+ videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB,
onFileUploadDone: function(activeVideos) {
$.ajax({
url: videoHandlerUrl,
diff --git a/cms/static/js/spec/views/active_video_upload_list_spec.js b/cms/static/js/spec/views/active_video_upload_list_spec.js
index 8e1ab82927..cfb0dec156 100644
--- a/cms/static/js/spec/views/active_video_upload_list_spec.js
+++ b/cms/static/js/spec/views/active_video_upload_list_spec.js
@@ -18,16 +18,16 @@ define(
this.postUrl = '/test/post/url';
this.uploadButton = $('');
this.videoSupportedFileFormats = ['.mp4', '.mov'];
+ this.videoUploadMaxFileSizeInGB = 5;
this.view = new ActiveVideoUploadListView({
concurrentUploadLimit: concurrentUploadLimit,
postUrl: this.postUrl,
uploadButton: this.uploadButton,
- videoSupportedFileFormats: this.videoSupportedFileFormats
+ videoSupportedFileFormats: this.videoSupportedFileFormats,
+ videoUploadMaxFileSizeInGB: this.videoUploadMaxFileSizeInGB
});
this.view.render();
jasmine.Ajax.install();
- this.globalAjaxError = jasmine.createSpy();
- $(document).ajaxError(this.globalAjaxError);
});
// Remove window unload handler triggered by the upload requests
@@ -89,6 +89,38 @@ define(
});
});
+ describe('Upload file', function() {
+ _.each(
+ [
+ {desc: 'larger than', additionalBytes: 1},
+ {desc: 'equal to', additionalBytes: 0},
+ {desc: 'smaller than', additionalBytes: - 1}
+ ],
+ function(caseInfo) {
+ it(caseInfo.desc + 'max file size', function() {
+ var maxFileSizeInBytes = this.view.getMaxFileSizeInBytes(),
+ fileSize = maxFileSizeInBytes + caseInfo.additionalBytes,
+ fileToUpload = {
+ files: [
+ {name: 'file.mp4', size: fileSize}
+ ]
+ };
+ 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(
+ 'file.mp4 exceeds maximum size of ' + this.videoUploadMaxFileSizeInGB + ' GB.'
+ );
+ } else {
+ this.view.$uploadForm.fileupload('add', fileToUpload);
+ expect(this.view.fileErrorMsg).toBeNull();
+ }
+ });
+ }
+ );
+ });
+
_.each(
[
{desc: 'a single file', numFiles: 1},
@@ -122,21 +154,25 @@ define(
});
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}; }
- )
+ var request,
+ self = this;
+ expect(jasmine.Ajax.requests.count()).toEqual(caseInfo.numFiles);
+ _.each(_.range(caseInfo.numFiles), function(index) {
+ request = jasmine.Ajax.requests.at(index);
+ expect(request.url).toEqual(self.postUrl);
+ expect(request.method).toEqual('POST');
+ expect(request.requestHeaders['Content-Type']).toEqual('application/json');
+ expect(request.requestHeaders.Accept).toContain('application/json');
+ expect(JSON.parse(request.params)).toEqual({
+ files: [{file_name: fileNames[index]}]
+ });
});
});
- it('should trigger the global AJAX error handler on server error', function() {
+ it('should trigger the notification error handler on server error', function() {
this.request.respondWith({status: 500});
- expect(this.globalAjaxError).toHaveBeenCalled();
+ expect(this.view.fileErrorMsg).toBeDefined();
+ expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
});
describe('and successful server response', function() {
@@ -269,8 +305,8 @@ define(
}
});
- it('should not trigger the global AJAX error handler', function() {
- expect(this.globalAjaxError).not.toHaveBeenCalled();
+ it('should not trigger the notification error handler', function() {
+ expect(this.view.fileErrorMsg).toBeNull();
});
if (caseInfo.numFiles > concurrentUploadLimit) {
diff --git a/cms/static/js/views/active_video_upload_list.js b/cms/static/js/views/active_video_upload_list.js
index dcdea705d1..dc7ffe5962 100644
--- a/cms/static/js/views/active_video_upload_list.js
+++ b/cms/static/js/views/active_video_upload_list.js
@@ -1,6 +1,7 @@
define([
'jquery',
'underscore',
+ 'underscore.string',
'backbone',
'js/models/active_video_upload',
'js/views/baseview',
@@ -10,10 +11,12 @@ define([
'text!templates/active-video-upload-list.underscore',
'jquery.fileupload'
],
- function($, _, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView, NotificationView, HtmlUtils,
+ function($, _, str, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView, NotificationView, HtmlUtils,
activeVideoUploadListTemplate) {
'use strict';
- var ActiveVideoUploadListView = BaseView.extend({
+ var ActiveVideoUploadListView,
+ CONVERSION_FACTOR_GBS_TO_BYTES = 1000 * 1000 * 1000;
+ ActiveVideoUploadListView = BaseView.extend({
tagName: 'div',
events: {
'click .file-drop-area': 'chooseFile',
@@ -29,6 +32,7 @@ define([
this.concurrentUploadLimit = options.concurrentUploadLimit || 0;
this.postUrl = options.postUrl;
this.videoSupportedFileFormats = options.videoSupportedFileFormats;
+ this.videoUploadMaxFileSizeInGB = options.videoUploadMaxFileSizeInGB;
this.onFileUploadDone = options.onFileUploadDone;
if (options.uploadButton) {
options.uploadButton.click(this.chooseFile.bind(this));
@@ -136,34 +140,48 @@ define([
uploadData.cid = model.cid; // eslint-disable-line no-param-reassign
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};
+ _.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
}
- )
- }),
- 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,
- videoId: file.edx_video_id,
- multipart: false,
- global: false, // Do not trigger global AJAX error handler
- redirected: true
- });
- }
- );
- });
+ view.showErrorMessage(errorMsg);
+ });
+ }
+ );
}
}
},
@@ -198,6 +216,10 @@ define([
this.setStatus(data.cid, ActiveVideoUpload.STATUS_FAILED);
},
+ getMaxFileSizeInBytes: function() {
+ return this.videoUploadMaxFileSizeInGB * CONVERSION_FACTOR_GBS_TO_BYTES;
+ },
+
hideErrorMessage: function() {
if (this.fileErrorMsg) {
this.fileErrorMsg.hide();
@@ -212,13 +234,16 @@ define([
},
showErrorMessage: function(errorMsg) {
- var titleMsg = gettext('Your file could not be uploaded');
- this.fileErrorMsg = new NotificationView.Error({
- title: titleMsg,
- message: errorMsg
- });
- this.fileErrorMsg.show();
- this.readMessages([titleMsg, 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) {
@@ -240,6 +265,14 @@ define([
.replace('{supportedFileFormats}', self.videoSupportedFileFormats.join(' and '));
return false;
}
+ if (file.size > self.getMaxFileSizeInBytes()) {
+ error = gettext(
+ '{filename} exceeds maximum size of {maxFileSizeInGB} GB.'
+ )
+ .replace('{filename}', fileName)
+ .replace('{maxFileSizeInGB}', self.videoUploadMaxFileSizeInGB);
+ return false;
+ }
});
return error;
},
diff --git a/cms/templates/videos_index.html b/cms/templates/videos_index.html
index 8e1103ef60..dceaa32daf 100644
--- a/cms/templates/videos_index.html
+++ b/cms/templates/videos_index.html
@@ -34,7 +34,8 @@
${concurrent_upload_limit | n, dump_js_escaped_json},
$(".nav-actions .upload-button"),
$contentWrapper.data("previous-uploads"),
- ${video_supported_file_formats | n, dump_js_escaped_json}
+ ${video_supported_file_formats | n, dump_js_escaped_json},
+ ${video_upload_max_file_size | n, dump_js_escaped_json}
);
});
%block>
@@ -70,7 +71,10 @@
file_formats=' or '.join(video_supported_file_formats)
)}
${_("Maximum Video File Size")}
- ${_("The maximum size for each video file that you upload is 5 GB. The upload process fails for larger files.")}
+ ${Text(_("The maximum size for each video file that you upload is {em_start}5 GB{em_end}. The upload process fails for larger files.")).format(
+ em_start=HTML(''),
+ em_end=HTML(' ')
+ )}
${_("Monitoring files as they upload")}
${_("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.")}
${_("Managing uploaded files")}