Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
5c433ec9
Commit
5c433ec9
authored
Dec 11, 2014
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5731 from Stanford-Online/ataki/upstream
Limit Upload File Sizes to GridFS
parents
b1e5002b
fb9320af
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
288 additions
and
54 deletions
+288
-54
AUTHORS
+1
-0
cms/djangoapps/contentstore/views/assets.py
+31
-0
cms/djangoapps/contentstore/views/tests/test_assets.py
+31
-2
cms/envs/common.py
+10
-0
cms/static/coffee/spec/main.coffee
+9
-1
cms/static/coffee/spec/main_squire.coffee
+9
-1
cms/static/coffee/spec/views/assets_spec.coffee
+49
-30
cms/static/js/factories/asset_index.js
+9
-3
cms/static/js/spec/views/assets_spec.js
+51
-2
cms/static/js/views/assets.js
+64
-13
cms/static/js_test.yml
+4
-0
cms/static/js_test_squire.yml
+4
-0
cms/static/require-config.js
+9
-1
cms/templates/asset_index.html
+7
-1
No files found.
AUTHORS
View file @
5c433ec9
...
...
@@ -181,3 +181,4 @@ Omar Al-Ithawi <oithawi@qrf.org>
Louis Pilfold <louis@lpil.uk>
Akiva Leffert <akiva@edx.org>
Mike Bifulco <mbifulco@aquent.com>
Jim Zheng <jimzheng@stanford.edu>
cms/djangoapps/contentstore/views/assets.py
View file @
5c433ec9
...
...
@@ -83,6 +83,9 @@ def _asset_index(request, course_key):
return
render_to_response
(
'asset_index.html'
,
{
'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
)
})
...
...
@@ -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
@ensure_csrf_cookie
@login_required
...
...
@@ -176,6 +187,26 @@ def _upload_asset(request, course_key):
upload_file
=
request
.
FILES
[
'file'
]
filename
=
upload_file
.
name
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
)
...
...
cms/djangoapps/contentstore/views/tests/test_assets.py
View file @
5c433ec9
...
...
@@ -5,6 +5,7 @@ from datetime import datetime
from
io
import
BytesIO
from
pytz
import
UTC
import
json
from
django.conf
import
settings
from
contentstore.tests.utils
import
CourseTestCase
from
contentstore.views
import
assets
from
contentstore.utils
import
reverse_course_url
...
...
@@ -16,10 +17,14 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore.xml_importer
import
import_from_xml
from
django.test.utils
import
override_settings
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
MAX_FILE_SIZE
=
settings
.
MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
*
1000
**
2
class
AssetsTestCase
(
CourseTestCase
):
"""
...
...
@@ -33,9 +38,14 @@ class AssetsTestCase(CourseTestCase):
"""
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
.
name
=
name
+
".txt"
return
self
.
client
.
post
(
self
.
url
,
{
"name"
:
name
,
"file"
:
f
})
return
f
class
BasicAssetsTestCase
(
AssetsTestCase
):
...
...
@@ -132,6 +142,7 @@ class PaginationTestCase(AssetsTestCase):
self
.
assertGreaterEqual
(
name2
,
name3
)
@ddt
class
UploadTestCase
(
AssetsTestCase
):
"""
Unit tests for uploading a file
...
...
@@ -148,6 +159,24 @@ class UploadTestCase(AssetsTestCase):
resp
=
self
.
client
.
post
(
self
.
url
,
{
"name"
:
"file.txt"
},
"application/json"
)
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
):
"""
...
...
cms/envs/common.py
View file @
5c433ec9
...
...
@@ -718,6 +718,16 @@ ADVANCED_SECURITY_CONFIG = {}
SHIBBOLETH_DOMAIN_PREFIX
=
'shib:'
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
=
[
...
...
cms/static/coffee/spec/main.coffee
View file @
5c433ec9
...
...
@@ -15,6 +15,8 @@ requirejs.config({
"jquery.cookie"
:
"xmodule_js/common_static/js/vendor/jquery.cookie"
,
"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-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.inputnumber"
:
"xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill"
,
"jquery.immediateDescendents"
:
"xmodule_js/common_static/coffee/src/jquery.immediateDescendents"
,
...
...
@@ -94,9 +96,15 @@ requirejs.config({
exports
:
"jQuery.fn.qtip"
},
"jquery.fileupload"
:
{
deps
:
[
"jquery.iframe-transport"
],
deps
:
[
"jquery.
ui"
,
"jquery.
iframe-transport"
],
exports
:
"jQuery.fn.fileupload"
},
"jquery.fileupload-process"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.fileupload-validate"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.inputnumber"
:
{
deps
:
[
"jquery"
],
exports
:
"jQuery.fn.inputNumber"
...
...
cms/static/coffee/spec/main_squire.coffee
View file @
5c433ec9
...
...
@@ -14,6 +14,8 @@ requirejs.config({
"jquery.cookie"
:
"xmodule_js/common_static/js/vendor/jquery.cookie"
,
"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-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.inputnumber"
:
"xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill"
,
"jquery.immediateDescendents"
:
"xmodule_js/common_static/coffee/src/jquery.immediateDescendents"
,
...
...
@@ -84,9 +86,15 @@ requirejs.config({
exports
:
"jQuery.fn.qtip"
},
"jquery.fileupload"
:
{
deps
:
[
"jquery.iframe-transport"
],
deps
:
[
"jquery.
ui"
,
"jquery.
iframe-transport"
],
exports
:
"jQuery.fn.fileupload"
},
"jquery.fileupload-process"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.fileupload-validate"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.inputnumber"
:
{
deps
:
[
"jquery"
],
exports
:
"jQuery.fn.inputNumber"
...
...
cms/static/coffee/spec/views/assets_spec.coffee
View file @
5c433ec9
...
...
@@ -48,9 +48,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
@
collection
=
new
AssetCollection
([
@
model
])
@
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 creat
ed"
,
1000
waitsFor
(
=>
@
createAssetView
),
"AssetsView Creation function was not initializ
ed"
,
1000
afterEach
->
@
injector
.
clean
()
...
...
@@ -58,10 +61,12 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe
"Basic"
,
->
it
"should render properly"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
()
@
view
.
render
()
expect
(
@
view
.
$el
).
toContainText
(
"test asset"
)
it
"should pop a delete confirmation when the delete button is clicked"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
()
@
view
.
render
().
$
(
".remove-asset-button"
).
click
()
expect
(
@
promptSpies
.
constructor
).
toHaveBeenCalled
()
ctorOptions
=
@
promptSpies
.
constructor
.
mostRecentCall
.
args
[
0
]
...
...
@@ -72,7 +77,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe
"AJAX"
,
->
it
"should destroy itself on confirmation"
,
->
requests
=
AjaxHelpers
[
"requests"
]
(
this
)
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
(
this
)
@
view
.
render
().
$
(
".remove-asset-button"
).
click
()
ctorOptions
=
@
promptSpies
.
constructor
.
mostRecentCall
.
args
[
0
]
...
...
@@ -92,7 +97,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
collection
.
contains
(
@
model
)).
toBeFalsy
()
it
"should not destroy itself if server errors"
,
->
requests
=
AjaxHelpers
[
"requests"
]
(
this
)
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
(
this
)
@
view
.
render
().
$
(
".remove-asset-button"
).
click
()
ctorOptions
=
@
promptSpies
.
constructor
.
mostRecentCall
.
args
[
0
]
...
...
@@ -106,7 +111,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
collection
.
contains
(
@
model
)).
toBeTruthy
()
it
"should lock the asset on confirmation"
,
->
requests
=
AjaxHelpers
[
"requests"
]
(
this
)
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
(
this
)
@
view
.
render
().
$
(
".lock-checkbox"
).
click
()
# AJAX request has been sent, but not yet returned
...
...
@@ -123,7 +128,7 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
model
.
get
(
"locked"
)).
toBeTruthy
()
it
"should not lock the asset if server errors"
,
->
requests
=
AjaxHelpers
[
"requests"
]
(
this
)
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetView
(
this
)
@
view
.
render
().
$
(
".lock-checkbox"
).
click
()
# return an error response
...
...
@@ -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
:
"paging-header-tpl"
,
type
:
"text/template"
}).
text
(
pagingHeaderTpl
))
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
.
course_location_analytics
=
jasmine
.
createSpy
()
appendSetFixtures
(
sandbox
({
id
:
"asset_table_body"
}))
...
...
@@ -182,12 +188,16 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
@
AssetModel
=
AssetModel
@
collection
=
new
AssetCollection
();
@
collection
.
url
=
"assets-url"
@
view
=
new
AssetsView
@
createAssetsView
=
(
test
)
=>
requests
=
AjaxHelpers
.
requests
(
test
)
view
=
new
AssetsView
collection
:
@
collection
el
:
$
(
'#asset_table_body'
)
@
view
.
render
()
view
.
render
()
return
{
view
:
view
,
requests
:
requests
}
waitsFor
(
=>
@
view
),
"AssetsView was not created"
,
1
000
waitsFor
(
=>
@
createAssetsView
),
"AssetsView Creation function was not initialized"
,
2
000
$
.
ajax
()
...
...
@@ -230,11 +240,9 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe
"Basic"
,
->
# Separate setup method to work-around mis-parenting of beforeEach methods
setup
=
->
requests
=
AjaxHelpers
.
requests
(
this
)
setup
=
(
requests
)
->
@
view
.
setPage
(
0
)
AjaxHelpers
.
respondWithJson
(
requests
,
@
mockAssetsResponse
)
return
requests
$
.
fn
.
fileupload
=
->
return
''
...
...
@@ -243,34 +251,38 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
$
(
html_selector
).
click
()
it
"should show upload modal on clicking upload asset button"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetsView
(
this
)
spyOn
(
@
view
,
"showUploadModal"
)
setup
.
call
(
this
)
setup
.
call
(
this
,
requests
)
expect
(
@
view
.
showUploadModal
).
not
.
toHaveBeenCalled
()
@
view
.
showUploadModal
(
clickEvent
(
".upload-button"
))
expect
(
@
view
.
showUploadModal
).
toHaveBeenCalled
()
it
"should show file selection menu on choose file button"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetsView
(
this
)
spyOn
(
@
view
,
"showFileSelectionMenu"
)
setup
.
call
(
this
)
setup
.
call
(
this
,
requests
)
expect
(
@
view
.
showFileSelectionMenu
).
not
.
toHaveBeenCalled
()
@
view
.
showFileSelectionMenu
(
clickEvent
(
".choose-file-button"
))
expect
(
@
view
.
showFileSelectionMenu
).
toHaveBeenCalled
()
it
"should hide upload modal on clicking close button"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetsView
(
this
)
spyOn
(
@
view
,
"hideModal"
)
setup
.
call
(
this
)
setup
.
call
(
this
,
requests
)
expect
(
@
view
.
hideModal
).
not
.
toHaveBeenCalled
()
@
view
.
hideModal
(
clickEvent
(
".close-button"
))
expect
(
@
view
.
hideModal
).
toHaveBeenCalled
()
it
"should show a status indicator while loading"
,
->
{
view
:
@
view
,
requests
:
requests
}
=
@
createAssetsView
(
this
)
appendSetFixtures
(
'<div class="ui-loading"/>'
)
expect
(
$
(
'.ui-loading'
).
is
(
':visible'
)).
toBe
(
true
)
setup
.
call
(
this
)
setup
.
call
(
this
,
requests
)
expect
(
$
(
'.ui-loading'
).
is
(
':visible'
)).
toBe
(
false
)
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"/>'
)
expect
(
$
(
'.ui-loading'
).
is
(
':visible'
)).
toBe
(
true
)
@
view
.
setPage
(
0
)
...
...
@@ -278,21 +290,24 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
$
(
'.ui-loading'
).
is
(
':visible'
)).
toBe
(
false
)
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 2"
)
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.
@
view
.
$
(
".remove-asset-button"
)[
1
].
click
()
@
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
).
not
.
toContainText
(
"test asset 2"
)
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.
@
view
.
$
(
".remove-asset-button"
)[
1
].
click
()
@
promptSpies
.
constructor
.
mostRecentCall
.
args
[
0
].
actions
.
primary
.
click
(
@
promptSpies
)
...
...
@@ -301,13 +316,15 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
view
.
$el
).
toContainText
(
"test asset 2"
)
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
)
expect
(
@
view
.
$el
).
toContainText
(
"new asset"
)
expect
(
@
collection
.
models
.
length
).
toBe
(
3
)
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
()
model
=
@
collection
.
models
[
1
]
@
view
.
addAsset
(
model
)
...
...
@@ -315,19 +332,19 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
describe
"Sorting"
,
->
# Separate setup method to work-around mis-parenting of beforeEach methods
setup
=
->
requests
=
AjaxHelpers
.
requests
(
this
)
setup
=
(
requests
)
->
@
view
.
setPage
(
0
)
AjaxHelpers
.
respondWithJson
(
requests
,
@
mockAssetsResponse
)
return
requests
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
.
collection
.
sortDirection
).
toBe
(
"desc"
)
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
.
collection
.
sortDirection
).
toBe
(
"desc"
)
@
view
.
$
(
"#js-asset-date-col"
).
click
()
...
...
@@ -340,7 +357,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
view
.
collection
.
sortDirection
).
toBe
(
"desc"
)
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
()
AjaxHelpers
.
respondWithJson
(
requests
,
@
mockAssetsResponse
)
expect
(
@
view
.
sortDisplayName
()).
toBe
(
"Name"
)
...
...
@@ -351,7 +369,8 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"],
expect
(
@
view
.
collection
.
sortDirection
).
toBe
(
"desc"
)
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
()
AjaxHelpers
.
respondWithJson
(
requests
,
@
mockAssetsResponse
)
addMockAsset
.
call
(
this
,
requests
)
...
...
cms/static/js/factories/asset_index.js
View file @
5c433ec9
...
...
@@ -2,12 +2,18 @@ define([
'jquery'
,
'js/collections/asset'
,
'js/views/assets'
,
'jquery.fileupload'
],
function
(
$
,
AssetCollection
,
AssetsView
)
{
'use strict'
;
return
function
(
assetCallbackUrl
)
{
return
function
(
config
)
{
var
assets
=
new
AssetCollection
(),
assetsView
;
assets
.
url
=
assetCallbackUrl
;
assetsView
=
new
AssetsView
({
collection
:
assets
,
el
:
$
(
'.assets-wrapper'
)});
assets
.
url
=
config
.
assetCallbackUrl
;
assetsView
=
new
AssetsView
({
collection
:
assets
,
el
:
$
(
'.assets-wrapper'
),
uploadChunkSizeInMBs
:
config
.
uploadChunkSizeInMBs
,
maxFileSizeInMBs
:
config
.
maxFileSizeInMBs
,
maxFileSizeRedirectUrl
:
config
.
maxFileSizeRedirectUrl
});
assetsView
.
render
();
};
});
cms/static/js/spec/views/assets_spec.js
View file @
5c433ec9
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
)
{
describe
(
"Assets"
,
function
()
{
var
assetsView
,
mockEmptyAssetsResponse
,
mockAssetUploadResponse
,
var
assetsView
,
mockEmptyAssetsResponse
,
mockAssetUploadResponse
,
mockFileUpload
,
assetLibraryTpl
,
assetTpl
,
pagingFooterTpl
,
pagingHeaderTpl
,
uploadModalTpl
;
assetLibraryTpl
=
readFixtures
(
'asset-library.underscore'
);
...
...
@@ -53,6 +53,10 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
msg
:
"Upload completed"
};
mockFileUpload
=
{
files
:
[{
name
:
'largefile'
,
size
:
0
}]
};
$
.
fn
.
fileupload
=
function
()
{
return
''
;
};
...
...
@@ -95,6 +99,15 @@ define([ "jquery", "js/common_helpers/ajax_helpers", "js/views/asset", "js/views
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
()
{
var
requests
=
setup
.
call
(
this
);
expect
(
assetsView
).
toBeDefined
();
...
...
@@ -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
(
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
();
});
});
});
});
cms/static/js/views/assets.js
View file @
5c433ec9
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"
],
function
(
$
,
_
,
gettext
,
AssetModel
,
PagingView
,
AssetView
,
PagingHeader
,
PagingFooter
,
ModalUtils
,
ViewUtils
)
{
"js/views/paging_header"
,
"js/views/paging_footer"
,
"js/utils/modal"
,
"js/views/utils/view_utils"
,
"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
({
// takes AssetCollection as model
...
...
@@ -10,7 +13,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
"click .upload-button"
:
"showUploadModal"
},
initialize
:
function
()
{
initialize
:
function
(
options
)
{
options
=
options
||
{};
PagingView
.
prototype
.
initialize
.
call
(
this
);
var
collection
=
this
.
collection
;
this
.
template
=
this
.
loadTemplate
(
"asset-library"
);
...
...
@@ -20,7 +25,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
this
.
setInitialSortColumn
(
'js-asset-date-col'
);
ViewUtils
.
showLoadingIndicator
();
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
;
// error message modal for large file uploads
this
.
largeFileErrorMsg
=
null
;
},
render
:
function
()
{
...
...
@@ -111,6 +125,9 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
}
$
(
'.file-input'
).
unbind
(
'change.startUpload'
);
ModalUtils
.
hideModal
();
if
(
assetsView
.
largeFileErrorMsg
)
{
assetsView
.
largeFileErrorMsg
.
hide
();
}
},
showUploadModal
:
function
(
event
)
{
...
...
@@ -122,23 +139,44 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
$
(
'.upload-modal .file-chooser'
).
fileupload
({
dataType
:
'json'
,
type
:
'POST'
,
maxChunkSize
:
100
*
1000
*
1000
,
// 100 MB
maxChunkSize
:
self
.
uploadChunkSizeInBytes
,
autoUpload
:
true
,
progressall
:
function
(
event
,
data
)
{
var
percentComplete
=
parseInt
((
100
*
data
.
loaded
)
/
data
.
total
,
10
);
self
.
showUploadFeedback
(
event
,
percentComplete
);
},
maxFileSize
:
100
*
1000
*
1000
,
// 100 MB
maxFileSize
:
self
.
maxFileSizeInBytes
,
maxNumberofFiles
:
100
,
add
:
function
(
event
,
data
)
{
data
.
process
().
done
(
function
()
{
data
.
submit
();
});
},
done
:
function
(
event
,
data
)
{
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",
startUpload
:
function
(
event
)
{
var
file
=
event
.
target
.
value
;
$
(
'.upload-modal h1'
).
text
(
gettext
(
'Uploading…
'
));
if
(
!
assetsView
.
largeFileErrorMsg
)
{
$
(
'.upload-modal h1'
).
text
(
gettext
(
'Uploading
'
));
$
(
'.upload-modal .file-name'
).
html
(
file
.
substring
(
file
.
lastIndexOf
(
"
\
\"
) + 1));
$('.upload-modal .choose-file-button').hide();
$('.upload-modal .progress-bar').removeClass('loaded').show();
}
},
resetUploadModal: function () {
...
...
@@ -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 .embeddable-xml-input').val('');
$('.upload-modal .embeddable').hide();
assetsView.largeFileErrorMsg = null;
},
showUploadFeedback: function (event, percentComplete) {
...
...
@@ -181,7 +222,7 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
var asset = resp.asset;
$('.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 .file-name').hide();
$('.upload-modal .progress-fill').html(resp.msg);
...
...
@@ -189,6 +230,16 @@ define(["jquery", "underscore", "gettext", "js/models/asset", "js/views/paging",
$('.upload-modal .progress-fill').width('100%');
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%');
}
});
...
...
cms/static/js_test.yml
View file @
5c433ec9
...
...
@@ -62,6 +62,10 @@ lib_paths:
-
xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
-
xmodule_js/common_static/coffee/src/xblock/
-
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
src_paths
:
...
...
cms/static/js_test_squire.yml
View file @
5c433ec9
...
...
@@ -57,6 +57,10 @@ lib_paths:
-
xmodule_js/common_static/js/test/i18n.js
-
xmodule_js/common_static/coffee/src/xblock/
-
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
src_paths
:
...
...
cms/static/require-config.js
View file @
5c433ec9
...
...
@@ -20,6 +20,8 @@ require.config({
"jquery.scrollTo"
:
"js/vendor/jquery.scrollTo-1.4.2-min"
,
"jquery.flot"
:
"js/vendor/flot/jquery.flot.min"
,
"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.inputnumber"
:
"js/vendor/html5-input-polyfills/number-polyfill"
,
"jquery.immediateDescendents"
:
"coffee/src/jquery.immediateDescendents"
,
...
...
@@ -128,9 +130,15 @@ require.config({
exports
:
"jQuery.fn.plot"
},
"jquery.fileupload"
:
{
deps
:
[
"jquery.iframe-transport"
],
deps
:
[
"jquery.
ui"
,
"jquery.
iframe-transport"
],
exports
:
"jQuery.fn.fileupload"
},
"jquery.fileupload-process"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.fileupload-validate"
:
{
deps
:
[
"jquery.fileupload"
]
},
"jquery.inputnumber"
:
{
deps
:
[
"jquery"
],
exports
:
"jQuery.fn.inputNumber"
...
...
cms/templates/asset_index.html
View file @
5c433ec9
...
...
@@ -19,7 +19,12 @@
<
%
block
name=
"requirejs"
>
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>
...
...
@@ -82,6 +87,7 @@
<a
href=
"#"
class=
"close-button"
><i
class=
"icon-remove-sign"
></i>
<span
class=
"sr"
>
${_('close')}
</span></a>
<div
class=
"modal-body"
>
<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"
>
<div
class=
"progress-bar"
>
<div
class=
"progress-fill"
></div>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment