diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f4d119c00..3ef4f2b30f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ 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: Add sorting by column to the Files & Uploads page. +See mongo_indexes.md for new indices that should be added. + Studio: Newly-created courses default to being published on Jan 1, 2030 Studio: Added pagination to the Files & Uploads page. diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py index 4c4e5b0d9a..61fb5161cf 100644 --- a/cms/djangoapps/contentstore/tests/test_assets.py +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -85,6 +85,10 @@ class PaginationTestCase(AssetsTestCase): self.assert_correct_asset_response(self.url, 0, 3, 3) self.assert_correct_asset_response(self.url + "?page_size=2", 0, 2, 3) self.assert_correct_asset_response(self.url + "?page_size=2&page=1", 2, 1, 3) + self.assert_correct_sort_response(self.url, 'date_added', 'asc') + self.assert_correct_sort_response(self.url, 'date_added', 'desc') + self.assert_correct_sort_response(self.url, 'display_name', 'asc') + self.assert_correct_sort_response(self.url, 'display_name', 'desc') # Verify querying outside the range of valid pages self.assert_correct_asset_response(self.url + "?page_size=2&page=-1", 0, 2, 3) @@ -99,6 +103,19 @@ class PaginationTestCase(AssetsTestCase): self.assertEquals(len(assets), expected_length) self.assertEquals(json_response['totalCount'], expected_total) + def assert_correct_sort_response(self, url, sort, direction): + resp = self.client.get(url + '?sort=' + sort + '&direction=' + direction, HTTP_ACCEPT='application/json') + json_response = json.loads(resp.content) + assets = json_response['assets'] + name1 = assets[0][sort] + name2 = assets[1][sort] + name3 = assets[2][sort] + if direction == 'asc': + self.assertLessEqual(name1, name2) + self.assertLessEqual(name2, name3) + else: + self.assertGreaterEqual(name1, name2) + self.assertGreaterEqual(name2, name3) class UploadTestCase(AssetsTestCase): """ diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index e7e4d508fb..39bb0e3147 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -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,7 +713,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): empty_asset_trashcan([course_location]) # make sure trashcan is empty - all_assets,count = 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) @@ -924,7 +924,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertEqual(len(items), 0) # assert that all content in the asset library is also deleted - assets,count = 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) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 4b81466009..513e30f0eb 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -84,7 +84,7 @@ 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,count = 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) @@ -115,8 +115,7 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): module_store.get_item(course_location) # make sure we have NO assets in our contentstore - all_assets,count = content_store.get_all_content_for_course(course_location) - print "len(all_assets)=%d" % len(all_assets) + all_assets, count = content_store.get_all_content_for_course(course_location) self.assertEqual(len(all_assets), 0) self.assertEqual(count, 0) diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 663cf22f64..255717c73e 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -1,5 +1,7 @@ import logging from functools import partial +import math +import json from django.http import HttpResponseBadRequest from django.contrib.auth.decorators import login_required @@ -24,10 +26,8 @@ from xmodule.modulestore.locator import BlockUsageLocator from util.json_request import JsonResponse from django.http import HttpResponseNotFound -import json from django.utils.translation import ugettext as _ -from pymongo import DESCENDING -import math +from pymongo import ASCENDING, DESCENDING __all__ = ['assets_handler'] @@ -41,10 +41,13 @@ 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 which will show all course assets. Note that only the asset container + html: return an 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). + json: returns a page of assets. The following parameters are supported: + page: the desired page of results (defaults to 0) + page_size: the number of items per page (defaults to 50) + sort: the asset field to sort by (defaults to "date_added") + direction: the sort direction (defaults to "descending") POST json: create (or update?) an asset. The only updating that can be done is changing the lock state. PUT @@ -91,7 +94,17 @@ def _assets_json(request, location): """ requested_page = int(request.REQUEST.get('page', 0)) requested_page_size = int(request.REQUEST.get('page_size', 50)) - sort = [('uploadDate', DESCENDING)] + requested_sort = request.REQUEST.get('sort', 'date_added') + sort_direction = DESCENDING + if request.REQUEST.get('direction', '').lower() == 'asc': + sort_direction = ASCENDING + + # Convert the field name to the Mongo name + if requested_sort == 'date_added': + requested_sort = 'uploadDate' + elif requested_sort == 'display_name': + requested_sort = 'displayname' + sort = [(requested_sort, sort_direction)] current_page = max(requested_page, 0) start = current_page * requested_page_size @@ -122,7 +135,8 @@ def _assets_json(request, location): 'page': current_page, 'pageSize': requested_page_size, 'totalCount': total_count, - 'assets': asset_json + 'assets': asset_json, + 'sort': requested_sort, }) diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee index e00e30c266..f43b5c2725 100644 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ b/cms/static/coffee/spec/views/assets_spec.coffee @@ -198,21 +198,51 @@ define ["jasmine", "js/spec/create_sinon", "squire"], @injector.clean() @injector.remove() + addMockAsset = (requests) -> + model = new @AssetModel + display_name: "new asset" + url: 'new_actual_asset_url' + portable_url: 'portable_url' + date_added: 'date' + 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 + }) + + describe "Basic", -> # Separate setup method to work-around mis-parenting of beforeEach methods - setup = (response) -> + setup = -> requests = create_sinon.requests(this) @view.setPage(0) - create_sinon.respondWithJson(requests, response) + create_sinon.respondWithJson(requests, @mockAssetsResponse) return requests it "should render both assets", -> - requests = setup.call(this, @mockAssetsResponse) + requests = setup.call(this) expect(@view.$el).toContainText("test asset 1") expect(@view.$el).toContainText("test asset 2") it "should remove the deleted asset from the view", -> - requests = setup.call(this, @mockAssetsResponse) + requests = setup.call(this) # Delete the 2nd asset with success from server. @view.$(".remove-asset-button")[1].click() @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies) @@ -221,7 +251,7 @@ define ["jasmine", "js/spec/create_sinon", "squire"], expect(@view.$el).not.toContainText("test asset 2") it "does not remove asset if deletion failed", -> - requests = setup.call(this, @mockAssetsResponse) + requests = setup.call(this) # Delete the 2nd asset, but mimic a failure from the server. @view.$(".remove-asset-button")[1].click() @promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies) @@ -230,39 +260,60 @@ define ["jasmine", "js/spec/create_sinon", "squire"], expect(@view.$el).toContainText("test asset 2") it "adds an asset if asset does not already exist", -> - requests = setup.call(this, @mockAssetsResponse) - model = new @AssetModel - display_name: "new asset" - url: 'new_actual_asset_url' - portable_url: 'portable_url' - date_added: 'date' - 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 - }) + requests = setup.call(this) + addMockAsset.call(this, requests) expect(@view.$el).toContainText("new asset") expect(@collection.models.length).toBe(3) it "does not add an asset if asset already exists", -> - setup.call(this, @mockAssetsResponse) + setup.call(this) spyOn(@collection, "add").andCallThrough() model = @collection.models[1] @view.addAsset(model) expect(@collection.add).not.toHaveBeenCalled() + + describe "Sorting", -> + # Separate setup method to work-around mis-parenting of beforeEach methods + setup = -> + requests = create_sinon.requests(this) + @view.setPage(0) + create_sinon.respondWithJson(requests, @mockAssetsResponse) + return requests + + it "should have the correct default sort order", -> + requests = setup.call(this) + expect(@view.sortDisplayName()).toBe("Date Added") + expect(@view.collection.sortDirection).toBe("desc") + + it "should toggle the sort order when clicking on the currently sorted column", -> + requests = setup.call(this) + expect(@view.sortDisplayName()).toBe("Date Added") + expect(@view.collection.sortDirection).toBe("desc") + @view.$("#js-asset-date-col").click() + create_sinon.respondWithJson(requests, @mockAssetsResponse) + expect(@view.sortDisplayName()).toBe("Date Added") + expect(@view.collection.sortDirection).toBe("asc") + @view.$("#js-asset-date-col").click() + create_sinon.respondWithJson(requests, @mockAssetsResponse) + expect(@view.sortDisplayName()).toBe("Date Added") + expect(@view.collection.sortDirection).toBe("desc") + + it "should switch the sort order when clicking on a different column", -> + requests = setup.call(this) + @view.$("#js-asset-name-col").click() + create_sinon.respondWithJson(requests, @mockAssetsResponse) + expect(@view.sortDisplayName()).toBe("Name") + expect(@view.collection.sortDirection).toBe("asc") + @view.$("#js-asset-name-col").click() + create_sinon.respondWithJson(requests, @mockAssetsResponse) + expect(@view.sortDisplayName()).toBe("Name") + expect(@view.collection.sortDirection).toBe("desc") + + it "should switch sort to most recent date added when a new asset is added", -> + requests = setup.call(this) + @view.$("#js-asset-name-col").click() + create_sinon.respondWithJson(requests, @mockAssetsResponse) + addMockAsset.call(this, requests) + create_sinon.respondWithJson(requests, @mockAssetsResponse) + expect(@view.sortDisplayName()).toBe("Date Added") + expect(@view.collection.sortDirection).toBe("desc") diff --git a/cms/static/js/collections/asset.js b/cms/static/js/collections/asset.js index 125ff24103..3d91a2c191 100644 --- a/cms/static/js/collections/asset.js +++ b/cms/static/js/collections/asset.js @@ -15,6 +15,8 @@ define(["backbone.paginator", "js/models/asset"], function(BackbonePaginator, As server_api: { 'page': function() { return this.currentPage; }, 'page_size': function() { return this.perPage; }, + 'sort': function() { return this.sortField; }, + 'direction': function() { return this.sortDirection; }, 'format': 'json' }, diff --git a/cms/static/js/spec/views/paging_spec.js b/cms/static/js/spec/views/paging_spec.js index 862000405d..f0560876bf 100644 --- a/cms/static/js/spec/views/paging_spec.js +++ b/cms/static/js/spec/views/paging_spec.js @@ -54,7 +54,12 @@ define([ "jquery", "js/spec/create_sinon", "URI", }; var MockPagingView = PagingView.extend({ - renderPageItems: function() {} + renderPageItems: function() {}, + initialize : function() { + this.registerSortableColumn('name-col', 'Name', 'name', 'asc'); + this.registerSortableColumn('date-col', 'Date', 'date', 'desc'); + this.setInitialSortColumn('date-col'); + } }); describe("Paging", function() { @@ -70,75 +75,125 @@ define([ "jquery", "js/spec/create_sinon", "URI", 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); + describe("setPage", 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('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); + describe("nextPage", function () { + 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('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); + describe("previousPage", function () { + + 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); + }); }); - 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); + describe("toggleSortOrder", function () { + + it('can toggle direction of the current sort', function () { + var requests = create_sinon.requests(this); + expect(pagingView.collection.sortDirection).toBe('desc'); + pagingView.toggleSortOrder('date-col'); + respondWithMockAssets(requests); + expect(pagingView.collection.sortDirection).toBe('asc'); + pagingView.toggleSortOrder('date-col'); + respondWithMockAssets(requests); + expect(pagingView.collection.sortDirection).toBe('desc'); + }); + + it('sets the correct default sort direction for a column', function () { + var requests = create_sinon.requests(this); + pagingView.toggleSortOrder('name-col'); + respondWithMockAssets(requests); + expect(pagingView.sortDisplayName()).toBe('Name'); + expect(pagingView.collection.sortDirection).toBe('asc'); + pagingView.toggleSortOrder('date-col'); + respondWithMockAssets(requests); + expect(pagingView.sortDisplayName()).toBe('Date'); + expect(pagingView.collection.sortDirection).toBe('desc'); + }); }); - 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); - }); + describe("sortableColumnInfo", function () { - 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('returns the registered info for a column', function () { + pagingView.registerSortableColumn('test-col', 'Test Column', 'testField', 'asc'); + var sortInfo = pagingView.sortableColumnInfo('test-col'); + expect(sortInfo.displayName).toBe('Test Column'); + expect(sortInfo.fieldName).toBe('testField'); + expect(sortInfo.defaultSortDirection).toBe('asc'); + }); - 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); + it('throws an exception for an unregistered column', function () { + expect(function() { + pagingView.sortableColumnInfo('no-such-column'); + }).toThrow(); + }); }); }); @@ -291,6 +346,22 @@ define([ "jquery", "js/spec/create_sinon", "URI", expect(pagingHeader.$('.count-total')).toHaveText('0 total'); }); }); + + describe("Sort order label", function () { + it('should show correct initial sort order', function () { + var requests = create_sinon.requests(this); + pagingView.setPage(0); + respondWithMockAssets(requests); + expect(pagingHeader.$('.sort-order')).toHaveText('Date'); + }); + + it('should show updated sort order', function () { + var requests = create_sinon.requests(this); + pagingView.toggleSortOrder('name-col'); + respondWithMockAssets(requests); + expect(pagingHeader.$('.sort-order')).toHaveText('Name'); + }); + }); }); describe("PagingFooter", function () { @@ -396,7 +467,7 @@ define([ "jquery", "js/spec/create_sinon", "URI", }); }); - describe("Page total label", function () { + describe("Current page label", function () { it('should show 1 on the first page', function () { var requests = create_sinon.requests(this); pagingView.setPage(0); diff --git a/cms/static/js/views/assets.js b/cms/static/js/views/assets.js index dc82f944c6..8291013c00 100644 --- a/cms/static/js/views/assets.js +++ b/cms/static/js/views/assets.js @@ -4,11 +4,18 @@ define(["js/views/paging", "js/views/asset", "js/views/paging_header", "js/views var AssetsView = PagingView.extend({ // takes AssetCollection as model + events : { + "click .column-sort-link": "onToggleColumn" + }, + initialize : function() { PagingView.prototype.initialize.call(this); var collection = this.collection; this.template = _.template($("#asset-library-tpl").text()); this.listenTo(collection, 'destroy', this.handleDestroy); + this.registerSortableColumn('js-asset-name-col', gettext('Name'), 'display_name', 'asc'); + this.registerSortableColumn('js-asset-date-col', gettext('Date Added'), 'date_added', 'desc'); + this.setInitialSortColumn('js-asset-date-col'); }, render: function() { @@ -52,12 +59,20 @@ var AssetsView = PagingView.extend({ }, addAsset: function (model) { + // Switch the sort column back to the default (most recent date added) and show the first page + // so that the new asset is shown at the top of the page. + this.setInitialSortColumn('js-asset-date-col'); this.setPage(0); analytics.track('Uploaded a File', { 'course': course_location_analytics, 'asset_url': model.get('url') }); + }, + + onToggleColumn: function(event) { + var columnName = event.target.id; + this.toggleSortOrder(columnName); } }); diff --git a/cms/static/js/views/paging.js b/cms/static/js/views/paging.js index f379f58e9a..fe693f1992 100644 --- a/cms/static/js/views/paging.js +++ b/cms/static/js/views/paging.js @@ -3,12 +3,21 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al var PagingView = Backbone.View.extend({ // takes a Backbone Paginator as a model + sortableColumns: {}, + 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)); + collection.bind('add', _.bind(this.onPageRefresh, this)); + collection.bind('remove', _.bind(this.onPageRefresh, this)); + collection.bind('reset', _.bind(this.onPageRefresh, this)); + }, + + onPageRefresh: function() { + var sortColumn = this.sortColumn; + this.renderPageItems(); + this.$('.column-sort-link').removeClass('current-sort'); + this.$('#' + sortColumn).addClass('current-sort'); }, setPage: function(page) { @@ -41,6 +50,67 @@ define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, Al if (currentPage > 0) { this.setPage(currentPage - 1); } + }, + + /** + * Registers information about a column that can be sorted. + * @param columnName The element name of the column. + * @param displayName The display name for the column in the current locale. + * @param fieldName The database field name that is represented by this column. + * @param defaultSortDirection The default sort direction for the column + */ + registerSortableColumn: function(columnName, displayName, fieldName, defaultSortDirection) { + this.sortableColumns[columnName] = { + displayName: displayName, + fieldName: fieldName, + defaultSortDirection: defaultSortDirection + }; + }, + + sortableColumnInfo: function(sortColumn) { + var sortInfo = this.sortableColumns[sortColumn]; + if (!sortInfo) { + throw "Unregistered sort column '" + sortColumn + '"'; + } + return sortInfo; + }, + + sortDisplayName: function() { + var sortColumn = this.sortColumn, + sortInfo = this.sortableColumnInfo(sortColumn); + return sortInfo.displayName; + }, + + sortDirectionName: function() { + var collection = this.collection; + if (collection.sortDirection === 'asc') { + return gettext("ascending"); + } else { + return gettext("descending"); + } + }, + + setInitialSortColumn: function(sortColumn) { + var collection = this.collection, + sortInfo = this.sortableColumns[sortColumn]; + collection.sortField = sortInfo.fieldName; + collection.sortDirection = sortInfo.defaultSortDirection; + this.sortColumn = sortColumn; + }, + + toggleSortOrder: function(sortColumn) { + var collection = this.collection, + sortInfo = this.sortableColumnInfo(sortColumn), + sortField = sortInfo.fieldName, + defaultSortDirection = sortInfo.defaultSortDirection; + if (collection.sortField === sortField) { + collection.sortDirection = collection.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + collection.sortField = sortField; + collection.sortDirection = defaultSortDirection; + } + this.sortColumn = sortColumn; + this.setPage(0); } }); diff --git a/cms/static/js/views/paging_header.js b/cms/static/js/views/paging_header.js index 76e30cc6b4..426e6fcb7d 100644 --- a/cms/static/js/views/paging_header.js +++ b/cms/static/js/views/paging_header.js @@ -35,15 +35,18 @@ define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) { collection = view.collection, start = collection.start, count = collection.size(), + sortName = view.sortDisplayName(), + sortDirectionName = view.sortDirectionName(), 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'); + 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 %(sort_direction)s'); return '
' + interpolate(fmts, {
start: Math.min(start + 1, end),
end: end,
total: total,
- sort_order: gettext('Date Added'),
+ sort_order: sortName,
+ sort_direction: sortDirectionName,
current_span: '',
total_span: '',
order_span: '',
diff --git a/cms/static/sass/views/_assets.scss b/cms/static/sass/views/_assets.scss
index 0dee6727d4..ec3a6954bb 100644
--- a/cms/static/sass/views/_assets.scss
+++ b/cms/static/sass/views/_assets.scss
@@ -87,7 +87,7 @@
.nav-link {
@include transition(all $tmg-f2 ease-in-out 0s);
display: block;
- padding: ($baseline/4) ($baseline*.75);
+ padding: ($baseline/4) ($baseline*0.75);
&.previous {
margin-right: ($baseline/2);
@@ -126,7 +126,7 @@
.total-pages {
@extend %t-copy-base;
width: ($baseline*2.5);
- margin: 0 ($baseline*.75);
+ margin: 0 ($baseline*0.75);
padding: ($baseline/4);
text-align: center;
color: $gray;
@@ -195,14 +195,26 @@
th {
@extend %t-copy-sub2;
background-color: $gray-l5;
- padding: 0 $baseline ($baseline*.75) $baseline;
+ padding: 0 ($baseline/2) ($baseline*0.75) ($baseline/2);
vertical-align: middle;
text-align: left;
color: $gray;
+ .column-sort-link {
+ cursor: pointer;
+ color: $blue;
+ }
+
+
.current-sort {
- font-weight: 700;
border-bottom: 1px solid $gray-l3;
+ font-weight: 700;
+ }
+
+ // CASE: embed column
+ &.embed-col {
+ padding-left: ($baseline*0.75);
+ padding-right: ($baseline*0.75);
}
}
@@ -285,7 +297,7 @@
.embed-col {
@include transition(all $tmg-f2 ease-in-out 0s);
- padding-left: ($baseline*.75);
+ padding-left: ($baseline*0.75);
color: $gray-l2;
.embeddable-xml-input {
@@ -455,7 +467,7 @@
@include transition(color $tmg-f2 ease-in-out 0s);
position: absolute;
top: 0;
- right: ($baseline*.75);
+ right: ($baseline*0.75);
border: none;
background: none;
padding: 0;
diff --git a/cms/templates/js/asset-library.underscore b/cms/templates/js/asset-library.underscore
index 35f0c0fecc..ad0a9f2f8e 100644
--- a/cms/templates/js/asset-library.underscore
+++ b/cms/templates/js/asset-library.underscore
@@ -14,8 +14,8 @@
diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py
index 0d42ec8a73..b20ba7a598 100644
--- a/common/lib/xmodule/xmodule/contentstore/mongo.py
+++ b/common/lib/xmodule/xmodule/contentstore/mongo.py
@@ -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'])
diff --git a/common/lib/xmodule/xmodule/contentstore/utils.py b/common/lib/xmodule/xmodule/contentstore/utils.py
index dca7194c4f..f354dbf420 100644
--- a/common/lib/xmodule/xmodule/contentstore/utils.py
+++ b/common/lib/xmodule/xmodule/contentstore/utils.py
@@ -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)
diff --git a/common/lib/xmodule/xmodule/modulestore/store_utilities.py b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
index c4c4127ebc..a01eb5e5f1 100644
--- a/common/lib/xmodule/xmodule/modulestore/store_utilities.py
+++ b/common/lib/xmodule/xmodule/modulestore/store_utilities.py
@@ -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
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index e8f1c5e34a..e1c08f8efd 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -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:
diff --git a/mongo_indexes.md b/mongo_indexes.md
index f42aed9a58..1a58e11e5a 100644
--- a/mongo_indexes.md
+++ b/mongo_indexes.md
@@ -19,3 +19,10 @@ location_map:
ensureIndex({'_id.org': 1, '_id.course': 1})
ensureIndex({'course_id': 1})
```
+
+fs.files:
+=========
+
+```
+ensureIndex({'displayname': 1})
+```
<%= gettext("Preview") %>
- <%= gettext("Name") %>
- <%= gettext("Date Added") %>
+ <%= gettext("Name") %>
+ <%= gettext("Date Added") %>
<%= gettext("URL") %>
<%= gettext("Actions") %>