Commit f9c45586 by Andy Armstrong

Add pagination to Studio's Files and Uploads page

These changes implement STUD-813. The commit consists of the
following logical changes:
 - a REST API has been implemented for a course's assets
 - the page itself now fetches the assets client-side
 - the Backbone.Paginator library is used to support pagination
 - the AssetCollection has been refactored to extend
   Backbone.Paginator.requestPager so that it can be paged
 - an abstract PagingView class has been added to generalize
   the communication with a paging REST API
 - the AssetsView has been reimplemented to extend PagingView
 - two new child views have been added:
   - PagingHeader: the paging controls above the list of assets
   - PagingFooter: the paging controls below the assets
parent 5dccff51
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Studio: Added pagination to the Files & Uploads page.
Blades: Video player improvements: Blades: Video player improvements:
- Disable edX controls on iPhone/iPod (native controls are used). - Disable edX controls on iPhone/iPod (native controls are used).
- Disable unsupported controls (volume, playback rate) on iPad/Android. - Disable unsupported controls (volume, playback rate) on iPad/Android.
......
...@@ -41,7 +41,7 @@ class AssetsTestCase(CourseTestCase): ...@@ -41,7 +41,7 @@ class AssetsTestCase(CourseTestCase):
class AssetsToyCourseTestCase(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): def test_toy_assets(self):
module_store = modulestore('direct') module_store = modulestore('direct')
...@@ -56,10 +56,17 @@ class AssetsToyCourseTestCase(CourseTestCase): ...@@ -56,10 +56,17 @@ class AssetsToyCourseTestCase(CourseTestCase):
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True) location = loc_mapper().translate_location(course.location.course_id, course.location, False, True)
url = location.url_reverse('assets/', '') url = location.url_reverse('assets/', '')
resp = self.client.get(url, HTTP_ACCEPT='text/html') self.assert_correct_asset_response(url, 0, 3, 3)
# Test a small portion of the asset data passed to the client. self.assert_correct_asset_response(url + "?page_size=2", 0, 2, 3)
self.assertContains(resp, "new AssetCollection([{") self.assert_correct_asset_response(url + "?page_size=2&page=1", 2, 1, 3)
self.assertContains(resp, "/c4x/edX/toy/asset/handouts_sample_handout.txt")
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): class UploadTestCase(CourseTestCase):
...@@ -82,10 +89,6 @@ class UploadTestCase(CourseTestCase): ...@@ -82,10 +89,6 @@ class UploadTestCase(CourseTestCase):
resp = self.client.post(self.url, {"name": "file.txt"}, "application/json") resp = self.client.post(self.url, {"name": "file.txt"}, "application/json")
self.assertEquals(resp.status_code, 400) self.assertEquals(resp.status_code, 400)
def test_get(self):
with self.assertRaises(NotImplementedError):
self.client.get(self.url)
class AssetToJsonTestCase(TestCase): class AssetToJsonTestCase(TestCase):
""" """
...@@ -163,80 +166,3 @@ class LockAssetTestCase(CourseTestCase): ...@@ -163,80 +166,3 @@ class LockAssetTestCase(CourseTestCase):
resp_asset = post_asset_update(False) resp_asset = post_asset_update(False)
self.assertFalse(resp_asset['locked']) self.assertFalse(resp_asset['locked'])
verify_asset_locked_state(False) 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): ...@@ -176,7 +176,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
Lock an arbitrary asset in the course Lock an arbitrary asset in the course
:param course_location: :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") self.assertGreater(len(course_assets), 0, "No assets to lock")
content_store.set_attr(course_assets[0]['_id'], 'locked', True) content_store.set_attr(course_assets[0]['_id'], 'locked', True)
return course_assets[0]['_id'] return course_assets[0]['_id']
...@@ -585,7 +585,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -585,7 +585,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertIsNotNone(course) self.assertIsNotNone(course)
# make sure we have some assets in our contentstore # 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) self.assertGreater(len(all_assets), 0)
# make sure we have some thumbnails in our contentstore # make sure we have some thumbnails in our contentstore
...@@ -698,7 +698,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -698,7 +698,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure there's something in the trashcan # make sure there's something in the trashcan
course_location = CourseDescriptor.id_to_location('edX/toy/6.002_Spring_2012') 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) self.assertGreater(len(all_assets), 0)
# make sure we have some thumbnails in our trashcan # make sure we have some thumbnails in our trashcan
...@@ -713,8 +713,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -713,8 +713,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
empty_asset_trashcan([course_location]) empty_asset_trashcan([course_location])
# make sure trashcan is empty # 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(len(all_assets), 0)
self.assertEqual(count, 0)
all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location) all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location)
self.assertEqual(len(all_thumbnails), 0) self.assertEqual(len(all_thumbnails), 0)
...@@ -923,8 +924,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -923,8 +924,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(len(items), 0) self.assertEqual(len(items), 0)
# assert that all content in the asset library is also deleted # 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(len(assets), 0)
self.assertEqual(count, 0)
def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''): def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
filesystem = OSFS(root_dir / 'test_export') filesystem = OSFS(root_dir / 'test_export')
......
...@@ -84,9 +84,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -84,9 +84,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
_, content_store, course, course_location = self.load_test_import_course() _, content_store, course, course_location = self.load_test_import_course()
# make sure we have ONE asset in our contentstore ("should_be_imported.html") # 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) print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 1) self.assertEqual(len(all_assets), 1)
self.assertEqual(count, 1)
content = None content = None
try: try:
...@@ -114,9 +115,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -114,9 +115,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
module_store.get_item(course_location) module_store.get_item(course_location)
# make sure we have NO assets in our contentstore # 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) print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 0) self.assertEqual(len(all_assets), 0)
self.assertEqual(count, 0)
def test_no_static_link_rewrites_on_import(self): def test_no_static_link_rewrites_on_import(self):
module_store = modulestore('direct') module_store = modulestore('direct')
......
...@@ -41,9 +41,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -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. deleting assets, and changing the "locked" state of an asset.
GET GET
html: return html page of all course assets (note though that a range of assets can be requested using start html: return html page which will show all course assets. Note that only the asset container
and max query parameters) is returned and that the actual assets are filled in with a client-side request.
json: not currently supported 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 POST
json: create (or update?) an asset. The only updating that can be done is changing the lock state. json: create (or update?) an asset. The only updating that can be done is changing the lock state.
PUT PUT
...@@ -55,9 +56,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -55,9 +56,10 @@ def assets_handler(request, tag=None, package_id=None, branch=None, version_guid
if not has_access(request.user, location): if not has_access(request.user, location):
raise PermissionDenied() 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': if request.method == 'GET':
raise NotImplementedError('coming soon') return _assets_json(request, location)
else: else:
return _update_asset(request, location, asset_id) return _update_asset(request, location, asset_id)
elif request.method == 'GET': # assume html elif request.method == 'GET': # assume html
...@@ -73,22 +75,32 @@ def _asset_index(request, location): ...@@ -73,22 +75,32 @@ def _asset_index(request, location):
Supports start (0-based index into the list of assets) and max query parameters. Supports start (0-based index into the list of assets) and max query parameters.
""" """
old_location = loc_mapper().translate_locator_to_location(location) old_location = loc_mapper().translate_locator_to_location(location)
course_module = modulestore().get_item(old_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) course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name)
if maxresults is not None: assets, total_count = contentstore().get_all_content_for_course(
maxresults = int(maxresults) course_reference, start=start, maxresults=requested_page_size, sort=[('uploadDate', DESCENDING)]
start = int(start) if start else 0 )
assets = contentstore().get_all_content_for_course( end = start + len(assets)
course_reference, start=start, maxresults=maxresults,
sort=[('uploadDate', DESCENDING)]
)
else:
assets = contentstore().get_all_content_for_course(
course_reference, sort=[('uploadDate', DESCENDING)]
)
asset_json = [] asset_json = []
for asset in assets: for asset in assets:
...@@ -101,10 +113,13 @@ def _asset_index(request, location): ...@@ -101,10 +113,13 @@ def _asset_index(request, location):
asset_locked = asset.get('locked', False) asset_locked = asset.get('locked', False)
asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked)) asset_json.append(_get_asset_json(asset['displayname'], asset['uploadDate'], asset_location, thumbnail_location, asset_locked))
return render_to_response('asset_index.html', { return JsonResponse({
'context_course': course_module, 'start': start,
'asset_list': json.dumps(asset_json), 'end': end,
'asset_callback_url': location.url_reverse('assets/', '') 'page': current_page,
'pageSize': requested_page_size,
'totalCount': total_count,
'assets': asset_json
}) })
......
...@@ -24,6 +24,7 @@ requirejs.config({ ...@@ -24,6 +24,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-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", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
...@@ -38,6 +39,7 @@ requirejs.config({ ...@@ -38,6 +39,7 @@ requirejs.config({
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async", "jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd", "draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady", "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", "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", "youtube": "//www.youtube.com/player_api?noext",
...@@ -115,6 +117,10 @@ requirejs.config({ ...@@ -115,6 +117,10 @@ requirejs.config({
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"youtube": { "youtube": {
exports: "YT" exports: "YT"
}, },
...@@ -139,6 +145,9 @@ requirejs.config({ ...@@ -139,6 +145,9 @@ requirejs.config({
] ]
MathJax.Hub.Configured() MathJax.Hub.Configured()
}, },
"URI": {
exports: "URI"
},
"xmodule": { "xmodule": {
exports: "XModule" exports: "XModule"
}, },
...@@ -197,10 +206,13 @@ define([ ...@@ -197,10 +206,13 @@ define([
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec", "js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec", "js/spec/transcripts/file_uploader_spec",
"js/spec/utils/module_spec",
"js/spec/models/explicit_url_spec" "js/spec/models/explicit_url_spec"
"js/spec/views/baseview_spec",
"js/spec/utils/handle_iframe_binding_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 # these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js # isolation issues with Squire.js
......
...@@ -23,6 +23,7 @@ requirejs.config({ ...@@ -23,6 +23,7 @@ requirejs.config({
"underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min",
"backbone": "xmodule_js/common_static/js/vendor/backbone-min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min",
"backbone.associations": "xmodule_js/common_static/js/vendor/backbone-associations-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", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
...@@ -34,6 +35,7 @@ requirejs.config({ ...@@ -34,6 +35,7 @@ requirejs.config({
"jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async", "jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async",
"draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd", "draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd",
"domReady": "xmodule_js/common_static/js/vendor/domReady", "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", "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", "youtube": "//www.youtube.com/player_api?noext",
...@@ -106,6 +108,10 @@ requirejs.config({ ...@@ -106,6 +108,10 @@ requirejs.config({
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"youtube": { "youtube": {
exports: "YT" exports: "YT"
}, },
...@@ -130,6 +136,9 @@ requirejs.config({ ...@@ -130,6 +136,9 @@ requirejs.config({
] ]
MathJax.Hub.Configured(); MathJax.Hub.Configured();
}, },
"URI": {
exports: "URI"
},
"xmodule": { "xmodule": {
exports: "XModule" exports: "XModule"
}, },
...@@ -166,4 +175,3 @@ jasmine.getFixtures().fixturesPath += 'coffee/fixtures' ...@@ -166,4 +175,3 @@ jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
define([ define([
"coffee/spec/views/assets_spec" "coffee/spec/views/assets_spec"
]) ])
...@@ -2,7 +2,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -2,7 +2,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
(jasmine, create_sinon, Squire) -> (jasmine, create_sinon, Squire) ->
feedbackTpl = readFixtures('system-feedback.underscore') feedbackTpl = readFixtures('system-feedback.underscore')
assetLibraryTpl = readFixtures('asset-library.underscore')
assetTpl = readFixtures('asset.underscore') assetTpl = readFixtures('asset.underscore')
pagingHeaderTpl = readFixtures('paging-header.underscore')
pagingFooterTpl = readFixtures('paging-footer.underscore')
describe "Asset view", -> describe "Asset view", ->
beforeEach -> beforeEach ->
...@@ -44,7 +47,7 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -44,7 +47,7 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
spyOn(@model, "save").andCallThrough() spyOn(@model, "save").andCallThrough()
@collection = new AssetCollection([@model]) @collection = new AssetCollection([@model])
@collection.url = "update-asset-url" @collection.url = "assets-url"
@view = new AssetView({model: @model}) @view = new AssetView({model: @model})
waitsFor (=> @view), "AssetView was not created", 1000 waitsFor (=> @view), "AssetView was not created", 1000
...@@ -131,7 +134,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -131,7 +134,10 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
describe "Assets view", -> describe "Assets view", ->
beforeEach -> 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.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy() window.course_location_analytics = jasmine.createSpy()
appendSetFixtures(sandbox({id: "asset_table_body"})) appendSetFixtures(sandbox({id: "asset_table_body"}))
...@@ -145,31 +151,43 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -145,31 +151,43 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
"Warning": @promptSpies.constructor "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 => runs =>
@injector.require ["js/models/asset", "js/collections/asset", "js/views/assets"], @injector.require ["js/models/asset", "js/collections/asset", "js/views/assets"],
(AssetModel, AssetCollection, AssetsView) => (AssetModel, AssetCollection, AssetsView) =>
@AssetModel = AssetModel @AssetModel = AssetModel
@collection = new AssetCollection [ @collection = new AssetCollection();
display_name: "test asset 1" @collection.url = "assets-url"
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"
@view = new AssetsView @view = new AssetsView
collection: @collection collection: @collection
el: $('#asset_table_body') el: $('#asset_table_body')
@view.render()
waitsFor (=> @view), "AssetView was not created", 1000 waitsFor (=> @view), "AssetsView was not created", 1000
$.ajax() $.ajax()
...@@ -181,33 +199,38 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -181,33 +199,38 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
@injector.remove() @injector.remove()
describe "Basic", -> 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", -> 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 1")
expect(@view.$el).toContainText("test asset 2") expect(@view.$el).toContainText("test asset 2")
it "should remove the deleted asset from the view", -> it "should remove the deleted asset from the view", ->
requests = create_sinon["requests"](this) requests = setup.call(this, @mockAssetsResponse)
# Delete the 2nd asset with success from server. # 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) @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
req.respond(200) for req in requests req.respond(200) for req in requests
expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2") expect(@view.$el).not.toContainText("test asset 2")
it "does not remove asset if deletion failed", -> it "does not remove asset if deletion failed", ->
requests = create_sinon["requests"](this) requests = setup.call(this, @mockAssetsResponse)
# Delete the 2nd asset, but mimic a failure from the server. # 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) @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
req.respond(404) for req in requests req.respond(404) for req in requests
expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2") expect(@view.$el).toContainText("test asset 2")
it "adds an asset if asset does not already exist", -> it "adds an asset if asset does not already exist", ->
@view.render() requests = setup.call(this, @mockAssetsResponse)
model = new @AssetModel model = new @AssetModel
display_name: "new asset" display_name: "new asset"
url: 'new_actual_asset_url' url: 'new_actual_asset_url'
...@@ -216,12 +239,29 @@ define ["jasmine", "js/spec/create_sinon", "squire"], ...@@ -216,12 +239,29 @@ define ["jasmine", "js/spec/create_sinon", "squire"],
thumbnail: null thumbnail: null
id: 'idx' id: 'idx'
@view.addAsset(model) @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(@view.$el).toContainText("new asset")
expect(@collection.models.indexOf(model)).toBe(0)
expect(@collection.models.length).toBe(3) expect(@collection.models.length).toBe(3)
it "does not add an asset if asset already exists", -> it "does not add an asset if asset already exists", ->
@view.render() setup.call(this, @mockAssetsResponse)
spyOn(@collection, "add").andCallThrough() spyOn(@collection, "add").andCallThrough()
model = @collection.models[1] model = @collection.models[1]
@view.addAsset(model) @view.addAsset(model)
......
define(["backbone", "js/models/asset"], function(Backbone, AssetModel){ define(["backbone.paginator", "js/models/asset"], function(BackbonePaginator, AssetModel) {
var AssetCollection = Backbone.Collection.extend({ var AssetCollection = BackbonePaginator.requestPager.extend({
model : AssetModel model : AssetModel,
}); paginator_core: {
return AssetCollection; 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) { ...@@ -33,7 +33,7 @@ define(["sinon"], function(sinon) {
var requests = []; var requests = [];
var xhr = sinon.useFakeXMLHttpRequest(); var xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) { xhr.onCreate = function(request) {
requests.push(request) requests.push(request);
}; };
that.after(function() { that.after(function() {
...@@ -43,8 +43,16 @@ define(["sinon"], function(sinon) { ...@@ -43,8 +43,16 @@ define(["sinon"], function(sinon) {
return requests; 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 { return {
"server": fakeServer, "server": fakeServer,
"requests": fakeRequests "requests": fakeRequests,
"respondWithJson": respondWithJson
}; };
}); });
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 // takes AssetCollection as model
initialize : function() { initialize : function() {
this.listenTo(this.collection, 'destroy', this.handleDestroy); PagingView.prototype.initialize.call(this);
this.render(); var collection = this.collection;
this.template = _.template($("#asset-library-tpl").text());
this.listenTo(collection, 'destroy', this.handleDestroy);
}, },
render: function() { 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; // Hide the contents until the collection has loaded the first time
this.collection.each( this.$('.asset-library').hide();
function(asset) { this.$('.no-asset-content').hide();
var view = new AssetView({model: asset});
self.$el.append(view.render().el);
});
return this; return this;
}, },
handleDestroy: function(model, collection, options) { renderPageItems: function() {
var index = options.index; var self = this,
this.$el.children().eq(index).remove(); 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', { analytics.track('Deleted Asset', {
'course': course_location_analytics, 'course': course_location_analytics,
'id': model.get('url') 'id': model.get('url')
...@@ -32,17 +52,12 @@ var AssetsView = BaseView.extend({ ...@@ -32,17 +52,12 @@ var AssetsView = BaseView.extend({
}, },
addAsset: function (model) { addAsset: function (model) {
// If asset is not already being shown, add it. this.setPage(0);
if (this.collection.findWhere({'url': model.get('url')}) === undefined) {
this.collection.add(model, {at: 0}); analytics.track('Uploaded a File', {
var view = new AssetView({model: model}); 'course': course_location_analytics,
this.$el.prepend(view.render().el); 'asset_url': model.get('url')
});
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': model.get('url')
});
}
} }
}); });
......
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();
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();
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: ...@@ -38,6 +38,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/underscore.string.min.js - 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-min.js
- xmodule_js/common_static/js/vendor/backbone-associations-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/timepicker/jquery.timepicker.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js - xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
- xmodule_js/common_static/js/vendor/jquery.ajaxQueue.js - xmodule_js/common_static/js/vendor/jquery.ajaxQueue.js
...@@ -55,6 +56,7 @@ lib_paths: ...@@ -55,6 +56,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/draggabilly.pkgd.js - xmodule_js/common_static/js/vendor/draggabilly.pkgd.js
- xmodule_js/common_static/js/vendor/date.js - xmodule_js/common_static/js/vendor/date.js
- xmodule_js/common_static/js/vendor/domReady.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/js/vendor/jquery.smooth-scroll.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/coffee/src/xblock - xmodule_js/common_static/coffee/src/xblock
......
...@@ -38,6 +38,7 @@ lib_paths: ...@@ -38,6 +38,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/underscore.string.min.js - 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-min.js
- xmodule_js/common_static/js/vendor/backbone-associations-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/timepicker/jquery.timepicker.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js - xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
- xmodule_js/common_static/js/vendor/jquery.form.js - xmodule_js/common_static/js/vendor/jquery.form.js
...@@ -49,6 +50,7 @@ lib_paths: ...@@ -49,6 +50,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js - xmodule_js/common_static/js/vendor/jasmine-imagediff.js
- xmodule_js/common_static/js/vendor/jasmine.async.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/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/src/xmodule.js - xmodule_js/src/xmodule.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/test/i18n.js
......
...@@ -27,9 +27,167 @@ ...@@ -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 { .asset-library {
@include clearfix; @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 { table {
width: 100%; width: 100%;
word-wrap: break-word; word-wrap: break-word;
...@@ -41,6 +199,11 @@ ...@@ -41,6 +199,11 @@
vertical-align: middle; vertical-align: middle;
text-align: left; text-align: left;
color: $gray; color: $gray;
.current-sort {
font-weight: 700;
border-bottom: 1px solid $gray-l3;
}
} }
td { td {
......
...@@ -9,21 +9,26 @@ ...@@ -9,21 +9,26 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%block name="header_extras"> <%block name="header_extras">
<script type="text/template" id="asset-tpl"> % for template_name in ["asset-library", "asset", "paging-header", "paging-footer"]:
<%static:include path="js/asset.underscore"/> <script type="text/template" id="${template_name}-tpl">
</script> <%static:include path="js/${template_name}.underscore" />
</script>
% endfor
</%block> </%block>
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript"> <script type="text/javascript">
require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/asset", require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/asset",
"js/views/assets", "js/views/feedback_prompt", "js/views/assets", "js/views/feedback_prompt",
"js/views/feedback_notification", "js/utils/modal", "jquery.fileupload"], "js/views/feedback_notification", "js/views/paging_header", "js/views/paging_footer",
function(domReady, $, gettext, AssetModel, AssetCollection, AssetsView, PromptView, NotificationView, ModalUtils) { "js/utils/modal", "jquery.fileupload"],
function(domReady, $, gettext, AssetModel, AssetCollection, AssetsView, PromptView, NotificationView,
var assets = new AssetCollection(${asset_list}); PagingHeader, PagingFooter, ModalUtils) {
var assets = new AssetCollection();
assets.url = "${asset_callback_url}"; 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) { var hideModal = function (e) {
if (e) { if (e) {
...@@ -142,30 +147,7 @@ require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/ass ...@@ -142,30 +147,7 @@ require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/ass
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<article class="asset-library content-primary" role="main"> <article id="asset-library" class="content-primary" role="main"></article>
<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>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <div class="bit">
......
...@@ -69,6 +69,7 @@ ...@@ -69,6 +69,7 @@
"underscore.string": "js/vendor/underscore.string.min", "underscore.string": "js/vendor/underscore.string.min",
"backbone": "js/vendor/backbone-min", "backbone": "js/vendor/backbone-min",
"backbone.associations": "js/vendor/backbone-associations-min", "backbone.associations": "js/vendor/backbone-associations-min",
"backbone.paginator": "js/vendor/backbone.paginator.min",
"tinymce": "js/vendor/tiny_mce/tiny_mce", "tinymce": "js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "js/vendor/tiny_mce/jquery.tinymce", "jquery.tinymce": "js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "/xmodule/xmodule", "xmodule": "/xmodule/xmodule",
...@@ -76,6 +77,7 @@ ...@@ -76,6 +77,7 @@
"utility": "js/src/utility", "utility": "js/src/utility",
"accessibility": "js/src/accessibility_tools", "accessibility": "js/src/accessibility_tools",
"draggabilly": "js/vendor/draggabilly.pkgd", "draggabilly": "js/vendor/draggabilly.pkgd",
"URI": "/js/vendor/URI.min",
// externally hosted files // externally hosted files
"tender": "//edxedge.tenderapp.com/tender_widget", "tender": "//edxedge.tenderapp.com/tender_widget",
...@@ -163,6 +165,10 @@ ...@@ -163,6 +165,10 @@
deps: ["backbone"], deps: ["backbone"],
exports: "Backbone.Associations" exports: "Backbone.Associations"
}, },
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"youtube": { "youtube": {
exports: "YT" exports: "YT"
}, },
...@@ -193,6 +199,9 @@ ...@@ -193,6 +199,9 @@
MathJax.Hub.Configured(); MathJax.Hub.Configured();
} }
}, },
"URI": {
exports: "URI"
},
"xblock/core": { "xblock/core": {
exports: "XBlock", exports: "XBlock",
deps: ["jquery", "jquery.immediateDescendents"] deps: ["jquery", "jquery.immediateDescendents"]
......
<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>
<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>
<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): ...@@ -177,7 +177,10 @@ class ContentStore(object):
def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None): 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): ...@@ -128,7 +128,7 @@ class MongoContentStore(ContentStore):
directory as the other policy files. directory as the other policy files.
""" """
policy = {} policy = {}
assets = self.get_all_content_for_course(course_location) assets,__ = self.get_all_content_for_course(course_location)
for asset in assets: for asset in assets:
asset_location = Location(asset['_id']) asset_location = Location(asset['_id'])
...@@ -141,7 +141,7 @@ class MongoContentStore(ContentStore): ...@@ -141,7 +141,7 @@ class MongoContentStore(ContentStore):
json.dump(policy, f) json.dump(policy, f)
def get_all_content_thumbnails_for_course(self, location): 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): def get_all_content_for_course(self, location, start=0, maxresults=-1, sort=None):
return self._get_all_content_for_course( return self._get_all_content_for_course(
...@@ -178,7 +178,8 @@ class MongoContentStore(ContentStore): ...@@ -178,7 +178,8 @@ class MongoContentStore(ContentStore):
) )
else: else:
items = self.fs_files.find(location_to_query(course_filter), sort=sort) 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): def set_attr(self, location, attr, value=True):
""" """
......
...@@ -19,7 +19,7 @@ def empty_asset_trashcan(course_locs): ...@@ -19,7 +19,7 @@ def empty_asset_trashcan(course_locs):
store.delete(id) store.delete(id)
# then delete all of the assets # 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: for asset in assets:
asset_loc = Location(asset["_id"]) asset_loc = Location(asset["_id"])
id = StaticContent.get_id_from_location(asset_loc) id = StaticContent.get_id_from_location(asset_loc)
......
...@@ -199,7 +199,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele ...@@ -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 # 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: for asset in assets:
asset_loc = Location(asset["_id"]) asset_loc = Location(asset["_id"])
content = contentstore.find(asset_loc) content = contentstore.find(asset_loc)
...@@ -260,7 +260,7 @@ def delete_course(modulestore, contentstore, source_location, commit=False): ...@@ -260,7 +260,7 @@ def delete_course(modulestore, contentstore, source_location, commit=False):
_delete_assets(contentstore, thumbs, commit) _delete_assets(contentstore, thumbs, commit)
# then delete all of the assets # 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) _delete_assets(contentstore, assets, commit)
# then delete all course modules # then delete all course modules
......
...@@ -223,7 +223,7 @@ class TestMongoModuleStore(object): ...@@ -223,7 +223,7 @@ class TestMongoModuleStore(object):
Test getting, setting, and defaulting the locked attr and arbitrary attrs. Test getting, setting, and defaulting the locked attr and arbitrary attrs.
""" """
location = Location('i4x', 'edX', 'toy', 'course', '2012_Fall') 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 assert len(course_content) > 0
# a bit overkill, could just do for content[0] # a bit overkill, could just do for content[0]
for content in course_content: for content in course_content:
......
window.gettext = window.ngettext = function(s){return s;}; 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())});
}
}
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
-e git+https://github.com/edx/XBlock.git@fa88607#egg=XBlock -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/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/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/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/event-tracking.git@f0211d702d#egg=event-tracking
-e git+https://github.com/edx/bok-choy.git@bc6f1adbe439618162079f1004b2b3db3b6f8916#egg=bok_choy -e git+https://github.com/edx/bok-choy.git@bc6f1adbe439618162079f1004b2b3db3b6f8916#egg=bok_choy
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment