diff options
author | Dirkjan Ochtman <dirkjan@ochtman.nl> | 2014-04-11 20:55:01 +0200 |
---|---|---|
committer | Dirkjan Ochtman <dirkjan@ochtman.nl> | 2014-04-11 20:55:01 +0200 |
commit | cce329390f991ee0c10ae2a9a576124f17efff85 (patch) | |
tree | 554c639759861619da113eb54810e4be4f1a2737 | |
parent | 79035f8dbe15bf5eefce2b5acb48f36acc94f001 (diff) | |
parent | 0b31731982f8edf1ec52166d4b7fc54f21723951 (diff) | |
download | couchdb-cce329390f991ee0c10ae2a9a576124f17efff85.tar.gz |
Merge master into 1.6.x once more
-rw-r--r-- | LICENSE | 221 | ||||
-rw-r--r-- | NOTICE | 4 | ||||
-rw-r--r-- | etc/couchdb/default.ini.tpl.in | 2 | ||||
-rw-r--r-- | share/doc/src/conf.py | 5 | ||||
-rw-r--r-- | share/doc/src/config/auth.rst | 24 | ||||
-rw-r--r-- | share/doc/templates/help.html | 4 | ||||
-rw-r--r-- | share/doc/templates/tracking.html | 15 | ||||
-rw-r--r-- | share/www/script/test/basics.js | 2 | ||||
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/couch_index/src/couch_index_updater.erl | 2 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_auth.erl | 17 | ||||
-rw-r--r-- | src/couchdb/couch_passwords.erl | 15 | ||||
-rw-r--r-- | src/fauxton/app/addons/databases/views.js | 2 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/resources.js | 190 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/routes.js | 81 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/templates/advanced_options.html | 5 | ||||
-rw-r--r-- | src/fauxton/app/addons/documents/views.js | 14 | ||||
-rw-r--r-- | src/fauxton/app/addons/fauxton/components.js | 16 | ||||
-rw-r--r-- | src/fauxton/app/config.js | 3 | ||||
-rw-r--r-- | src/fauxton/app/core/utils.js | 6 | ||||
-rw-r--r-- | src/fauxton/app/helpers.js | 17 | ||||
-rw-r--r-- | src/fauxton/assets/js/plugins/cloudant.pagingcollection.js | 224 |
22 files changed, 587 insertions, 283 deletions
@@ -1105,26 +1105,207 @@ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -for src/fauxton/asserts/js/plugins/backbone.fetch-cache.js +for src/fauxton/assets/js/plugins/cloudant.pagingcollection.js -The MIT License (MIT) - -Copyright (c) 2012-2013 Andrew Appleton, http://floatleft.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. @@ -186,6 +186,6 @@ This product also includes the following third-party components: Copyright (c) 2010, Ajax.org B.V. - * src/fauxton/asserts/js/plugins/backbone.fetch-cache.js + * src/fauxton/asserts/js/plugins/cloudant.pagingcollection.js - Copyright (c) 2012-2013 Andrew Appleton, http://floatleft.com + Copyright (c) 2014, Cloudant http://cloudant.com diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index a0dd7db20..934c6cd44 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -72,6 +72,8 @@ timeout = 600 ; number of seconds before automatic logout auth_cache_size = 50 ; size is number of cache entries allow_persistent_cookies = false ; set to true to allow persistent cookies iterations = 10 ; iterations for password hashing +; min_iterations = 1 +; max_iterations = 1000000000 ; comma-separated list of public fields, 404 if empty ; public_fields = diff --git a/share/doc/src/conf.py b/share/doc/src/conf.py index 14a93f5c2..03c5dd646 100644 --- a/share/doc/src/conf.py +++ b/share/doc/src/conf.py @@ -106,7 +106,9 @@ html_additional_pages = { 'index': 'pages/index.html' } -html_context = {} +html_context = { + "ga_code": "UA-658988-6" +} html_sidebars = { "**": [ @@ -115,6 +117,7 @@ html_sidebars = { "relations.html", "utilities.html", "help.html", + "tracking.html", ] } diff --git a/share/doc/src/config/auth.rst b/share/doc/src/config/auth.rst index 41272887c..831114094 100644 --- a/share/doc/src/config/auth.rst +++ b/share/doc/src/config/auth.rst @@ -166,6 +166,30 @@ Authentication Configuration [couch_httpd_auth] iterations = 10000 + .. config:option:: min_iterations :: Minimum PBKDF2 iterations count + + .. versionadded:: 1.6 + + The minimum number of iterations allowed for passwords hashed by + the PBKDF2 algorithm. Any user with fewer iterations is forbidden. + + :: + + [couch_httpd_auth] + min_iterations = 100 + + .. config:option:: max_iterations :: Maximum PBKDF2 iterations count + + .. versionadded:: 1.6 + + The maximum number of iterations allowed for passwords hashed by + the PBKDF2 algorithm. Any user with greater iterations is forbidden. + + :: + + [couch_httpd_auth] + max_iterations = 100000 + .. config:option:: proxy_use_secret :: Force proxy auth use secret token diff --git a/share/doc/templates/help.html b/share/doc/templates/help.html index be0cb9190..a6b7859bf 100644 --- a/share/doc/templates/help.html +++ b/share/doc/templates/help.html @@ -16,9 +16,9 @@ specific language governing permissions and limitations under the License. <h3>More Help</h3> <ul> -<li><a href="https://couchdb.apache.org/">Homepage</a></li> +<li><a href="https://couchdb.apache.org/"{% if not local %} onclick="_gaq.push(['_link', 'https://couchdb.apache.org/']); return false;"{% endif %}>Homepage</a></li> <li><a href="http://wiki.apache.org/couchdb/">Wiki</a></li> -<li><a href="https://couchdb.apache.org/#mailing-list">Mailing Lists</a></li> +<li><a href="https://couchdb.apache.org/#mailing-list"{% if not local %} onclick="_gaq.push(['_link', 'https://couchdb.apache.org/#mailing-list']); return false;"{% endif %}>Mailing Lists</a></li> <li><a href="http://webchat.freenode.net/?channels=couchdb">IRC</a></li> <li><a href="https://issues.apache.org/jira/browse/CouchDB">Issues</a></li> <li><a href="{{ pathto('download') }}">Download</a></li> diff --git a/share/doc/templates/tracking.html b/share/doc/templates/tracking.html new file mode 100644 index 000000000..e6d4037bd --- /dev/null +++ b/share/doc/templates/tracking.html @@ -0,0 +1,15 @@ +{% if not local %} +<script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', '{{ ga_code }}']); + _gaq.push(['_setDomainName', 'couchdb.org']); + _gaq.push(['_setAllowLinker', true]); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); +</script> +{% endif %} diff --git a/share/www/script/test/basics.js b/share/www/script/test/basics.js index c1ba0af67..993456c72 100644 --- a/share/www/script/test/basics.js +++ b/share/www/script/test/basics.js @@ -267,7 +267,7 @@ couchTests.basics = function(debug) { TEquals(400, xhr.status, "should return a bad request"); result = JSON.parse(xhr.responseText); TEquals("bad_request", result.error); - TEquals("You tried to DELETE a database with a ?=rev parameter. Did you mean to DELETE a document instead?", result.reason); + TEquals("You tried to DELETE a database with a ?rev= parameter. Did you mean to DELETE a document instead?", result.reason); // On restart, a request for creating a database that already exists can // not override the existing database file diff --git a/src/Makefile.am b/src/Makefile.am index e1007f962..07ee5e150 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -257,6 +257,7 @@ FAUXTON_FILES = \ fauxton/assets/js/libs/require.js \ fauxton/assets/js/libs/spin.min.js \ fauxton/assets/js/plugins/backbone.layoutmanager.js \ + fauxton/assets/js/plugins/cloudant.pagingcollection.js \ fauxton/assets/js/plugins/jquery.form.js \ fauxton/assets/js/plugins/prettify.js \ fauxton/assets/js/plugins/beautify.js\ diff --git a/src/couch_index/src/couch_index_updater.erl b/src/couch_index/src/couch_index_updater.erl index c6d3059f9..ec3de5411 100644 --- a/src/couch_index/src/couch_index_updater.erl +++ b/src/couch_index/src/couch_index_updater.erl @@ -35,7 +35,7 @@ start_link(Index, Module) -> run(Pid, IdxState) -> - gen_server:call(Pid, {update, IdxState}). + gen_server:call(Pid, {update, IdxState}, infinity). is_running(Pid) -> diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl index 08841fb67..6888f0691 100644 --- a/src/couchdb/couch_httpd_auth.erl +++ b/src/couchdb/couch_httpd_auth.erl @@ -368,11 +368,28 @@ authenticate(Pass, UserProps) -> couch_util:get_value(<<"password_sha">>, UserProps, nil)}; <<"pbkdf2">> -> Iterations = couch_util:get_value(<<"iterations">>, UserProps, 10000), + verify_iterations(Iterations), {couch_passwords:pbkdf2(Pass, UserSalt, Iterations), couch_util:get_value(<<"derived_key">>, UserProps, nil)} end, couch_passwords:verify(PasswordHash, ExpectedHash). +verify_iterations(Iterations) when is_integer(Iterations) -> + Min = list_to_integer(couch_config:get("couch_httpd_auth", "min_iterations", "1")), + Max = list_to_integer(couch_config:get("couch_httpd_auth", "max_iterations", "1000000000")), + case Iterations < Min of + true -> + throw({forbidden, <<"Iteration count is too low for this server">>}); + false -> + ok + end, + case Iterations > Max of + true -> + throw({forbidden, <<"Iteration count is too high for this server">>}); + false -> + ok + end. + auth_name(String) when is_list(String) -> [_,_,_,_,_,Name|_] = re:split(String, "[\\W_]", [{return, list}]), ?l2b(Name). diff --git a/src/couchdb/couch_passwords.erl b/src/couchdb/couch_passwords.erl index d9e6836db..bbf6d9ac3 100644 --- a/src/couchdb/couch_passwords.erl +++ b/src/couchdb/couch_passwords.erl @@ -22,12 +22,12 @@ %% legacy scheme, not used for new passwords. -spec simple(binary(), binary()) -> binary(). -simple(Password, Salt) -> +simple(Password, Salt) when is_binary(Password), is_binary(Salt) -> ?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))). %% CouchDB utility functions -spec hash_admin_password(binary()) -> binary(). -hash_admin_password(ClearPassword) -> +hash_admin_password(ClearPassword) when is_binary(ClearPassword) -> Iterations = couch_config:get("couch_httpd_auth", "iterations", "10000"), Salt = couch_uuids:random(), DerivedKey = couch_passwords:pbkdf2(couch_util:to_binary(ClearPassword), @@ -50,7 +50,10 @@ get_unhashed_admins() -> %% Current scheme, much stronger. -spec pbkdf2(binary(), binary(), integer()) -> binary(). -pbkdf2(Password, Salt, Iterations) -> +pbkdf2(Password, Salt, Iterations) when is_binary(Password), + is_binary(Salt), + is_integer(Iterations), + Iterations > 0 -> {ok, Result} = pbkdf2(Password, Salt, Iterations, ?SHA1_OUTPUT_LENGTH), Result. @@ -59,7 +62,11 @@ pbkdf2(Password, Salt, Iterations) -> pbkdf2(_Password, _Salt, _Iterations, DerivedLength) when DerivedLength > ?MAX_DERIVED_KEY_LENGTH -> {error, derived_key_too_long}; -pbkdf2(Password, Salt, Iterations, DerivedLength) -> +pbkdf2(Password, Salt, Iterations, DerivedLength) when is_binary(Password), + is_binary(Salt), + is_integer(Iterations), + Iterations > 0, + is_integer(DerivedLength) -> L = ceiling(DerivedLength / ?SHA1_OUTPUT_LENGTH), <<Bin:DerivedLength/binary,_/binary>> = iolist_to_binary(pbkdf2(Password, Salt, Iterations, L, 1, [])), diff --git a/src/fauxton/app/addons/databases/views.js b/src/fauxton/app/addons/databases/views.js index d63248602..0806b929b 100644 --- a/src/fauxton/app/addons/databases/views.js +++ b/src/fauxton/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({ diff --git a/src/fauxton/app/addons/documents/resources.js b/src/fauxton/app/addons/documents/resources.js index efd7f69f5..a787f0d78 100644 --- a/src/fauxton/app/addons/documents/resources.js +++ b/src/fauxton/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(){ @@ -211,7 +149,7 @@ function(app, FauxtonAPI) { if (doc) { return new Documents.Doc(doc, {database: this.database}); - } + } return this; }, @@ -253,7 +191,6 @@ function(app, FauxtonAPI) { if (typeof(this.id) === "undefined") { resp._id = resp.id; } - delete resp.id; } if (resp.ok) { delete resp.ok; @@ -311,7 +248,7 @@ function(app, FauxtonAPI) { }); Documents.ViewRow = FauxtonAPI.Model.extend({ - // this is a hack so that backbone.collections doesn't group + // this is a hack so that backbone.collections doesn't group // these by id and reduce the number of items returned. idAttribute: "_id", @@ -358,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"; }, @@ -390,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) { @@ -416,6 +334,10 @@ function(app, FauxtonAPI) { } }, + url: function () { + return this.urlRef.apply(this, arguments); + }, + simple: function () { var docs = this.map(function (item) { return { @@ -430,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"; }, @@ -457,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"; @@ -499,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)) { @@ -520,7 +410,7 @@ function(app, FauxtonAPI) { } else if (this.params) { query = "?" + $.param(this.params); } - + var startOfUrl = app.host; if (context === 'app') { startOfUrl = 'database'; @@ -534,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() { @@ -580,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(){ @@ -607,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(){ @@ -646,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"; @@ -662,8 +526,6 @@ function(app, FauxtonAPI) { this.params = _.extend({limit: 20, reduce: false}, options.params); this.idxType = "_view"; - - this.saveDefaultParameters(); }, url: function () { @@ -718,7 +580,7 @@ function(app, FauxtonAPI) { allDocs: function(){ return this.models; } - })); + }); diff --git a/src/fauxton/app/addons/documents/routes.js b/src/fauxton/app/addons/documents/routes.js index 699a496ba..5e8834ff8 100644 --- a/src/fauxton/app/addons/documents/routes.js +++ b/src/fauxton/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); } } diff --git a/src/fauxton/app/addons/documents/templates/advanced_options.html b/src/fauxton/app/addons/documents/templates/advanced_options.html index d8d57cd7c..55c59462e 100644 --- a/src/fauxton/app/addons/documents/templates/advanced_options.html +++ b/src/fauxton/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"> diff --git a/src/fauxton/app/addons/documents/views.js b/src/fauxton/app/addons/documents/views.js index cc23e195e..351b2b0e2 100644 --- a/src/fauxton/app/addons/documents/views.js +++ b/src/fauxton/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); diff --git a/src/fauxton/app/addons/fauxton/components.js b/src/fauxton/app/addons/fauxton/components.js index 25f623c27..47f4726c0 100644 --- a/src/fauxton/app/addons/fauxton/components.js +++ b/src/fauxton/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) { diff --git a/src/fauxton/app/config.js b/src/fauxton/app/config.js index 4a2f1368a..edcd9a2a2 100644 --- a/src/fauxton/app/config.js +++ b/src/fauxton/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: '/', diff --git a/src/fauxton/app/core/utils.js b/src/fauxton/app/core/utils.js index 44945e846..6bc8aea01 100644 --- a/src/fauxton/app/core/utils.js +++ b/src/fauxton/app/core/utils.js @@ -12,7 +12,7 @@ // This file creates a set of helper functions that will be loaded for all html -// templates. These functions should be self contained and not rely on any +// templates. These functions should be self contained and not rely on any // external dependencies as they are loaded prior to the application. We may // want to change this later, but for now this should be thought of as a // "purely functional" helper system. @@ -58,7 +58,7 @@ function($, _ ) { addWindowResize: function(fun, key){ onWindowResize[key]=fun; - // You shouldn't need to call it here. Just define it at startup and each time it will loop + // You shouldn't need to call it here. Just define it at startup and each time it will loop // through all the functions in the hash. //app.initWindowResize(); }, @@ -84,7 +84,7 @@ function($, _ ) { safeURLName: function(name){ var testName = name || ""; - var checkforBad = testName.match(/[\$\-/_,+-]/g); + var checkforBad = testName.match(/[\$\-/,+-]/g); return (checkforBad !== null)?encodeURIComponent(name):name; } }; diff --git a/src/fauxton/app/helpers.js b/src/fauxton/app/helpers.js index d4836b909..208b0d9d0 100644 --- a/src/fauxton/app/helpers.js +++ b/src/fauxton/app/helpers.js @@ -12,20 +12,25 @@ // This file creates a set of helper functions that will be loaded for all html -// templates. These functions should be self contained and not rely on any +// templates. These functions should be self contained and not rely on any // external dependencies as they are loaded prior to the application. We may // want to change this later, but for now this should be thought of as a // "purely functional" helper system. define([ + "core/utils", "d3" ], -function() { +function(utils, d3) { var Helpers = {}; + Helpers.removeSpecialCharacters = utils.removeSpecialCharacters; + + Helpers.safeURL = utils.safeURLName; + Helpers.imageUrl = function(path) { // TODO: add dynamic path for different deploy targets return path; @@ -33,7 +38,7 @@ function() { // Get the URL for documentation, wiki, wherever we store it. - // update the URLs in documentation_urls.js + // update the URLs in documentation_urls.js Helpers.docs = { "docs": "http://docs.couchdb.org/en/latest/intro/api.html#documents", "all_dbs": "http://docs.couchdb.org/en/latest/api/server/common.html?highlight=all_dbs#get--_all_dbs", @@ -50,8 +55,8 @@ function() { "config": "http://docs.couchdb.org/en/latest/config/index.html", "views": "http://docs.couchdb.org/en/latest/intro/overview.html#views", "changes": "http://docs.couchdb.org/en/latest/api/database/changes.html?highlight=changes#post--db-_changes" - }; - + }; + Helpers.getDocUrl = function(docKey){ return Helpers.docs[docKey] || '#'; }; @@ -70,7 +75,7 @@ function() { }; Helpers.formatDate = function(timestamp){ - format = d3.time.format("%b. %e at %H:%M%p"); + var format = d3.time.format("%b. %e at %H:%M%p"); return format(new Date(timestamp*1000)); }; diff --git a/src/fauxton/assets/js/plugins/cloudant.pagingcollection.js b/src/fauxton/assets/js/plugins/cloudant.pagingcollection.js new file mode 100644 index 000000000..2ab5eafc3 --- /dev/null +++ b/src/fauxton/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; +})); + |