Commit 5c433ec9 by Sarina Canelake

Merge pull request #5731 from Stanford-Online/ataki/upstream

Limit Upload File Sizes to GridFS
parents b1e5002b fb9320af
...@@ -180,4 +180,5 @@ Eugeny Kolpakov <eugeny.kolpakov@gmail.com> ...@@ -180,4 +180,5 @@ Eugeny Kolpakov <eugeny.kolpakov@gmail.com>
Omar Al-Ithawi <oithawi@qrf.org> Omar Al-Ithawi <oithawi@qrf.org>
Louis Pilfold <louis@lpil.uk> Louis Pilfold <louis@lpil.uk>
Akiva Leffert <akiva@edx.org> Akiva Leffert <akiva@edx.org>
Mike Bifulco <mbifulco@aquent.com> Mike Bifulco <mbifulco@aquent.com>
\ No newline at end of file Jim Zheng <jimzheng@stanford.edu>
...@@ -83,6 +83,9 @@ def _asset_index(request, course_key): ...@@ -83,6 +83,9 @@ def _asset_index(request, course_key):
return render_to_response('asset_index.html', { return render_to_response('asset_index.html', {
'context_course': course_module, 'context_course': course_module,
'max_file_size_in_mbs': settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
'chunk_size_in_mbs': settings.UPLOAD_CHUNK_SIZE_IN_MB,
'max_file_size_redirect_url': settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL,
'asset_callback_url': reverse_course_url('assets_handler', course_key) 'asset_callback_url': reverse_course_url('assets_handler', course_key)
}) })
...@@ -152,6 +155,14 @@ def _get_assets_for_page(request, course_key, current_page, page_size, sort): ...@@ -152,6 +155,14 @@ def _get_assets_for_page(request, course_key, current_page, page_size, sort):
) )
def get_file_size(upload_file):
"""
Helper method for getting file size of an upload file.
Can be used for mocking test file sizes.
"""
return upload_file.size
@require_POST @require_POST
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
...@@ -176,6 +187,26 @@ def _upload_asset(request, course_key): ...@@ -176,6 +187,26 @@ def _upload_asset(request, course_key):
upload_file = request.FILES['file'] upload_file = request.FILES['file']
filename = upload_file.name filename = upload_file.name
mime_type = upload_file.content_type mime_type = upload_file.content_type
size = get_file_size(upload_file)
# If file is greater than a specified size, reject the upload
# request and send a message to the user. Note that since
# the front-end may batch large file uploads in smaller chunks,
# we validate the file-size on the front-end in addition to
# validating on the backend. (see cms/static/js/views/assets.js)
max_file_size_in_bytes = settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB * 1000 ** 2
if size > max_file_size_in_bytes:
return JsonResponse({
'error': _(
'File {filename} exceeds maximum size of '
'{size_mb} MB. Please follow the instructions here '
'to upload a file elsewhere and link to it instead: '
'{faq_url}').format(
filename=filename,
size_mb=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
faq_url=settings.MAX_ASSET_UPLOAD_FILE_SIZE_URL,
)
}, status=413)
content_loc = StaticContent.compute_location(course_key, filename) content_loc = StaticContent.compute_location(course_key, filename)
......
...@@ -5,6 +5,7 @@ from datetime import datetime ...@@ -5,6 +5,7 @@ from datetime import datetime
from io import BytesIO from io import BytesIO
from pytz import UTC from pytz import UTC
import json import json
from django.conf import settings
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.views import assets from contentstore.views import assets
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
...@@ -16,10 +17,14 @@ from xmodule.modulestore.django import modulestore ...@@ -16,10 +17,14 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from django.test.utils import override_settings from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from django.conf import settings import mock
from ddt import ddt
from ddt import data
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
MAX_FILE_SIZE = settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB * 1000 ** 2
class AssetsTestCase(CourseTestCase): class AssetsTestCase(CourseTestCase):
""" """
...@@ -33,9 +38,14 @@ class AssetsTestCase(CourseTestCase): ...@@ -33,9 +38,14 @@ class AssetsTestCase(CourseTestCase):
""" """
Post to the asset upload url Post to the asset upload url
""" """
f = self.get_sample_asset(name)
return self.client.post(self.url, {"name": name, "file": f})
def get_sample_asset(self, name):
"""Returns an in-memory file with the given name for testing"""
f = BytesIO(name) f = BytesIO(name)
f.name = name + ".txt" f.name = name + ".txt"
return self.client.post(self.url, {"name": name, "file": f}) return f
class BasicAssetsTestCase(AssetsTestCase): class BasicAssetsTestCase(AssetsTestCase):
...@@ -132,6 +142,7 @@ class PaginationTestCase(AssetsTestCase): ...@@ -132,6 +142,7 @@ class PaginationTestCase(AssetsTestCase):
self.assertGreaterEqual(name2, name3) self.assertGreaterEqual(name2, name3)
@ddt
class UploadTestCase(AssetsTestCase): class UploadTestCase(AssetsTestCase):
""" """
Unit tests for uploading a file Unit tests for uploading a file
...@@ -148,6 +159,24 @@ class UploadTestCase(AssetsTestCase): ...@@ -148,6 +159,24 @@ class UploadTestCase(AssetsTestCase):
resp = self.client.post(self.url, {"name": "file.txt"}, "application/json") resp = self.client.post(self.url, {"name": "file.txt"}, "application/json")
self.assertEquals(resp.status_code, 400) self.assertEquals(resp.status_code, 400)
@data(
(int(MAX_FILE_SIZE / 2.0), "small.file.test", 200),
(MAX_FILE_SIZE, "justequals.file.test", 200),
(MAX_FILE_SIZE + 90, "large.file.test", 413),
)
@mock.patch('contentstore.views.assets.get_file_size')
def test_file_size(self, case, get_file_size):
max_file_size, name, status_code = case
get_file_size.return_value = max_file_size
f = self.get_sample_asset(name=name)
resp = self.client.post(self.url, {
"name": name,
"file": f
})
self.assertEquals(resp.status_code, status_code)
class DownloadTestCase(AssetsTestCase): class DownloadTestCase(AssetsTestCase):
""" """
......
...@@ -718,6 +718,16 @@ ADVANCED_SECURITY_CONFIG = {} ...@@ -718,6 +718,16 @@ ADVANCED_SECURITY_CONFIG = {}
SHIBBOLETH_DOMAIN_PREFIX = 'shib:' SHIBBOLETH_DOMAIN_PREFIX = 'shib:'
OPENID_DOMAIN_PREFIX = 'openid:' OPENID_DOMAIN_PREFIX = 'openid:'
### Size of chunks into which asset uploads will be divided
UPLOAD_CHUNK_SIZE_IN_MB = 10
### Max size of asset uploads to GridFS
MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB = 10
# FAQ url to direct users to if they upload
# a file that exceeds the above size
MAX_ASSET_UPLOAD_FILE_SIZE_URL = ""
################ ADVANCED_COMPONENT_TYPES ############### ################ ADVANCED_COMPONENT_TYPES ###############
ADVANCED_COMPONENT_TYPES = [ ADVANCED_COMPONENT_TYPES = [
......
...@@ -15,6 +15,8 @@ requirejs.config({ ...@@ -15,6 +15,8 @@ requirejs.config({
"jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie", "jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie",
"jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min", "jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min",
"jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload", "jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload",
"jquery.fileupload-process": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process",
"jquery.fileupload-validate": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate",
"jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", "jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
"jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill", "jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill",
"jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents",
...@@ -94,9 +96,15 @@ requirejs.config({ ...@@ -94,9 +96,15 @@ requirejs.config({
exports: "jQuery.fn.qtip" exports: "jQuery.fn.qtip"
}, },
"jquery.fileupload": { "jquery.fileupload": {
deps: ["jquery.iframe-transport"], deps: ["jquery.ui", "jquery.iframe-transport"],
exports: "jQuery.fn.fileupload" exports: "jQuery.fn.fileupload"
}, },
"jquery.fileupload-process": {
deps: ["jquery.fileupload"]
},
"jquery.fileupload-validate": {
deps: ["jquery.fileupload"]
},
"jquery.inputnumber": { "jquery.inputnumber": {
deps: ["jquery"], deps: ["jquery"],
exports: "jQuery.fn.inputNumber" exports: "jQuery.fn.inputNumber"
......
...@@ -14,6 +14,8 @@ requirejs.config({ ...@@ -14,6 +14,8 @@ requirejs.config({
"jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie", "jquery.cookie": "xmodule_js/common_static/js/vendor/jquery.cookie",
"jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min", "jquery.qtip": "xmodule_js/common_static/js/vendor/jquery.qtip.min",
"jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload", "jquery.fileupload": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload",
"jquery.fileupload-process": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-process",
"jquery.fileupload-validate": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate",
"jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", "jquery.iframe-transport": "xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
"jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill", "jquery.inputnumber": "xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill",
"jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents",
...@@ -84,9 +86,15 @@ requirejs.config({ ...@@ -84,9 +86,15 @@ requirejs.config({
exports: "jQuery.fn.qtip" exports: "jQuery.fn.qtip"
}, },
"jquery.fileupload": { "jquery.fileupload": {
deps: ["jquery.iframe-transport"], deps: ["jquery.ui", "jquery.iframe-transport"],
exports: "jQuery.fn.fileupload" exports: "jQuery.fn.fileupload"
}, },
"jquery.fileupload-process": {
deps: ["jquery.fileupload"]
},
"jquery.fileupload-validate": {
deps: ["jquery.fileupload"]
},
"jquery.inputnumber": { "jquery.inputnumber": {
deps: ["jquery"], deps: ["jquery"],
exports: "jQuery.fn.inputNumber" exports: "jQuery.fn.inputNumber"
......
...@@ -48,9 +48,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -48,9 +48,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
@collection = new AssetCollection([@model]) @collection = new AssetCollection([@model])
@collection.url = "assets-url" @collection.url = "assets-url"
@view = new AssetView({model: @model}) @createAssetView = (test) =>
view = new AssetView({model: @model})
requests = if test then AjaxHelpers["requests"](test) else null
return {view: view, requests: requests}
waitsFor (=> @view), "AssetView was not created", 1000 waitsFor (=> @createAssetView), "AssetsView Creation function was not initialized", 1000
afterEach -> afterEach ->
@injector.clean() @injector.clean()
...@@ -58,10 +61,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -58,10 +61,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "Basic", -> describe "Basic", ->
it "should render properly", -> it "should render properly", ->
{view: @view, requests: requests} = @createAssetView()
@view.render() @view.render()
expect(@view.$el).toContainText("test asset") expect(@view.$el).toContainText("test asset")
it "should pop a delete confirmation when the delete button is clicked", -> it "should pop a delete confirmation when the delete button is clicked", ->
{view: @view, requests: requests} = @createAssetView()
@view.render().$(".remove-asset-button").click() @view.render().$(".remove-asset-button").click()
expect(@promptSpies.constructor).toHaveBeenCalled() expect(@promptSpies.constructor).toHaveBeenCalled()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
...@@ -72,7 +77,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -72,7 +77,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "AJAX", -> describe "AJAX", ->
it "should destroy itself on confirmation", -> it "should destroy itself on confirmation", ->
requests = AjaxHelpers["requests"](this) {view: @view, requests: requests} = @createAssetView(this)
@view.render().$(".remove-asset-button").click() @view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
...@@ -92,7 +97,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -92,7 +97,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@collection.contains(@model)).toBeFalsy() expect(@collection.contains(@model)).toBeFalsy()
it "should not destroy itself if server errors", -> it "should not destroy itself if server errors", ->
requests = AjaxHelpers["requests"](this) {view: @view, requests: requests} = @createAssetView(this)
@view.render().$(".remove-asset-button").click() @view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0] ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
...@@ -106,7 +111,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -106,7 +111,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@collection.contains(@model)).toBeTruthy() expect(@collection.contains(@model)).toBeTruthy()
it "should lock the asset on confirmation", -> it "should lock the asset on confirmation", ->
requests = AjaxHelpers["requests"](this) {view: @view, requests: requests} = @createAssetView(this)
@view.render().$(".lock-checkbox").click() @view.render().$(".lock-checkbox").click()
# AJAX request has been sent, but not yet returned # AJAX request has been sent, but not yet returned
...@@ -123,7 +128,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -123,7 +128,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@model.get("locked")).toBeTruthy() expect(@model.get("locked")).toBeTruthy()
it "should not lock the asset if server errors", -> it "should not lock the asset if server errors", ->
requests = AjaxHelpers["requests"](this) {view: @view, requests: requests} = @createAssetView(this)
@view.render().$(".lock-checkbox").click() @view.render().$(".lock-checkbox").click()
# return an error response # return an error response
...@@ -138,6 +143,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -138,6 +143,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
appendSetFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl)) appendSetFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
appendSetFixtures($("<script>", {id: "paging-header-tpl", type: "text/template"}).text(pagingHeaderTpl)) appendSetFixtures($("<script>", {id: "paging-header-tpl", type: "text/template"}).text(pagingHeaderTpl))
appendSetFixtures($("<script>", {id: "paging-footer-tpl", type: "text/template"}).text(pagingFooterTpl)) appendSetFixtures($("<script>", {id: "paging-footer-tpl", type: "text/template"}).text(pagingFooterTpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedbackTpl))
window.analytics = jasmine.createSpyObj('analytics', ['track']) window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy() window.course_location_analytics = jasmine.createSpy()
appendSetFixtures(sandbox({id: "asset_table_body"})) appendSetFixtures(sandbox({id: "asset_table_body"}))
...@@ -182,12 +188,16 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -182,12 +188,16 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
@AssetModel = AssetModel @AssetModel = AssetModel
@collection = new AssetCollection(); @collection = new AssetCollection();
@collection.url = "assets-url" @collection.url = "assets-url"
@view = new AssetsView @createAssetsView = (test) =>
collection: @collection requests = AjaxHelpers.requests(test)
el: $('#asset_table_body') view = new AssetsView
@view.render() collection: @collection
el: $('#asset_table_body')
view.render()
return {view: view, requests: requests}
waitsFor (=> @view), "AssetsView was not created", 1000 waitsFor (=> @createAssetsView), "AssetsView Creation function was not initialized", 2000
$.ajax() $.ajax()
...@@ -230,11 +240,9 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -230,11 +240,9 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "Basic", -> describe "Basic", ->
# Separate setup method to work-around mis-parenting of beforeEach methods # Separate setup method to work-around mis-parenting of beforeEach methods
setup = -> setup = (requests) ->
requests = AjaxHelpers.requests(this)
@view.setPage(0) @view.setPage(0)
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
return requests
$.fn.fileupload = -> $.fn.fileupload = ->
return '' return ''
...@@ -243,34 +251,38 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -243,34 +251,38 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
$(html_selector).click() $(html_selector).click()
it "should show upload modal on clicking upload asset button", -> it "should show upload modal on clicking upload asset button", ->
{view: @view, requests: requests} = @createAssetsView(this)
spyOn(@view, "showUploadModal") spyOn(@view, "showUploadModal")
setup.call(this) setup.call(this, requests)
expect(@view.showUploadModal).not.toHaveBeenCalled() expect(@view.showUploadModal).not.toHaveBeenCalled()
@view.showUploadModal(clickEvent(".upload-button")) @view.showUploadModal(clickEvent(".upload-button"))
expect(@view.showUploadModal).toHaveBeenCalled() expect(@view.showUploadModal).toHaveBeenCalled()
it "should show file selection menu on choose file button", -> it "should show file selection menu on choose file button", ->
{view: @view, requests: requests} = @createAssetsView(this)
spyOn(@view, "showFileSelectionMenu") spyOn(@view, "showFileSelectionMenu")
setup.call(this) setup.call(this, requests)
expect(@view.showFileSelectionMenu).not.toHaveBeenCalled() expect(@view.showFileSelectionMenu).not.toHaveBeenCalled()
@view.showFileSelectionMenu(clickEvent(".choose-file-button")) @view.showFileSelectionMenu(clickEvent(".choose-file-button"))
expect(@view.showFileSelectionMenu).toHaveBeenCalled() expect(@view.showFileSelectionMenu).toHaveBeenCalled()
it "should hide upload modal on clicking close button", -> it "should hide upload modal on clicking close button", ->
{view: @view, requests: requests} = @createAssetsView(this)
spyOn(@view, "hideModal") spyOn(@view, "hideModal")
setup.call(this) setup.call(this, requests)
expect(@view.hideModal).not.toHaveBeenCalled() expect(@view.hideModal).not.toHaveBeenCalled()
@view.hideModal(clickEvent(".close-button")) @view.hideModal(clickEvent(".close-button"))
expect(@view.hideModal).toHaveBeenCalled() expect(@view.hideModal).toHaveBeenCalled()
it "should show a status indicator while loading", -> it "should show a status indicator while loading", ->
{view: @view, requests: requests} = @createAssetsView(this)
appendSetFixtures('<div class="ui-loading"/>') appendSetFixtures('<div class="ui-loading"/>')
expect($('.ui-loading').is(':visible')).toBe(true) expect($('.ui-loading').is(':visible')).toBe(true)
setup.call(this) setup.call(this, requests)
expect($('.ui-loading').is(':visible')).toBe(false) expect($('.ui-loading').is(':visible')).toBe(false)
it "should hide the status indicator if an error occurs while loading", -> it "should hide the status indicator if an error occurs while loading", ->
requests = AjaxHelpers.requests(this) {view: @view, requests: requests} = @createAssetsView(this)
appendSetFixtures('<div class="ui-loading"/>') appendSetFixtures('<div class="ui-loading"/>')
expect($('.ui-loading').is(':visible')).toBe(true) expect($('.ui-loading').is(':visible')).toBe(true)
@view.setPage(0) @view.setPage(0)
...@@ -278,21 +290,24 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -278,21 +290,24 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect($('.ui-loading').is(':visible')).toBe(false) expect($('.ui-loading').is(':visible')).toBe(false)
it "should render both assets", -> it "should render both assets", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2") expect(@view.$el).toContainText("test asset 2")
it "should remove the deleted asset from the view", -> it "should remove the deleted asset from the view", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
# Delete the 2nd asset with success from server. # Delete the 2nd asset with success from server.
@view.$(".remove-asset-button")[1].click() @view.$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies) @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
req.respond(200) for req in requests req.respond(200) for req in requests.slice(1)
expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2") expect(@view.$el).not.toContainText("test asset 2")
it "does not remove asset if deletion failed", -> it "does not remove asset if deletion failed", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
# Delete the 2nd asset, but mimic a failure from the server. # Delete the 2nd asset, but mimic a failure from the server.
@view.$(".remove-asset-button")[1].click() @view.$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies) @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
...@@ -301,13 +316,15 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -301,13 +316,15 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@view.$el).toContainText("test asset 2") expect(@view.$el).toContainText("test asset 2")
it "adds an asset if asset does not already exist", -> it "adds an asset if asset does not already exist", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
addMockAsset.call(this, requests) addMockAsset.call(this, requests)
expect(@view.$el).toContainText("new asset") expect(@view.$el).toContainText("new asset")
expect(@collection.models.length).toBe(3) expect(@collection.models.length).toBe(3)
it "does not add an asset if asset already exists", -> it "does not add an asset if asset already exists", ->
setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
spyOn(@collection, "add").andCallThrough() spyOn(@collection, "add").andCallThrough()
model = @collection.models[1] model = @collection.models[1]
@view.addAsset(model) @view.addAsset(model)
...@@ -315,19 +332,19 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -315,19 +332,19 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe "Sorting", -> describe "Sorting", ->
# Separate setup method to work-around mis-parenting of beforeEach methods # Separate setup method to work-around mis-parenting of beforeEach methods
setup = -> setup = (requests) ->
requests = AjaxHelpers.requests(this)
@view.setPage(0) @view.setPage(0)
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
return requests
it "should have the correct default sort order", -> it "should have the correct default sort order", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc") expect(@view.collection.sortDirection).toBe("desc")
it "should toggle the sort order when clicking on the currently sorted column", -> it "should toggle the sort order when clicking on the currently sorted column", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
expect(@view.sortDisplayName()).toBe("Date Added") expect(@view.sortDisplayName()).toBe("Date Added")
expect(@view.collection.sortDirection).toBe("desc") expect(@view.collection.sortDirection).toBe("desc")
@view.$("#js-asset-date-col").click() @view.$("#js-asset-date-col").click()
...@@ -340,7 +357,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -340,7 +357,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@view.collection.sortDirection).toBe("desc") expect(@view.collection.sortDirection).toBe("desc")
it "should switch the sort order when clicking on a different column", -> it "should switch the sort order when clicking on a different column", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
@view.$("#js-asset-name-col").click() @view.$("#js-asset-name-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
expect(@view.sortDisplayName()).toBe("Name") expect(@view.sortDisplayName()).toBe("Name")
...@@ -351,7 +369,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], ...@@ -351,7 +369,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect(@view.collection.sortDirection).toBe("desc") expect(@view.collection.sortDirection).toBe("desc")
it "should switch sort to most recent date added when a new asset is added", -> it "should switch sort to most recent date added when a new asset is added", ->
requests = setup.call(this) {view: @view, requests: requests} = @createAssetsView(this)
setup.call(this, requests)
@view.$("#js-asset-name-col").click() @view.$("#js-asset-name-col").click()
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse) AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
addMockAsset.call(this, requests) addMockAsset.call(this, requests)
......
...@@ -2,12 +2,18 @@ define([ ...@@ -2,12 +2,18 @@ define([
'jquery', 'js/collections/asset', 'js/views/assets', 'jquery.fileupload' 'jquery', 'js/collections/asset', 'js/views/assets', 'jquery.fileupload'
], function($, AssetCollection, AssetsView) { ], function($, AssetCollection, AssetsView) {
'use strict'; 'use strict';
return function (assetCallbackUrl) { return function (config) {
var assets = new AssetCollection(), var assets = new AssetCollection(),
assetsView; assetsView;
assets.url = assetCallbackUrl; assets.url = config.assetCallbackUrl;
assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')}); assetsView = new AssetsView({
collection: assets,
el: $('.assets-wrapper'),
uploadChunkSizeInMBs: config.uploadChunkSizeInMBs,
maxFileSizeInMBs: config.maxFileSizeInMBs,
maxFileSizeRedirectUrl: config.maxFileSizeRedirectUrl
});
assetsView.render(); assetsView.render();
}; };
}); });
define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views/assets", define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views/assets",
"js/models/asset", "js/collections/asset", "js/spec_helpers/view_helpers" ], "js/models/asset", "js/collections/asset", "js/spec_helpers/view_helpers"],
function ($, AjaxHelpers, AssetView, AssetsView, AssetModel, AssetCollection, ViewHelpers) { function ($, AjaxHelpers, AssetView, AssetsView, AssetModel, AssetCollection, ViewHelpers) {
describe("Assets", function() { describe("Assets", function() {
var assetsView, mockEmptyAssetsResponse, mockAssetUploadResponse, var assetsView, mockEmptyAssetsResponse, mockAssetUploadResponse, mockFileUpload,
assetLibraryTpl, assetTpl, pagingFooterTpl, pagingHeaderTpl, uploadModalTpl; assetLibraryTpl, assetTpl, pagingFooterTpl, pagingHeaderTpl, uploadModalTpl;
assetLibraryTpl = readFixtures('asset-library.underscore'); assetLibraryTpl = readFixtures('asset-library.underscore');
...@@ -53,6 +53,10 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views ...@@ -53,6 +53,10 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
msg: "Upload completed" msg: "Upload completed"
}; };
mockFileUpload = {
files: [{name: 'largefile', size: 0}]
};
$.fn.fileupload = function() { $.fn.fileupload = function() {
return ''; return '';
}; };
...@@ -95,6 +99,15 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views ...@@ -95,6 +99,15 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
expect($('.upload-modal').is(':visible')).toBe(false); expect($('.upload-modal').is(':visible')).toBe(false);
}); });
it('has properly initialized constants for handling upload file errors', function() {
expect(assetsView).toBeDefined();
expect(assetsView.uploadChunkSizeInMBs).toBeDefined();
expect(assetsView.maxFileSizeInMBs).toBeDefined();
expect(assetsView.uploadChunkSizeInBytes).toBeDefined();
expect(assetsView.maxFileSizeInBytes).toBeDefined();
expect(assetsView.largeFileErrorMsg).toBeNull();
});
it('uploads file properly', function () { it('uploads file properly', function () {
var requests = setup.call(this); var requests = setup.call(this);
expect(assetsView).toBeDefined(); expect(assetsView).toBeDefined();
...@@ -122,6 +135,42 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views ...@@ -122,6 +135,42 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
expect($('#asset_table_body').html()).toContain("dummy.jpg"); expect($('#asset_table_body').html()).toContain("dummy.jpg");
expect(assetsView.collection.length).toBe(1); expect(assetsView.collection.length).toBe(1);
}); });
it('blocks file uploads larger than the max file size', function() {
expect(assetsView).toBeDefined();
mockFileUpload.files[0].size = assetsView.maxFileSize * 10;
$('.choose-file-button').click();
$(".upload-modal .file-chooser").fileupload('add', mockFileUpload);
expect($('.upload-modal h1').text()).not.toContain("Uploading");
expect(assetsView.largeFileErrorMsg).toBeDefined();
expect($('div.progress-bar').text()).not.toContain("Upload completed");
expect($('div.progress-fill').width()).toBe(0);
});
it('allows file uploads equal in size to the max file size', function() {
expect(assetsView).toBeDefined();
mockFileUpload.files[0].size = assetsView.maxFileSize;
$('.choose-file-button').click();
$(".upload-modal .file-chooser").fileupload('add', mockFileUpload);
expect(assetsView.largeFileErrorMsg).toBeNull();
});
it('allows file uploads smaller than the max file size', function() {
expect(assetsView).toBeDefined();
mockFileUpload.files[0].size = assetsView.maxFileSize / 100;
$('.choose-file-button').click();
$(".upload-modal .file-chooser").fileupload('add', mockFileUpload);
expect(assetsView.largeFileErrorMsg).toBeNull();
});
}); });
}); });
}); });
define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", "js/views/asset", define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", "js/views/asset",
"js/views/paging_header", "js/views/paging_footer", "js/utils/modal", "js/views/utils/view_utils"], "js/views/paging_header", "js/views/paging_footer", "js/utils/modal", "js/views/utils/view_utils",
function($, _, gettext, AssetModel, PagingView, AssetView, PagingHeader, PagingFooter, ModalUtils, ViewUtils) { "js/views/feedback_notification", "jquery.fileupload-process", "jquery.fileupload-validate"],
function($, _, gettext, AssetModel, PagingView, AssetView, PagingHeader, PagingFooter, ModalUtils, ViewUtils, NotificationView) {
var CONVERSION_FACTOR_MBS_TO_BYTES = 1000 * 1000;
var AssetsView = PagingView.extend({ var AssetsView = PagingView.extend({
// takes AssetCollection as model // takes AssetCollection as model
...@@ -10,7 +13,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -10,7 +13,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
"click .upload-button": "showUploadModal" "click .upload-button": "showUploadModal"
}, },
initialize : function() { initialize : function(options) {
options = options || {};
PagingView.prototype.initialize.call(this); PagingView.prototype.initialize.call(this);
var collection = this.collection; var collection = this.collection;
this.template = this.loadTemplate("asset-library"); this.template = this.loadTemplate("asset-library");
...@@ -20,7 +25,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -20,7 +25,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
this.setInitialSortColumn('js-asset-date-col'); this.setInitialSortColumn('js-asset-date-col');
ViewUtils.showLoadingIndicator(); ViewUtils.showLoadingIndicator();
this.setPage(0); this.setPage(0);
// set default file size for uploads via template var,
// and default to static old value if none exists
this.uploadChunkSizeInMBs = options.uploadChunkSizeInMBs || 10;
this.maxFileSizeInMBs = options.maxFileSizeInMBs || 10;
this.uploadChunkSizeInBytes = this.uploadChunkSizeInMBs * CONVERSION_FACTOR_MBS_TO_BYTES;
this.maxFileSizeInBytes = this.maxFileSizeInMBs * CONVERSION_FACTOR_MBS_TO_BYTES;
this.maxFileSizeRedirectUrl = options.maxFileSizeRedirectUrl || '';
assetsView = this; assetsView = this;
// error message modal for large file uploads
this.largeFileErrorMsg = null;
}, },
render: function() { render: function() {
...@@ -111,6 +125,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -111,6 +125,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
} }
$('.file-input').unbind('change.startUpload'); $('.file-input').unbind('change.startUpload');
ModalUtils.hideModal(); ModalUtils.hideModal();
if (assetsView.largeFileErrorMsg) {
assetsView.largeFileErrorMsg.hide();
}
}, },
showUploadModal: function (event) { showUploadModal: function (event) {
...@@ -122,23 +139,44 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -122,23 +139,44 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
$('.upload-modal .file-chooser').fileupload({ $('.upload-modal .file-chooser').fileupload({
dataType: 'json', dataType: 'json',
type: 'POST', type: 'POST',
maxChunkSize: 100 * 1000 * 1000, // 100 MB maxChunkSize: self.uploadChunkSizeInBytes,
autoUpload: true, autoUpload: true,
progressall: function(event, data) { progressall: function(event, data) {
var percentComplete = parseInt((100 * data.loaded) / data.total, 10); var percentComplete = parseInt((100 * data.loaded) / data.total, 10);
self.showUploadFeedback(event, percentComplete); self.showUploadFeedback(event, percentComplete);
}, },
maxFileSize: 100 * 1000 * 1000, // 100 MB maxFileSize: self.maxFileSizeInBytes,
maxNumberofFiles: 100, maxNumberofFiles: 100,
add: function(event, data) {
data.process().done(function () {
data.submit();
});
},
done: function(event, data) { done: function(event, data) {
self.displayFinishedUpload(data.result); self.displayFinishedUpload(data.result);
} },
processfail: function(event, data) {
var filename = data.files[data.index].name;
var error = gettext("File {filename} exceeds maximum size of {maxFileSizeInMBs} MB")
.replace("{filename}", filename)
.replace("{maxFileSizeInMBs}", self.maxFileSizeInMBs)
// disable second part of message for any falsy value,
// which can be null or an empty string
if(self.maxFileSizeRedirectUrl) {
var instructions = gettext("Please follow the instructions here to upload a file elsewhere and link to it: {maxFileSizeRedirectUrl}")
.replace("{maxFileSizeRedirectUrl}", self.maxFileSizeRedirectUrl);
error = error + " " + instructions;
}
assetsView.largeFileErrorMsg = new NotificationView.Error({
"title": gettext("Your file could not be uploaded"),
"message": error
});
assetsView.largeFileErrorMsg.show();
assetsView.displayFailedUpload({
"msg": gettext("Max file size exceeded")
});
},
processdone: function(event, data) {
assetsView.largeFileErrorMsg = null;
}
}); });
}, },
...@@ -149,11 +187,12 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -149,11 +187,12 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
startUpload: function (event) { startUpload: function (event) {
var file = event.target.value; var file = event.target.value;
if (!assetsView.largeFileErrorMsg) {
$('.upload-modal h1').text(gettext('Uploading…')); $('.upload-modal h1').text(gettext('Uploading'));
$('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1)); $('.upload-modal .file-name').html(file.substring(file.lastIndexOf("\\") + 1));
$('.upload-modal .choose-file-button').hide(); $('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show(); $('.upload-modal .progress-bar').removeClass('loaded').show();
}
}, },
resetUploadModal: function () { resetUploadModal: function () {
...@@ -169,6 +208,8 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -169,6 +208,8 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
$('.upload-modal .choose-file-button').text(gettext('Choose File')); $('.upload-modal .choose-file-button').text(gettext('Choose File'));
$('.upload-modal .embeddable-xml-input').val(''); $('.upload-modal .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide(); $('.upload-modal .embeddable').hide();
assetsView.largeFileErrorMsg = null;
}, },
showUploadFeedback: function (event, percentComplete) { showUploadFeedback: function (event, percentComplete) {
...@@ -181,7 +222,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -181,7 +222,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
var asset = resp.asset; var asset = resp.asset;
$('.upload-modal h1').text(gettext('Upload New File')); $('.upload-modal h1').text(gettext('Upload New File'));
$('.upload-modal .embeddable-xml-input').val(asset.portable_url); $('.upload-modal .embeddable-xml-input').val(asset.portable_url).show();
$('.upload-modal .embeddable').show(); $('.upload-modal .embeddable').show();
$('.upload-modal .file-name').hide(); $('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg); $('.upload-modal .progress-fill').html(resp.msg);
...@@ -189,6 +230,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging", ...@@ -189,6 +230,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
$('.upload-modal .progress-fill').width('100%'); $('.upload-modal .progress-fill').width('100%');
assetsView.addAsset(new AssetModel(asset)); assetsView.addAsset(new AssetModel(asset));
},
displayFailedUpload: function (resp) {
$('.upload-modal h1').text(gettext('Upload New File'));
$('.upload-modal .embeddable-xml-input').hide();
$('.upload-modal .embeddable').hide();
$('.upload-modal .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg);
$('.upload-modal .choose-file-button').text(gettext('Load Another File')).show();
$('.upload-modal .progress-fill').width('0%');
} }
}); });
......
...@@ -62,6 +62,10 @@ lib_paths: ...@@ -62,6 +62,10 @@ lib_paths:
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/coffee/src/xblock/ - xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
- 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
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
......
...@@ -57,6 +57,10 @@ lib_paths: ...@@ -57,6 +57,10 @@ lib_paths:
- xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/coffee/src/xblock/ - xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js
- 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
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
......
...@@ -20,6 +20,8 @@ require.config({ ...@@ -20,6 +20,8 @@ require.config({
"jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min", "jquery.scrollTo": "js/vendor/jquery.scrollTo-1.4.2-min",
"jquery.flot": "js/vendor/flot/jquery.flot.min", "jquery.flot": "js/vendor/flot/jquery.flot.min",
"jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload", "jquery.fileupload": "js/vendor/jQuery-File-Upload/js/jquery.fileupload",
"jquery.fileupload-process": "js/vendor/jQuery-File-Upload/js/jquery.fileupload-process",
"jquery.fileupload-validate": "js/vendor/jQuery-File-Upload/js/jquery.fileupload-validate",
"jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport", "jquery.iframe-transport": "js/vendor/jQuery-File-Upload/js/jquery.iframe-transport",
"jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill", "jquery.inputnumber": "js/vendor/html5-input-polyfills/number-polyfill",
"jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents", "jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents",
...@@ -128,9 +130,15 @@ require.config({ ...@@ -128,9 +130,15 @@ require.config({
exports: "jQuery.fn.plot" exports: "jQuery.fn.plot"
}, },
"jquery.fileupload": { "jquery.fileupload": {
deps: ["jquery.iframe-transport"], deps: ["jquery.ui", "jquery.iframe-transport"],
exports: "jQuery.fn.fileupload" exports: "jQuery.fn.fileupload"
}, },
"jquery.fileupload-process": {
deps: ["jquery.fileupload"]
},
"jquery.fileupload-validate": {
deps: ["jquery.fileupload"]
},
"jquery.inputnumber": { "jquery.inputnumber": {
deps: ["jquery"], deps: ["jquery"],
exports: "jQuery.fn.inputNumber" exports: "jQuery.fn.inputNumber"
......
...@@ -19,7 +19,12 @@ ...@@ -19,7 +19,12 @@
<%block name="requirejs"> <%block name="requirejs">
require(["js/factories/asset_index"], function (AssetIndexFactory) { require(["js/factories/asset_index"], function (AssetIndexFactory) {
AssetIndexFactory("${asset_callback_url}"); AssetIndexFactory({
assetCallbackUrl: "${asset_callback_url}",
uploadChunkSizeInMBs: ${chunk_size_in_mbs},
maxFileSizeInMBs: ${max_file_size_in_mbs},
maxFileSizeRedirectUrl: "${max_file_size_redirect_url}"
});
}); });
</%block> </%block>
...@@ -82,6 +87,7 @@ ...@@ -82,6 +87,7 @@
<a href="#" class="close-button"><i class="icon-remove-sign"></i> <span class="sr">${_('close')}</span></a> <a href="#" class="close-button"><i class="icon-remove-sign"></i> <span class="sr">${_('close')}</span></a>
<div class="modal-body"> <div class="modal-body">
<h1 class="title">${_("Upload New File")}</h1> <h1 class="title">${_("Upload New File")}</h1>
<h2>${_("Max per-file size: {max_filesize}MB").format(max_filesize=max_file_size_in_mbs)}</h2>
<p class="file-name"> <p class="file-name">
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill"></div> <div class="progress-fill"></div>
......
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