diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee index f43b5c2725..560b676a18 100644 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ b/cms/static/coffee/spec/views/assets_spec.coffee @@ -236,6 +236,20 @@ define ["jasmine", "js/spec/create_sinon", "squire"], create_sinon.respondWithJson(requests, @mockAssetsResponse) return requests + it "should show a status indicator while loading", -> + appendSetFixtures('
') + expect($('.ui-loading').is(':visible')).toBe(true) + setup.call(this) + expect($('.ui-loading').is(':visible')).toBe(false) + + it "should hide the status indicator if an error occurs while loading", -> + requests = create_sinon.requests(this) + appendSetFixtures('
') + expect($('.ui-loading').is(':visible')).toBe(true) + @view.setPage(0) + create_sinon.respondWithError(requests) + expect($('.ui-loading').is(':visible')).toBe(false) + it "should render both assets", -> requests = setup.call(this) expect(@view.$el).toContainText("test asset 1") diff --git a/cms/static/js/spec/create_sinon.js b/cms/static/js/spec/create_sinon.js index 45a2d67143..e7cb03893c 100644 --- a/cms/static/js/spec/create_sinon.js +++ b/cms/static/js/spec/create_sinon.js @@ -1,4 +1,6 @@ define(["sinon"], function(sinon) { + var fakeServer, fakeRequests, respondWithJson, respondWithError; + /* These utility methods are used by Jasmine tests to create a mock server or * get reference to mock requests. In either case, the cleanup (restore) is done with * an after function. @@ -15,7 +17,7 @@ define(["sinon"], function(sinon) { * Get a reference to the mocked server, and respond * to all requests with the specified statusCode. */ - var fakeServer = function (statusCode, that) { + fakeServer = function (statusCode, that) { var server = sinon.fakeServer.create(); that.after(function() { server.restore(); @@ -29,9 +31,9 @@ define(["sinon"], function(sinon) { * return a reference to the Array. This allows tests * to respond for individual requests. */ - var fakeRequests = function (that) { - var requests = []; - var xhr = sinon.useFakeXMLHttpRequest(); + fakeRequests = function (that) { + var requests = [], + xhr = sinon.useFakeXMLHttpRequest(); xhr.onCreate = function(request) { requests.push(request); }; @@ -43,16 +45,24 @@ define(["sinon"], function(sinon) { return requests; }; - var respondWithJson = function(requests, jsonResponse, requestIndex) { + respondWithJson = function(requests, jsonResponse, requestIndex) { requestIndex = requestIndex || requests.length - 1; requests[requestIndex].respond(200, { "Content-Type": "application/json" }, JSON.stringify(jsonResponse)); }; + respondWithError = function(requests, requestIndex) { + requestIndex = requestIndex || requests.length - 1; + requests[requestIndex].respond(500, + { "Content-Type": "application/json" }, + JSON.stringify({ })); + }; + return { "server": fakeServer, "requests": fakeRequests, - "respondWithJson": respondWithJson + "respondWithJson": respondWithJson, + "respondWithError": respondWithError }; }); diff --git a/cms/static/js/views/assets.js b/cms/static/js/views/assets.js index 8291013c00..5c6b73e020 100644 --- a/cms/static/js/views/assets.js +++ b/cms/static/js/views/assets.js @@ -1,80 +1,98 @@ -define(["js/views/paging", "js/views/asset", "js/views/paging_header", "js/views/paging_footer"], - function(PagingView, AssetView, PagingHeader, PagingFooter) { +define(["jquery", "underscore", "gettext", "js/views/paging", "js/views/asset", "js/views/paging_header", "js/views/paging_footer"], + function($, _, gettext, PagingView, AssetView, PagingHeader, PagingFooter) { -var AssetsView = PagingView.extend({ - // takes AssetCollection as model + var AssetsView = PagingView.extend({ + // takes AssetCollection as model - events : { - "click .column-sort-link": "onToggleColumn" - }, + 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'); - }, + 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'); + this.showLoadingIndicator(); + }, - render: function() { - 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(); + render: function() { + // Wait until the content is loaded the first time to render + return this; + }, - // Hide the contents until the collection has loaded the first time - this.$('.asset-library').hide(); - this.$('.no-asset-content').hide(); + getTableBody: function() { + var tableBody = this.tableBody; + if (!tableBody) { + this.hideLoadingIndicator(); - return this; - }, + // Create the table + this.$el.html(this.template()); + tableBody = this.$('#asset-table-body'); + this.tableBody = tableBody; + 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(); - renderPageItems: function() { - var self = this, - assets = this.collection, - hasAssets = assets.length > 0; - self.tableBody.empty(); - if (hasAssets) { - assets.each( - function(asset) { - var view = new AssetView({model: asset}); - self.tableBody.append(view.render().el); + // Hide the contents until the collection has loaded the first time + this.$('.asset-library').hide(); + this.$('.no-asset-content').hide(); + } + return tableBody; + }, + + renderPageItems: function() { + var self = this, + assets = this.collection, + hasAssets = assets.length > 0, + tableBody = this.getTableBody(); + tableBody.empty(); + if (hasAssets) { + assets.each( + function(asset) { + var view = new AssetView({model: asset}); + tableBody.append(view.render().el); + } + ); + } + self.$('.asset-library').toggle(hasAssets); + self.$('.no-asset-content').toggle(!hasAssets); + return this; + }, + + onError: function() { + this.hideLoadingIndicator(); + }, + + handleDestroy: function(model) { + this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items + analytics.track('Deleted Asset', { + 'course': course_location_analytics, + 'id': model.get('url') }); - } - self.$('.asset-library').toggle(hasAssets); - self.$('.no-asset-content').toggle(!hasAssets); - return this; - }, + }, - handleDestroy: function(model, collection, options) { - this.collection.fetch({reset: true}); // reload the collection to get a fresh page full of items - analytics.track('Deleted Asset', { - 'course': course_location_analytics, - 'id': model.get('url') + 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); + } }); - }, - 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); - } -}); - -return AssetsView; -}); // end define(); + return AssetsView; + }); // end define(); diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js index e07bf6dddf..61566d038a 100644 --- a/cms/static/js/views/baseview.js +++ b/cms/static/js/views/baseview.js @@ -46,6 +46,14 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"], event.preventDefault(); target.closest('.expand-collapse').toggleClass('expand').toggleClass('collapse'); target.closest('.is-collapsible, .window').toggleClass('collapsed'); + }, + + showLoadingIndicator: function() { + $('.ui-loading').show(); + }, + + hideLoadingIndicator: function() { + $('.ui-loading').hide(); } }); diff --git a/cms/static/js/views/paging.js b/cms/static/js/views/paging.js index fe693f1992..966542ed04 100644 --- a/cms/static/js/views/paging.js +++ b/cms/static/js/views/paging.js @@ -1,118 +1,121 @@ -define(["backbone", "js/views/feedback_alert", "gettext"], function(Backbone, AlertView, gettext) { +define(["underscore", "js/views/baseview", "js/views/feedback_alert", "gettext"], + function(_, BaseView, AlertView, gettext) { - var PagingView = Backbone.View.extend({ - // takes a Backbone Paginator as a model + var PagingView = BaseView.extend({ + // takes a Backbone Paginator as a model - sortableColumns: {}, + sortableColumns: {}, - initialize: function() { - Backbone.View.prototype.initialize.call(this); - var collection = this.collection; - collection.bind('add', _.bind(this.onPageRefresh, this)); - collection.bind('remove', _.bind(this.onPageRefresh, this)); - collection.bind('reset', _.bind(this.onPageRefresh, this)); - }, + initialize: function() { + BaseView.prototype.initialize.call(this); + var collection = this.collection; + 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'); - }, + onPageRefresh: function() { + var sortColumn = this.sortColumn; + this.renderPageItems(); + this.$('.column-sort-link').removeClass('current-sort'); + this.$('#' + sortColumn).addClass('current-sort'); + }, - 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; + 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) { + collection.currentPage = oldPage; + self.onError(); + } + }); + }, + + onError: function() { + // Do nothing by default + }, + + nextPage: function() { + var collection = this.collection, + currentPage = collection.currentPage, + lastPage = collection.totalPages - 1; + if (currentPage < lastPage) { + this.setPage(currentPage + 1); } - }); - }, + }, - 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); + } + }, + + /** + * 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, + ascending = collection.sortDirection === 'asc'; + return ascending ? gettext("ascending") : 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); } - }, + }); - previousPage: function() { - var collection = this.collection, - currentPage = collection.currentPage; - 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); - } - }); - - return PagingView; -}); // end define(); + return PagingView; + }); // end define(); diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 96bbd31632..3cb6b34a07 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -27,7 +27,7 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset", var assets = new AssetCollection(); assets.url = "${asset_callback_url}"; - var assetsView = new AssetsView({collection: assets, el: $('#asset-library')}); + var assetsView = new AssetsView({collection: assets, el: $('.assets-wrapper')}); assetsView.render(); assetsView.setPage(0); @@ -148,7 +148,12 @@ require(["domReady", "jquery", "js/models/asset", "js/collections/asset",
-
+
+
+
+

${_("Loading…")}

+
+