Merge pull request #1912 from edx/andy/asset-pagination
Add pagination to Studio's Files and Uploads page. These changes are for STUD-813.
This commit is contained in:
2
AUTHORS
2
AUTHORS
@@ -102,3 +102,5 @@ Carson Gee <cgee@mit.edu>
|
||||
Gang Chen <goncha@gmail.com>
|
||||
Bertrand Marron <bertrand.marron@ionis-group.com>
|
||||
Yihua Lou <supermouselyh@hotmail.com>
|
||||
Andy Armstrong <andya@edx.org>
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Studio: Added pagination to the Files & Uploads page.
|
||||
|
||||
Blades: Video player improvements:
|
||||
- Disable edX controls on iPhone/iPod (native controls are used).
|
||||
- Disable unsupported controls (volume, playback rate) on iPad/Android.
|
||||
|
||||
@@ -41,7 +41,7 @@ class AssetsTestCase(CourseTestCase):
|
||||
|
||||
class AssetsToyCourseTestCase(CourseTestCase):
|
||||
"""
|
||||
Tests the assets returned from assets_handler (full page content) for the toy test course.
|
||||
Tests the assets returned from assets_handler for the toy test course.
|
||||
"""
|
||||
def test_toy_assets(self):
|
||||
module_store = modulestore('direct')
|
||||
@@ -56,10 +56,17 @@ class AssetsToyCourseTestCase(CourseTestCase):
|
||||
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
|
||||
url = location.url_reverse('assets/', '')
|
||||
|
||||
resp = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
# Test a small portion of the asset data passed to the client.
|
||||
self.assertContains(resp, "new AssetCollection([{")
|
||||
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
|
||||
self.assert_correct_asset_response(url, 0, 3, 3)
|
||||
self.assert_correct_asset_response(url + "?page_size=2", 0, 2, 3)
|
||||
self.assert_correct_asset_response(url + "?page_size=2&page=1", 2, 1, 3)
|
||||
|
||||
def assert_correct_asset_response(self, url, expected_start, expected_length, expected_total):
|
||||
resp = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
json_response = json.loads(resp.content)
|
||||
assets = json_response['assets']
|
||||
self.assertEquals(json_response['start'], expected_start)
|
||||
self.assertEquals(len(assets), expected_length)
|
||||
self.assertEquals(json_response['totalCount'], expected_total)
|
||||
|
||||
|
||||
class UploadTestCase(CourseTestCase):
|
||||
@@ -82,10 +89,6 @@ class UploadTestCase(CourseTestCase):
|
||||
resp = self.client.post(self.url, {"name": "file.txt"}, "application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_get(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.client.get(self.url)
|
||||
|
||||
|
||||
class AssetToJsonTestCase(TestCase):
|
||||
"""
|
||||
@@ -163,80 +166,3 @@ class LockAssetTestCase(CourseTestCase):
|
||||
resp_asset = post_asset_update(False)
|
||||
self.assertFalse(resp_asset['locked'])
|
||||
verify_asset_locked_state(False)
|
||||
|
||||
|
||||
class TestAssetIndex(CourseTestCase):
|
||||
"""
|
||||
Test getting asset lists via http (Note, the assets don't actually exist)
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create fake asset entries for the other tests to use
|
||||
"""
|
||||
super(TestAssetIndex, self).setUp()
|
||||
self.entry_filter = self.create_asset_entries(contentstore(), 100)
|
||||
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
|
||||
self.url = location.url_reverse('assets/', '')
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Get rid of the entries
|
||||
"""
|
||||
contentstore().fs_files.remove(self.entry_filter)
|
||||
|
||||
def create_asset_entries(self, cstore, number):
|
||||
"""
|
||||
Create the fake entries
|
||||
"""
|
||||
course_filter = Location(
|
||||
XASSET_LOCATION_TAG, category='asset', course=self.course.location.course, org=self.course.location.org
|
||||
)
|
||||
# purge existing entries (a bit brutal but hopefully tests are independent enuf to not trip on this)
|
||||
cstore.fs_files.remove(location_to_query(course_filter))
|
||||
base_entry = {
|
||||
'displayname': 'foo.jpg',
|
||||
'chunkSize': 262144,
|
||||
'length': 0,
|
||||
'uploadDate': datetime(2012, 1, 2, 0, 0),
|
||||
'contentType': 'image/jpeg',
|
||||
}
|
||||
for i in range(number):
|
||||
base_entry['displayname'] = '{:03x}.jpeg'.format(i)
|
||||
base_entry['uploadDate'] += timedelta(hours=i)
|
||||
base_entry['_id'] = course_filter.replace(name=base_entry['displayname']).dict()
|
||||
cstore.fs_files.insert(base_entry)
|
||||
|
||||
return course_filter.dict()
|
||||
|
||||
ASSET_LIST_RE = re.compile(r'AssetCollection\((.*)\);$', re.MULTILINE)
|
||||
|
||||
def check_page_content(self, resp_content, entry_count, last_date=None):
|
||||
"""
|
||||
:param entry_count:
|
||||
:param last_date:
|
||||
"""
|
||||
match = self.ASSET_LIST_RE.search(resp_content)
|
||||
asset_list = json.loads(match.group(1))
|
||||
self.assertEqual(len(asset_list), entry_count)
|
||||
for row in asset_list:
|
||||
datetext = row['date_added']
|
||||
parsed_date = datetime.strptime(datetext, "%b %d, %Y at %H:%M UTC")
|
||||
if last_date is None:
|
||||
last_date = parsed_date
|
||||
else:
|
||||
self.assertGreaterEqual(last_date, parsed_date)
|
||||
return last_date
|
||||
|
||||
def test_query_assets(self):
|
||||
"""
|
||||
The actual test
|
||||
"""
|
||||
# get all
|
||||
resp = self.client.get(self.url, HTTP_ACCEPT='text/html')
|
||||
self.check_page_content(resp.content, 100)
|
||||
# get first page of 10
|
||||
resp = self.client.get(self.url + "?max=10", HTTP_ACCEPT='text/html')
|
||||
last_date = self.check_page_content(resp.content, 10)
|
||||
# get next of 20
|
||||
resp = self.client.get(self.url + "?start=10&max=20", HTTP_ACCEPT='text/html')
|
||||
self.check_page_content(resp.content, 20, last_date)
|
||||
|
||||
@@ -176,7 +176,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
Lock an arbitrary asset in the course
|
||||
:param course_location:
|
||||
"""
|
||||
course_assets = content_store.get_all_content_for_course(course_location)
|
||||
course_assets,__ = content_store.get_all_content_for_course(course_location)
|
||||
self.assertGreater(len(course_assets), 0, "No assets to lock")
|
||||
content_store.set_attr(course_assets[0]['_id'], 'locked', True)
|
||||
return course_assets[0]['_id']
|
||||
@@ -585,7 +585,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
# make sure we have some assets in our contentstore
|
||||
all_assets = content_store.get_all_content_for_course(course_location)
|
||||
all_assets,__ = content_store.get_all_content_for_course(course_location)
|
||||
self.assertGreater(len(all_assets), 0)
|
||||
|
||||
# make sure we have some thumbnails in our contentstore
|
||||
@@ -698,7 +698,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# make sure there's something in the trashcan
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012')
|
||||
all_assets = trash_store.get_all_content_for_course(course_location)
|
||||
all_assets,__ = trash_store.get_all_content_for_course(course_location)
|
||||
self.assertGreater(len(all_assets), 0)
|
||||
|
||||
# make sure we have some thumbnails in our trashcan
|
||||
@@ -713,8 +713,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
empty_asset_trashcan([course_location])
|
||||
|
||||
# make sure trashcan is empty
|
||||
all_assets = trash_store.get_all_content_for_course(course_location)
|
||||
all_assets,count = trash_store.get_all_content_for_course(course_location)
|
||||
self.assertEqual(len(all_assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
|
||||
self.assertEqual(len(all_thumbnails), 0)
|
||||
@@ -923,8 +924,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
# assert that all content in the asset library is also deleted
|
||||
assets = content_store.get_all_content_for_course(location)
|
||||
assets,count = content_store.get_all_content_for_course(location)
|
||||
self.assertEqual(len(assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
filesystem = OSFS(root_dir / 'test_export')
|
||||
|
||||
@@ -84,9 +84,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
_, content_store, course, course_location = self.load_test_import_course()
|
||||
|
||||
# make sure we have ONE asset in our contentstore ("should_be_imported.html")
|
||||
all_assets = content_store.get_all_content_for_course(course_location)
|
||||
all_assets,count = content_store.get_all_content_for_course(course_location)
|
||||
print "len(all_assets)=%d" % len(all_assets)
|
||||
self.assertEqual(len(all_assets), 1)
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
content = None
|
||||
try:
|
||||
@@ -114,9 +115,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
module_store.get_item(course_location)
|
||||
|
||||
# make sure we have NO assets in our contentstore
|
||||
all_assets = content_store.get_all_content_for_course(course_location)
|
||||
all_assets,count = content_store.get_all_content_for_course(course_location)
|
||||
print "len(all_assets)=%d" % len(all_assets)
|
||||
self.assertEqual(len(all_assets), 0)
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def test_no_static_link_rewrites_on_import(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
@@ -41,9 +41,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
deleting assets, and changing the "locked" state of an asset.
|
||||
|
||||
GET
|
||||
html: return html page of all course assets (note though that a range of assets can be requested using start
|
||||
and max query parameters)
|
||||
json: not currently supported
|
||||
html: return html page which will show all course assets. Note that only the asset container
|
||||
is returned and that the actual assets are filled in with a client-side request.
|
||||
json: returns a page of assets. A page parameter specifies the desired page, and the
|
||||
optional page_size parameter indicates the number of items per page (defaults to 50).
|
||||
POST
|
||||
json: create (or update?) an asset. The only updating that can be done is changing the lock state.
|
||||
PUT
|
||||
@@ -55,9 +56,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
response_format = request.REQUEST.get('format', 'html')
|
||||
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if request.method == 'GET':
|
||||
raise NotImplementedError('coming soon')
|
||||
return _assets_json(request, location)
|
||||
else:
|
||||
return _update_asset(request, location, asset_id)
|
||||
elif request.method == 'GET': # assume html
|
||||
@@ -73,22 +75,32 @@ def _asset_index(request, location):
|
||||
Supports start (0-based index into the list of assets) and max query parameters.
|
||||
"""
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
course_module = modulestore().get_item(old_location)
|
||||
maxresults = request.REQUEST.get('max', None)
|
||||
start = request.REQUEST.get('start', None)
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
'context_course': course_module,
|
||||
'asset_callback_url': location.url_reverse('assets/', '')
|
||||
})
|
||||
|
||||
|
||||
def _assets_json(request, location):
|
||||
"""
|
||||
Display an editable asset library.
|
||||
|
||||
Supports start (0-based index into the list of assets) and max query parameters.
|
||||
"""
|
||||
requested_page = int(request.REQUEST.get('page', 0))
|
||||
requested_page_size = int(request.REQUEST.get('page_size', 50))
|
||||
current_page = max(requested_page, 0)
|
||||
start = current_page * requested_page_size
|
||||
|
||||
old_location = loc_mapper().translate_locator_to_location(location)
|
||||
|
||||
course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name)
|
||||
if maxresults is not None:
|
||||
maxresults = int(maxresults)
|
||||
start = int(start) if start else 0
|
||||
assets = contentstore().get_all_content_for_course(
|
||||
course_reference, start=start, maxresults=maxresults,
|
||||
sort=[('uploadDate', DESCENDING)]
|
||||
)
|
||||
else:
|
||||
assets = contentstore().get_all_content_for_course(
|
||||
course_reference, sort=[('uploadDate', DESCENDING)]
|
||||
)
|
||||
assets, total_count = contentstore().get_all_content_for_course(
|
||||
course_reference, start=start, maxresults=requested_page_size, sort=[('uploadDate', DESCENDING)]
|
||||
)
|
||||
end = start + len(assets)
|
||||
|
||||
asset_json = []
|
||||
for asset in assets:
|
||||
@@ -101,10 +113,13 @@ def _asset_index(request, location):
|
||||
asset_locked = asset.get('locked', False)
|
||||
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked))
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
'context_course': course_module,
|
||||
'asset_list': json.dumps(asset_json),
|
||||
'asset_callback_url': location.url_reverse('assets/', '')
|
||||
return JsonResponse({
|
||||
'start': start,
|
||||
'end': end,
|
||||
'page': current_page,
|
||||
'pageSize': requested_page_size,
|
||||
'totalCount': total_count,
|
||||
'assets': asset_json
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ requirejs.config({
|
||||
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
|
||||
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
|
||||
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
|
||||
"backbone.paginator": "xmodule_js/common_static/js/vendor/backbone.paginator.min",
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
@@ -38,6 +39,7 @@ requirejs.config({
|
||||
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
|
||||
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
|
||||
"domReady": "xmodule_js/common_static/js/vendor/domReady",
|
||||
"URI": "xmodule_js/common_static/js/vendor/URI.min",
|
||||
|
||||
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
|
||||
"youtube": "//www.youtube.com/player_api?noext",
|
||||
@@ -115,6 +117,10 @@ requirejs.config({
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"backbone.paginator": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Paginator"
|
||||
},
|
||||
"youtube": {
|
||||
exports: "YT"
|
||||
},
|
||||
@@ -139,6 +145,9 @@ requirejs.config({
|
||||
]
|
||||
MathJax.Hub.Configured()
|
||||
},
|
||||
"URI": {
|
||||
exports: "URI"
|
||||
},
|
||||
"xmodule": {
|
||||
exports: "XModule"
|
||||
},
|
||||
@@ -197,10 +206,13 @@ define([
|
||||
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
|
||||
"js/spec/transcripts/file_uploader_spec",
|
||||
|
||||
"js/spec/utils/module_spec",
|
||||
"js/spec/models/explicit_url_spec"
|
||||
"js/spec/views/baseview_spec",
|
||||
|
||||
"js/spec/utils/handle_iframe_binding_spec",
|
||||
"js/spec/utils/module_spec",
|
||||
|
||||
"js/spec/views/baseview_spec",
|
||||
"js/spec/views/paging_spec",
|
||||
|
||||
# these tests are run separate in the cms-squire suite, due to process
|
||||
# isolation issues with Squire.js
|
||||
|
||||
@@ -23,6 +23,7 @@ requirejs.config({
|
||||
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
|
||||
"backbone": "xmodule_js/common_static/js/vendor/backbone-min",
|
||||
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-min",
|
||||
"backbone.paginator": "xmodule_js/common_static/js/vendor/backbone.paginator.min",
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
@@ -34,6 +35,7 @@ requirejs.config({
|
||||
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
|
||||
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
|
||||
"domReady": "xmodule_js/common_static/js/vendor/domReady",
|
||||
"URI": "xmodule_js/common_static/js/vendor/URI.min",
|
||||
|
||||
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
|
||||
"youtube": "//www.youtube.com/player_api?noext",
|
||||
@@ -106,6 +108,10 @@ requirejs.config({
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"backbone.paginator": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Paginator"
|
||||
},
|
||||
"youtube": {
|
||||
exports: "YT"
|
||||
},
|
||||
@@ -130,6 +136,9 @@ requirejs.config({
|
||||
]
|
||||
MathJax.Hub.Configured();
|
||||
},
|
||||
"URI": {
|
||||
exports: "URI"
|
||||
},
|
||||
"xmodule": {
|
||||
exports: "XModule"
|
||||
},
|
||||
@@ -166,4 +175,3 @@ jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
define([
|
||||
"coffee/spec/views/assets_spec"
|
||||
])
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
(jasmine, create_sinon, Squire) ->
|
||||
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
assetLibraryTpl = readFixtures('asset-library.underscore')
|
||||
assetTpl = readFixtures('asset.underscore')
|
||||
pagingHeaderTpl = readFixtures('paging-header.underscore')
|
||||
pagingFooterTpl = readFixtures('paging-footer.underscore')
|
||||
|
||||
describe "Asset view", ->
|
||||
beforeEach ->
|
||||
@@ -44,7 +47,7 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
|
||||
@collection = new AssetCollection([@model])
|
||||
@collection.url = "update-asset-url"
|
||||
@collection.url = "assets-url"
|
||||
@view = new AssetView({model: @model})
|
||||
|
||||
waitsFor (=> @view), "AssetView was not created", 1000
|
||||
@@ -131,7 +134,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
|
||||
describe "Assets view", ->
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
setFixtures($("<script>", {id: "asset-library-tpl", type: "text/template"}).text(assetLibraryTpl))
|
||||
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))
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
appendSetFixtures(sandbox({id: "asset_table_body"}))
|
||||
@@ -145,31 +151,43 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
"Warning": @promptSpies.constructor
|
||||
})
|
||||
|
||||
@mockAsset1 = {
|
||||
display_name: "test asset 1"
|
||||
url: 'actual_asset_url_1'
|
||||
portable_url: 'portable_url_1'
|
||||
date_added: 'date_1'
|
||||
thumbnail: null
|
||||
id: 'id_1'
|
||||
}
|
||||
@mockAsset2 = {
|
||||
display_name: "test asset 2"
|
||||
url: 'actual_asset_url_2'
|
||||
portable_url: 'portable_url_2'
|
||||
date_added: 'date_2'
|
||||
thumbnail: null
|
||||
id: 'id_2'
|
||||
}
|
||||
@mockAssetsResponse = {
|
||||
assets: [ @mockAsset1, @mockAsset2 ],
|
||||
start: 0,
|
||||
end: 1,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 2
|
||||
}
|
||||
|
||||
runs =>
|
||||
@injector.require ["js/models/asset", "js/collections/asset", "js/views/assets"],
|
||||
(AssetModel, AssetCollection, AssetsView) =>
|
||||
@AssetModel = AssetModel
|
||||
@collection = new AssetCollection [
|
||||
display_name: "test asset 1"
|
||||
url: 'actual_asset_url_1'
|
||||
portable_url: 'portable_url_1'
|
||||
date_added: 'date_1'
|
||||
thumbnail: null
|
||||
id: 'id_1'
|
||||
,
|
||||
display_name: "test asset 2"
|
||||
url: 'actual_asset_url_2'
|
||||
portable_url: 'portable_url_2'
|
||||
date_added: 'date_2'
|
||||
thumbnail: null
|
||||
id: 'id_2'
|
||||
]
|
||||
@collection.url = "update-asset-url"
|
||||
@collection = new AssetCollection();
|
||||
@collection.url = "assets-url"
|
||||
@view = new AssetsView
|
||||
collection: @collection
|
||||
el: $('#asset_table_body')
|
||||
@view.render()
|
||||
|
||||
waitsFor (=> @view), "AssetView was not created", 1000
|
||||
waitsFor (=> @view), "AssetsView was not created", 1000
|
||||
|
||||
$.ajax()
|
||||
|
||||
@@ -181,33 +199,38 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
@injector.remove()
|
||||
|
||||
describe "Basic", ->
|
||||
# Separate setup method to work-around mis-parenting of beforeEach methods
|
||||
setup = (response) ->
|
||||
requests = create_sinon.requests(this)
|
||||
@view.setPage(0)
|
||||
create_sinon.respondWithJson(requests, response)
|
||||
return requests
|
||||
|
||||
it "should render both assets", ->
|
||||
@view.render()
|
||||
requests = setup.call(this, @mockAssetsResponse)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "should remove the deleted asset from the view", ->
|
||||
requests = create_sinon["requests"](this)
|
||||
|
||||
requests = setup.call(this, @mockAssetsResponse)
|
||||
# Delete the 2nd asset with success from server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@view.$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
req.respond(200) for req in requests
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).not.toContainText("test asset 2")
|
||||
|
||||
it "does not remove asset if deletion failed", ->
|
||||
requests = create_sinon["requests"](this)
|
||||
|
||||
requests = setup.call(this, @mockAssetsResponse)
|
||||
# Delete the 2nd asset, but mimic a failure from the server.
|
||||
@view.render().$(".remove-asset-button")[1].click()
|
||||
@view.$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
|
||||
req.respond(404) for req in requests
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "adds an asset if asset does not already exist", ->
|
||||
@view.render()
|
||||
requests = setup.call(this, @mockAssetsResponse)
|
||||
model = new @AssetModel
|
||||
display_name: "new asset"
|
||||
url: 'new_actual_asset_url'
|
||||
@@ -216,12 +239,29 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
|
||||
thumbnail: null
|
||||
id: 'idx'
|
||||
@view.addAsset(model)
|
||||
create_sinon.respondWithJson(requests,
|
||||
{
|
||||
assets: [ @mockAsset1, @mockAsset2,
|
||||
{
|
||||
display_name: "new asset"
|
||||
url: 'new_actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'idx'
|
||||
}
|
||||
],
|
||||
start: 0,
|
||||
end: 2,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 3
|
||||
})
|
||||
expect(@view.$el).toContainText("new asset")
|
||||
expect(@collection.models.indexOf(model)).toBe(0)
|
||||
expect(@collection.models.length).toBe(3)
|
||||
|
||||
it "does not add an asset if asset already exists", ->
|
||||
@view.render()
|
||||
setup.call(this, @mockAssetsResponse)
|
||||
spyOn(@collection, "add").andCallThrough()
|
||||
model = @collection.models[1]
|
||||
@view.addAsset(model)
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
define(["backbone", "js/models/asset"], function(Backbone, AssetModel){
|
||||
var AssetCollection = Backbone.Collection.extend({
|
||||
model : AssetModel
|
||||
});
|
||||
return AssetCollection;
|
||||
define(["backbone.paginator", "js/models/asset"], function(BackbonePaginator, AssetModel) {
|
||||
var AssetCollection = BackbonePaginator.requestPager.extend({
|
||||
model : AssetModel,
|
||||
paginator_core: {
|
||||
type: 'GET',
|
||||
accepts: 'application/json',
|
||||
dataType: 'json',
|
||||
url: function() { return this.url; }
|
||||
},
|
||||
paginator_ui: {
|
||||
firstPage: 0,
|
||||
currentPage: 0,
|
||||
perPage: 50
|
||||
},
|
||||
server_api: {
|
||||
'page': function() { return this.currentPage; },
|
||||
'page_size': function() { return this.perPage; },
|
||||
'format': 'json'
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
var totalCount = response.totalCount,
|
||||
start = response.start,
|
||||
currentPage = response.page,
|
||||
pageSize = response.pageSize,
|
||||
totalPages = Math.ceil(totalCount / pageSize);
|
||||
this.totalCount = totalCount;
|
||||
this.totalPages = Math.max(totalPages, 1); // Treat an empty collection as having 1 page...
|
||||
this.currentPage = currentPage;
|
||||
this.start = start;
|
||||
return response.assets;
|
||||
}
|
||||
});
|
||||
return AssetCollection;
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ define(["sinon"], function(sinon) {
|
||||
var requests = [];
|
||||
var xhr = sinon.useFakeXMLHttpRequest();
|
||||
xhr.onCreate = function(request) {
|
||||
requests.push(request)
|
||||
requests.push(request);
|
||||
};
|
||||
|
||||
that.after(function() {
|
||||
@@ -43,8 +43,16 @@ define(["sinon"], function(sinon) {
|
||||
return requests;
|
||||
};
|
||||
|
||||
var respondWithJson = function(requests, jsonResponse, requestIndex) {
|
||||
requestIndex = requestIndex || requests.length - 1;
|
||||
requests[requestIndex].respond(200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify(jsonResponse));
|
||||
};
|
||||
|
||||
return {
|
||||
"server": fakeServer,
|
||||
"requests": fakeRequests
|
||||
"requests": fakeRequests,
|
||||
"respondWithJson": respondWithJson
|
||||
};
|
||||
});
|
||||
|
||||
486
cms/static/js/spec/views/paging_spec.js
Normal file
486
cms/static/js/spec/views/paging_spec.js
Normal file
@@ -0,0 +1,486 @@
|
||||
define([ "jquery", "js/spec/create_sinon", "URI",
|
||||
"js/views/paging", "js/views/paging_header", "js/views/paging_footer",
|
||||
"js/models/asset", "js/collections/asset" ],
|
||||
function ($, create_sinon, URI, PagingView, PagingHeader, PagingFooter, AssetModel, AssetCollection) {
|
||||
|
||||
var createMockAsset = function(index) {
|
||||
var id = 'asset_' + index;
|
||||
return {
|
||||
id: id,
|
||||
display_name: id,
|
||||
url: id
|
||||
};
|
||||
};
|
||||
|
||||
var mockFirstPage = {
|
||||
assets: [
|
||||
createMockAsset(1),
|
||||
createMockAsset(2),
|
||||
createMockAsset(3)
|
||||
],
|
||||
pageSize: 3,
|
||||
totalCount: 4,
|
||||
page: 0,
|
||||
start: 0,
|
||||
end: 2
|
||||
};
|
||||
var mockSecondPage = {
|
||||
assets: [
|
||||
createMockAsset(4)
|
||||
],
|
||||
pageSize: 3,
|
||||
totalCount: 4,
|
||||
page: 1,
|
||||
start: 3,
|
||||
end: 4
|
||||
};
|
||||
var mockEmptyPage = {
|
||||
assets: [],
|
||||
pageSize: 3,
|
||||
totalCount: 0,
|
||||
page: 0,
|
||||
start: 0,
|
||||
end: 0
|
||||
};
|
||||
|
||||
var respondWithMockAssets = function(requests) {
|
||||
var requestIndex = requests.length - 1;
|
||||
var request = requests[requestIndex];
|
||||
var url = new URI(request.url);
|
||||
var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
|
||||
var page = queryParameters.page;
|
||||
var response = page === "0" ? mockFirstPage : mockSecondPage;
|
||||
create_sinon.respondWithJson(requests, response, requestIndex);
|
||||
};
|
||||
|
||||
var MockPagingView = PagingView.extend({
|
||||
renderPageItems: function() {}
|
||||
});
|
||||
|
||||
describe("Paging", function() {
|
||||
var pagingView;
|
||||
|
||||
beforeEach(function () {
|
||||
var assets = new AssetCollection();
|
||||
assets.url = "assets_url";
|
||||
var feedbackTpl = readFixtures('system-feedback.underscore');
|
||||
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTpl));
|
||||
pagingView = new MockPagingView({collection: assets});
|
||||
});
|
||||
|
||||
|
||||
describe("PagingView", function () {
|
||||
it('can set the current page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should not change page after a server error', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.setPage(1);
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('does not move forward after a server error', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.nextPage();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('can move to the next page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.nextPage();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('can not move forward from the final page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.nextPage();
|
||||
expect(requests.length).toBe(1);
|
||||
});
|
||||
|
||||
it('can move back a page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.previousPage();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('can not move back from the first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.previousPage();
|
||||
expect(requests.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not move back after a server error', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingView.previousPage();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PagingHeader", function () {
|
||||
var pagingHeader;
|
||||
|
||||
beforeEach(function () {
|
||||
var pagingHeaderTpl = readFixtures('paging-header.underscore');
|
||||
appendSetFixtures($("<script>", { id: "paging-header-tpl", type: "text/template" }).text(pagingHeaderTpl));
|
||||
pagingHeader = new PagingHeader({view: pagingView});
|
||||
});
|
||||
|
||||
describe("Next page button", function () {
|
||||
beforeEach(function () {
|
||||
// Render the page and header so that they can react to events
|
||||
pagingView.render();
|
||||
pagingHeader.render();
|
||||
});
|
||||
|
||||
it('does not move forward if a server error occurs', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingHeader.$('.next-page-link').click();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('can move to the next page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingHeader.$('.next-page-link').click();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should be enabled when there is at least one more page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.next-page-link')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be disabled on the final page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
|
||||
it('should be disabled on an empty page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Previous page button", function () {
|
||||
beforeEach(function () {
|
||||
// Render the page and header so that they can react to events
|
||||
pagingView.render();
|
||||
pagingHeader.render();
|
||||
});
|
||||
|
||||
it('does not move back if a server error occurs', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingHeader.$('.previous-page-link').click();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('can go back a page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingHeader.$('.previous-page-link').click();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('should be disabled on the first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be enabled on the second page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.previous-page-link')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be disabled for an empty page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Asset count label", function () {
|
||||
it('should show correct count on first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.count-current-shown')).toHaveHtml('1-3');
|
||||
});
|
||||
|
||||
it('should show correct count on second page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.count-current-shown')).toHaveHtml('4-4');
|
||||
});
|
||||
|
||||
it('should show correct count for an empty collection', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingHeader.$('.count-current-shown')).toHaveHtml('0-0');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Asset total label", function () {
|
||||
it('should show correct total on the first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.count-total')).toHaveText('4 total');
|
||||
});
|
||||
|
||||
it('should show correct total on the second page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingHeader.$('.count-total')).toHaveText('4 total');
|
||||
});
|
||||
|
||||
it('should show zero total for an empty collection', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingHeader.$('.count-total')).toHaveText('0 total');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PagingFooter", function () {
|
||||
var pagingFooter;
|
||||
|
||||
beforeEach(function () {
|
||||
var pagingFooterTpl = readFixtures('paging-footer.underscore');
|
||||
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
|
||||
pagingFooter = new PagingFooter({view: pagingView});
|
||||
});
|
||||
|
||||
describe("Next page button", function () {
|
||||
beforeEach(function () {
|
||||
// Render the page and header so that they can react to events
|
||||
pagingView.render();
|
||||
pagingFooter.render();
|
||||
});
|
||||
|
||||
it('does not move forward if a server error occurs', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.next-page-link').click();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('can move to the next page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.next-page-link').click();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should be enabled when there is at least one more page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.next-page-link')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be disabled on the final page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be disabled on an empty page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Previous page button", function () {
|
||||
beforeEach(function () {
|
||||
// Render the page and header so that they can react to events
|
||||
pagingView.render();
|
||||
pagingFooter.render();
|
||||
});
|
||||
|
||||
it('does not move back if a server error occurs', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.previous-page-link').click();
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('can go back a page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.previous-page-link').click();
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
});
|
||||
|
||||
it('should be disabled on the first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be enabled on the second page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.previous-page-link')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('should be disabled for an empty page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Page total label", function () {
|
||||
it('should show 1 on the first page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.current-page')).toHaveText('1');
|
||||
});
|
||||
|
||||
it('should show 2 on the second page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(1);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.current-page')).toHaveText('2');
|
||||
});
|
||||
|
||||
it('should show 1 for an empty collection', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingFooter.$('.current-page')).toHaveText('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Page total label", function () {
|
||||
it('should show the correct value with more than one page', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.total-pages')).toHaveText('2');
|
||||
});
|
||||
|
||||
it('should show page 1 when there are no assets', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
create_sinon.respondWithJson(requests, mockEmptyPage);
|
||||
expect(pagingFooter.$('.total-pages')).toHaveText('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Page input field", function () {
|
||||
var input;
|
||||
|
||||
beforeEach(function () {
|
||||
pagingFooter.render();
|
||||
});
|
||||
|
||||
it('should initially have a blank page input', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
expect(pagingFooter.$('.page-number-input')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should handle invalid page requests', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.page-number-input').val('abc');
|
||||
pagingFooter.$('.page-number-input').trigger('change');
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
expect(pagingFooter.$('.page-number-input')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should switch pages via the input field', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.page-number-input').val('2');
|
||||
pagingFooter.$('.page-number-input').trigger('change');
|
||||
create_sinon.respondWithJson(requests, mockSecondPage);
|
||||
expect(pagingView.collection.currentPage).toBe(1);
|
||||
expect(pagingFooter.$('.page-number-input')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should handle AJAX failures when switching pages via the input field', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
pagingView.setPage(0);
|
||||
respondWithMockAssets(requests);
|
||||
pagingFooter.$('.page-number-input').val('2');
|
||||
pagingFooter.$('.page-number-input').trigger('change');
|
||||
requests[1].respond(500);
|
||||
expect(pagingView.collection.currentPage).toBe(0);
|
||||
expect(pagingFooter.$('.page-number-input')).toHaveValue('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,30 +1,50 @@
|
||||
define(["js/views/baseview", "js/views/asset"], function(BaseView, AssetView) {
|
||||
define(["js/views/paging", "js/views/asset", "js/views/paging_header", "js/views/paging_footer"],
|
||||
function(PagingView, AssetView, PagingHeader, PagingFooter) {
|
||||
|
||||
var AssetsView = BaseView.extend({
|
||||
var AssetsView = PagingView.extend({
|
||||
// takes AssetCollection as model
|
||||
|
||||
initialize : function() {
|
||||
this.listenTo(this.collection, 'destroy', this.handleDestroy);
|
||||
this.render();
|
||||
PagingView.prototype.initialize.call(this);
|
||||
var collection = this.collection;
|
||||
this.template = _.template($("#asset-library-tpl").text());
|
||||
this.listenTo(collection, 'destroy', this.handleDestroy);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.empty();
|
||||
this.$el.html(this.template());
|
||||
this.tableBody = this.$('#asset-table-body');
|
||||
this.pagingHeader = new PagingHeader({view: this, el: $('#asset-paging-header')});
|
||||
this.pagingFooter = new PagingFooter({view: this, el: $('#asset-paging-footer')});
|
||||
this.pagingHeader.render();
|
||||
this.pagingFooter.render();
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function(asset) {
|
||||
var view = new AssetView({model: asset});
|
||||
self.$el.append(view.render().el);
|
||||
});
|
||||
// Hide the contents until the collection has loaded the first time
|
||||
this.$('.asset-library').hide();
|
||||
this.$('.no-asset-content').hide();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
handleDestroy: function(model, collection, options) {
|
||||
var index = options.index;
|
||||
this.$el.children().eq(index).remove();
|
||||
renderPageItems: function() {
|
||||
var self = this,
|
||||
assets = this.collection,
|
||||
hasAssets = assets.length > 0;
|
||||
self.tableBody.empty();
|
||||
if (hasAssets) {
|
||||
assets.each(
|
||||
function(asset) {
|
||||
var view = new AssetView({model: asset});
|
||||
self.tableBody.append(view.render().el);
|
||||
});
|
||||
}
|
||||
self.$('.asset-library').toggle(hasAssets);
|
||||
self.$('.no-asset-content').toggle(!hasAssets);
|
||||
return this;
|
||||
},
|
||||
|
||||
handleDestroy: function(model, collection, options) {
|
||||
this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items
|
||||
analytics.track('Deleted Asset', {
|
||||
'course': course_location_analytics,
|
||||
'id': model.get('url')
|
||||
@@ -32,17 +52,12 @@ var AssetsView = BaseView.extend({
|
||||
},
|
||||
|
||||
addAsset: function (model) {
|
||||
// If asset is not already being shown, add it.
|
||||
if (this.collection.findWhere({'url': model.get('url')}) === undefined) {
|
||||
this.collection.add(model, {at: 0});
|
||||
var view = new AssetView({model: model});
|
||||
this.$el.prepend(view.render().el);
|
||||
this.setPage(0);
|
||||
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': model.get('url')
|
||||
});
|
||||
}
|
||||
analytics.track('Uploaded a File', {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': model.get('url')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
48
cms/static/js/views/paging.js
Normal file
48
cms/static/js/views/paging.js
Normal file
@@ -0,0 +1,48 @@
|
||||
define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, AlertView, gettext) {
|
||||
|
||||
var PagingView = Backbone.View.extend({
|
||||
// takes a Backbone Paginator as a model
|
||||
|
||||
initialize: function() {
|
||||
Backbone.View.prototype.initialize.call(this);
|
||||
var collection = this.collection;
|
||||
collection.bind('add', _.bind(this.renderPageItems, this));
|
||||
collection.bind('remove', _.bind(this.renderPageItems, this));
|
||||
collection.bind('reset', _.bind(this.renderPageItems, this));
|
||||
},
|
||||
|
||||
setPage: function(page) {
|
||||
var self = this,
|
||||
collection = self.collection,
|
||||
oldPage = collection.currentPage;
|
||||
collection.goTo(page, {
|
||||
reset: true,
|
||||
success: function() {
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
error: function(collection, response, options) {
|
||||
collection.currentPage = oldPage;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
var collection = this.collection,
|
||||
currentPage = collection.currentPage,
|
||||
lastPage = collection.totalPages - 1;
|
||||
if (currentPage < lastPage) {
|
||||
this.setPage(currentPage + 1);
|
||||
}
|
||||
},
|
||||
|
||||
previousPage: function() {
|
||||
var collection = this.collection,
|
||||
currentPage = collection.currentPage;
|
||||
if (currentPage > 0) {
|
||||
this.setPage(currentPage - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return PagingView;
|
||||
}); // end define();
|
||||
57
cms/static/js/views/paging_footer.js
Normal file
57
cms/static/js/views/paging_footer.js
Normal file
@@ -0,0 +1,57 @@
|
||||
define(["backbone", "underscore"], function(Backbone, _) {
|
||||
|
||||
var PagingFooter = Backbone.View.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage",
|
||||
"change .page-number-input": "changePage"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = _.template($("#paging-footer-tpl").text());
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage,
|
||||
lastPage = collection.totalPages - 1;
|
||||
this.$el.html(this.template({
|
||||
current_page: collection.currentPage,
|
||||
total_pages: collection.totalPages
|
||||
}));
|
||||
this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0);
|
||||
this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage);
|
||||
return this;
|
||||
},
|
||||
|
||||
changePage: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage + 1,
|
||||
pageInput = this.$("#page-number-input"),
|
||||
pageNumber = parseInt(pageInput.val(), 10);
|
||||
if (pageNumber && pageNumber !== currentPage) {
|
||||
view.setPage(pageNumber - 1);
|
||||
}
|
||||
pageInput.val(""); // Clear the value as the label will show beneath it
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
this.view.nextPage();
|
||||
},
|
||||
|
||||
previousPage: function() {
|
||||
this.view.previousPage();
|
||||
}
|
||||
});
|
||||
|
||||
return PagingFooter;
|
||||
}); // end define();
|
||||
64
cms/static/js/views/paging_header.js
Normal file
64
cms/static/js/views/paging_header.js
Normal file
@@ -0,0 +1,64 @@
|
||||
define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
|
||||
|
||||
var PagingHeader = Backbone.View.extend({
|
||||
events : {
|
||||
"click .next-page-link": "nextPage",
|
||||
"click .previous-page-link": "previousPage"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var view = options.view,
|
||||
collection = view.collection;
|
||||
this.view = view;
|
||||
this.template = _.template($("#paging-header-tpl").text());
|
||||
collection.bind('add', _.bind(this.render, this));
|
||||
collection.bind('remove', _.bind(this.render, this));
|
||||
collection.bind('reset', _.bind(this.render, this));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
currentPage = collection.currentPage,
|
||||
lastPage = collection.totalPages - 1,
|
||||
messageHtml = this.messageHtml();
|
||||
this.$el.html(this.template({
|
||||
messageHtml: messageHtml
|
||||
}));
|
||||
this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0);
|
||||
this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage);
|
||||
return this;
|
||||
},
|
||||
|
||||
messageHtml: function() {
|
||||
var view = this.view,
|
||||
collection = view.collection,
|
||||
start = collection.start,
|
||||
count = collection.size(),
|
||||
end = start + count,
|
||||
total = collection.totalCount,
|
||||
fmts = gettext('Showing %(current_span)s%(start)s-%(end)s%(end_span)s out of %(total_span)s%(total)s total%(end_span)s, sorted by %(order_span)s%(sort_order)s%(end_span)s');
|
||||
|
||||
return '<p>' + interpolate(fmts, {
|
||||
start: Math.min(start + 1, end),
|
||||
end: end,
|
||||
total: total,
|
||||
sort_order: gettext('Date Added'),
|
||||
current_span: '<span class="count-current-shown">',
|
||||
total_span: '<span class="count-total">',
|
||||
order_span: '<span class="sort-order">',
|
||||
end_span: '</span>'
|
||||
}, true) + "</p>";
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
this.view.nextPage();
|
||||
},
|
||||
|
||||
previousPage: function() {
|
||||
this.view.previousPage();
|
||||
}
|
||||
});
|
||||
|
||||
return PagingHeader;
|
||||
}); // end define();
|
||||
@@ -38,6 +38,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/underscore.string.min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone-min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
|
||||
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.ajaxQueue.js
|
||||
@@ -55,6 +56,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/draggabilly.pkgd.js
|
||||
- xmodule_js/common_static/js/vendor/date.js
|
||||
- xmodule_js/common_static/js/vendor/domReady.js
|
||||
- xmodule_js/common_static/js/vendor/URI.min.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js
|
||||
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
|
||||
- xmodule_js/common_static/coffee/src/xblock
|
||||
|
||||
@@ -38,6 +38,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/underscore.string.min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone-min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
|
||||
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.form.js
|
||||
@@ -49,6 +50,7 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine.async.js
|
||||
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
|
||||
- xmodule_js/common_static/js/vendor/URI.min.js
|
||||
- xmodule_js/src/xmodule.js
|
||||
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
|
||||
- xmodule_js/common_static/js/test/i18n.js
|
||||
|
||||
@@ -27,9 +27,167 @@
|
||||
|
||||
}
|
||||
|
||||
.no-asset-content {
|
||||
@extend %ui-well;
|
||||
padding: ($baseline*2);
|
||||
background-color: $gray-l4;
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
|
||||
.new-button {
|
||||
@include font-size(14);
|
||||
margin-left: $baseline;
|
||||
|
||||
[class^="icon-"] {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-library {
|
||||
@include clearfix;
|
||||
|
||||
.meta-wrap {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
.meta {
|
||||
@extend %t-copy-sub2;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: flex-grid(9, 12);
|
||||
color: $gray-l1;
|
||||
|
||||
.count-current-shown,
|
||||
.count-total,
|
||||
.sort-order {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
@include clearfix;
|
||||
display: inline-block;
|
||||
width: flex-grid(3, 12);
|
||||
|
||||
&.pagination-compact {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.pagination-full {
|
||||
display: block;
|
||||
width: flex-grid(4, 12);
|
||||
margin: $baseline auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
display: block;
|
||||
padding: ($baseline/4) ($baseline*.75);
|
||||
|
||||
&.previous {
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
|
||||
&.next {
|
||||
margin-left: ($baseline/2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $blue;
|
||||
border-radius: 3px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
background-color: transparent;
|
||||
color: $gray-l2;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
@extend .sr;
|
||||
}
|
||||
|
||||
.pagination-form,
|
||||
.current-page,
|
||||
.page-divider,
|
||||
.total-pages {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.current-page,
|
||||
.page-number-input,
|
||||
.total-pages {
|
||||
@extend %t-copy-base;
|
||||
width: ($baseline*2.5);
|
||||
margin: 0 ($baseline*.75);
|
||||
padding: ($baseline/4);
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-page {
|
||||
@extend %ui-depth1;
|
||||
position: absolute;
|
||||
left: -($baseline/4);
|
||||
}
|
||||
|
||||
.page-divider {
|
||||
@extend %t-title4;
|
||||
vertical-align: middle;
|
||||
color: $gray-l2;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
.pagination-form {
|
||||
@extend %ui-depth2;
|
||||
position: relative;
|
||||
|
||||
.page-number-label,
|
||||
.submit-pagination-form {
|
||||
@extend .sr;
|
||||
}
|
||||
|
||||
.page-number-input {
|
||||
@include transition(all $tmg-f2 ease-in-out 0s);
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px dotted $gray-l2;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $white;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
// borrowing the base input focus styles to match overall app
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
opacity: 1.0;
|
||||
box-shadow: 0 0 3px $shadow-d1 inset;
|
||||
background-color: $white;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
@@ -41,6 +199,11 @@
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
color: $gray;
|
||||
|
||||
.current-sort {
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
@@ -9,21 +9,26 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="header_extras">
|
||||
<script type="text/template" id="asset-tpl">
|
||||
<%static:include path="js/asset.underscore"/>
|
||||
</script>
|
||||
% for template_name in ["asset-library", "asset", "paging-header", "paging-footer"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/asset",
|
||||
"js/views/assets", "js/views/feedback_prompt",
|
||||
"js/views/feedback_notification", "js/utils/modal", "jquery.fileupload"],
|
||||
function(domReady, $, gettext, AssetModel, AssetCollection, AssetsView, PromptView, NotificationView, ModalUtils) {
|
||||
|
||||
var assets = new AssetCollection(${asset_list});
|
||||
"js/views/feedback_notification", "js/views/paging_header", "js/views/paging_footer",
|
||||
"js/utils/modal", "jquery.fileupload"],
|
||||
function(domReady, $, gettext, AssetModel, AssetCollection, AssetsView, PromptView, NotificationView,
|
||||
PagingHeader, PagingFooter, ModalUtils) {
|
||||
var assets = new AssetCollection();
|
||||
assets.url = "${asset_callback_url}";
|
||||
var assetsView = new AssetsView({collection: assets, el: $('#asset_table_body')});
|
||||
var assetsView = new AssetsView({collection: assets, el: $('#asset-library')});
|
||||
assetsView.render();
|
||||
assetsView.setPage(0);
|
||||
|
||||
var hideModal = function (e) {
|
||||
if (e) {
|
||||
@@ -142,30 +147,7 @@ require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/ass
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="asset-library content-primary" role="main">
|
||||
<table>
|
||||
<caption class="sr">${_("List of uploaded files and assets in this course")}</caption>
|
||||
<colgroup>
|
||||
<col class="thumb-cols" />
|
||||
<col class="name-cols" />
|
||||
<col class="date-cols" />
|
||||
<col class="embed-cols" />
|
||||
<col class="actions-cols" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="thumb-col">${_("Preview")}</th>
|
||||
<th class="name-col">${_("Name")}</th>
|
||||
<th class="date-col">${_("Date Added")}</th>
|
||||
<th class="embed-col">${_("URL")}</th>
|
||||
<th class="actions-col"><span class="sr">${_("Actions")}</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="asset_table_body" >
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article id="asset-library" class="content-primary" role="main"></article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"backbone.associations": "js/vendor/backbone-associations-min",
|
||||
"backbone.paginator": "js/vendor/backbone.paginator.min",
|
||||
"tinymce": "js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "js/vendor/tiny_mce/jquery.tinymce",
|
||||
"xmodule": "/xmodule/xmodule",
|
||||
@@ -76,6 +77,7 @@
|
||||
"utility": "js/src/utility",
|
||||
"accessibility": "js/src/accessibility_tools",
|
||||
"draggabilly": "js/vendor/draggabilly.pkgd",
|
||||
"URI": "/js/vendor/URI.min",
|
||||
|
||||
// externally hosted files
|
||||
"tender": "//edxedge.tenderapp.com/tender_widget",
|
||||
@@ -163,6 +165,10 @@
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Associations"
|
||||
},
|
||||
"backbone.paginator": {
|
||||
deps: ["backbone"],
|
||||
exports: "Backbone.Paginator"
|
||||
},
|
||||
"youtube": {
|
||||
exports: "YT"
|
||||
},
|
||||
@@ -193,6 +199,9 @@
|
||||
MathJax.Hub.Configured();
|
||||
}
|
||||
},
|
||||
"URI": {
|
||||
exports: "URI"
|
||||
},
|
||||
"xblock/core": {
|
||||
exports: "XBlock",
|
||||
deps: ["jquery", "jquery.immediateDescendents"]
|
||||
|
||||
32
cms/templates/js/asset-library.underscore
Normal file
32
cms/templates/js/asset-library.underscore
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="asset-library">
|
||||
|
||||
<div id="asset-paging-header"></div>
|
||||
|
||||
<table>
|
||||
<caption class="sr"><%= gettext("List of uploaded files and assets in this course") %></caption>
|
||||
<colgroup>
|
||||
<col class="thumb-cols" />
|
||||
<col class="name-cols" />
|
||||
<col class="date-cols" />
|
||||
<col class="embed-cols" />
|
||||
<col class="actions-cols" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="thumb-col"><%= gettext("Preview") %></th>
|
||||
<th class="name-col"><%= gettext("Name") %></th>
|
||||
<th class="date-col"><span class="current-sort" href=""><%= gettext("Date Added") %></span></th>
|
||||
<th class="embed-col"><%= gettext("URL") %></th>
|
||||
<th class="actions-col"><span class="sr"><%= gettext("Actions") %></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="asset-table-body" ></tbody>
|
||||
</table>
|
||||
|
||||
<div id="asset-paging-footer"></div>
|
||||
|
||||
</div>
|
||||
<div class="no-asset-content">
|
||||
<p><%= gettext("You haven't added any assets to this course yet.") %> <a href="#" class="button upload-button new-button"><i class="icon-plus"></i><%= gettext("Upload your first asset") %></a></p>
|
||||
</div>
|
||||
16
cms/templates/js/paging-footer.underscore
Normal file
16
cms/templates/js/paging-footer.underscore
Normal file
@@ -0,0 +1,16 @@
|
||||
<nav class="pagination pagination-full bottom">
|
||||
<ol>
|
||||
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
|
||||
<li class="nav-item page">
|
||||
<div class="pagination-form">
|
||||
<label class="page-number-label" for="page-number"><%= gettext("Page number") %></label>
|
||||
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" />
|
||||
</div>
|
||||
|
||||
<span class="current-page"><%= current_page + 1 %></span>
|
||||
<span class="page-divider">/</span>
|
||||
<span class="total-pages"><%= total_pages %></span>
|
||||
</li>
|
||||
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
11
cms/templates/js/paging-header.underscore
Normal file
11
cms/templates/js/paging-header.underscore
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="meta-wrap">
|
||||
<div class="meta">
|
||||
<%= messageHtml %>
|
||||
</div>
|
||||
<nav class="pagination pagination-compact top">
|
||||
<ol>
|
||||
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
|
||||
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -177,7 +177,10 @@ class ContentStore(object):
|
||||
|
||||
def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None):
|
||||
'''
|
||||
Returns a list of all static assets for a course. The return format is a list of dictionary elements. Example:
|
||||
Returns a list of static assets for a course, followed by the total number of assets.
|
||||
By default all assets are returned, but start and maxresults can be provided to limit the query.
|
||||
|
||||
The return format is a list of dictionary elements. Example:
|
||||
|
||||
[
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class MongoContentStore(ContentStore):
|
||||
directory as the other policy files.
|
||||
"""
|
||||
policy = {}
|
||||
assets = self.get_all_content_for_course(course_location)
|
||||
assets,__ = self.get_all_content_for_course(course_location)
|
||||
|
||||
for asset in assets:
|
||||
asset_location = Location(asset['_id'])
|
||||
@@ -141,7 +141,7 @@ class MongoContentStore(ContentStore):
|
||||
json.dump(policy, f)
|
||||
|
||||
def get_all_content_thumbnails_for_course(self, location):
|
||||
return self._get_all_content_for_course(location, get_thumbnails=True)
|
||||
return self._get_all_content_for_course(location, get_thumbnails=True)[0]
|
||||
|
||||
def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None):
|
||||
return self._get_all_content_for_course(
|
||||
@@ -178,7 +178,8 @@ class MongoContentStore(ContentStore):
|
||||
)
|
||||
else:
|
||||
items = self.fs_files.find(location_to_query(course_filter), sort=sort)
|
||||
return list(items)
|
||||
count = items.count()
|
||||
return list(items), count
|
||||
|
||||
def set_attr(self, location, attr, value=True):
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ def empty_asset_trashcan(course_locs):
|
||||
store.delete(id)
|
||||
|
||||
# then delete all of the assets
|
||||
assets = store.get_all_content_for_course(course_loc)
|
||||
assets,__ = store.get_all_content_for_course(course_loc)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_loc)
|
||||
|
||||
@@ -199,7 +199,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
|
||||
# now iterate through all of the assets, also updating the thumbnail pointer
|
||||
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
assets,__ = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
@@ -260,7 +260,7 @@ def delete_course(modulestore, contentstore, source_location, commit=False):
|
||||
_delete_assets(contentstore, thumbs, commit)
|
||||
|
||||
# then delete all of the assets
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
assets,__ = contentstore.get_all_content_for_course(source_location)
|
||||
_delete_assets(contentstore, assets, commit)
|
||||
|
||||
# then delete all course modules
|
||||
|
||||
@@ -223,7 +223,7 @@ class TestMongoModuleStore(object):
|
||||
Test getting, setting, and defaulting the locked attr and arbitrary attrs.
|
||||
"""
|
||||
location = Location('i4x', 'edX', 'toy', 'course', '2012_Fall')
|
||||
course_content = TestMongoModuleStore.content_store.get_all_content_for_course(location)
|
||||
course_content,__ = TestMongoModuleStore.content_store.get_all_content_for_course(location)
|
||||
assert len(course_content) > 0
|
||||
# a bit overkill, could just do for content[0]
|
||||
for content in course_content:
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
window.gettext = window.ngettext = function(s){return s;};
|
||||
|
||||
function interpolate(fmt, obj, named) {
|
||||
if (named) {
|
||||
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
|
||||
} else {
|
||||
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
|
||||
}
|
||||
}
|
||||
|
||||
47
common/static/js/vendor/URI.min.js
vendored
Normal file
47
common/static/js/vendor/URI.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
common/static/js/vendor/backbone.paginator.min.js
vendored
Normal file
4
common/static/js/vendor/backbone.paginator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@
|
||||
-e git+https://github.com/edx/XBlock.git@fa88607#egg=XBlock
|
||||
-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
|
||||
-e git+https://github.com/edx/diff-cover.git@v0.2.6#egg=diff_cover
|
||||
-e git+https://github.com/edx/js-test-tool.git@v0.1.4#egg=js_test_tool
|
||||
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
|
||||
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
|
||||
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
|
||||
-e git+https://github.com/edx/bok-choy.git@bc6f1adbe439618162079f1004b2b3db3b6f8916#egg=bok_choy
|
||||
|
||||
Reference in New Issue
Block a user