Fauxton: Improved pagination This is a new version of pagination in Fauxton using skip. It uses a PagingCollection that has the main algorithm for pagination and exposes a nice api.
This is an intermediate step as this is a much better pagination than we have at the moment. However using just skip for pagination is not optimal as there are two cases where skip pagination fails - For very large skips and for when documents that a user have paginated past have been deleted. The next step once this has landed will be to add in a startkey_docid pagination as well. The PagingCollection would then decided which method to use to paginate for an index. Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/0f2c148a Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/0f2c148a Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/0f2c148a Branch: refs/heads/import-master Commit: 0f2c148a9a796750f0e70badb68e526fa2bbe8f1 Parents: becb46e Author: Garren Smith <[email protected]> Authored: Thu Mar 20 09:46:59 2014 +0200 Committer: Garren Smith <[email protected]> Committed: Thu Apr 10 12:04:52 2014 +0200 ---------------------------------------------------------------------- app/addons/databases/views.js | 2 +- app/addons/documents/resources.js | 181 ++------------- app/addons/documents/routes.js | 81 +++---- .../documents/templates/advanced_options.html | 5 +- app/addons/documents/views.js | 14 +- app/addons/fauxton/components.js | 16 +- app/config.js | 3 +- assets/js/plugins/cloudant.pagingcollection.js | 224 +++++++++++++++++++ 8 files changed, 288 insertions(+), 238 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/databases/views.js ---------------------------------------------------------------------- diff --git a/app/addons/databases/views.js b/app/addons/databases/views.js index d632486..0806b92 100644 --- a/app/addons/databases/views.js +++ b/app/addons/databases/views.js @@ -81,7 +81,7 @@ function(app, Components, FauxtonAPI, Databases) { // TODO: switch to using a model, or Databases.databaseUrl() // Neither of which are in scope right now // var db = new Database.Model({id: dbname}); - var url = ["/database/", app.utils.safeURLName(dbname), "/_all_docs?limit=" + Databases.DocLimit].join(''); + var url = ["/database/", app.utils.safeURLName(dbname), "/_all_docs"].join(''); FauxtonAPI.navigate(url); } else { FauxtonAPI.addNotification({ http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/resources.js b/app/addons/documents/resources.js index e47bd61..a787f0d 100644 --- a/app/addons/documents/resources.js +++ b/app/addons/documents/resources.js @@ -12,10 +12,11 @@ define([ "app", - "api" + "api", + "cloudant.pagingcollection" ], -function(app, FauxtonAPI) { +function(app, FauxtonAPI, PagingCollection) { var Documents = FauxtonAPI.addon(); Documents.QueryParams = (function () { @@ -40,70 +41,7 @@ function(app, FauxtonAPI) { }; })(); - Documents.paginate = { - history: [], - calculate: function (doc, defaultParams, currentParams, _isAllDocs) { - var docId = '', - lastId = '', - isView = !!!_isAllDocs, - key; - - if (currentParams.keys) { - throw "Cannot paginate when keys is specfied"; - } - - if (_.isUndefined(doc)) { - throw "Require docs to paginate"; - } - - // defaultParams should always override the user-specified parameters - _.extend(currentParams, defaultParams); - - lastId = doc.id || doc._id; - - // If we are paginating on a view, we need to set a ``key`` and a ``docId`` - // and expect that they are different values. - if (isView) { - key = doc.key; - docId = lastId; - } else { - docId = key = lastId; - } - - // Set parameters to paginate - if (isView) { - currentParams.startkey_docid = docId; - currentParams.startkey = key; - } else if (currentParams.startkey) { - currentParams.startkey = key; - } else { - currentParams.startkey_docid = docId; - } - - return currentParams; - }, - - next: function (docs, currentParams, perPage, _isAllDocs) { - var params = {limit: perPage, skip: 1}, - doc = _.last(docs); - - this.history.push(_.clone(currentParams)); - return this.calculate(doc, params, currentParams, _isAllDocs); - }, - - previous: function (docs, currentParams, perPage, _isAllDocs) { - var params = this.history.pop(), - doc = _.first(docs); - - params.limit = perPage; - return params; - }, - - reset: function () { - this.history = []; - } - }; - + Documents.Doc = FauxtonAPI.Model.extend({ idAttribute: "_id", documentation: function(){ @@ -357,25 +295,8 @@ function(app, FauxtonAPI) { }); - var DefaultParametersMixin = function() { - // keep this variable private - var defaultParams; - - return { - saveDefaultParameters: function() { - // store the default parameters so we can reset to the first page - defaultParams = _.clone(this.params); - }, - - restoreDefaultParameters: function() { - this.params = _.clone(defaultParams); - } - }; - }; - - Documents.AllDocs = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(), { + Documents.AllDocs = PagingCollection.extend({ model: Documents.Doc, - isAllDocs: true, documentation: function(){ return "docs"; }, @@ -389,11 +310,9 @@ function(app, FauxtonAPI) { if (!this.params.limit) { this.params.limit = this.perPageLimit; } - - this.saveDefaultParameters(); }, - url: function(context, params) { + urlRef: function(context, params) { var query = ""; if (params) { @@ -415,6 +334,10 @@ function(app, FauxtonAPI) { } }, + url: function () { + return this.urlRef.apply(this, arguments); + }, + simple: function () { var docs = this.map(function (item) { return { @@ -429,15 +352,6 @@ function(app, FauxtonAPI) { }); }, - updateLimit: function (limit) { - this.perPageLimit = limit; - this.params.limit = limit; - }, - - updateParams: function (params) { - this.params = params; - }, - totalRows: function() { return this.viewMeta.total_rows || "unknown"; }, @@ -456,37 +370,17 @@ function(app, FauxtonAPI) { parse: function(resp) { var rows = resp.rows; - this.viewMeta = { - total_rows: resp.total_rows, - offset: resp.offset, - update_seq: resp.update_seq - }; - - //Paginating, don't show first item as it was the last - //item in the previous page - if (this.skipFirstItem) { - rows = rows.splice(1); - } - // remove any query errors that may return without doc info // important for when querying keys on all docs - var noQueryErrors = _.filter(rows, function(row){ + resp.rows = _.filter(rows, function(row){ return row.value; }); - return _.map(noQueryErrors, function(row) { - return { - _id: row.id, - _rev: row.value.rev, - value: row.value, - key: row.key, - doc: row.doc || undefined - }; - }); + return PagingCollection.prototype.parse.call(this, resp); } - })); + }); - Documents.IndexCollection = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(), { + Documents.IndexCollection = PagingCollection.extend({ model: Documents.ViewRow, documentation: function(){ return "docs"; @@ -498,17 +392,14 @@ function(app, FauxtonAPI) { this.idxType = "_view"; this.view = options.view; this.design = options.design.replace('_design/',''); - this.skipFirstItem = false; this.perPageLimit = options.perPageLimit || 20; if (!this.params.limit) { this.params.limit = this.perPageLimit; } - - this.saveDefaultParameters(); }, - url: function(context, params) { + urlRef: function(context, params) { var query = ""; if (params) { if (!_.isEmpty(params)) { @@ -533,18 +424,8 @@ function(app, FauxtonAPI) { return url.join("/") + query; }, - updateParams: function (params) { - this.params = params; - }, - - updateLimit: function (limit) { - if (this.params.startkey_docid && this.params.startkey) { - //we are paginating so set limit + 1 - this.params.limit = limit + 1; - return; - } - - this.params.limit = limit; + url: function () { + return this.urlRef.apply(this, arguments); }, totalRows: function() { @@ -579,23 +460,7 @@ function(app, FauxtonAPI) { this.endTime = new Date().getTime(); this.requestDuration = (this.endTime - this.startTime); - if (this.skipFirstItem) { - rows = rows.splice(1); - } - - this.viewMeta = { - total_rows: resp.total_rows, - offset: resp.offset, - update_seq: resp.update_seq - }; - return _.map(rows, function(row) { - return { - value: row.value, - key: row.key, - doc: row.doc, - id: row.id - }; - }); + return PagingCollection.prototype.parse.apply(this, arguments); }, buildAllDocs: function(){ @@ -606,7 +471,7 @@ function(app, FauxtonAPI) { // we can get the request duration fetch: function () { this.startTime = new Date().getTime(); - return FauxtonAPI.Collection.prototype.fetch.call(this); + return PagingCollection.prototype.fetch.call(this); }, allDocs: function(){ @@ -645,10 +510,10 @@ function(app, FauxtonAPI) { return timeString; } - })); + }); - Documents.PouchIndexCollection = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(), { + Documents.PouchIndexCollection = PagingCollection.extend({ model: Documents.ViewRow, documentation: function(){ return "docs"; @@ -661,8 +526,6 @@ function(app, FauxtonAPI) { this.params = _.extend({limit: 20, reduce: false}, options.params); this.idxType = "_view"; - - this.saveDefaultParameters(); }, url: function () { @@ -717,7 +580,7 @@ function(app, FauxtonAPI) { allDocs: function(){ return this.models; } - })); + }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/routes.js b/app/addons/documents/routes.js index 699a496..5e8834f 100644 --- a/app/addons/documents/routes.js +++ b/app/addons/documents/routes.js @@ -168,10 +168,14 @@ function(app, FauxtonAPI, Documents, Databases) { this.data.designDocs = new Documents.AllDocs(null, { database: this.data.database, + paging: { + pageSize: 500 + }, params: { - startkey: '"_design"', - endkey: '"_design1"', - include_docs: true + startkey: '_design', + endkey: '_design1', + include_docs: true, + limit: 500 } }); @@ -182,11 +186,11 @@ function(app, FauxtonAPI, Documents, Databases) { }, establish: function () { - return this.data.designDocs.fetch(); + return this.data.designDocs.fetch({reset: true}); }, createParams: function (options) { - var urlParams = app.getParams(options); + var urlParams = Documents.QueryParams.parse(app.getParams(options)); return { urlParams: urlParams, docParams: _.extend(_.clone(urlParams), {limit: this.getDocPerPageLimit(urlParams, 20)}) @@ -223,6 +227,8 @@ function(app, FauxtonAPI, Documents, Databases) { collection: this.data.database.allDocs })); + this.data.database.allDocs.paging.pageSize = this.getDocPerPageLimit(urlParams, parseInt(docParams.limit, 10)); + this.setView("#dashboard-upper-content", new Documents.Views.AllDocsLayout({ database: this.data.database, collection: this.data.database.allDocs, @@ -240,9 +246,7 @@ function(app, FauxtonAPI, Documents, Databases) { {"name": this.data.database.id, "link": Databases.databaseUrl(this.data.database)} ]; - this.apiUrl = [this.data.database.allDocs.url("apiurl", urlParams), this.data.database.allDocs.documentation() ]; - //reset the pagination history - the history is used for pagination.previous - Documents.paginate.reset(); + this.apiUrl = [this.data.database.allDocs.urlRef("apiurl", urlParams), this.data.database.allDocs.documentation() ]; }, viewFn: function (databaseName, ddoc, view) { @@ -257,7 +261,10 @@ function(app, FauxtonAPI, Documents, Databases) { database: this.data.database, design: decodeDdoc, view: view, - params: docParams + params: docParams, + paging: { + pageSize: this.getDocPerPageLimit(urlParams, parseInt(docParams.limit, 10)) + } }); this.viewEditor = this.setView("#dashboard-upper-content", new Documents.Views.ViewEditor({ @@ -290,8 +297,7 @@ function(app, FauxtonAPI, Documents, Databases) { ]; }; - this.apiUrl = [this.data.indexedDocs.url("apiurl", urlParams), "docs"]; - Documents.paginate.reset(); + this.apiUrl = [this.data.indexedDocs.urlRef("apiurl", urlParams), "docs"]; }, ddocInfo: function (designDoc, designDocs, view) { @@ -344,22 +350,27 @@ function(app, FauxtonAPI, Documents, Databases) { urlParams = params.urlParams, docParams = params.docParams, ddoc = event.ddoc, + pageSize, collection; - docParams.limit = this.getDocPerPageLimit(urlParams, this.documentsView.perPage()); + docParams.limit = pageSize = this.getDocPerPageLimit(urlParams, this.documentsView.perPage()); this.documentsView.forceRender(); if (event.allDocs) { this.eventAllDocs = true; // this is horrible. But I cannot get the trigger not to fire the route! this.data.database.buildAllDocs(docParams); collection = this.data.database.allDocs; + collection.paging.pageSize = pageSize; } else { collection = this.data.indexedDocs = new Documents.IndexCollection(null, { database: this.data.database, design: ddoc, view: view, - params: docParams + params: docParams, + paging: { + pageSize: pageSize + } }); if (!this.documentsView) { @@ -378,8 +389,7 @@ function(app, FauxtonAPI, Documents, Databases) { this.documentsView.setCollection(collection); this.documentsView.setParams(docParams, urlParams); - this.apiUrl = [collection.url("apiurl", urlParams), "docs"]; - Documents.paginate.reset(); + this.apiUrl = [collection.urlRef("apiurl", urlParams), "docs"]; }, updateAllDocsFromPreview: function (event) { @@ -405,47 +415,18 @@ function(app, FauxtonAPI, Documents, Databases) { perPageChange: function (perPage) { // We need to restore the collection parameters to the defaults (1st page) // and update the page size - var params = this.documentsView.collection.restoreDefaultParameters(); this.perPage = perPage; - this.documentsView.updatePerPage(perPage); this.documentsView.forceRender(); - this.documentsView.collection.params.limit = perPage; + this.documentsView.collection.pageSizeReset(perPage, {fetch: false}); this.setDocPerPageLimit(perPage); }, paginate: function (options) { - var params = {}, - urlParams = app.getParams(), - collection = this.documentsView.collection; + var collection = this.documentsView.collection; this.documentsView.forceRender(); - - // this is really ugly. But we basically need to make sure that - // all parameters are in the correct state and have been parsed before we - // calculate how to paginate the collection - collection.params = Documents.QueryParams.parse(collection.params); - urlParams = Documents.QueryParams.parse(urlParams); - - if (options.direction === 'next') { - params = Documents.paginate.next(collection.toJSON(), - collection.params, - options.perPage, - !!collection.isAllDocs); - } else { - params = Documents.paginate.previous(collection.toJSON(), - collection.params, - options.perPage, - !!collection.isAllDocs); - } - - // use the perPage sent from IndexPagination as it calculates how many - // docs to fetch for next page - params.limit = options.perPage; - - // again not pretty but need to make sure all the parameters can be correctly - // built into a query - params = Documents.QueryParams.stringify(params); - collection.updateParams(params); + collection.paging.pageSize = options.perPage; + var promise = collection[options.direction]({fetch: false}); }, reloadDesignDocs: function (event) { @@ -476,9 +457,9 @@ function(app, FauxtonAPI, Documents, Databases) { } if (!urlParams.limit || urlParams.limit > storedPerPage) { - return storedPerPage; + return parseInt(storedPerPage, 10); } else { - return urlParams.limit; + return parseInt(urlParams.limit, 10); } } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/templates/advanced_options.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/advanced_options.html b/app/addons/documents/templates/advanced_options.html index d8d57cd..55c5946 100644 --- a/app/addons/documents/templates/advanced_options.html +++ b/app/addons/documents/templates/advanced_options.html @@ -50,13 +50,14 @@ the License. <label class="drop-down inline"> Limit: <select name="limit" class="input-small"> + <option selected="selected">None</option> <option>5</option> <option>10</option> - <option selected="selected">20</option> + <option>20</option> <option>30</option> <option>50</option> <option>100</option> - <option>500</option> + <option>500</option> </select> </label> <label for="skipRows" class="inline drop-down"> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/views.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/views.js b/app/addons/documents/views.js index cc23e19..351b2b0 100644 --- a/app/addons/documents/views.js +++ b/app/addons/documents/views.js @@ -681,8 +681,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum }, addPagination: function () { - var collection = this.collection; - this.pagination = new Components.IndexPagination({ collection: this.collection, scrollToSelector: '#dashboard-content', @@ -703,9 +701,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum this.addPagination(); } - if (!this.params.keys) { //cannot paginate with keys - this.insertView('#documents-pagination', this.pagination); - } + this.insertView('#documents-pagination', this.pagination); if (!this.allDocsNumber) { this.allDocsNumber = new Views.AllDocsNumber({ @@ -749,10 +745,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum perPage: function () { return this.allDocsNumber.perPage(); - }, - - updatePerPage: function (newPerPage) { - this.collection.updateLimit(newPerPage); } }); @@ -1741,9 +1733,8 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum this.ddocID = this.model.id; } else { var ddocDecode = decodeURIComponent(this.ddocID); - this.model = this.ddocs.get(ddocDecode).dDocModel(); + this.model = this.ddocs.get(this.ddocID).dDocModel(); this.reduceFunStr = this.model.viewHasReduce(this.viewName); - } this.designDocSelector = this.setView('.design-doc-group', new Views.DesignDocSelector({ @@ -1752,7 +1743,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum database: this.database })); - if (!this.newView) { this.eventer = _.extend({}, Backbone.Events); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/fauxton/components.js ---------------------------------------------------------------------- diff --git a/app/addons/fauxton/components.js b/app/addons/fauxton/components.js index 25f623c..47f4726 100644 --- a/app/addons/fauxton/components.js +++ b/app/addons/fauxton/components.js @@ -84,28 +84,18 @@ function(app, FauxtonAPI, ace, spin) { }, canShowPreviousfn: function () { - if (this._pageStart === 1 || !this.enabled) { - return false; - } - return true; + if (!this.enabled) { return this.enabled; } + return this.collection.hasPrevious(); }, canShowNextfn: function () { if (!this.enabled) { return this.enabled; } - if (this.collection.length < (this.perPage -1)) { - return false; - } - if ((this.pageStart() + this.perPage) >= this.docLimit) { return false; } - if (this.collection.viewMeta && this.collection.viewMeta.total_rows <= this.pageStart() + this.perPage) { - return false; - } - - return true; + return this.collection.hasNext(); }, previousClicked: function (event) { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/config.js ---------------------------------------------------------------------- diff --git a/app/config.js b/app/config.js index 4a2f136..edcd9a2 100644 --- a/app/config.js +++ b/app/config.js @@ -30,7 +30,8 @@ require.config({ spin: "../assets/js/libs/spin.min", d3: "../assets/js/libs/d3", "nv.d3": "../assets/js/libs/nv.d3", - "ace":"../assets/js/libs/ace" + "ace":"../assets/js/libs/ace", + "cloudant.pagingcollection": "../assets/js/plugins/cloudant.pagingcollection" }, baseUrl: '/', http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/assets/js/plugins/cloudant.pagingcollection.js ---------------------------------------------------------------------- diff --git a/assets/js/plugins/cloudant.pagingcollection.js b/assets/js/plugins/cloudant.pagingcollection.js new file mode 100644 index 0000000..2ab5eaf --- /dev/null +++ b/assets/js/plugins/cloudant.pagingcollection.js @@ -0,0 +1,224 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +(function(root, factory) { + "use strict"; + // start with AMD, so paginate could then be used by ``var paginate = require('paginate');`` + if (typeof define === 'function' && define.amd) { + define(['underscore', 'backbone', 'jquery'], function(_, Backbone, $) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global paginate. + return factory(root, null, _, Backbone, $.param); + }); + + // Next check for Node.js or CommonJS. Also look to see if either + // underscore or lodash are the modules being used + } else if (typeof exports !== 'undefined') { + var Backbone = require('Backbone'), + $param = require('querystring').stringify, + _; + try { + _ = require('underscore'); + } catch(e) { + _ = require('lodash'); + } + factory(root, exports, _, Backbone, $param); + + // Finally, register as a browser global. + } else { + root.PagingCollection = factory(root, {}, root._, root.Backbone, root.$.param); + } + +}(this, function(root, exports, _, Backbone, $param) { + "use strict"; + + //PagingCollection + //---------------- + + // A PagingCollection knows how to build appropriate requests to the + // CouchDB-like server and how to fetch. The Collection will always contain a + // single page of documents. + + var PagingCollection = Backbone.Collection.extend({ + + // initialize parameters and page size + constructor: function() { + Backbone.Collection.apply(this, arguments); + this.configure.apply(this, arguments); + }, + + configure: function(collections, options) { + var querystring = _.result(this, "url").split("?")[1] || ""; + this.paging = _.defaults((options.paging || {}), { + defaultParams: _.defaults({}, options.params, this._parseQueryString(querystring)), + hasNext: false, + hasPrevious: false, + params: {}, + pageSize: 20, + direction: undefined + }); + + this.paging.params = _.clone(this.paging.defaultParams); + this.updateUrlQuery(this.paging.defaultParams); + }, + + calculateParams: function(currentParams, skipIncrement, limitIncrement) { + + var params = _.clone(currentParams); + params.skip = (parseInt(currentParams.skip, 10) || 0) + skipIncrement; + + // guard against hard limits + if(this.paging.defaultParams.limit) { + params.limit = Math.min(this.paging.defaultParams.limit, params.limit); + } + // request an extra row so we know that there are more results + params.limit = limitIncrement + 1; + // prevent illegal skip values + params.skip = Math.max(params.skip, 0); + + return params; + }, + + pageSizeReset: function(pageSize, opts) { + var options = _.defaults((opts || {}), {fetch: true}); + this.paging.direction = undefined; + this.paging.pageSize = pageSize; + this.paging.params = this.paging.defaultParams; + this.paging.params.limit = pageSize; + this.updateUrlQuery(this.paging.params); + if (options.fetch) { + return this.fetch(); + } + }, + + _parseQueryString: function(uri) { + var queryString = decodeURI(uri).split(/&/); + + return _.reduce(queryString, function (parsedQuery, item) { + var nameValue = item.split(/=/); + if (nameValue.length === 2) { + parsedQuery[nameValue[0]] = nameValue[1]; + } + + return parsedQuery; + }, {}); + }, + + _iterate: function(offset, opts) { + var options = _.defaults((opts || {}), {fetch: true}); + + this.paging.params = this.calculateParams(this.paging.params, offset, this.paging.pageSize); + + // Fetch the next page of documents + this.updateUrlQuery(this.paging.params); + if (options.fetch) { + return this.fetch({reset: true}); + } + }, + + // `next` is called with the number of items for the next page. + // It returns the fetch promise. + next: function(options){ + this.paging.direction = "next"; + return this._iterate(this.paging.pageSize, options); + }, + + // `previous` is called with the number of items for the previous page. + // It returns the fetch promise. + previous: function(options){ + this.paging.direction = "previous"; + return this._iterate(0 - this.paging.pageSize, options); + }, + + shouldStringify: function (val) { + try { + JSON.parse(val); + return false; + } catch(e) { + return true; + } + }, + + // Encodes the parameters so that couchdb will understand them + // and then sets the url with the new url. + updateUrlQuery: function (params) { + var url = _.result(this, "url").split("?")[0]; + + _.each(['startkey', 'endkey', 'key'], function (key) { + if (_.has(params, key) && this.shouldStringify(params[key])) { + params[key] = JSON.stringify(params[key]); + } + }, this); + + this.url = url + '?' + $param(params); + }, + + fetch: function () { + // if this is a fetch for the first time, fetch one extra to see if there is a next + if (!this.paging.direction && this.paging.params.limit > 0) { + this.paging.direction = 'fetch'; + this.paging.params.limit = this.paging.params.limit + 1; + this.updateUrlQuery(this.paging.params); + } + + return Backbone.Collection.prototype.fetch.apply(this, arguments); + }, + + parse: function (resp) { + var rows = resp.rows; + + this.paging.hasNext = this.paging.hasPrevious = false; + + this.viewMeta = { + total_rows: resp.total_rows, + offset: resp.offset, + update_seq: resp.update_seq + }; + + var skipLimit = this.paging.defaultParams.skip || 0; + if(this.paging.params.skip > skipLimit) { + this.paging.hasPrevious = true; + } + + if(rows.length === this.paging.pageSize + 1) { + this.paging.hasNext = true; + + // remove the next page marker result + rows.pop(); + this.viewMeta.total_rows = this.viewMeta.total_rows - 1; + } + return rows; + }, + + hasNext: function() { + return this.paging.hasNext; + }, + + hasPrevious: function() { + return this.paging.hasPrevious; + } + }); + + + if (exports) { + // Overload the Backbone.ajax method, this allows PagingCollection to be able to + // work in node.js + exports.setAjax = function (ajax) { + Backbone.ajax = ajax; + }; + + exports.PagingCollection = PagingCollection; + } + + return PagingCollection; +})); +
