Add teams-for-topic list view to Teams page.
TNL-1898 Also adds a generic paginated list view and Teams list view. The common PaginatedView takes a collection and a view class for its items and creates a header and footer with correct pagination. The topics list view is refactored to use this generic view. Authors: - Peter Fogg <pfogg@edx.org> - Daniel Friedman <dfriedman58@gmail.com>
This commit is contained in:
43
common/static/common/js/components/views/paginated_view.js
Normal file
43
common/static/common/js/components/views/paginated_view.js
Normal file
@@ -0,0 +1,43 @@
|
||||
;(function(define) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'common/js/components/views/paging_header',
|
||||
'common/js/components/views/paging_footer',
|
||||
'common/js/components/views/list',
|
||||
'text!common/templates/components/paginated-view.underscore'
|
||||
], function (Backbone, _, PagingHeader, PagingFooter, ListView, paginatedViewTemplate) {
|
||||
var PaginatedView = Backbone.View.extend({
|
||||
initialize: function () {
|
||||
var ItemListView = ListView.extend({
|
||||
tagName: 'div',
|
||||
className: this.type + '-container',
|
||||
itemViewClass: this.itemViewClass
|
||||
});
|
||||
this.listView = new ItemListView({collection: this.options.collection});
|
||||
this.headerView = this.headerView = new PagingHeader({collection: this.options.collection});
|
||||
this.footerView = new PagingFooter({
|
||||
collection: this.options.collection, hideWhenOnePage: true
|
||||
});
|
||||
this.collection.on('page_changed', function () {
|
||||
this.$('.sr-is-focusable.sr-' + this.type + '-view').focus();
|
||||
}, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(_.template(paginatedViewTemplate, {type: this.type}));
|
||||
this.assign(this.listView, '.' + this.type + '-list');
|
||||
this.assign(this.headerView, '.' + this.type + '-paging-header');
|
||||
this.assign(this.footerView, '.' + this.type + '-paging-footer');
|
||||
return this;
|
||||
},
|
||||
|
||||
assign: function (view, selector) {
|
||||
view.setElement(this.$(selector)).render();
|
||||
}
|
||||
});
|
||||
|
||||
return PaginatedView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -23,7 +23,8 @@
|
||||
var onFirstPage = !this.collection.hasPreviousPage(),
|
||||
onLastPage = !this.collection.hasNextPage();
|
||||
if (this.hideWhenOnePage) {
|
||||
if (this.collection.totalPages <= 1) {
|
||||
if (_.isUndefined(this.collection.totalPages)
|
||||
|| this.collection.totalPages <= 1) {
|
||||
this.$el.addClass('hidden');
|
||||
} else if (this.$el.hasClass('hidden')) {
|
||||
this.$el.removeClass('hidden');
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
render: function () {
|
||||
var message,
|
||||
start = this.collection.start,
|
||||
start = _.isUndefined(this.collection.start) ? 0 : this.collection.start,
|
||||
end = start + this.collection.length,
|
||||
num_items = this.collection.totalCount,
|
||||
num_items = _.isUndefined(this.collection.totalCount) ? 0 : this.collection.totalCount,
|
||||
context = {first_index: Math.min(start + 1, end), last_index: end, num_items: num_items};
|
||||
if (end <= 1) {
|
||||
message = interpolate(gettext('Showing %(first_index)s out of %(num_items)s total'), context, true);
|
||||
|
||||
184
common/static/common/js/spec/components/paginated_view_spec.js
Normal file
184
common/static/common/js/spec/components/paginated_view_spec.js
Normal file
@@ -0,0 +1,184 @@
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'common/js/components/views/paginated_view',
|
||||
'common/js/components/collections/paging_collection'
|
||||
], function (Backbone, _, AjaxHelpers, PaginatedView, PagingCollection) {
|
||||
'use strict';
|
||||
describe('PaginatedView', function () {
|
||||
var TestItemView = Backbone.View.extend({
|
||||
className: 'test-item',
|
||||
tagName: 'div',
|
||||
initialize: function () {
|
||||
this.render();
|
||||
},
|
||||
render: function () {
|
||||
this.$el.text(this.model.get('text'));
|
||||
return this;
|
||||
}
|
||||
}),
|
||||
TestPaginatedView = PaginatedView.extend({type: 'test', itemViewClass: TestItemView}),
|
||||
testCollection,
|
||||
testView,
|
||||
initialItems,
|
||||
nextPageButtonCss = '.next-page-link',
|
||||
previousPageButtonCss = '.previous-page-link',
|
||||
generateItems = function (numItems) {
|
||||
return _.map(_.range(numItems), function (i) {
|
||||
return {
|
||||
text: 'item ' + i
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
setFixtures('<div class="test-container"></div>');
|
||||
initialItems = generateItems(5);
|
||||
testCollection = new PagingCollection({
|
||||
count: 6,
|
||||
num_pages: 2,
|
||||
current_page: 1,
|
||||
start: 0,
|
||||
results: initialItems
|
||||
}, {parse: true});
|
||||
testView = new TestPaginatedView({el: '.test-container', collection: testCollection}).render();
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that the view's header reflects the page we're currently viewing.
|
||||
* @param matchString the header we expect to see
|
||||
*/
|
||||
function expectHeader(matchString) {
|
||||
expect(testView.$('.test-paging-header').text()).toMatch(matchString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the list view renders the expected items
|
||||
* @param expectedItems an array of topic objects we expect to see
|
||||
*/
|
||||
function expectItems(expectedItems) {
|
||||
var $items = testView.$('.test-item');
|
||||
_.each(expectedItems, function (item, index) {
|
||||
var currentItem = $items.eq(index);
|
||||
expect(currentItem.text()).toMatch(item.text);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the footer reflects the current pagination
|
||||
* @param options a parameters hash containing:
|
||||
* - currentPage: the one-indexed page we expect to be viewing
|
||||
* - totalPages: the total number of pages to page through
|
||||
* - isHidden: whether the footer is expected to be visible
|
||||
*/
|
||||
function expectFooter(options) {
|
||||
var footerEl = testView.$('.test-paging-footer');
|
||||
expect(footerEl.text())
|
||||
.toMatch(new RegExp(options.currentPage + '\\s+out of\\s+\/\\s+' + testCollection.totalPages));
|
||||
expect(footerEl.hasClass('hidden')).toBe(options.isHidden);
|
||||
}
|
||||
|
||||
it('can render the first of many pages', function () {
|
||||
expectHeader('Showing 1-5 out of 6 total');
|
||||
expectItems(initialItems);
|
||||
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
|
||||
});
|
||||
|
||||
it('can render the only page', function () {
|
||||
initialItems = generateItems(1);
|
||||
testCollection.set(
|
||||
{
|
||||
"count": 1,
|
||||
"num_pages": 1,
|
||||
"current_page": 1,
|
||||
"start": 0,
|
||||
"results": initialItems
|
||||
},
|
||||
{parse: true}
|
||||
);
|
||||
expectHeader('Showing 1 out of 1 total');
|
||||
expectItems(initialItems);
|
||||
expectFooter({currentPage: 1, totalPages: 1, isHidden: true});
|
||||
});
|
||||
|
||||
it('can change to the next page', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
newItems = generateItems(1);
|
||||
expectHeader('Showing 1-5 out of 6 total');
|
||||
expectItems(initialItems);
|
||||
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
|
||||
expect(requests.length).toBe(0);
|
||||
testView.$(nextPageButtonCss).click();
|
||||
expect(requests.length).toBe(1);
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
"count": 6,
|
||||
"num_pages": 2,
|
||||
"current_page": 2,
|
||||
"start": 5,
|
||||
"results": newItems
|
||||
});
|
||||
expectHeader('Showing 6-6 out of 6 total');
|
||||
expectItems(newItems);
|
||||
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
|
||||
});
|
||||
|
||||
it('can change to the previous page', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
previousPageItems;
|
||||
initialItems = generateItems(1);
|
||||
testCollection.set(
|
||||
{
|
||||
"count": 6,
|
||||
"num_pages": 2,
|
||||
"current_page": 2,
|
||||
"start": 5,
|
||||
"results": initialItems
|
||||
},
|
||||
{parse: true}
|
||||
);
|
||||
expectHeader('Showing 6-6 out of 6 total');
|
||||
expectItems(initialItems);
|
||||
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
|
||||
testView.$(previousPageButtonCss).click();
|
||||
previousPageItems = generateItems(5);
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
"count": 6,
|
||||
"num_pages": 2,
|
||||
"current_page": 1,
|
||||
"start": 0,
|
||||
"results": previousPageItems
|
||||
});
|
||||
expectHeader('Showing 1-5 out of 6 total');
|
||||
expectItems(previousPageItems);
|
||||
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
|
||||
});
|
||||
|
||||
it('sets focus for screen readers', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
spyOn($.fn, 'focus');
|
||||
testView.$(nextPageButtonCss).click();
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
"count": 6,
|
||||
"num_pages": 2,
|
||||
"current_page": 2,
|
||||
"start": 5,
|
||||
"results": generateItems(1)
|
||||
});
|
||||
expect(testView.$('.sr-is-focusable').focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not change on server error', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
expectInitialState = function () {
|
||||
expectHeader('Showing 1-5 out of 6 total');
|
||||
expectItems(initialItems);
|
||||
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
|
||||
};
|
||||
expectInitialState();
|
||||
testView.$(nextPageButtonCss).click();
|
||||
requests[0].respond(500);
|
||||
expectInitialState();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="sr-is-focusable sr-<%= type %>-view" tabindex="-1"></div>
|
||||
<div class="<%= type %>-paging-header"></div>
|
||||
<div class="<%= type %>-list"></div>
|
||||
<div class="<%= type %>-paging-footer"></div>
|
||||
Reference in New Issue
Block a user