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
This commit is contained in:
@@ -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
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user