upgrade Pagingcollection to edx-ui-toolkit's PagingCollection

This commit is contained in:
Ehtesham
2016-05-02 21:22:07 +05:00
parent f6d9c9a324
commit b31ba9d041
62 changed files with 809 additions and 1088 deletions

View File

@@ -1,273 +0,0 @@
/**
* A generic paging collection for use with a ListView and PagingFooter.
*
* By default this collection is designed to work with Django Rest Framework APIs, but can be configured to work with
* others. There is support for ascending or descending sort on a particular field, as well as filtering on a field.
* While the backend API may use either zero or one indexed page numbers, this collection uniformly exposes a one
* indexed interface to make consumption easier for views.
*
* Subclasses may want to override the following properties:
* - url (string): The base url for the API endpoint.
* - isZeroIndexed (boolean): If true, API calls will use page numbers starting at zero. Defaults to false.
* - perPage (number): Count of elements to fetch for each page.
* - server_api (object): Query parameters for the API call. Subclasses may add entries as necessary. By default,
* a 'sort_order' field is included to specify the field to sort on. This field may be removed for subclasses
* that do not support sort ordering, or support it in a non-standard way. By default filterField and
* sortDirection do not affect the API calls. It is up to subclasses to add this information to the appropriate
* query string parameters in server_api.
*/
;(function (define) {
'use strict';
define(['backbone.paginator'], function (BackbonePaginator) {
var PagingCollection = BackbonePaginator.requestPager.extend({
initialize: function (models, options) {
options = options || {};
if (options.url) {
this.url = options.url;
}
var self = this;
// These must be initialized in the constructor because otherwise all PagingCollections would point
// to the same object references for sortableFields and filterableFields.
this.sortableFields = {};
this.filterableFields = {};
this.paginator_core = {
type: 'GET',
dataType: 'json',
url: function () { return this.url; }
};
this.paginator_ui = {
firstPage: function () { return self.isZeroIndexed ? 0 : 1; },
// Specifies the initial page during collection initialization
currentPage: self.isZeroIndexed ? 0 : 1,
perPage: function () { return self.perPage; }
};
this.currentPage = this.paginator_ui.currentPage;
this.server_api = {
page: function () { return self.currentPage; },
page_size: function () { return self.perPage; },
text_search: function () { return self.searchString ? self.searchString : ''; },
sort_order: function () { return self.sortField; }
};
},
isZeroIndexed: false,
perPage: 10,
isStale: false,
sortField: '',
sortDirection: 'descending',
sortableFields: {},
filterField: '',
filterableFields: {},
searchString: null,
parse: function (response) {
this.totalCount = response.count;
this.currentPage = response.current_page;
this.totalPages = response.num_pages;
this.start = response.start;
// Note: sort_order is not returned when performing a search
if (response.sort_order) {
this.sortField = response.sort_order;
}
return response.results;
},
/**
* Returns the current page number as if numbering starts on page one, regardless of the indexing of the
* underlying server API.
*/
getPage: function () {
// TODO: this.currentPage is currently returning a function sometimes when it is called.
// It is possible it always did this, but we either need to investigate more, or just wait until
// we replace this code with the pattern library.
return this.currentPage + (this.isZeroIndexed ? 1 : 0);
},
/**
* Sets the current page of the collection. Page is assumed to be one indexed, regardless of the indexing
* of the underlying server API. If there is an error fetching the page, the Backbone 'error' event is
* triggered and the page does not change. A 'page_changed' event is triggered on a successful page change.
* @param page one-indexed page to change to
*/
setPage: function (page) {
var oldPage = this.currentPage,
self = this,
deferred = $.Deferred();
this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then(
function () {
self.isStale = false;
self.trigger('page_changed');
deferred.resolve();
},
function () {
self.currentPage = oldPage;
deferred.fail();
}
);
return deferred.promise();
},
/**
* Refreshes the collection if it has been marked as stale.
* @returns {promise} Returns a promise representing the refresh.
*/
refresh: function() {
var deferred = $.Deferred();
if (this.isStale) {
this.setPage(1)
.done(function() {
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise();
},
/**
* Returns true if the collection has a next page, false otherwise.
*/
hasNextPage: function () {
return this.getPage() < this.totalPages;
},
/**
* Returns true if the collection has a previous page, false otherwise.
*/
hasPreviousPage: function () {
return this.getPage() > 1;
},
/**
* Moves the collection to the next page if it exists.
*/
nextPage: function () {
if (this.hasNextPage()) {
this.setPage(this.getPage() + 1);
}
},
/**
* Moves the collection to the previous page if it exists.
*/
previousPage: function () {
if (this.hasPreviousPage()) {
this.setPage(this.getPage() - 1);
}
},
/**
* Adds the given field to the list of fields that can be sorted on.
* @param fieldName name of the field for the server API
* @param displayName name of the field to display to the user
*/
registerSortableField: function (fieldName, displayName) {
this.addField(this.sortableFields, fieldName, displayName);
},
/**
* Adds the given field to the list of fields that can be filtered on.
* @param fieldName name of the field for the server API
* @param displayName name of the field to display to the user
*/
registerFilterableField: function (fieldName, displayName) {
this.addField(this.filterableFields, fieldName, displayName);
},
/**
* For internal use only. Adds the given field to the given collection of fields.
* @param fields object of existing fields
* @param fieldName name of the field for the server API
* @param displayName name of the field to display to the user
*/
addField: function (fields, fieldName, displayName) {
fields[fieldName] = {
displayName: displayName
};
},
/**
* Returns the display name of the field that the collection is currently sorted on.
*/
sortDisplayName: function () {
return this.sortableFields[this.sortField].displayName;
},
/**
* Returns the display name of the field that the collection is currently filtered on.
*/
filterDisplayName: function () {
return this.filterableFields[this.filterField].displayName;
},
/**
* Sets the field to sort on. Sends a request to the server to fetch the first page of the collection with
* the new sort order. If successful, the collection resets to page one with the new data.
* @param fieldName name of the field to sort on
* @param toggleDirection if true, the sort direction is toggled if the given field was already set
*/
setSortField: function (fieldName, toggleDirection) {
if (toggleDirection) {
if (this.sortField === fieldName) {
this.sortDirection = PagingCollection.SortDirection.flip(this.sortDirection);
} else {
this.sortDirection = PagingCollection.SortDirection.DESCENDING;
}
}
this.sortField = fieldName;
this.isStale = true;
},
/**
* Sets the direction of the sort. Sends a request to the server to fetch the first page of the collection
* with the new sort order. If successful, the collection resets to page one with the new data.
* @param direction either ASCENDING or DESCENDING from PagingCollection.SortDirection.
*/
setSortDirection: function (direction) {
this.sortDirection = direction;
this.isStale = true;
},
/**
* Sets the field to filter on. Sends a request to the server to fetch the first page of the collection
* with the new filter options. If successful, the collection resets to page one with the new data.
* @param fieldName name of the field to filter on
*/
setFilterField: function (fieldName) {
this.filterField = fieldName;
this.isStale = true;
},
/**
* Sets the string to use for a text search. If no string is specified then
* the search is cleared.
* @param searchString A string to search on, or null if no search is to be applied.
*/
setSearchString: function(searchString) {
if (searchString !== this.searchString) {
this.searchString = searchString;
this.isStale = true;
}
}
}, {
SortDirection: {
ASCENDING: 'ascending',
DESCENDING: 'descending',
flip: function (direction) {
return direction === this.ASCENDING ? this.DESCENDING : this.ASCENDING;
}
}
});
return PagingCollection;
});
}).call(this, define || RequireJS.define);

View File

@@ -19,11 +19,12 @@
define([
'backbone',
'underscore',
'edx-ui-toolkit/js/utils/html-utils',
'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) {
], function (Backbone, _, HtmlUtils, PagingHeader, PagingFooter, ListView, paginatedViewTemplate) {
var PaginatedView = Backbone.View.extend({
initialize: function () {
var ItemListView = this.listViewClass.extend({
@@ -51,13 +52,14 @@
createFooterView: function() {
return new PagingFooter({
collection: this.collection, hideWhenOnePage: true,
collection: this.collection,
hideWhenOnePage: true,
paginationLabel: this.paginationLabel
});
},
render: function () {
this.$el.html(_.template(this.viewTemplate)({type: this.type}));
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.viewTemplate)({type: this.type}));
this.assign(this.listView, '.' + this.type + '-list');
if (this.headerView) {
this.assign(this.headerView, '.' + this.type + '-paging-header');

View File

@@ -1,7 +1,13 @@
;(function (define) {
'use strict';
define(["underscore", "gettext", "backbone", "text!common/templates/components/paging-footer.underscore"],
function(_, gettext, Backbone, paging_footer_template) {
define([
"underscore",
"gettext",
"backbone",
"edx-ui-toolkit/js/utils/html-utils",
"text!common/templates/components/paging-footer.underscore"
],
function(_, gettext, Backbone, HtmlUtils, pagingFooterTemplate) {
var PagingFooter = Backbone.View.extend({
events : {
@@ -17,25 +23,27 @@
this.collection.bind('add', _.bind(this.render, this));
this.collection.bind('remove', _.bind(this.render, this));
this.collection.bind('reset', _.bind(this.render, this));
this.render();
},
render: function() {
var onFirstPage = !this.collection.hasPreviousPage(),
onLastPage = !this.collection.hasNextPage();
if (this.hideWhenOnePage) {
if (_.isUndefined(this.collection.totalPages)
|| this.collection.totalPages <= 1) {
if (this.collection.getTotalPages() <= 1) {
this.$el.addClass('hidden');
} else if (this.$el.hasClass('hidden')) {
this.$el.removeClass('hidden');
}
}
this.$el.html(_.template(paging_footer_template)({
current_page: this.collection.getPage(),
total_pages: this.collection.totalPages,
paginationLabel: this.paginationLabel
}));
HtmlUtils.setHtml(
this.$el,
HtmlUtils.template(pagingFooterTemplate)({
current_page: this.collection.getPageNumber(),
total_pages: this.collection.getTotalPages(),
paginationLabel: this.paginationLabel
})
);
this.$(".previous-page-link").toggleClass("is-disabled", onFirstPage).attr('aria-disabled', onFirstPage);
this.$(".next-page-link").toggleClass("is-disabled", onLastPage).attr('aria-disabled', onLastPage);
return this;
@@ -43,11 +51,11 @@
changePage: function() {
var collection = this.collection,
currentPage = collection.getPage(),
currentPage = collection.getPageNumber(),
pageInput = this.$("#page-number-input"),
pageNumber = parseInt(pageInput.val(), 10),
validInput = true;
if (!pageNumber || pageNumber > collection.totalPages || pageNumber < 1) {
if (!pageNumber || pageNumber > collection.getTotalPages() || pageNumber < 1) {
validInput = false;
}
// If we still have a page number by this point,

View File

@@ -4,8 +4,10 @@
'backbone',
'underscore',
'gettext',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils',
'text!common/templates/components/paging-header.underscore'
], function (Backbone, _, gettext, headerTemplate) {
], function (Backbone, _, gettext, HtmlUtils, StringUtils, headerTemplate) {
var PagingHeader = Backbone.View.extend({
initialize: function (options) {
this.srInfo = options.srInfo;
@@ -21,25 +23,37 @@
render: function () {
var message,
start = _.isUndefined(this.collection.start) ? 0 : this.collection.start,
end = start + this.collection.length,
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};
start = (this.collection.getPageNumber() - 1) * this.collection.getPageSize(),
end = start + this.collection.size(),
numItems = this.collection.getTotalRecords(),
context = {
firstIndex: Math.min(start + 1, end),
lastIndex: end,
numItems: numItems
};
if (end <= 1) {
message = interpolate(gettext('Showing %(first_index)s out of %(num_items)s total'), context, true);
message = StringUtils.interpolate(
gettext('Showing {firstIndex} out of {numItems} total'),
context
);
} else {
message = interpolate(
gettext('Showing %(first_index)s-%(last_index)s out of %(num_items)s total'),
context, true
message = StringUtils.interpolate(
gettext('Showing {firstIndex}-{lastIndex} out of {numItems} total'),
context
);
}
this.$el.html(_.template(headerTemplate)({
message: message,
srInfo: this.srInfo,
sortableFields: this.collection.sortableFields,
sortOrder: this.sortOrder,
showSortControls: this.showSortControls
}));
HtmlUtils.setHtml(
this.$el,
HtmlUtils.template(headerTemplate)({
message: message,
srInfo: this.srInfo,
sortableFields: this.collection.sortableFields,
sortOrder: this.sortOrder,
showSortControls: this.showSortControls
})
);
return this;
},

View File

@@ -6,8 +6,14 @@
;(function (define) {
'use strict';
define(['backbone', 'jquery', 'underscore', 'text!common/templates/components/search-field.underscore'],
function (Backbone, $, _, searchFieldTemplate) {
define([
'backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/html-utils',
'text!common/templates/components/search-field.underscore'
],
function (Backbone, $, _, HtmlUtils, searchFieldTemplate) {
return Backbone.View.extend({
events: {
@@ -16,7 +22,7 @@
'keyup .search-field': 'refreshState',
'click .action-clear': 'clearSearch',
'mouseover .action-clear': 'setMouseOverState',
'mouseout .action-clear': 'setMouseOutState',
'mouseout .action-clear': 'setMouseOutState'
},
initialize: function(options) {
@@ -29,6 +35,7 @@
var searchField = this.$('.search-field'),
clearButton = this.$('.action-clear'),
searchString = $.trim(searchField.val());
if (searchString) {
clearButton.removeClass('is-hidden');
} else {
@@ -37,11 +44,14 @@
},
render: function() {
this.$el.html(_.template(searchFieldTemplate)({
type: this.type,
searchString: this.collection.searchString,
searchLabel: this.label
}));
HtmlUtils.setHtml(
this.$el,
HtmlUtils.template(searchFieldTemplate)({
type: this.type,
searchString: this.collection.searchString,
searchLabel: this.label
})
);
this.refreshState();
return this;
},

View File

@@ -2,10 +2,10 @@ define([
'jquery',
'backbone',
'underscore',
'edx-ui-toolkit/js/pagination/paging-collection',
'common/js/spec_helpers/ajax_helpers',
'common/js/components/views/paginated_view',
'common/js/components/collections/paging_collection'
], function ($, Backbone, _, AjaxHelpers, PaginatedView, PagingCollection) {
'common/js/components/views/paginated_view'
], function ($, Backbone, _, PagingCollection, AjaxHelpers, PaginatedView) {
'use strict';
describe('PaginatedView', function () {
var TestItemView = Backbone.View.extend({
@@ -36,11 +36,18 @@ define([
beforeEach(function () {
setFixtures('<div class="test-container"></div>');
initialItems = generateItems(5);
testCollection = new PagingCollection({
var TestPagingCollection = PagingCollection.extend({
state: {
pageSize: 5
}
});
testCollection = new TestPagingCollection();
testCollection.url = '/dummy/url';
testCollection.set({
count: 6,
num_pages: 2,
current_page: 1,
start: 0,
page: 1,
results: initialItems
}, {parse: true});
testView = new TestPaginatedView({el: '.test-container', collection: testCollection}).render();
@@ -76,7 +83,7 @@ define([
function expectFooter(options) {
var footerEl = testView.$('.test-paging-footer');
expect(footerEl.text())
.toMatch(new RegExp(options.currentPage + '\\s+out of\\s+\/\\s+' + testCollection.totalPages));
.toMatch(new RegExp(options.currentPage + '\\s+out of\\s+\/\\s+' + options.totalPages));
expect(footerEl.hasClass('hidden')).toBe(options.isHidden);
}
@@ -90,11 +97,11 @@ define([
initialItems = generateItems(1);
testCollection.set(
{
"count": 1,
"num_pages": 1,
"current_page": 1,
"start": 0,
"results": initialItems
count: 1,
num_pages: 1,
page: 1,
start: 0,
results: initialItems
},
{parse: true}
);
@@ -112,11 +119,10 @@ define([
AjaxHelpers.expectNoRequests(requests);
testView.$(nextPageButtonCss).click();
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": newItems
count: 6,
num_pages: 2,
page: 2,
results: newItems
});
expectHeader('Showing 6-6 out of 6 total');
expectItems(newItems);
@@ -129,11 +135,10 @@ define([
initialItems = generateItems(1);
testCollection.set(
{
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": initialItems
count: 6,
num_pages: 2,
page: 2,
results: initialItems
},
{parse: true}
);
@@ -143,11 +148,10 @@ define([
testView.$(previousPageButtonCss).click();
previousPageItems = generateItems(5);
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": previousPageItems
count: 6,
num_pages: 2,
page: 1,
results: previousPageItems
});
expectHeader('Showing 1-5 out of 6 total');
expectItems(previousPageItems);
@@ -159,11 +163,10 @@ define([
spyOn($.fn, 'focus');
testView.$(nextPageButtonCss).click();
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": generateItems(1)
count: 6,
num_pages: 2,
page: 2,
results: generateItems(1)
});
expect(testView.$('.sr-is-focusable').focus).toHaveBeenCalled();
});

View File

@@ -1,256 +0,0 @@
define(['jquery',
'backbone',
'underscore',
'URI',
'common/js/components/collections/paging_collection',
'common/js/spec_helpers/ajax_helpers',
'common/js/spec_helpers/spec_helpers'
],
function ($, Backbone, _, URI, PagingCollection, AjaxHelpers, SpecHelpers) {
'use strict';
describe('PagingCollection', function () {
var collection;
var server = {
isZeroIndexed: false,
count: 43,
respond: function (requests) {
var request = AjaxHelpers.currentRequest(requests),
params = (new URI(request.url)).query(true),
page = parseInt(params['page'], 10),
page_size = parseInt(params['page_size'], 10),
page_count = Math.ceil(this.count / page_size);
// Make zeroPage consistently start at zero for ease of calculation
var zeroPage = page - (this.isZeroIndexed ? 0 : 1);
if (zeroPage < 0 || zeroPage > page_count) {
AjaxHelpers.respondWithError(requests, 404);
} else {
AjaxHelpers.respondWithJson(requests, {
'count': this.count,
'current_page': page,
'num_pages': page_count,
'start': zeroPage * page_size,
'results': []
});
}
}
};
var assertQueryParams = function (requests, params) {
var request = AjaxHelpers.currentRequest(requests),
urlParams = (new URI(request.url)).query(true);
_.each(params, function (value, key) {
expect(urlParams[key]).toBe(value);
});
};
beforeEach(function () {
collection = new PagingCollection();
collection.perPage = 10;
server.isZeroIndexed = false;
server.count = 43;
});
it('can register sortable fields', function () {
collection.registerSortableField('test_field', 'Test Field');
expect('test_field' in collection.sortableFields).toBe(true);
expect(collection.sortableFields['test_field'].displayName).toBe('Test Field');
});
it('can register filterable fields', function () {
collection.registerFilterableField('test_field', 'Test Field');
expect('test_field' in collection.filterableFields).toBe(true);
expect(collection.filterableFields['test_field'].displayName).toBe('Test Field');
});
it('sets the sort field based on the server response', function () {
var sort_order = 'my_sort_order';
collection = new PagingCollection({sort_order: sort_order}, {parse: true});
expect(collection.sortField).toBe(sort_order);
});
it('can set the sort field', function () {
var requests = AjaxHelpers.requests(this);
collection.registerSortableField('test_field', 'Test Field');
collection.setSortField('test_field', false);
collection.refresh();
assertQueryParams(requests, {'sort_order': 'test_field'});
expect(collection.sortField).toBe('test_field');
expect(collection.sortDisplayName()).toBe('Test Field');
});
it('can set the filter field', function () {
collection.registerFilterableField('test_field', 'Test Field');
collection.setFilterField('test_field');
collection.refresh();
// The default implementation does not send any query params for filtering
expect(collection.filterField).toBe('test_field');
expect(collection.filterDisplayName()).toBe('Test Field');
});
it('can set the sort direction', function () {
collection.setSortDirection(PagingCollection.SortDirection.ASCENDING);
// The default implementation does not send any query params for sort direction
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.ASCENDING);
collection.setSortDirection(PagingCollection.SortDirection.DESCENDING);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING);
});
it('can toggle the sort direction when setting the sort field', function () {
collection.registerSortableField('test_field', 'Test Field');
collection.registerSortableField('test_field_2', 'Test Field 2');
collection.setSortField('test_field', true);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING);
collection.setSortField('test_field', true);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.ASCENDING);
collection.setSortField('test_field', true);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING);
collection.setSortField('test_field_2', true);
expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING);
});
SpecHelpers.withData({
'queries with page, page_size, and sort_order parameters when zero indexed': [true, 2],
'queries with page, page_size, and sort_order parameters when one indexed': [false, 3],
}, function (isZeroIndexed, page) {
var requests = AjaxHelpers.requests(this);
collection.isZeroIndexed = isZeroIndexed;
collection.perPage = 5;
collection.sortField = 'test_field';
collection.setPage(3);
assertQueryParams(requests, {'page': page.toString(), 'page_size': '5', 'sort_order': 'test_field'});
});
SpecHelpers.withConfiguration({
'using a zero indexed collection': [true],
'using a one indexed collection': [false]
}, function (isZeroIndexed) {
collection.isZeroIndexed = isZeroIndexed;
server.isZeroIndexed = isZeroIndexed;
}, function () {
describe('setPage', function() {
it('triggers a reset event when the page changes successfully', function () {
var requests = AjaxHelpers.requests(this),
resetTriggered = false;
collection.on('reset', function () { resetTriggered = true; });
collection.setPage(3);
server.respond(requests);
expect(resetTriggered).toBe(true);
});
it('triggers an error event when the requested page is out of range', function () {
var requests = AjaxHelpers.requests(this),
errorTriggered = false;
collection.on('error', function () { errorTriggered = true; });
collection.setPage(17);
server.respond(requests);
expect(errorTriggered).toBe(true);
});
it('triggers an error event if the server responds with a 500', function () {
var requests = AjaxHelpers.requests(this),
errorTriggered = false;
collection.on('error', function () { errorTriggered = true; });
collection.setPage(2);
expect(collection.getPage()).toBe(2);
server.respond(requests);
collection.setPage(3);
AjaxHelpers.respondWithError(requests, 500);
expect(errorTriggered).toBe(true);
expect(collection.getPage()).toBe(2);
});
});
describe('getPage', function () {
it('returns the correct page', function () {
var requests = AjaxHelpers.requests(this);
collection.setPage(1);
server.respond(requests);
expect(collection.getPage()).toBe(1);
collection.setPage(3);
server.respond(requests);
expect(collection.getPage()).toBe(3);
});
});
describe('hasNextPage', function () {
SpecHelpers.withData(
{
'returns false for a single page': [1, 3, false],
'returns true on the first page': [1, 43, true],
'returns true on the penultimate page': [4, 43, true],
'returns false on the last page': [5, 43, false]
},
function (page, count, result) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond(requests);
expect(collection.hasNextPage()).toBe(result);
}
);
});
describe('hasPreviousPage', function () {
SpecHelpers.withData(
{
'returns false for a single page': [1, 3, false],
'returns true on the last page': [5, 43, true],
'returns true on the second page': [2, 43, true],
'returns false on the first page': [1, 43, false]
},
function (page, count, result) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond(requests);
expect(collection.hasPreviousPage()).toBe(result);
}
);
});
describe('nextPage', function () {
SpecHelpers.withData(
{
'advances to the next page': [2, 43, 3],
'silently fails on the last page': [5, 43, 5]
},
function (page, count, newPage) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond(requests);
expect(collection.getPage()).toBe(page);
collection.nextPage();
if (requests.length > 1) {
server.respond(requests);
}
expect(collection.getPage()).toBe(newPage);
}
);
});
describe('previousPage', function () {
SpecHelpers.withData(
{
'moves to the previous page': [2, 43, 1],
'silently fails on the first page': [1, 43, 1]
},
function (page, count, newPage) {
var requests = AjaxHelpers.requests(this);
server.count = count;
collection.setPage(page);
server.respond(requests);
expect(collection.getPage()).toBe(page);
collection.previousPage();
if (requests.length > 1) {
server.respond(requests);
}
expect(collection.getPage()).toBe(newPage);
}
);
});
});
});
}
);

View File

@@ -2,10 +2,10 @@ define([
'jquery',
'URI',
'underscore',
'edx-ui-toolkit/js/pagination/paging-collection',
'common/js/spec_helpers/ajax_helpers',
'common/js/components/views/paging_footer',
'common/js/components/collections/paging_collection'
], function ($, URI, _, AjaxHelpers, PagingFooter, PagingCollection) {
'common/js/components/views/paging_footer'
], function ($, URI, _, PagingCollection, AjaxHelpers, PagingFooter) {
'use strict';
describe("PagingFooter", function () {
var pagingFooter,
@@ -15,10 +15,10 @@ define([
}
return {
count: null,
current_page: currentPage,
page: currentPage,
num_pages: numPages,
start: null,
results: _.map(_.range(collectionLength), function() { return {}; }) // need to have non-empty collection to render
// need to have non-empty collection to render
results: _.map(_.range(collectionLength), function() { return {}; })
};
},
nextPageCss = '.next-page-link',
@@ -29,9 +29,12 @@ define([
beforeEach(function () {
setFixtures('<div class="paging-footer"></div>');
var collection = new PagingCollection(mockPage(1, 2), {parse: true});
collection.url = '/test/url/';
pagingFooter = new PagingFooter({
el: $('.paging-footer'),
collection: new PagingCollection(mockPage(1, 2), {parse: true})
collection: collection
}).render();
});
@@ -75,7 +78,7 @@ define([
var requests = AjaxHelpers.requests(this);
pagingFooter.$(nextPageCss).click();
AjaxHelpers.respondWithJson(requests, mockPage(2, 2));
expect(pagingFooter.collection.currentPage).toBe(2);
expect(pagingFooter.collection.getPageNumber()).toBe(2);
});
it('should be enabled when there is at least one more page', function () {
@@ -91,7 +94,7 @@ define([
});
});
describe("Previous page button", function () {
describe('Previous page button', function () {
it('does not move back if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingFooter.collection.reset(mockPage(2, 2), {parse: true});

View File

@@ -1,8 +1,8 @@
define([
'underscore',
'common/js/components/views/paging_header',
'common/js/components/collections/paging_collection'
], function (_, PagingHeader, PagingCollection) {
'edx-ui-toolkit/js/pagination/paging-collection',
'common/js/components/views/paging_header'
], function (_, PagingCollection, PagingHeader) {
'use strict';
describe('PagingHeader', function () {
var pagingHeader,

View File

@@ -1,30 +1,31 @@
define([
'underscore',
'URI',
'edx-ui-toolkit/js/pagination/paging-collection',
'common/js/components/views/search_field',
'common/js/components/collections/paging_collection',
'common/js/spec_helpers/ajax_helpers'
], function (_, SearchFieldView, PagingCollection, AjaxHelpers) {
], function (_, URI, PagingCollection, SearchFieldView, AjaxHelpers) {
'use strict';
describe('SearchFieldView', function () {
var searchFieldView,
mockUrl = '/api/mock_collection';
var newCollection = function (size, perPage) {
var pageSize = 5,
results = _.map(_.range(size), function (i) { return {foo: i}; });
var collection = new PagingCollection(
[],
{
url: mockUrl,
count: results.length,
num_pages: results.length / pageSize,
current_page: 1,
start: 0,
results: _.first(results, perPage)
var results = _.map(_.range(size), function (i) { return {foo: i}; });
var TestPagingCollection = PagingCollection.extend({
state: {
pageSize: 5
}
);
collection.start = 0;
collection.totalCount = results.length;
});
var collection = new TestPagingCollection({
count: results.length,
num_pages: Math.ceil(results.length / perPage),
page: 1,
results: _.first(results, perPage)
}, {parse: true});
collection.url = mockUrl;
return collection;
};
@@ -40,6 +41,18 @@ define([
return new SearchFieldView(options);
};
var assertQueryParams = function (request, expectedParameters) {
var urlParams = new URI(request.url).query(true);
_.each(expectedParameters, function (value, key) {
expect(urlParams[key]).toBe(value);
});
};
var assertNotInQueryParams = function (request, param) {
var urlParams = new URI(request.url).query(true);
return !urlParams.hasOwnProperty(param);
};
beforeEach(function() {
setFixtures('<section class="test-search"></section>');
});
@@ -62,17 +75,16 @@ define([
searchFieldView = createSearchFieldView().render();
searchFieldView.$('.search-field').val('foo');
searchFieldView.$('.action-search').click();
AjaxHelpers.expectRequestURL(requests, mockUrl, {
assertQueryParams(requests[0], {
page: '1',
page_size: '10',
sort_order: '',
page_size: '5',
text_search: 'foo'
});
AjaxHelpers.respondWithJson(requests, {
count: 10,
current_page: 1,
page: 1,
num_pages: 1,
start: 0,
results: []
});
expect(searchFieldView.$('.search-field').val(), 'foo');
@@ -84,17 +96,12 @@ define([
searchString: 'foo'
}).render();
searchFieldView.$('.action-clear').click();
AjaxHelpers.expectRequestURL(requests, mockUrl, {
page: '1',
page_size: '10',
sort_order: '',
text_search: ''
});
assertNotInQueryParams('text_search');
AjaxHelpers.respondWithJson(requests, {
count: 10,
current_page: 1,
page: 1,
num_pages: 1,
start: 0,
results: []
});
expect(searchFieldView.$('.search-field').val(), '');

View File

@@ -30,7 +30,7 @@
'underscore.string': 'common/js/vendor/underscore.string',
'backbone': 'common/js/vendor/backbone',
'backbone.associations': 'js/vendor/backbone-associations-min',
'backbone.paginator': 'js/vendor/backbone.paginator.min',
'backbone.paginator': 'common/js/vendor/backbone.paginator',
'backbone-super': 'js/vendor/backbone-super',
'jasmine-imagediff': 'js/vendor/jasmine-imagediff',
'URI': 'js/vendor/URI.min',
@@ -131,7 +131,7 @@
},
'backbone.paginator': {
deps: ['backbone'],
exports: 'Backbone.Paginator'
exports: 'Backbone.PageableCollection'
},
"backbone-super": {
deps: ["backbone"]
@@ -166,7 +166,6 @@
'common/js/spec/components/feedback_spec.js',
'common/js/spec/components/list_spec.js',
'common/js/spec/components/paginated_view_spec.js',
'common/js/spec/components/paging_collection_spec.js',
'common/js/spec/components/paging_header_spec.js',
'common/js/spec/components/paging_footer_spec.js',
'common/js/spec/components/search_field_spec.js',

View File

@@ -1,9 +1,9 @@
<% if (!_.isUndefined(srInfo)) { %>
<h2 class="sr" id="<%= srInfo.id %>"><%- srInfo.text %></h2>
<h2 class="sr" id="<%- srInfo.id %>"><%- srInfo.text %></h2>
<% } %>
<div class="search-tools listing-tools">
<span class="search-count listing-count">
<%= message %>
<%- message %>
</span>
<% if (showSortControls) { %>
|
@@ -11,7 +11,7 @@
<label class="field-label" for="paging-header-select"><%- gettext("Sorted by") %></label>
<select id="paging-header-select" name="paging-header-select" class="field-input input-select listing-sort-select">
<% _.each(sortableFields, function (option, key) { %>
<option value="<%= key %>" <% if (key === sortOrder) { %> selected="true" <% } %>>
<option value="<%- key %>" <% if (key === sortOrder) { %> selected="true" <% } %>>
<%- option.displayName %>
</option>
<% }) %>

View File

@@ -1,9 +1,9 @@
<div class="page-header-search wrapper-search-<%= type %>">
<div class="page-header-search wrapper-search-<%- type %>">
<form class="search-form">
<div class="wrapper-search-input">
<label for="search-<%= type %>" class="search-label"><%- searchLabel %></label>
<input id="search-<%= type %>" class="search-field" type="text" value="<%- searchString %>" placeholder="<%- searchLabel %>" />
<button type="button" class="action action-clear <%= searchLabel ? '' : 'is-hidden' %>" aria-label="<%- gettext('Clear search') %>">
<label for="search-<%- type %>" class="search-label"><%- searchLabel %></label>
<input id="search-<%- type %>" class="search-field" type="text" value="<%- searchString %>" placeholder="<%- searchLabel %>" />
<button type="button" class="action action-clear <%- searchLabel ? '' : 'is-hidden' %>" aria-label="<%- gettext('Clear search') %>">
<i class="icon fa fa-times-circle" aria-hidden="true"></i><span class="sr"><%- gettext('Search') %></span>
</button>
</div>

File diff suppressed because one or more lines are too long