You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by kx...@apache.org on 2014/06/07 23:04:35 UTC

[11/15] fauxton commit: updated refs/heads/import-master to 8cb432c

Fauxton: Implement bulk deletion for all-docs-listing

Introduce a collection which keeps track of documents that will
deleted using the CouchDB Bulk-update API.

The collection fires events, so the view is noticed.

Closes COUCHDB-2153


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/c50fca5b
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/c50fca5b
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/c50fca5b

Branch: refs/heads/import-master
Commit: c50fca5baf5d65feed90cc56ac6e0be81ff99150
Parents: 4510053
Author: Robert Kowalski <ro...@kowalski.gd>
Authored: Fri May 2 21:39:21 2014 +0200
Committer: Robert Kowalski <ro...@kowalski.gd>
Committed: Sat May 31 22:57:25 2014 +0200

----------------------------------------------------------------------
 app/addons/documents/resources.js               |  90 +++++++++-
 app/addons/documents/routes.js                  |   3 +-
 .../documents/templates/all_docs_item.html      |   4 +-
 .../documents/templates/all_docs_list.html      |   6 +-
 app/addons/documents/tests/resourcesSpec.js     |  83 ++++++++-
 app/addons/documents/views.js                   | 178 ++++++++++++-------
 6 files changed, 290 insertions(+), 74 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/resources.js b/app/addons/documents/resources.js
index 21ee55f..21cdfdd 100644
--- a/app/addons/documents/resources.js
+++ b/app/addons/documents/resources.js
@@ -44,7 +44,7 @@ function(app, FauxtonAPI, PagingCollection) {
     };
   })();
 
-  
+
   Documents.Doc = FauxtonAPI.Model.extend({
     idAttribute: "_id",
     documentation: function(){
@@ -302,6 +302,94 @@ function(app, FauxtonAPI, PagingCollection) {
 
   });
 
+  Documents.BulkDeleteDoc = FauxtonAPI.Model.extend({
+    idAttribute: "_id"
+  });
+
+  Documents.BulkDeleteDocCollection = FauxtonAPI.Collection.extend({
+
+    model: Documents.BulkDeleteDoc,
+
+    sync: function () {
+
+    },
+
+    initialize: function (models, options) {
+      this.databaseId = options.databaseId;
+    },
+
+    bulkDelete: function () {
+      var payload = this.createPayload(this.toJSON()),
+          that = this;
+
+      $.ajax({
+        type: 'POST',
+        url: app.host + '/' + this.databaseId + '/_bulk_docs',
+        contentType: 'application/json',
+        dataType: 'json',
+        data: JSON.stringify(payload),
+      })
+      .then(function (res) {
+        that.handleResponse(res);
+      })
+      .fail(function () {
+        var ids = _.reduce(that.toArray(), function (acc, doc) {
+          acc.push(doc.id);
+          return acc;
+        }, []);
+        that.trigger('error', ids);
+      });
+    },
+
+    handleResponse: function (res) {
+      var ids = _.reduce(res, function (ids, doc) {
+        if (doc.error) {
+          ids.errorIds.push(doc.id);
+        }
+
+        if (doc.ok === true) {
+          ids.successIds.push(doc.id);
+        }
+
+        return ids;
+      }, {errorIds: [], successIds: []});
+
+      this.removeDocuments(ids.successIds);
+
+      if (ids.errorIds.length) {
+        this.trigger('error', ids.errorIds);
+      }
+
+      this.trigger('updated');
+    },
+
+    removeDocuments: function (ids) {
+      var reloadDesignDocs = false;
+      _.each(ids, function (id) {
+        if (/_design/.test(id)) {
+          reloadDesignDocs = true;
+        }
+
+        this.remove(this.get(id));
+      }, this);
+
+      if (reloadDesignDocs) {
+        FauxtonAPI.triggerRouteEvent('reloadDesignDocs');
+      }
+
+      this.trigger('removed', ids);
+    },
+
+    createPayload: function (documents) {
+      var documentList = documents;
+
+      return {
+        docs: documentList
+      };
+    }
+  });
+
+
   Documents.AllDocs = PagingCollection.extend({
     model: Documents.Doc,
     documentation: function(){

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes.js b/app/addons/documents/routes.js
index a24a3bd..6d67dae 100644
--- a/app/addons/documents/routes.js
+++ b/app/addons/documents/routes.js
@@ -224,7 +224,8 @@ function(app, FauxtonAPI, Documents, Databases) {
         database: this.data.database,
         collection: this.data.database.allDocs,
         docParams: docParams,
-        params: urlParams
+        params: urlParams,
+        bulkDeleteDocsCollection: new Documents.BulkDeleteDocCollection([], {databaseId: this.data.database.get('id')})
       }));
 
       this.crumbs = [

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/templates/all_docs_item.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/all_docs_item.html b/app/addons/documents/templates/all_docs_item.html
index bfedaaa..a8ef20f 100644
--- a/app/addons/documents/templates/all_docs_item.html
+++ b/app/addons/documents/templates/all_docs_item.html
@@ -12,13 +12,13 @@ License for the specific language governing permissions and limitations under
 the License.
 -->
 
-<td class="select"><input type="checkbox" class="row-select"></td>
+<td class="select"><input <%- checked ? 'checked="checked"' : '' %> type="checkbox" class="js-row-select"></td>
 <td>
   <div>
     <pre class="prettyprint"><%- doc.prettyJSON() %></pre>
     <% if (doc.isEditable()) { %>
       <div class="btn-group">
-        <a href="#<%= doc.url('web-index') %>" class="btn btn-small edits">Edit <%= doc.docType() %></a>
+        <a href="#<%= doc.url('web-index') %>" class="btn btn-small edits">Edit <%- doc.docType() %></a>
         <button href="#" class="btn btn-small btn-danger delete" title="Delete this document."><i class="icon icon-trash"></i></button>
       </div>
     <% } %>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/templates/all_docs_list.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/all_docs_list.html b/app/addons/documents/templates/all_docs_list.html
index a521ff9..a643427 100644
--- a/app/addons/documents/templates/all_docs_list.html
+++ b/app/addons/documents/templates/all_docs_list.html
@@ -17,7 +17,7 @@ the License.
     <div class="row">
       <div class="btn-toolbar span6">
         <button type="button" class="btn btn-small all" data-toggle="button">✓ All</button>
-        <button class="btn btn-small disabled bulk-delete"><i class="icon-trash"></i></button>
+        <button class="btn btn-small disabled js-bulk-delete"><i class="icon-trash"></i></button>
         <% if (expandDocs) { %>
         <button id="collapse" class="btn btn-small"><i class="icon-minus"></i> Collapse</button>
         <% } else { %>
@@ -32,8 +32,8 @@ the License.
   <table class="all-docs table table-striped table-condensed">
     <tbody></tbody>
   </table>
-  
-  <% if (endOfResults) { %>  
+
+  <% if (endOfResults) { %>
   <div class="text-center well">
     <p class="muted">
       End of results - <a id="js-end-results" href="#query" data-bypass="true" data-toggle="tab">edit query</a>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/resourcesSpec.js b/app/addons/documents/tests/resourcesSpec.js
index e120582..1159360 100644
--- a/app/addons/documents/tests/resourcesSpec.js
+++ b/app/addons/documents/tests/resourcesSpec.js
@@ -10,8 +10,8 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 define([
-      'addons/documents/resources',
-      'testUtils'
+        'addons/documents/resources',
+        'testUtils'
 ], function (Models, testUtils) {
   var assert = testUtils.assert;
 
@@ -50,7 +50,6 @@ define([
       });
 
     });
-
   });
 
   describe('QueryParams', function() {
@@ -148,5 +147,81 @@ define([
       });
     });
   });
-});
 
+  describe('Bulk Delete', function () {
+    var databaseId = 'ente',
+        collection,
+        values;
+
+    values = [{
+      _id: '1',
+      _rev: '1234561',
+      _deleted: true
+    },
+    {
+      _id: '2',
+      _rev: '1234562',
+      _deleted: true
+    },
+    {
+      _id: '3',
+      _rev: '1234563',
+      _deleted: true
+    }];
+
+    beforeEach(function () {
+      collection = new Models.BulkDeleteDocCollection(values, {
+        databaseId: databaseId
+      });
+    });
+
+    it("contains the models", function () {
+      collection = new Models.BulkDeleteDocCollection(values, {
+        databaseId: databaseId
+      });
+
+      assert.equal(collection.length, 3);
+    });
+
+    it("clears the memory if no errors happened", function () {
+      collection.handleResponse([
+        {"ok":true,"id":"1","rev":"10-72cd2edbcc0d197ce96188a229a7af01"},
+        {"ok":true,"id":"2","rev":"6-da537822b9672a4b2f42adb1be04a5b1"}
+      ]);
+
+      assert.equal(collection.length, 1);
+    });
+
+    it("triggers a removed event with all ids", function () {
+      collection.listenToOnce(collection, 'removed', function (ids) {
+        assert.deepEqual(ids, ['Deferred', 'DeskSet']);
+      });
+
+      collection.handleResponse([
+        {"ok":true,"id":"Deferred","rev":"10-72cd2edbcc0d197ce96188a229a7af01"},
+        {"ok":true,"id":"DeskSet","rev":"6-da537822b9672a4b2f42adb1be04a5b1"}
+      ]);
+    });
+
+    it("triggers a error event with all errored ids", function () {
+      collection.listenToOnce(collection, 'error', function (ids) {
+        assert.deepEqual(ids, ['Deferred']);
+      });
+      collection.handleResponse([
+        {"error":"confclict","id":"Deferred","rev":"10-72cd2edbcc0d197ce96188a229a7af01"},
+        {"ok":true,"id":"DeskSet","rev":"6-da537822b9672a4b2f42adb1be04a5b1"}
+      ]);
+    });
+
+    it("removes successfull deleted from the collection but keeps one with errors", function () {
+      collection.handleResponse([
+        {"error":"confclict","id":"1","rev":"10-72cd2edbcc0d197ce96188a229a7af01"},
+        {"ok":true,"id":"2","rev":"6-da537822b9672a4b2f42adb1be04a5b1"},
+        {"error":"conflict","id":"3","rev":"6-da537822b9672a4b2f42adb1be04a5b1"}
+      ]);
+      assert.ok(collection.get('1'));
+      assert.ok(collection.get('3'));
+      assert.notOk(collection.get('2'));
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/c50fca5b/app/addons/documents/views.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/views.js b/app/addons/documents/views.js
index 87bc7ae..97f82b4 100644
--- a/app/addons/documents/views.js
+++ b/app/addons/documents/views.js
@@ -33,6 +33,15 @@ define([
 
 function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
          resizeColumns, beautify, prettify, ZeroClipboard) {
+
+  function showError (msg) {
+    FauxtonAPI.addNotification({
+      msg: msg,
+      type: 'error',
+      clear:  true
+    });
+  }
+
   var Views = {};
 
   Views.SearchBox = FauxtonAPI.View.extend({
@@ -243,6 +252,10 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
     tagName: "tr",
     className: "all-docs-item",
 
+    initialize: function (options) {
+      this.checked = options.checked;
+    },
+
     events: {
       "click button.delete": "destroy",
       "dblclick pre.prettyprint": "edit"
@@ -256,7 +269,8 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
 
     serialize: function() {
       return {
-        doc: this.model
+        doc: this.model,
+        checked: this.checked
       };
     },
 
@@ -279,7 +293,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
 
       this.model.destroy().then(function(resp) {
         FauxtonAPI.addNotification({
-          msg: "Succesfully destroyed your doc",
+          msg: "Succesfully deleted your doc",
           clear:  true
         });
         that.$el.fadeOut(function () {
@@ -292,7 +306,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
         }
       }, function(resp) {
         FauxtonAPI.addNotification({
-          msg: "Failed to destroy your doc!",
+          msg: "Failed to deleted your doc!",
           type: "error",
           clear:  true
         });
@@ -499,29 +513,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
     template: "addons/documents/templates/all_docs_list",
     events: {
       "click button.all": "selectAll",
-      "click button.bulk-delete": "bulkDelete",
+      "click button.js-bulk-delete": "bulkDelete",
       "click #collapse": "collapse",
-      "change .row-select":"toggleTrash",
+      "click .all-docs-item": "toggleDocument",
       "click #js-end-results": "scrollToQuery"
     },
 
-    toggleTrash: function () {
-      if (this.$('.row-select:checked').length > 0) {
-        this.$('.bulk-delete').removeClass('disabled');
-      } else {
-        this.$('.bulk-delete').addClass('disabled');
-      }
-    },
-
-    scrollToQuery: function () {
-      $('#dashboard-content').animate({ scrollTop: 0 }, 'slow');
-    },
-
-    initialize: function(options){
+    initialize: function (options) {
       this.nestedView = options.nestedView || Views.Document;
       this.rows = {};
-      this.viewList = !! options.viewList;
-      this.database = options.database;
+      this.viewList = !!options.viewList;
 
       if (options.ddocInfo) {
         this.designDocs = options.ddocInfo.designDocs;
@@ -532,6 +533,74 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
       this.params = options.params || {};
       this.expandDocs = true;
       this.perPageDefault = this.docParams.limit || 20;
+
+      // some doclists don't have an option to delete
+      if (!this.viewList) {
+        this.bulkDeleteDocsCollection = options.bulkDeleteDocsCollection;
+      }
+    },
+
+    removeDocuments: function (ids) {
+      _.each(ids, function (id) {
+        this.removeDocument(id);
+      }, this);
+
+      this.pagination.updatePerPage(parseInt(this.$('#select-per-page :selected').val(), 10));
+      FauxtonAPI.triggerRouteEvent('perPageChange', this.pagination.documentsLeftToFetch());
+    },
+
+    removeDocument: function (id) {
+      var that = this;
+
+      if (!this.rows[id]) {
+        return;
+      }
+
+      this.rows[id].$el.fadeOut('slow', function () {
+        that.rows[id].remove();
+      });
+    },
+
+    showError: function (ids) {
+      if (ids) {
+        showError('Failed to delete: ' + ids.join(', '));
+        return;
+      }
+
+      showError('Failed to delete your doc!');
+    },
+
+    toggleDocument: function (event) {
+      var $row = this.$(event.target).closest('tr'),
+          docId = $row.attr('data-id'),
+          db = this.database.get('id'),
+          rev = this.collection.get(docId).get('_rev'),
+          data = {_id: docId, _rev: rev, _deleted: true};
+
+      if (!$row.hasClass('js-to-delete')) {
+        this.bulkDeleteDocsCollection.add(data);
+      } else {
+        this.bulkDeleteDocsCollection.remove(this.bulkDeleteDocsCollection.get(docId));
+      }
+
+      $row.find('.js-row-select').prop('checked', !$row.hasClass('js-to-delete'));
+      $row.toggleClass('js-to-delete');
+
+      this.toggleTrash();
+    },
+
+    toggleTrash: function () {
+      var $bulkdDeleteButton = this.$('.js-bulk-delete');
+
+      if (this.bulkDeleteDocsCollection.length > 0) {
+        $bulkdDeleteButton.removeClass('disabled');
+      } else {
+        $bulkdDeleteButton.addClass('disabled');
+      }
+    },
+
+    scrollToQuery: function () {
+      $('#dashboard-content').animate({ scrollTop: 0 }, 'slow');
     },
 
     establish: function() {
@@ -551,7 +620,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
 
           //now redirect back to alldocs
           FauxtonAPI.navigate(model.database.url("index") + "?limit=100");
-          console.log("ERROR: ", arguments);
         }
       });
     },
@@ -580,48 +648,17 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
       this.render();
     },
 
-    /*
-     * TODO: this should be reconsidered
-     * This currently performs delete operations on the model level,
-     * when we could be using bulk docs with _deleted = true. Using
-     * individual models is cleaner from a backbone standpoint, but
-     * not from the couchdb api.
-     * Also, the delete method is naive and leaves the body intact,
-     * when we should switch the doc to only having id/rev/deleted.
-     */
     bulkDelete: function() {
-      var that = this;
-      // yuck, data binding ftw?
-      var eles = this.$el.find("input.row-select:checked")
-                         .parents("tr.all-docs-item")
-                         .map(function(e) { return $(this).attr("data-id"); })
-                         .get();
+      var that = this,
+          documentsLength = this.bulkDeleteDocsCollection.length,
+          msg;
 
-      if (eles.length === 0 || !window.confirm("Are you sure you want to delete these " + eles.length + " docs?")) {
+      msg = "Are you sure you want to delete these " + documentsLength + " docs?";
+      if (documentsLength === 0 || !window.confirm(msg)) {
         return false;
       }
 
-      _.each(eles, function(ele) {
-        var model = this.collection.get(ele);
-
-        model.destroy().then(function(resp) {
-          that.rows[ele].$el.fadeOut(function () {
-            $(this).remove();
-          });
-
-          model.collection.remove(model.id);
-          if (!!model.id.match('_design')) {
-            FauxtonAPI.triggerRouteEvent('reloadDesignDocs');
-          }
-          that.$('.bulk-delete').addClass('disabled');
-        }, function(resp) {
-          FauxtonAPI.addNotification({
-            msg: "Failed to destroy your doc!",
-            type: "error",
-            clear:  true
-          });
-        });
-      }, this);
+      this.bulkDeleteDocsCollection.bulkDelete();
     },
 
     addPagination: function () {
@@ -640,6 +677,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
     },
 
     beforeRender: function() {
+      var docs;
 
       if (!this.pagination) {
         this.addPagination();
@@ -658,11 +696,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
 
       this.setView('#item-numbers', this.allDocsNumber);
 
-      var docs = this.expandDocs ? this.collection : this.collection.simple();
+      docs = this.expandDocs ? this.collection : this.collection.simple();
 
       docs.each(function(doc) {
+        var isChecked;
+        if (this.bulkDeleteDocsCollection) {
+          isChecked = this.bulkDeleteDocsCollection.get(doc.id);
+        }
         this.rows[doc.id] = this.insertView("table.all-docs tbody", new this.nestedView({
-          model: doc
+          model: doc,
+          checked: isChecked
         }));
       }, this);
     },
@@ -683,8 +726,17 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
       }
     },
 
-    afterRender: function(){
+    afterRender: function () {
       prettyPrint();
+
+      if (this.bulkDeleteDocsCollection) {
+        this.stopListening(this.bulkDeleteDocsCollection);
+        this.listenTo(this.bulkDeleteDocsCollection, 'error', this.showError);
+        this.listenTo(this.bulkDeleteDocsCollection, 'removed', this.removeDocuments);
+        this.listenTo(this.bulkDeleteDocsCollection, 'updated', this.toggleTrash);
+      }
+
+      this.toggleTrash();
     },
 
     perPage: function () {
@@ -731,13 +783,13 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
 
       this.model.destroy().then(function(resp) {
         FauxtonAPI.addNotification({
-          msg: "Succesfully destroyed your doc",
+          msg: "Succesfully deleted your doc",
           clear:  true
         });
         FauxtonAPI.navigate(database.url("index"));
       }, function(resp) {
         FauxtonAPI.addNotification({
-          msg: "Failed to destroy your doc!",
+          msg: "Failed to delete your doc!",
           type: "error",
           clear:  true
         });