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:
Daniel Friedman
2015-07-08 17:41:08 -04:00
parent 6d6bfa7f35
commit 21b39ca467
38 changed files with 1449 additions and 314 deletions

View 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);

View File

@@ -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');

View File

@@ -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);

View 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();
});
});
});

View File

@@ -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>