couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From beno...@apache.org
Subject [32/57] [abbrv] [partial] inital move to rebar compilation
Date Tue, 07 Jan 2014 00:36:52 GMT
http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/fauxton/app/modules/documents/resources.js
----------------------------------------------------------------------
diff --git a/apps/fauxton/app/modules/documents/resources.js b/apps/fauxton/app/modules/documents/resources.js
new file mode 100644
index 0000000..83589ca
--- /dev/null
+++ b/apps/fauxton/app/modules/documents/resources.js
@@ -0,0 +1,638 @@
+// 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.
+
+define([
+  "app",
+  "api"
+],
+
+function(app, FauxtonAPI) {
+  var Documents = app.module();
+
+  Documents.Doc = Backbone.Model.extend({
+    idAttribute: "_id",
+    documentation: function(){
+      return "docs";
+    },
+    url: function(context) {
+      if (context === "app") {
+        return this.getDatabase().url("app") + "/" + this.safeID();
+      } else if (context === "web-index") {
+        return this.getDatabase().url("app") + "/" + app.mixins.safeURLName(this.id);
+      } else if (context === "apiurl"){
+        return window.location.origin + "/" + this.getDatabase().safeID() + "/" + this.safeID();
+      } else {
+        return app.host + "/" + this.getDatabase().safeID() + "/" + this.safeID();
+      }
+    },
+
+    initialize: function(_attrs, options) {
+      if (this.collection && this.collection.database) {
+        this.database = this.collection.database;
+      } else if (options.database) {
+        this.database = options.database;
+      }
+    },
+
+    // HACK: the doc needs to know about the database, but it may be
+    // set directly or indirectly in all docs
+    getDatabase: function() {
+      return this.database ? this.database : this.collection.database;
+    },
+
+    validate: function(attrs, options) {
+      if (this.id && this.id !== attrs._id && this.get('_rev') ) {
+        return "Cannot change a documents id.";
+      }
+    },
+
+    docType: function() {
+      return this.id.match(/^_design/) ? "design doc" : "doc";
+    },
+
+    isEditable: function() {
+      return this.docType() != "reduction";
+    },
+
+    isDdoc: function() {
+      return this.docType() === "design doc";
+    },
+
+    hasViews: function() {
+      if (!this.isDdoc()) return false;
+      var doc = this.get('doc');
+      if (doc) {
+        return doc && doc.views && _.keys(doc.views).length > 0;
+      }
+
+      var views = this.get('views');
+      return views && _.keys(views).length > 0;
+    },
+
+    hasAttachments: function () {
+      return !!this.get('_attachments');
+    },
+
+    getDdocView: function(view) {
+      if (!this.isDdoc() || !this.hasViews()) return false;
+
+      var doc = this.get('doc');
+      if (doc) {
+        return doc.views[view];
+      }
+
+      return this.get('views')[view];
+    },
+
+    setDdocView: function (view, map, reduce) {
+      if (!this.isDdoc()) return false;
+      var views = this.get('views');
+          tempView = views[view] || {};
+
+      if (reduce) {
+        tempView.reduce=reduce;
+      } else {
+        delete tempView.reduce;
+      }
+      tempView.map= map;
+
+      views[view] = tempView;
+      this.set({views: views});
+
+      return true;
+    },
+
+    removeDdocView: function (viewName) {
+      if (!this.isDdoc()) return false;
+      var views = this.get('views');
+
+      delete views[viewName];
+      this.set({views: views});
+    },
+
+    dDocModel: function () {
+      if (!this.isDdoc()) return false;
+      var doc = this.get('doc');
+
+      if (doc) {
+        return new Documents.Doc(doc, {database: this.database});
+      } 
+
+      return this;
+    },
+
+    viewHasReduce: function(viewName) {
+      var view = this.getDdocView(viewName);
+
+      return view && view.reduce;
+    },
+
+    // Need this to work around backbone router thinking _design/foo
+    // is a separate route. Alternatively, maybe these should be
+    // treated separately. For instance, we could default into the
+    // json editor for docs, or into a ddoc specific page.
+    safeID: function() {
+      if (this.isDdoc()){
+        var ddoc = this.id.replace(/^_design\//,"");
+        return "_design/"+app.mixins.safeURLName(ddoc);
+      }else{
+        return app.mixins.safeURLName(this.id);
+      }
+    },
+
+    destroy: function() {
+      var url = this.url() + "?rev=" + this.get('_rev');
+      return $.ajax({
+        url: url,
+        dataType: 'json',
+        type: 'DELETE'
+      });
+    },
+
+    parse: function(resp) {
+      if (resp.rev) {
+        resp._rev = resp.rev;
+        delete resp.rev;
+      }
+      if (resp.id) {
+        if (typeof(this.id) === "undefined") {
+          resp._id = resp.id;
+        }
+        delete resp.id;
+      }
+      if (resp.ok) {
+        delete resp.ok;
+      }
+      return resp;
+    },
+
+    prettyJSON: function() {
+      var data = this.get("doc") ? this.get("doc") : this;
+
+      return JSON.stringify(data, null, "  ");
+    },
+
+    copy: function (copyId) {
+      return $.ajax({
+        type: 'COPY',
+        url: '/' + this.database.safeID() + '/' + this.safeID(),
+        headers: {Destination: copyId}
+      });
+    },
+
+    isNewDoc: function () {
+      return this.get('_rev') ? false : true;
+    }
+  });
+
+  Documents.DdocInfo = Backbone.Model.extend({
+    idAttribute: "_id",
+    documentation: function(){
+      return "docs";
+    },
+    initialize: function (_attrs, options) {
+      this.database = options.database;
+    },
+
+    url: function(context) {
+      if (context === "app") {
+        return this.database.url("app") + "/" + this.safeID() + '/_info';
+      } else if (context === "apiurl"){
+        return window.location.origin + "/" + this.database.safeID() + "/" + this.safeID() + '/_info';
+      } else {
+        return app.host + "/" + this.database.safeID() + "/" + this.safeID() + '/_info';
+      }
+    },
+
+    // Need this to work around backbone router thinking _design/foo
+    // is a separate route. Alternatively, maybe these should be
+    // treated separately. For instance, we could default into the
+    // json editor for docs, or into a ddoc specific page.
+    safeID: function() {
+      var ddoc = this.id.replace(/^_design\//,"");
+      return "_design/"+app.mixins.safeURLName(ddoc);
+    }
+
+  });
+
+  Documents.ViewRow = Backbone.Model.extend({
+    docType: function() {
+      if (!this.id) return "reduction";
+
+      return this.id.match(/^_design/) ? "design doc" : "doc";
+    },
+    documentation: function(){
+      return "docs";
+    },
+    url: function(context) {
+      if (!this.isEditable()) return false;
+
+      return this.collection.database.url(context) + "/" + this.safeID();
+    },
+
+    isEditable: function() {
+      return this.docType() != "reduction";
+    },
+    safeID: function() {
+      return app.mixins.safeURLName(this.id);
+    },
+
+    prettyJSON: function() {
+      //var data = this.get("doc") ? this.get("doc") : this;
+      return JSON.stringify(this, null, "  ");
+    }
+  });
+
+  Documents.NewDoc = Documents.Doc.extend({
+    fetch: function() {
+      var uuid = new FauxtonAPI.UUID();
+      var deferred = this.deferred = $.Deferred();
+      var that = this;
+
+      uuid.fetch().done(function() {
+        that.set("_id", uuid.next());
+        deferred.resolve();
+      });
+
+      return deferred.promise();
+    }
+
+  });
+
+  Documents.AllDocs = Backbone.Collection.extend({
+    model: Documents.Doc,
+    documentation: function(){
+      return "docs";
+    },
+    initialize: function(_models, options) {
+      this.database = options.database;
+      this.params = options.params;
+      this.skipFirstItem = false;
+
+      this.on("remove",this.decrementTotalRows , this);
+    },
+
+    url: function(context) {
+      var query = "";
+      if (this.params) {
+        query = "?" + $.param(this.params);
+      }
+
+      if (context === 'app') {
+        return 'database/' + this.database.safeID() + "/_all_docs" + query;
+      } else if (context === "apiurl"){
+        return window.location.origin + "/" + this.database.safeID() + "/_all_docs" + query;
+      } else {
+        return app.host + "/" + this.database.safeID() + "/_all_docs" + query;
+      }
+    },
+
+    simple: function () {
+      var docs = this.map(function (item) {
+        return {
+          _id: item.id,
+          _rev: item.get('_rev'),
+        };
+      });
+
+      return new Documents.AllDocs(docs, {
+        database: this.database,
+        params: this.params
+      });
+    },
+
+    urlNextPage: function (num, lastId) {
+      if (!lastId) {
+        var doc = this.last();
+
+        if (doc) {
+          lastId = doc.id;
+        } else {
+          lastId = '';
+        }
+      }
+
+      this.params.startkey_docid = '"' + lastId + '"';
+      this.params.startkey = '"' + lastId + '"';
+      // when paginating forward, fetch 21 and don't show
+      // the first item as it was the last item in the previous list
+      this.params.limit = num + 1;
+      return this.url('app');
+    },
+
+    urlPreviousPage: function (num, firstId) {
+      this.params.limit = num;
+      if (firstId) { 
+        this.params.startkey_docid = '"' + firstId + '"';
+        this.params.startkey = '"' + firstId + '"';
+      } else {
+        delete this.params.startkey;
+        delete this.params.startkey_docid;
+      }
+      return this.url('app');
+    },
+
+    totalRows: function() {
+      return this.viewMeta.total_rows || "unknown";
+    },
+
+    decrementTotalRows: function () {
+      if (this.viewMeta.total_rows) {
+        this.viewMeta.total_rows = this.viewMeta.total_rows -1;
+        this.trigger('totalRows:decrement');
+      }
+    },
+
+    updateSeq: function() {
+      return this.viewMeta.update_seq || false;
+    },
+
+    recordStart: function () {
+      if (this.viewMeta.offset === 0) {
+        return 1;
+      }
+
+      if (this.skipFirstItem) {
+        return this.viewMeta.offset + 2;
+      }
+
+      return this.viewMeta.offset + 1;
+    },
+
+    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);
+      }
+      return _.map(rows, function(row) {
+        return {
+          _id: row.id,
+          _rev: row.value.rev,
+          value: row.value,
+          key: row.key,
+          doc: row.doc || undefined
+        };
+      });
+    }
+  });
+
+  Documents.IndexCollection = Backbone.Collection.extend({
+    model: Documents.ViewRow,
+    documentation: function(){
+      return "docs";
+    },
+    initialize: function(_models, options) {
+      this.database = options.database;
+      this.params = _.extend({limit: 20, reduce: false}, options.params);
+      this.idxType = "_view";
+      this.view = options.view;
+      this.design = options.design.replace('_design/','');
+      this.skipFirstItem = false;
+    },
+
+    url: function(context) {
+      var query = "";
+      if (this.params) {
+        query = "?" + $.param(this.params);
+      }
+      
+      var startOfUrl = app.host;
+      if (context === 'app') {
+        startOfUrl = 'database';
+      } else if (context === "apiurl"){
+        startOfUrl = window.location.origin;
+      }
+      var design = app.mixins.safeURLName(this.design),
+          view = app.mixins.safeURLName(this.view);
+
+      var url = [startOfUrl, this.database.safeID(), "_design", design, this.idxType, view];
+      return url.join("/") + query;
+    },
+
+    urlNextPage: function (num, lastId) {
+      if (!lastId) {
+        lastId = this.last().id;
+      }
+
+      this.params.startkey_docid = '"' + lastId + '"';
+      this.params.startkey = '"' + lastId + '"';
+      this.params.limit = num;
+      return this.url('app');
+    },
+
+     urlPreviousPage: function (num, firstId) {
+      this.params.limit = num;
+      if (firstId) { 
+        this.params.startkey_docid = '"' + firstId + '"';
+        this.params.startkey = '"' + firstId + '"';
+      } else {
+        delete this.params.startkey;
+        delete this.params.startkey_docid;
+      }
+      return this.url('app');
+    },
+
+    recordStart: function () {
+      if (this.viewMeta.offset === 0) {
+        return 1;
+      }
+
+      if (this.skipFirstItem) {
+        return this.viewMeta.offset + 2;
+      }
+
+      return this.viewMeta.offset + 1;
+    },
+
+    totalRows: function() {
+      return this.viewMeta.total_rows || "unknown";
+    },
+
+    updateSeq: function() {
+      return this.viewMeta.update_seq || false;
+    },
+
+    simple: function () {
+      var docs = this.map(function (item) {
+        return {
+          _id: item.id,
+          key: item.get('key'),
+          value: item.get('value')
+        };
+      });
+
+      return new Documents.IndexCollection(docs, {
+        database: this.database,
+        params: this.params,
+        view: this.view,
+        design: this.design
+      });
+    },
+
+    parse: function(resp) {
+      var rows = resp.rows;
+      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
+        };
+      });
+    },
+
+    buildAllDocs: function(){
+      this.fetch();
+    },
+
+    // We implement our own fetch to store the starttime so we that
+    // we can get the request duration
+    fetch: function () {
+      this.startTime = new Date().getTime();
+      return Backbone.Collection.prototype.fetch.call(this);
+    },
+
+    allDocs: function(){
+      return this.models;
+    },
+
+    // This is taken from futon.browse.js $.timeString
+    requestDurationInString: function () {
+      var ms, sec, min, h, timeString, milliseconds = this.requestDuration;
+
+      sec = Math.floor(milliseconds / 1000.0);
+      min = Math.floor(sec / 60.0);
+      sec = (sec % 60.0).toString();
+      if (sec.length < 2) {
+         sec = "0" + sec;
+      }
+
+      h = (Math.floor(min / 60.0)).toString();
+      if (h.length < 2) {
+        h = "0" + h;
+      }
+
+      min = (min % 60.0).toString();
+      if (min.length < 2) {
+        min = "0" + min;
+      }
+
+      timeString = h + ":" + min + ":" + sec;
+
+      ms = (milliseconds % 1000.0).toString();
+      while (ms.length < 3) {
+        ms = "0" + ms;
+      }
+      timeString += "." + ms;
+
+      return timeString;
+    }
+  });
+
+  
+  Documents.PouchIndexCollection = Backbone.Collection.extend({
+    model: Documents.ViewRow,
+    documentation: function(){
+      return "docs";
+    },
+    initialize: function(_models, options) {
+      this.database = options.database;
+      this.rows = options.rows;
+      this.view = options.view;
+      this.design = options.design.replace('_design/','');
+      this.params = _.extend({limit: 20, reduce: false}, options.params);
+      this.idxType = "_view";
+    },
+
+    url: function () {
+      return '';
+    },
+
+    simple: function () {
+      var docs = this.map(function (item) {
+        return {
+          _id: item.id,
+          key: item.get('key'),
+          value: item.get('value')
+        };
+      });
+
+      return new Documents.PouchIndexCollection(docs, {
+        database: this.database,
+        params: this.params,
+        view: this.view,
+        design: this.design,
+        rows: this.rows
+      });
+
+    },
+
+    fetch: function() {
+      var deferred = FauxtonAPI.Deferred();
+      this.reset(this.rows, {silent: true});
+
+      this.viewMeta = {
+        total_rows: this.rows.length,
+        offset: 0,
+        update_seq: false
+      };
+
+      deferred.resolve();
+      return deferred;
+    },
+
+    recordStart: function () {
+      return 1;
+    },
+
+    totalRows: function() {
+      return this.viewMeta.total_rows || "unknown";
+    },
+
+    updateSeq: function() {
+      return this.viewMeta.update_seq || false;
+    },
+
+    buildAllDocs: function(){
+      this.fetch();
+    },
+
+    allDocs: function(){
+      return this.models;
+    }
+  });
+
+
+
+  return Documents;
+});

http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/fauxton/app/modules/documents/routes.js
----------------------------------------------------------------------
diff --git a/apps/fauxton/app/modules/documents/routes.js b/apps/fauxton/app/modules/documents/routes.js
new file mode 100644
index 0000000..538cd56
--- /dev/null
+++ b/apps/fauxton/app/modules/documents/routes.js
@@ -0,0 +1,408 @@
+// 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.
+
+define([
+       "app",
+
+       "api",
+
+       // Modules
+       "modules/documents/views",
+       "modules/databases/base"
+],
+
+function(app, FauxtonAPI, Documents, Databases) {
+
+  var DocEditorRouteObject = FauxtonAPI.RouteObject.extend({
+    layout: "one_pane",
+    disableLoader: true,
+    selectedHeader: "Databases",
+    initialize: function(route, masterLayout, options) {
+      var databaseName = options[0];
+      this.docID = options[1]||'new';
+
+      this.database = this.database || new Databases.Model({id: databaseName});
+      this.doc = new Documents.Doc({
+        _id: this.docID
+      }, {
+        database: this.database
+      });
+
+      this.tabsView = this.setView("#tabs", new Documents.Views.FieldEditorTabs({
+        disableLoader: true,
+        selected: "code_editor",
+        model: this.doc
+      }));
+
+    },
+
+    routes: {
+      // We are hiding the field_editor for this first release
+      // "database/:database/:doc/field_editor": "field_editor",
+      "database/:database/:doc/code_editor": "code_editor",
+      "database/:database/:doc": "code_editor"
+    },
+
+    events: {
+      "route:reRenderDoc": "reRenderDoc",
+      "route:duplicateDoc": "duplicateDoc"
+    },
+
+    crumbs: function() {
+      return [
+        {"name": this.database.id, "link": Databases.databaseUrl(this.database)},
+        {"name": this.docID, "link": "#"}
+      ];
+    },
+
+    code_editor: function (database, doc) {
+      this.tabsView.updateSelected('code_editor');
+
+      this.docView = this.setView("#dashboard-content", new Documents.Views.Doc({
+        model: this.doc,
+        database: this.database
+      }));
+    },
+
+    reRenderDoc: function () {
+      this.docView.forceRender();
+    },
+
+    field_editor: function(events) {
+      this.tabsView.updateSelected('field_editor');
+      this.docView = this.setView("#dashboard-content", new Documents.Views.DocFieldEditor({
+        model: this.doc
+      }));
+    },
+
+    duplicateDoc: function (newId) {
+      var doc = this.doc,
+      docView = this.docView,
+      database = this.database;
+
+      doc.copy(newId).then(function () {
+        doc.set({_id: newId}); 
+        docView.forceRender();
+        FauxtonAPI.navigate('/database/' + database.safeID() + '/' + app.mixins.safeURLName(newId), {trigger: true});
+        FauxtonAPI.addNotification({
+          msg: "Document has been duplicated."
+        });
+
+      }, function (error) {
+        var errorMsg = "Could not duplicate document, reason: " + error.responseText + ".";
+        FauxtonAPI.addNotification({
+          msg: errorMsg,
+          type: "error"
+        });
+      });
+    },
+
+    apiUrl: function() {
+      return [this.doc.url("apiurl"), this.doc.documentation()];
+    }
+  });
+
+  var NewDocEditorRouteObject = DocEditorRouteObject.extend({
+    initialize: function (route, masterLayout, options) {
+      var databaseName = options[0];
+
+      this.database = this.database || new Databases.Model({id: databaseName});
+      this.doc = new Documents.NewDoc(null,{
+        database: this.database
+      });
+
+      this.tabsView = this.setView("#tabs", new Documents.Views.FieldEditorTabs({
+        selected: "code_editor",
+        model: this.doc
+      }));
+
+    },
+    crumbs: function() {
+      return [
+        {"name": this.database.id, "link": Databases.databaseUrl(this.database)},
+        {"name": "New", "link": "#"}
+      ];
+    },
+    routes: {
+      "database/:database/new": "code_editor"
+    },
+    selectedHeader: "Databases",
+
+  });
+
+  var DocumentsRouteObject = FauxtonAPI.RouteObject.extend({
+    layout: "with_tabs_sidebar",
+    selectedHeader: "Databases",
+    routes: {
+      "database/:database/_all_docs(:extra)": "allDocs", 
+      "database/:database/_design/:ddoc/_view/:view": {
+        route: "viewFn",
+        roles: ['_admin']
+      },
+      "database/:database/new_view": "newViewEditor"
+    },
+
+    events: {
+      "route:updateAllDocs": "updateAllDocsFromView",
+      "route:updatePreviewDocs": "updateAllDocsFromPreview",
+      "route:reloadDesignDocs": "reloadDesignDocs",
+      "route:paginate": "paginate"
+    },
+
+    initialize: function (route, masterLayout, options) {
+      var docOptions = app.getParams();
+      docOptions.include_docs = true;
+
+      this.databaseName = options[0];
+
+      this.data = {
+        database: new Databases.Model({id:this.databaseName})
+      };
+
+      this.data.designDocs = new Documents.AllDocs(null, {
+        database: this.data.database,
+        params: {startkey: '"_design"',
+          endkey: '"_design1"',
+          include_docs: true}
+      });
+
+      this.sidebar = this.setView("#sidebar-content", new Documents.Views.Sidebar({
+        collection: this.data.designDocs,
+        database: this.data.database
+      }));
+    },
+
+    establish: function () {
+      return this.data.designDocs.fetch();
+    },
+
+    allDocs: function(databaseName, options) {
+      var docOptions = app.getParams(options);
+
+      docOptions.include_docs = true;
+      this.data.database.buildAllDocs(docOptions);
+
+      if (docOptions.startkey && docOptions.startkey.indexOf('_design') > -1) {
+        this.sidebar.setSelectedTab('design-docs');
+      } else {
+        this.sidebar.setSelectedTab('all-docs');
+      }
+
+      if (this.viewEditor) { this.viewEditor.remove(); }
+
+      this.toolsView = this.setView("#dashboard-upper-menu", new Documents.Views.JumpToDoc({
+        database: this.data.database,
+        collection: this.data.database.allDocs
+      }));
+
+      this.setView("#dashboard-upper-content", new Documents.Views.AllDocsLayout({
+        database: this.data.database,
+        collection: this.data.database.allDocs,
+        params: docOptions
+      }));
+
+      this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
+        collection: this.data.database.allDocs
+      }));
+
+      this.crumbs = [
+        {"name": this.data.database.id, "link": Databases.databaseUrl(this.data.database)}
+      ];
+
+      this.apiUrl = [this.data.database.allDocs.url("apiurl"), this.data.database.allDocs.documentation() ];
+    },
+
+    viewFn: function (databaseName, ddoc, view) {
+      var params = app.getParams(),
+          decodeDdoc = decodeURIComponent(ddoc);
+
+      view = view.replace(/\?.*$/,'');
+
+      this.data.indexedDocs = new Documents.IndexCollection(null, {
+        database: this.data.database,
+        design: decodeDdoc,
+        view: view,
+        params: params
+      });
+
+
+
+      var ddocInfo = {
+        id: "_design/" + decodeDdoc,
+        currView: view,
+        designDocs: this.data.designDocs
+      };
+
+      this.viewEditor = this.setView("#dashboard-upper-content", new Documents.Views.ViewEditor({
+        model: this.data.database,
+        ddocs: this.data.designDocs,
+        viewName: view,
+        params: params,
+        newView: false,
+        database: this.data.database,
+        ddocInfo: ddocInfo
+      }));
+
+      if (this.toolsView) { this.toolsView.remove(); }
+
+      this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
+        database: this.data.database,
+        collection: this.data.indexedDocs,
+        nestedView: Documents.Views.Row,
+        viewList: true,
+        ddocInfo: ddocInfo
+      }));
+
+      this.sidebar.setSelectedTab(app.mixins.removeSpecialCharacters(ddoc) + '_' + app.mixins.removeSpecialCharacters(view));
+
+      this.crumbs = function () {
+        return [
+          {"name": this.data.database.id, "link": Databases.databaseUrl(this.data.database)},
+        ];
+      };
+
+      this.apiUrl = [this.data.indexedDocs.url("apiurl"), "docs"];
+    },
+
+    newViewEditor: function () {
+      var params = app.getParams();
+
+      if (this.toolsView) {
+        this.toolsView.remove();
+      }
+
+      this.viewEditor = this.setView("#dashboard-upper-content", new Documents.Views.ViewEditor({
+        ddocs: this.data.designDocs,
+        params: params,
+        database: this.data.database,
+        newView: true
+      }));
+
+      this.sidebar.setSelectedTab('new-view');
+      this.crumbs = function () {
+        return [
+          {"name": this.data.database.id, "link": Databases.databaseUrl(this.data.database)},
+        ];
+      };
+    },
+
+    updateAllDocsFromView: function (event) {
+      var view = event.view,
+          docOptions = app.getParams(),
+          ddoc = event.ddoc;
+
+      if (event.allDocs) {
+        docOptions.include_docs = true;
+        this.data.database.buildAllDocs(docOptions);
+        return;
+      }
+
+      this.data.indexedDocs = new Documents.IndexCollection(null, {
+        database: this.data.database,
+        design: ddoc,
+        view: view,
+        params: app.getParams()
+      });
+
+      this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
+        database: this.data.database,
+        collection: this.data.indexedDocs,
+        nestedView: Documents.Views.Row,
+        viewList: true
+      }));
+    },
+
+    updateAllDocsFromPreview: function (event) {
+      var view = event.view,
+      rows = event.rows,
+      ddoc = event.ddoc;
+
+      this.data.indexedDocs = new Documents.PouchIndexCollection(null, {
+        database: this.data.database,
+        design: ddoc,
+        view: view,
+        rows: rows
+      });
+
+      this.documentsView = this.setView("#dashboard-lower-content", new Documents.Views.AllDocsList({
+        database: this.data.database,
+        collection: this.data.indexedDocs,
+        nestedView: Documents.Views.Row,
+        viewList: true
+      }));
+    },
+
+    paginate: function (direction) {
+      _.extend(this.documentsView.collection.params, app.getParams());
+      this.documentsView.forceRender();
+      if (direction === 'next') {
+        this.documentsView.collection.skipFirstItem = true;
+      } else {
+        this.documentsView.collection.skipFirstItem = false;
+      }
+    },
+
+    reloadDesignDocs: function (event) {
+      this.sidebar.forceRender();
+
+      if (event && event.selectedTab) {
+        this.sidebar.setSelectedTab(event.selectedTab);
+      }
+    }
+
+  });
+
+  var ChangesRouteObject = FauxtonAPI.RouteObject.extend({
+    layout: "with_tabs",
+    selectedHeader: "Databases",
+    crumbs: function () {
+      return [
+        {"name": this.database.id, "link": Databases.databaseUrl(this.database)},
+        {"name": "_changes", "link": "/_changes"}
+      ];
+    },
+
+    routes: {
+      "database/:database/_changes(:params)": "changes"
+    },
+
+    initialize: function (route, masterLayout, options) {
+      this.databaseName = options[0];
+      this.database = new Databases.Model({id: this.databaseName});
+
+      var docOptions = app.getParams();
+
+      this.database.buildChanges(docOptions);
+
+      this.setView("#tabs", new Documents.Views.Tabs({
+        collection: this.designDocs,
+        database: this.database,
+        active_id: 'changes'
+      }));
+    },
+
+    changes: function (event) {
+      this.setView("#dashboard-content", new Documents.Views.Changes({
+        model: this.database
+      }));
+    },
+
+    apiUrl: function() {
+      return [this.database.url("apiurl"), this.database.documentation()];
+    }
+
+  });
+
+  Documents.RouteObjects = [DocEditorRouteObject, NewDocEditorRouteObject, DocumentsRouteObject, ChangesRouteObject];
+
+  return Documents;
+});

http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/fauxton/app/modules/documents/tests/resourcesSpec.js
----------------------------------------------------------------------
diff --git a/apps/fauxton/app/modules/documents/tests/resourcesSpec.js b/apps/fauxton/app/modules/documents/tests/resourcesSpec.js
new file mode 100644
index 0000000..e78a3c3
--- /dev/null
+++ b/apps/fauxton/app/modules/documents/tests/resourcesSpec.js
@@ -0,0 +1,84 @@
+// 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.
+define([
+       'modules/documents/resources',
+      'testUtils'
+], function (Models, testUtils) {
+  var assert = testUtils.assert;
+
+  describe('IndexCollection', function () {
+    var collection;
+    beforeEach(function () {
+      collection = new Models.IndexCollection([{
+        id:'myId1',
+        doc: 'num1'
+      },
+      {
+        id:'myId2',
+        doc: 'num2'
+      }], {
+        database: {id: 'databaseId'},
+        design: '_design/myDoc'
+      });
+
+    });
+
+    it('Should return urlNext', function () {
+      var url = collection.urlNextPage(20);
+
+      assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=20&reduce=false&startkey_docid=%22myId2%22&startkey=%22myId2%22');
+
+    });
+
+    it('Should return urlPrevious', function () {
+      var url = collection.urlPreviousPage(20, 'myId1');
+
+      assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=20&reduce=false&startkey_docid=%22myId1%22&startkey=%22myId1%22');
+
+    });
+
+  });
+
+  describe('AllDocs', function () {
+    var collection;
+    beforeEach(function () {
+      collection = new Models.AllDocs([{
+        _id:'myId1',
+        doc: 'num1'
+      },
+      {
+        _id:'myId2',
+        doc: 'num2'
+      }], {
+        database: {id: 'databaseId'},
+        params: {limit: 20}
+      });
+
+    });
+
+    it('Should return urlNext', function () {
+      var url = collection.urlNextPage(20);
+
+      assert.equal(url, 'database/databaseId/_all_docs?limit=21&startkey_docid=%22myId2%22&startkey=%22myId2%22');
+
+    });
+
+     it('Should return urlPrevious', function () {
+      var url = collection.urlPreviousPage(20, 'myId1');
+      assert.equal(url, 'database/databaseId/_all_docs?limit=20&startkey_docid=%22myId1%22&startkey=%22myId1%22');
+    });
+
+
+  });
+
+});
+

http://git-wip-us.apache.org/repos/asf/couchdb/blob/add91738/apps/fauxton/app/modules/documents/views.js
----------------------------------------------------------------------
diff --git a/apps/fauxton/app/modules/documents/views.js b/apps/fauxton/app/modules/documents/views.js
new file mode 100644
index 0000000..e155435
--- /dev/null
+++ b/apps/fauxton/app/modules/documents/views.js
@@ -0,0 +1,1805 @@
+// 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.
+
+define([
+       "app",
+
+       "api",
+       "modules/fauxton/components",
+
+       "modules/documents/resources",
+       "modules/databases/resources",
+       "modules/pouchdb/base",
+
+       // Libs
+       "resizeColumns",
+
+       // Plugins
+       "plugins/prettify"
+
+],
+
+function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColumns) {
+  var Views = {};
+
+  // Views.Tabs = FauxtonAPI.View.extend({
+  //   template: "templates/documents/tabs",
+  //   initialize: function(options){
+  //     this.collection = options.collection;
+  //     this.database = options.database;
+  //     this.active_id = options.active_id;
+  //   },
+
+  //   events: {
+  //     "click #delete-database": "delete_database"
+  //   },
+
+  //   serialize: function () {
+  //     return {
+  //       // TODO make this not hard coded here
+  //       changes_url: '#' + this.database.url('changes'),
+  //       db_url: '#' + this.database.url('index') + '?limit=' + Databases.DocLimit,
+  //     };
+  //   },
+
+  //   beforeRender: function(manage) {
+  //     this.insertView("#search", new Views.SearchBox({
+  //       collection: this.collection,
+  //       database: this.database.id
+  //     }));
+  //   },
+
+  //   afterRender: function () {
+  //     if (this.active_id) {
+  //       this.$('.active').removeClass('active');
+  //       this.$('#'+this.active_id).addClass('active');
+  //     }
+  //   },
+
+  //   delete_database: function (event) {
+  //     event.preventDefault();
+
+  //     var result = confirm("Are you sure you want to delete this database?");
+
+  //     if (!result) { return; }
+  //     FauxtonAPI.addNotification({
+  //       msg: "Deleting your database...",
+  //       type: "error"
+  //     });
+  //     return this.database.destroy().done(function () {
+  //       app.router.navigate('#/_all_dbs', {trigger: true});
+  //     });
+  //   }
+  // });
+
+  Views.SearchBox = FauxtonAPI.View.extend({
+    template: "templates/documents/search",
+    tagName: "form",
+    initialize: function(options){
+      this.collection = options.collection;
+      this.database = options.database;
+    },
+    afterRender: function(){
+      var collection = this.collection;
+      var form = this.$el;
+      var searchbox = form.find("input#searchbox");
+      var database = this.database;
+
+      form.submit(function(evt){
+        evt.preventDefault();
+        var viewname = form.find("input#view").val().split('/');
+        var url = "#database/" + database + "/_design/";
+        url += viewname[0] + "/_view/" + viewname[1];
+        if (searchbox.val() !== ""){
+          // TODO: this'll need to work when val() is a number etc.
+          url += '?startkey="' + searchbox.val() + '"';
+        }
+        FauxtonAPI.navigate(url);
+      });
+
+      searchbox.typeahead({
+        source: function(query, process) {
+          // TODO: include _all_docs and view keys somehow
+          var views = _.map(collection.pluck('doc'), function(d){
+            return _.map(_.keys(d.views), function(view){
+              return d._id.split('/')[1] + "/" + view;
+            });
+          });
+          return _.flatten(views);
+        },
+        minLength: 3,
+        updater: function(item){
+          // TODO: some way to return the original search box
+          this.$element.removeClass('span12');
+          this.$element.addClass('span6');
+          this.$element.attr('placeholder', 'Search by view key');
+          $('<span class="add-on span6">' + item +'</span>').insertBefore(this.$element);
+          $('<input type="hidden" id="view" value="' + item +'"/>').insertBefore(this.$element);
+          // Remove the type ahead for now
+          $('.typehead').unbind();
+        }
+      });
+    }
+  });
+
+  Views.UploadModal = FauxtonAPI.View.extend({
+    template: "templates/documents/upload_modal",
+
+    disableLoader: true,
+    
+    initialize: function (options) {
+      _.bindAll(this);
+    },
+
+    events: {
+      "click a#upload-btn": "uploadFile"
+    },
+
+    uploadFile: function (event) {
+      event.preventDefault();
+
+      var docRev = this.model.get('_rev'),
+          that = this,
+          $form = this.$('#file-upload');
+
+      if (!docRev) {
+        return this.set_error_msg('The document needs to be saved before adding an attachment.');
+      }
+
+      if ($('input[type="file"]')[0].files.length === 0) {
+        return this.set_error_msg('Selected a file to be uploaded.');
+      }
+
+      this.$('#_rev').val(docRev);
+
+      $form.ajaxSubmit({
+        url: this.model.url(),
+        type: 'POST',
+        beforeSend: this.beforeSend,
+        uploadProgress: this.uploadProgress,
+        success: this.success,
+        error: function (resp) {
+          console.log('ERR on upload', resp);
+          return that.set_error_msg('Could not upload document: ' + JSON.parse(resp.responseText).reason);
+        }
+      });
+    },
+
+    success: function (resp) {
+      var hideModal = this.hideModal,
+          $form = this.$('#file-upload');
+
+      FauxtonAPI.triggerRouteEvent('reRenderDoc');
+      //slight delay to make this transistion a little more fluid and less jumpy
+      setTimeout(function () {
+        $form.clearForm();
+        hideModal();
+        $('.modal-backdrop').remove();
+      }, 1000);
+    },
+
+    uploadProgress: function(event, position, total, percentComplete) {
+      this.$('.bar').css({width: percentComplete + '%'});
+    },
+
+    beforeSend: function () {
+      this.$('.progress').removeClass('hide');
+    },
+
+    showModal: function () {
+      this.$('.bar').css({width: '0%'});
+      this.$('.progress').addClass('hide');
+      this.clear_error_msg();
+      this.$('.modal').modal();
+      // hack to get modal visible 
+      $('.modal-backdrop').css('z-index',1025);
+    },
+
+    hideModal: function () {
+      this.$('.modal').modal('hide');
+    },
+
+    set_error_msg: function (msg) {
+      var text;
+      if (typeof(msg) == 'string') {
+        text = msg;
+      } else {
+        text = JSON.parse(msg.responseText).reason;
+      }
+      this.$('#modal-error').text(text).removeClass('hide');
+    },
+
+    clear_error_msg: function () {
+      this.$('#modal-error').text(' ').addClass('hide');
+    },
+
+    serialize: function () {
+      return this.model.toJSON();
+    }
+  });
+
+  Views.DuplicateDocModal = FauxtonAPI.View.extend({
+    template: "templates/documents/duplicate_doc_modal",
+
+    initialize: function () {
+      _.bindAll(this);
+    },
+
+    events: {
+      "click #duplicate-btn":"duplicate"
+
+    },
+
+    duplicate: function (event) {
+      event.preventDefault();
+      var newId = this.$('#dup-id').val(),
+          encodedID = app.mixins.safeURLName(newId);
+
+      this.hideModal();
+      FauxtonAPI.triggerRouteEvent('duplicateDoc', encodedID);
+    },
+
+    _showModal: function () {
+      this.$('.bar').css({width: '0%'});
+      this.$('.progress').addClass('hide');
+      this.clear_error_msg();
+      this.$('.modal').modal();
+      // hack to get modal visible 
+      $('.modal-backdrop').css('z-index',1025);
+    },
+
+    showModal: function () {
+      var showModal = this._showModal,
+          setDefaultIdValue = this.setDefaultIdValue,
+          uuid = new FauxtonAPI.UUID();
+
+      uuid.fetch().then(function () {
+        setDefaultIdValue(uuid.next());
+        showModal();
+      });
+    },
+
+    setDefaultIdValue: function (id) {
+      this.$('#dup-id').val(id);
+    },
+
+    hideModal: function () {
+      this.$('.modal').modal('hide');
+    },
+
+    set_error_msg: function (msg) {
+      var text;
+      if (typeof(msg) == 'string') {
+        text = msg;
+      } else {
+        text = JSON.parse(msg.responseText).reason;
+      }
+      this.$('#modal-error').text(text).removeClass('hide');
+    },
+
+    clear_error_msg: function () {
+      this.$('#modal-error').text(' ').addClass('hide');
+    },
+
+    serialize: function () {
+      return this.model.toJSON();
+    }
+
+  });
+
+  Views.FieldEditorTabs = FauxtonAPI.View.extend({
+    template: "templates/documents/doc_field_editor_tabs",
+    disableLoader: true,
+    initialize: function(options) {
+      this.selected = options.selected;
+    },
+
+    events: {
+    },
+    updateSelected: function (selected) {
+      this.selected = selected;
+      this.$('.active').removeClass('active');
+      this.$('#'+this.selected).addClass('active');
+    },
+
+    serialize: function() {
+      var selected = this.selected;
+      return {
+        doc: this.model,
+        isNewDoc: this.model.isNewDoc(),
+        isSelectedClass: function(item) {
+          return item && item === selected ? "active" : "";
+        }
+      };
+    },
+
+    establish: function() {
+      return [this.model.fetch()];
+    }
+  });
+
+  Views.Document = FauxtonAPI.View.extend({
+    template: "templates/documents/all_docs_item",
+    tagName: "tr",
+    className: "all-docs-item",
+
+    events: {
+      "click button.delete": "destroy",
+      "dblclick pre.prettyprint": "edit"
+    },
+
+    attributes: function() {
+      return {
+        "data-id": this.model.id
+      };
+    },
+
+    serialize: function() {
+      return {
+        doc: this.model
+      };
+    },
+
+    establish: function() {
+      return [this.model.fetch()];
+    },
+
+    edit: function(event) {
+      event.preventDefault();
+      FauxtonAPI.navigate("#" + this.model.url('app'));
+    },
+
+    destroy: function(event) {
+      event.preventDefault();
+      var that = this;
+
+      if (!window.confirm("Are you sure you want to delete this doc?")) {
+        return false;
+      }
+
+      this.model.destroy().then(function(resp) {
+        FauxtonAPI.addNotification({
+          msg: "Succesfully destroyed your doc"
+        });
+        that.$el.fadeOut(function () {
+          that.remove();
+        });
+
+        that.model.collection.remove(that.model.id);
+        if (!!that.model.id.match('_design')) {
+          FauxtonAPI.triggerRouteEvent('reloadDesignDocs');
+        }
+      }, function(resp) {
+        FauxtonAPI.addNotification({
+          msg: "Failed to destroy your doc!",
+          type: "error"
+        });
+      });
+    }
+  });
+
+  Views.Row = FauxtonAPI.View.extend({
+    template: "templates/documents/index_row_docular",
+    tagName: "tr",
+
+    events: {
+      "click button.delete": "destroy"
+    },
+
+    destroy: function (event) {
+      event.preventDefault(); 
+      
+      window.alert('Cannot delete a document generated from a view.');
+    },
+
+    serialize: function() {
+      return {
+        doc: this.model
+      };
+    }
+  });
+
+  Views.IndexItem = FauxtonAPI.View.extend({
+    template: "templates/documents/index_menu_item",
+    tagName: "li",
+
+    initialize: function(options){
+      this.index = options.index;
+      this.ddoc = options.ddoc;
+      this.database = options.database;
+      this.selected = !! options.selected;
+    },
+
+    serialize: function() {
+      return {
+        index: this.index,
+        ddoc: this.ddoc,
+        database: this.database,
+        index_clean: app.mixins.removeSpecialCharacters(this.index),
+        ddoc_clean: app.mixins.removeSpecialCharacters(this.ddoc), 
+        index_encoded: app.mixins.safeURLName(this.index),
+        ddoc_encoded: app.mixins.safeURLName(this.ddoc),
+        database_encoded: app.mixins.safeURLName(this.database),
+        selected: this.selected
+      };
+    },
+
+    afterRender: function() {
+      if (this.selected) {
+        $("#sidenav ul.nav-list li").removeClass("active");
+        this.$el.addClass("active");
+      }
+    }
+  });
+
+  Views.AllDocsNumber = FauxtonAPI.View.extend({
+    template: "templates/documents/all_docs_number",
+
+    initialize: function (options) {
+      this.newView = options.newView || false;
+      
+      this.listenTo(this.collection, 'totalRows:decrement', this.render);
+    },
+
+    serialize: function () {
+       var totalRows = 0,
+          recordStart = 0,
+          updateSeq = false;
+
+      if (!this.newView) {
+        totalRows = this.collection.totalRows();
+        updateSeq = this.collection.updateSeq();
+      }
+
+      recordStart = this.collection.recordStart();
+
+      return {
+        database: app.mixins.safeURLName(this.collection.database.id),
+        updateSeq: updateSeq,
+        offset: recordStart,
+        totalRows: totalRows,
+        numModels: this.collection.models.length + recordStart - 1,
+      };
+    }
+
+  });
+
+  Views.AllDocsLayout = FauxtonAPI.View.extend({
+    template: "templates/documents/all_docs_layout",
+    className: "row",
+
+    initialize: function (options) {
+      this.database = options.database;
+      this.params = options.params;
+    },
+
+    events: {
+      'click #toggle-query': "toggleQuery"
+    },
+
+    toggleQuery: function (event) {
+      this.$('#query').toggle('fast');
+    },
+
+    beforeRender: function () {
+      this.advancedOptions = this.insertView('#query', new Views.AdvancedOptions({
+        updateViewFn: this.updateView,
+        previewFn: this.previewView,
+        hasReduce: false,
+        showPreview: false,
+        database: this.database
+      }));
+
+      this.$('#query').hide();
+    },
+
+    afterRender: function () {
+      if (this.params) {
+        this.advancedOptions.updateFromParams(this.params);
+      }
+
+    },
+
+    updateView: function (event, paramInfo) {
+      event.preventDefault();
+
+      var errorParams = paramInfo.errorParams,
+          params = paramInfo.params;
+
+      if (_.any(errorParams)) {
+        _.map(errorParams, function(param) {
+
+          // TODO: Where to add this error?
+          // bootstrap wants the error on a control-group div, but we're not using that
+          //$('form.view-query-update input[name='+param+'], form.view-query-update select[name='+param+']').addClass('error');
+          return FauxtonAPI.addNotification({
+            msg: "JSON Parse Error on field: "+param.name,
+            type: "error",
+            selector: ".advanced-options .errors-container"
+          });
+        });
+        FauxtonAPI.addNotification({
+          msg: "Make sure that strings are properly quoted and any other values are valid JSON structures",
+          type: "warning",
+          selector: ".advanced-options .errors-container"
+        });
+
+        return false;
+      }
+
+      var fragment = window.location.hash.replace(/\?.*$/, '');
+      fragment = fragment + '?' + $.param(params);
+      FauxtonAPI.navigate(fragment, {trigger: false});
+
+      FauxtonAPI.triggerRouteEvent('updateAllDocs', {allDocs: true});
+    },
+
+    previewView: function (event) {
+      event.preventDefault();
+    }
+
+  });
+
+  // TODO: Rename to reflect that this is a list of rows or documents
+  Views.AllDocsList = FauxtonAPI.View.extend({
+    template: "templates/documents/all_docs_list",
+    events: {
+      "click button.all": "selectAll",
+      "click button.bulk-delete": "bulkDelete",
+      "click #collapse": "collapse",
+      "change .row-select":"toggleTrash"
+    },
+
+    toggleTrash: function () {
+      if (this.$('.row-select:checked').length > 0) {
+        this.$('.bulk-delete').removeClass('disabled');
+      } else {
+        this.$('.bulk-delete').addClass('disabled');
+      }
+    },
+
+    initialize: function(options){
+      this.nestedView = options.nestedView || Views.Document;
+      this.rows = {};
+      this.viewList = !! options.viewList;
+      this.database = options.database;
+      if (options.ddocInfo) {
+        this.designDocs = options.ddocInfo.designDocs;
+        this.ddocID = options.ddocInfo.id;
+      }
+      this.newView = options.newView || false;
+      this.expandDocs = true;
+      this.addPagination();
+    },
+
+    establish: function() {
+      if (this.newView) { return null; }
+
+      return this.collection.fetch({reset: true}).fail(function() {
+        // TODO: handle error requests that slip through
+        // This should just throw a notification, not break the page
+        console.log("ERROR: ", arguments);
+      });
+    },
+
+    selectAll: function(evt){
+      $("input:checkbox").prop('checked', !$(evt.target).hasClass('active')).trigger('change');
+    },
+
+    serialize: function() {
+      var requestDuration = false;
+
+      if (this.collection.requestDurationInString) {
+        requestDuration = this.collection.requestDurationInString();
+      }
+
+      return {
+        viewList: this.viewList,
+        requestDuration: requestDuration,
+        expandDocs: this.expandDocs
+      };
+    },
+
+    collapse: function (event) {
+      event.preventDefault();
+
+      if (this.expandDocs) {
+        this.expandDocs = false;
+      } else {
+        this.expandDocs = true;
+      }
+
+      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();
+
+      if (eles.length === 0 || !window.confirm("Are you sure you want to delete these " + eles.length + " docs?")) {
+        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);
+          console.log(model.id.match('_design'), !!model.id.match('_design'));
+          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"
+          });
+        });
+      }, this);
+    },
+
+    addPagination: function () {
+      var collection = this.collection;
+
+      this.pagination = new Components.IndexPagination({
+        collection: this.collection,
+        scrollToSelector: '#dashboard-content',
+        previousUrlfn: function () {
+          return collection.urlPreviousPage(20, this.previousIds.pop());
+        },
+        canShowPreviousfn: function () {
+          if (collection.viewMeta.offset === 0) {
+            return false;
+          }
+
+          return true;
+        },
+        canShowNextfn: function () {
+          if (collection.length === 0 || (collection.viewMeta.offset + collection.length + 2) >= collection.viewMeta.total_rows) {
+            return false;
+          }
+
+          return true;
+        },
+        
+        nextUrlfn: function () {
+          return collection.urlNextPage(20);
+        }
+      });
+    },
+
+    beforeRender: function() {
+      this.allDocsNumber = this.setView('#item-numbers', new Views.AllDocsNumber({
+        collection: this.collection,
+        newView: this.newView
+      }));
+
+      this.insertView('#documents-pagination', this.pagination);
+      var docs = this.expandDocs ? this.collection : this.collection.simple();
+
+      docs.each(function(doc) {
+        this.rows[doc.id] = this.insertView("table.all-docs tbody", new this.nestedView({
+          model: doc
+        }));
+      }, this);
+    },
+
+    afterRender: function(){
+      prettyPrint();
+    }
+  });
+
+  Views.Doc = FauxtonAPI.View.extend({
+    template: "templates/documents/doc",
+    events: {
+      "click button.save-doc": "saveDoc",
+      "click button.delete": "destroy",
+      "click button.duplicate": "duplicate",
+      "click button.upload": "upload",
+      "click button.cancel-button": "goback"
+    },
+    disableLoader: true,
+    initialize: function (options) {
+      this.database = options.database;
+      _.bindAll(this);
+    },
+    goback: function(){
+      FauxtonAPI.navigate(this.database.url("index") + "?limit=100");
+    },
+    destroy: function(event) {
+      if (this.model.isNewDoc()) {
+        FauxtonAPI.addNotification({
+          msg: 'This document has not been saved yet.',
+          type: 'warning'
+        });
+        return;
+      }
+
+      if (!window.confirm("Are you sure you want to delete this doc?")) {
+        return false;
+      }
+
+      var database = this.model.database;
+
+      this.model.destroy().then(function(resp) {
+        FauxtonAPI.addNotification({
+          msg: "Succesfully destroyed your doc"
+        });
+        FauxtonAPI.navigate(database.url("index"));
+      }, function(resp) {
+        FauxtonAPI.addNotification({
+          msg: "Failed to destroy your doc!",
+          type: "error"
+        });
+      });
+    },
+
+    beforeRender: function () {
+      this.uploadModal = this.setView('#upload-modal', new Views.UploadModal({model: this.model}));
+      this.uploadModal.render();
+
+      this.duplicateModal = this.setView('#duplicate-modal', new Views.DuplicateDocModal({model: this.model}));
+      this.duplicateModal.render();
+    },
+
+    upload: function (event) {
+      event.preventDefault();
+      if (this.model.isNewDoc()) {
+        FauxtonAPI.addNotification({
+          msg: 'Please save the document before uploading an attachment.',
+          type: 'warning'
+        });
+        return;
+      }
+      this.uploadModal.showModal();
+    },
+
+    duplicate: function(event) {
+      if (this.model.isNewDoc()) {
+        FauxtonAPI.addNotification({
+          msg: 'Please save the document before duplicating it.',
+          type: 'warning'
+        });
+        return;
+      }
+      event.preventDefault();
+      this.duplicateModal.showModal();
+    },
+
+    updateValues: function() {
+      var notification;
+      if (this.model.changedAttributes()) {
+        notification = FauxtonAPI.addNotification({
+          msg: "Document saved successfully.",
+          type: "success",
+          clear: true
+        });
+        this.editor.setValue(this.model.prettyJSON());
+      }
+    },
+
+    establish: function() {
+      var promise = this.model.fetch(),
+          databaseId = this.database.safeID(),
+          deferred = $.Deferred(),
+          that = this;
+
+      promise.then(function () {
+        deferred.resolve();
+      }, function (xhr, reason, msg) {
+        if (xhr.status === 404) {
+          FauxtonAPI.addNotification({
+            msg: 'The document does not exist',
+            type: 'error',
+            clear: true
+          });
+          that.goback();
+        }
+        deferred.reject();
+     });
+      
+      return deferred;
+    },
+
+    saveDoc: function(event) {
+      var json, notification, 
+      that = this,
+      editor = this.editor,
+      validDoc = this.getDocFromEditor();
+
+      if (validDoc) {
+        this.getDocFromEditor();
+
+        notification = FauxtonAPI.addNotification({msg: "Saving document."});
+
+        this.model.save().then(function () {
+          editor.editSaved();
+          FauxtonAPI.navigate('/database/' + that.database.safeID() + '/' + that.model.id);
+        }).fail(function(xhr) {
+          var responseText = JSON.parse(xhr.responseText).reason;
+          notification = FauxtonAPI.addNotification({
+            msg: "Save failed: " + responseText,
+            type: "error",
+            clear: true,
+            selector: "#doc .errors-container"
+          });
+        });
+      } else if(this.model.validationError && this.model.validationError === 'Cannot change a documents id.') {
+          notification = FauxtonAPI.addNotification({
+            msg: "Cannot save: " + 'Cannot change a documents _id, try Duplicate doc instead!',
+            type: "error",
+            selector: "#doc .errors-container"
+          });
+        delete this.model.validationError;
+      } else {
+        notification = FauxtonAPI.addNotification({
+          msg: "Please fix the JSON errors and try again.",
+          type: "error",
+          selector: "#doc .errors-container"
+        });
+      }
+    },
+
+    getDocFromEditor: function () {
+      if (!this.hasValidCode()) {
+        return false;
+      }
+
+      json = JSON.parse(this.editor.getValue());
+
+      this.model.clear().set(json, {validate: true});
+      if (this.model.validationError) {
+        return false;
+      }
+
+      return this.model;
+    },
+
+    hasValidCode: function() {
+      var errors = this.editor.getAnnotations();
+      return errors.length === 0;
+    },
+
+    serialize: function() {
+      return {
+        doc: this.model,
+        attachments: this.getAttachments()
+      };
+    },
+
+    getAttachments: function () {
+      var attachments = this.model.get('_attachments');
+
+      if (!attachments) { return false; }
+
+      return _.map(attachments, function (att, key) {
+        return {
+          fileName: key,
+          size: att.length,
+          contentType: att.content_type,
+          url: this.model.url() + '/' + key
+        };
+      }, this);
+    },
+
+    afterRender: function() {
+      var saveDoc = this.saveDoc;
+
+      this.editor = new Components.Editor({
+        editorId: "editor-container",
+        commands: [{
+          name: 'save',
+          bindKey: {win: 'Ctrl-S',  mac: 'Ctrl-S'},
+          exec: function(editor) {
+            saveDoc();
+          },
+          readOnly: true // false if this command should not apply in readOnly mode
+        }]
+      });
+      this.editor.render();
+      this.model.on("sync", this.updateValues, this);
+    },
+
+    cleanup: function () {
+      if (this.editor) this.editor.remove();
+    }
+  });
+
+  Views.DocFieldEditor = FauxtonAPI.View.extend({
+    template: "templates/documents/doc_field_editor",
+    disableLoader: true,
+    events: {
+      "click button.save": "saveDoc"
+    },
+
+    saveDoc: function(event) {
+      FauxtonAPI.addNotification({
+        type: "warning",
+        msg: "Save functionality coming soon."
+      });
+    },
+
+    serialize: function() {
+      return {
+        doc: this.getModelWithoutAttachments(),
+        attachments: this.getAttachments()
+      };
+    },
+
+    getModelWithoutAttachments: function() {
+      var model = this.model.toJSON();
+      delete model._attachments;
+      return model;
+    },
+
+    getAttachments: function () {
+      var attachments = this.model.get('_attachments');
+
+      if (!attachments) { return []; }
+
+      return _.map(attachments, function (att, key) {
+        return {
+          fileName: key,
+          size: att.length,
+          contentType: att.content_type,
+          url: this.model.url() + '/' + key
+        };
+      }, this);
+    },
+
+    establish: function() {
+      return [this.model.fetch()];
+    }
+  });
+
+  Views.AdvancedOptions = FauxtonAPI.View.extend({
+    template: "templates/documents/advanced_options",
+    className: "advanced-options well",
+
+    initialize: function (options) {
+      this.database = options.database;
+      this.ddocName = options.ddocName;
+      this.viewName = options.viewName;
+      this.updateViewFn = options.updateViewFn;
+      this.previewFn = options.previewFn;
+      //this.hadReduce = options.hasReduce || true;
+
+      if (typeof(options.hasReduce) === 'undefined') {
+        this.hasReduce = true;
+      } else {
+        this.hasReduce = options.hasReduce;
+      }
+
+      if (typeof(options.showPreview) === 'undefined') {
+        this.showPreview = true;
+      } else {
+        this.showPreview = options.showPreview;
+      }
+    },
+
+    events: {
+      "change form.view-query-update input": "updateFilters",
+      "change form.view-query-update select": "updateFilters",
+      "submit form.view-query-update": "updateView",
+      "click button.preview": "previewView"
+    },
+
+    beforeRender: function () {
+      if (this.viewName && this.ddocName) {
+        var buttonViews = FauxtonAPI.getExtensions('advancedOptions:ViewButton');
+        _.each(buttonViews, function (view) {
+          this.insertView('#button-options', view);
+          view.update(this.database, this.ddocName, this.viewName);
+        }, this);
+      }
+    },
+
+    renderOnUpdatehasReduce: function (hasReduce) {
+      this.hasReduce = hasReduce;
+      this.render();
+    },
+
+    queryParams: function () {
+      var $form = this.$(".view-query-update");
+      // Ignore params without a value
+      var params = _.filter($form.serializeArray(), function(param) {
+        return param.value;
+      });
+
+      // Validate *key* params to ensure they're valid JSON
+      var keyParams = ["key","keys","startkey","endkey"];
+      var errorParams = _.filter(params, function(param) {
+        if (_.contains(keyParams, param.name)) {
+          try {
+            JSON.parse(param.value);
+            return false;
+          } catch(e) {
+            return true;
+          }
+        } else {
+          return false;
+        }
+      });
+
+      return {params: params, errorParams: errorParams};
+    },
+
+    updateFilters: function(event) {
+      event.preventDefault();
+      var $ele = $(event.currentTarget);
+      var name = $ele.attr('name');
+      this.updateFiltersFor(name, $ele);
+    },
+
+    updateFiltersFor: function(name, $ele) {
+      var $form = $ele.parents("form.view-query-update:first");
+      switch (name) {
+        // Reduce constraints
+        //   - Can't include_docs for reduce=true
+        //   - can't include group_level for reduce=false
+        case "reduce":
+          if ($ele.prop('checked') === true) {
+          if ($form.find("input[name=include_docs]").prop("checked") === true) {
+            $form.find("input[name=include_docs]").prop("checked", false);
+            var notification = FauxtonAPI.addNotification({
+              msg: "include_docs has been disabled as you cannot include docs on a reduced view",
+              type: "warn",
+              selector: ".view.show .all-docs-list.errors-container"
+            });
+          }
+          $form.find("input[name=include_docs]").prop("disabled", true);
+          $form.find("select[name=group_level]").prop("disabled", false);
+        } else {
+          $form.find("select[name=group_level]").prop("disabled", true);
+          $form.find("input[name=include_docs]").prop("disabled", false);
+        }
+        break;
+        case "include_docs":
+          break;
+      }
+    },
+
+    updateFromParams: function (params) {
+      var $form = this.$el.find("form.view-query-update");
+      _.each(params, function(val, key) {
+        var $ele;
+        switch (key) {
+          case "limit":
+            case "group_level":
+            $form.find("select[name='"+key+"']").val(val);
+          break;
+          case "include_docs":
+            case "stale":
+            case "descending":
+            case "inclusive_end":
+            $form.find("input[name='"+key+"']").prop('checked', true);
+          break;
+          case "reduce":
+            $ele = $form.find("input[name='"+key+"']");
+          if (val == "true") {
+            $ele.prop('checked', true);
+          }
+          this.updateFiltersFor(key, $ele);
+          break;
+          default:
+            $form.find("input[name='"+key+"']").val(val);
+          break;
+        }
+      }, this);
+    },
+
+    updateView: function (event) {
+      this.updateViewFn(event, this.queryParams());
+    },
+
+    previewView: function (event) {
+      this.previewFn(event, this.queryParams());
+    },
+
+    serialize: function () {
+      return {
+        hasReduce: this.hasReduce,
+        showPreview: this.showPreview
+      };
+    }
+  });
+
+  Views.DesignDocSelector = FauxtonAPI.View.extend({
+    template: "templates/documents/design_doc_selector",
+
+    events: {
+      "change select#ddoc": "updateDesignDoc"
+    },
+
+    initialize: function (options) {
+      this.ddocName = options.ddocName;
+      this.database = options.database;
+      this.listenTo(this.collection, 'add', this.ddocAdded);
+      this.DocModel = options.DocModel || Documents.Doc;
+    },
+
+    ddocAdded: function (ddoc) {
+      this.ddocName = ddoc.id;
+      this.render();
+    },
+
+    serialize: function () {
+      return {
+        ddocName: this.ddocName,
+        ddocs: this.collection
+      };
+    },
+
+    updateDesignDoc: function () {
+      if (this.newDesignDoc()) {
+        this.$('#new-ddoc-section').show();
+      } else {
+        this.$('#new-ddoc-section').hide();
+      }
+    },
+
+    newDesignDoc: function () {
+
+      return this.$('#ddoc').val() === 'new-doc';
+    },
+
+    newDocValidation: function(){
+      return this.newDesignDoc() && this.$('#new-ddoc').val()==="";
+    },
+    getCurrentDesignDoc: function () {
+      if (this.newDesignDoc()) {
+        var doc = {
+          _id: '_design/' + this.$('#new-ddoc').val(),
+          views: {},
+          language: "javascript"
+        };
+        var ddoc = new this.DocModel(doc, {database: this.database});
+        //this.collection.add(ddoc);
+        return ddoc;
+      } else if ( !this.newDesignDoc() ) {
+        var ddocName = this.$('#ddoc').val();
+        return this.collection.find(function (ddoc) {
+          return ddoc.id === ddocName;
+        }).dDocModel();
+      }
+    }
+  });
+
+  Views.ViewEditor = FauxtonAPI.View.extend({
+    template: "templates/documents/view_editor",
+    builtinReduces: ['_sum', '_count', '_stats'],
+
+    events: {
+      "click button.save": "saveView",
+      "click button.delete": "deleteView",
+      "change select#reduce-function-selector": "updateReduce",
+      "click button.preview": "previewView",
+      "click #db-views-tabs-nav": 'toggleIndexNav'
+    },
+
+    langTemplates: {
+      "javascript": {
+        map: "function(doc) {\n  emit(doc._id, 1);\n}",
+        reduce: "function(keys, values, rereduce){\n  if (rereduce){\n    return sum(values);\n  } else {\n    return values.length;\n  }\n}"
+      }
+    },
+
+    defaultLang: "javascript",
+
+    initialize: function(options) {
+      this.newView = options.newView || false;
+      this.ddocs = options.ddocs;
+      this.params = options.params;
+      this.database = options.database;
+      if (this.newView) {
+        this.viewName = 'newView';
+      } else {
+        this.ddocID = options.ddocInfo.id;
+        this.viewName = options.viewName;
+        this.ddocInfo = new Documents.DdocInfo({_id: this.ddocID},{database: this.database});
+      }
+
+      this.showIndex = false;
+      _.bindAll(this);
+    },
+
+    establish: function () {
+      if (this.ddocInfo) {
+        return this.ddocInfo.fetch();
+      }
+    },
+
+    updateValues: function() {
+      var notification;
+      if (this.model.changedAttributes()) {
+        notification = FauxtonAPI.addNotification({
+          msg: "Document saved successfully.",
+          type: "success",
+          clear: true
+        });
+        this.editor.setValue(this.model.prettyJSON());
+      }
+    },
+
+    updateReduce: function(event) {
+      var $ele = $("#reduce-function-selector");
+      var $reduceContainer = $(".control-group.reduce-function");
+      if ($ele.val() == "CUSTOM") {
+        this.createReduceEditor();
+        this.reduceEditor.setValue(this.langTemplates.javascript.reduce);
+        $reduceContainer.show();
+      } else {
+        $reduceContainer.hide();
+      }
+    },
+
+    deleteView: function (event) {
+      event.preventDefault();
+
+      if (this.newView) { return alert('Cannot delete a new view.'); }
+      if (!confirm('Are you sure you want to delete this view?')) {return;}
+
+      var that = this,
+          promise,
+          viewName = this.$('#index-name').val(),
+          ddocName = this.$('#ddoc :selected').val(),
+          ddoc = this.getCurrentDesignDoc();
+
+      ddoc.removeDdocView(viewName);
+
+      if (ddoc.hasViews()) {
+        promise = ddoc.save(); 
+      } else {
+        promise = ddoc.destroy();
+      }
+
+      promise.then(function () {
+        FauxtonAPI.navigate('/database/' + that.database.safeID() + '/_all_docs?limit=' + Databases.DocLimit);
+        FauxtonAPI.triggerRouteEvent('reloadDesignDocs');
+      });
+    },
+
+    saveView: function(event) {
+      var json, notification,
+      that = this;
+
+      if (event) { event.preventDefault();}
+
+      if (this.hasValidCode() && this.$('#new-ddoc:visible').val() !=="") {
+        var mapVal = this.mapEditor.getValue(), 
+        reduceVal = this.reduceVal(),
+        viewName = this.$('#index-name').val(),
+        ddoc = this.getCurrentDesignDoc(),
+        ddocName = ddoc.id;
+
+        this.viewName = viewName;
+
+        notification = FauxtonAPI.addNotification({
+          msg: "Saving document.",
+          selector: "#define-view .errors-container"
+        });
+
+        ddoc.setDdocView(viewName, mapVal, reduceVal);
+
+        ddoc.save().then(function () {
+          that.ddocs.add(ddoc);
+
+          that.mapEditor.editSaved();
+          that.reduceEditor && that.reduceEditor.editSaved();
+
+          FauxtonAPI.addNotification({
+            msg: "View has been saved.",
+            type: "success",
+            selector: "#define-view .errors-container"
+          });
+
+          if (that.newView) {
+            var fragment = '/database/' + that.database.safeID() +'/' + ddoc.safeID() + '/_view/' + app.mixins.safeURLName(viewName); 
+
+            FauxtonAPI.navigate(fragment, {trigger: false});
+            that.newView = false;
+            that.ddocID = ddoc.safeID();
+            that.viewName = viewName;
+            that.ddocInfo = ddoc;
+            that.showIndex = true;
+            that.render();
+            FauxtonAPI.triggerRouteEvent('reloadDesignDocs', {
+              selectedTab: app.mixins.removeSpecialCharacters(ddocName.replace(/_design\//,'')) + '_' + app.mixins.removeSpecialCharacters(viewName)
+            });
+          }
+
+          if (that.reduceFunStr !== reduceVal) {
+            that.reduceFunStr = reduceVal;
+            that.advancedOptions.renderOnUpdatehasReduce(that.hasReduce()); 
+          }
+
+          FauxtonAPI.triggerRouteEvent('updateAllDocs', {ddoc: ddocName, view: viewName});
+
+        }, function(xhr) {
+          var responseText = JSON.parse(xhr.responseText).reason;
+          notification = FauxtonAPI.addNotification({
+            msg: "Save failed: " + responseText,
+            type: "error",
+            clear: true
+          });
+        });
+      } else {
+        var errormessage = (this.$('#new-ddoc:visible').val() ==="")?"Enter a design doc name":"Please fix the Javascript errors and try again.";
+        notification = FauxtonAPI.addNotification({
+          msg: errormessage,
+          type: "error",
+          selector: "#define-view .errors-container"
+        });
+      }
+    },
+
+    updateView: function(event, paramInfo) {
+       event.preventDefault();
+ 
+       if (this.newView) { return alert('Please save this new view before querying it.'); }
+ 
+       var errorParams = paramInfo.errorParams,
+           params = paramInfo.params;
+ 
+       if (_.any(errorParams)) {
+         _.map(errorParams, function(param) {
+ 
+           // TODO: Where to add this error?
+           // bootstrap wants the error on a control-group div, but we're not using that
+           //$('form.view-query-update input[name='+param+'], form.view-query-update select[name='+param+']').addClass('error');
+           return FauxtonAPI.addNotification({
+             msg: "JSON Parse Error on field: "+param.name,
+             type: "error",
+             selector: ".advanced-options .errors-container"
+           });
+         });
+         FauxtonAPI.addNotification({
+           msg: "Make sure that strings are properly quoted and any other values are valid JSON structures",
+           type: "warning",
+           selector: ".advanced-options .errors-container"
+         });
+ 
+         return false;
+      }
+ 
+       var fragment = window.location.hash.replace(/\?.*$/, '');
+       fragment = fragment + '?' + $.param(params);
+       FauxtonAPI.navigate(fragment, {trigger: false});
+ 
+       FauxtonAPI.triggerRouteEvent('updateAllDocs', {ddoc: this.ddocID, view: this.viewName});
+    },
+
+
+    previewView: function(event, paramsInfo) {
+      event.preventDefault();
+      var that = this,
+      mapVal = this.mapVal(),
+      reduceVal = this.reduceVal(),
+      paramsArr = [];
+
+      if (paramsInfo && paramsInfo.params) {
+        paramsArr = paramsInfo.params;
+      }
+
+      var params = _.reduce(paramsArr, function (params, param) {
+        params[param.name] = param.value;
+        return params;
+      }, {reduce: false});
+
+      event.preventDefault();
+
+      FauxtonAPI.addNotification({
+        msg: "<strong>Warning!</strong> Preview executes the Map/Reduce functions in your browser, and may behave differently from CouchDB.",
+        type: "warning",
+        selector: ".advanced-options .errors-container",
+        fade: true
+      });
+
+      var promise = FauxtonAPI.Deferred();
+
+      if (!this.database.allDocs) {
+        this.database.buildAllDocs({limit: Databases.DocLimit.toString(), include_docs: true});
+        promise = this.database.allDocs.fetch();
+      } else {
+        promise.resolve();
+      }
+
+      promise.then(function () {
+        params.docs = that.database.allDocs.map(function (model) { return model.get('doc');}); 
+
+        var queryPromise = pouchdb.runViewQuery({map: mapVal, reduce: reduceVal}, params);
+        queryPromise.then(function (results) {
+          FauxtonAPI.triggerRouteEvent('updatePreviewDocs', {rows: results.rows, ddoc: that.getCurrentDesignDoc().id, view: that.viewName});
+        });
+      });
+    },
+
+    getCurrentDesignDoc: function () {
+      return this.designDocSelector.getCurrentDesignDoc();
+    },
+    
+    isCustomReduceEnabled: function() {
+      return $("#reduce-function-selector").val() == "CUSTOM";
+    },
+
+    mapVal: function () {
+      if (this.mapEditor) {
+        return this.mapEditor.getValue();
+      }
+
+      return this.$('#map-function').text();
+    },
+
+    reduceVal: function() {
+      var reduceOption = this.$('#reduce-function-selector :selected').val(),
+      reduceVal = "";
+
+      if (reduceOption === 'CUSTOM') {
+        reduceVal = this.reduceEditor.getValue();
+      } else if ( reduceOption !== 'NONE') {
+        reduceVal = reduceOption;
+      }
+
+      return reduceVal;
+    },
+
+
+    hasValidCode: function() {
+      return _.every(["mapEditor", "reduceEditor"], function(editorName) {
+        var editor = this[editorName];
+        if (editorName === "reduceEditor" && ! this.isCustomReduceEnabled()) {
+          return true;
+        } 
+        return editor.hadValidCode();
+      }, this);
+    },
+
+    toggleIndexNav: function (event) {
+      var $targetId = this.$(event.target).attr('id'),
+          $previousTab = this.$(this.$('li.active a').attr('href')),
+          $targetTab = this.$(this.$(event.target).attr('href'));
+
+      if ($targetTab.attr('id') !== $previousTab.attr('id')) {
+        $previousTab.removeAttr('style');
+      }
+
+      if ($targetId === 'index-nav') {
+        if (this.newView) { return; }
+        var that = this;
+        $targetTab.toggle('slow', function(){
+           that.showEditors();
+        });
+      } else {
+        $targetTab.toggle('slow');
+      }
+    },
+
+    serialize: function() {
+      return {
+        ddocs: this.ddocs,
+        ddoc: this.model,
+        ddocName: this.model.id,
+        viewName: this.viewName,
+        reduceFunStr: this.reduceFunStr,
+        isCustomReduce: this.hasCustomReduce(),
+        newView: this.newView,
+        langTemplates: this.langTemplates.javascript
+      };
+    },
+
+    hasCustomReduce: function() {
+      return this.reduceFunStr && ! _.contains(this.builtinReduces, this.reduceFunStr);
+    },
+
+    hasReduce: function () {
+      return this.reduceFunStr || false;
+    },
+
+    createReduceEditor: function () {
+      if (this.reduceEditor) {
+        this.reduceEditor.remove();
+      }
+
+      this.reduceEditor = new Components.Editor({
+        editorId: "reduce-function",
+        mode: "javascript",
+        couchJSHINT: true
+      });
+      this.reduceEditor.render();
+    },
+
+    beforeRender: function () {
+
+      if (this.newView) {
+        this.reduceFunStr = '_sum';
+        if (this.ddocs.length === 0) {
+          this.model = new Documents.Doc(null, {database: this.database});
+        } else {
+          this.model = this.ddocs.first().dDocModel();
+        }
+        this.ddocID = this.model.id;
+      } else {
+        this.model = this.ddocs.get(this.ddocID).dDocModel();
+        this.reduceFunStr = this.model.viewHasReduce(this.viewName);
+        this.setView('#ddoc-info', new Views.DdocInfo({model: this.ddocInfo }));
+      }
+
+      this.designDocSelector = this.setView('.design-doc-group', new Views.DesignDocSelector({
+        collection: this.ddocs,
+        ddocName: this.model.id,
+        database: this.database
+      }));
+      
+      this.advancedOptions = this.insertView('#query', new Views.AdvancedOptions({
+        updateViewFn: this.updateView,
+        previewFn: this.previewView,
+        database: this.database,
+        viewName: this.viewName,
+        ddocName: this.model.id,
+        hasReduce: this.hasReduce()
+      }));
+    },
+
+    afterRender: function() {
+      if (this.params) {
+        this.advancedOptions.updateFromParams(this.params);
+      }
+
+      this.designDocSelector.updateDesignDoc();
+      if (this.newView || this.showIndex) {
+        this.showEditors();
+        this.showIndex = false;
+      } else {
+        this.$('#index').hide();
+        this.$('#index-nav').parent().removeClass('active');
+      }
+    },
+
+    showEditors: function () {
+      this.mapEditor = new Components.Editor({
+        editorId: "map-function",
+        mode: "javascript",
+        couchJSHINT: true
+      });
+      this.mapEditor.render();
+
+      if (this.hasCustomReduce()) {
+        this.createReduceEditor();
+      } else {
+        $(".control-group.reduce-function").hide();
+      }
+
+      if (this.newView) {
+        this.mapEditor.setValue(this.langTemplates[this.defaultLang].map);
+        //Use a built in view by default
+        //this.reduceEditor.setValue(this.langTemplates[this.defaultLang].reduce);
+      } 
+
+      this.mapEditor.editSaved();
+      this.reduceEditor && this.reduceEditor.editSaved();
+    },
+
+    cleanup: function () {
+      this.mapEditor && this.mapEditor.remove();
+      this.reduceEditor && this.reduceEditor.remove();
+    }
+  });
+
+  Views.JumpToDoc = FauxtonAPI.View.extend({
+    template: "templates/documents/jumpdoc",
+
+    initialize: function (options) {
+      this.database = options.database;
+    },
+
+    events: {
+      "submit #jump-to-doc": "jumpToDoc"
+    },
+
+    jumpToDoc: function (event) {
+      event.preventDefault();
+      var docId = this.$('#jump-to-doc-id').val().trim();
+      FauxtonAPI.navigate('/database/' + app.mixins.safeURLName(this.database.id) +'/' + app.mixins.safeURLName(docId), {trigger: true});
+    },
+
+    afterRender: function () {
+     this.typeAhead = new Components.DocSearchTypeahead({el: '#jump-to-doc-id', database: this.database});
+     this.typeAhead.render();
+    }
+  });
+
+  Views.Sidebar = FauxtonAPI.View.extend({
+    template: "templates/documents/sidebar",
+    events: {
+      "click button#delete-database": "deleteDatabase"
+    },
+
+    initialize: function(options) {
+      this.database = options.database;
+      if (options.ddocInfo) {
+        this.ddocID = options.ddocInfo.id;
+        this.currView = options.ddocInfo.currView;
+      }
+    },
+
+    deleteDatabase: function (event) {
+      event.preventDefault();
+
+      var result = confirm('Are you sure you want to delete this database?');
+
+      if (!result) { return; }
+      var databaseName = this.database.id;
+      FauxtonAPI.addNotification({
+        msg: "Deleting your database...",
+        type: "error",
+        clear: true
+      });
+
+      this.database.destroy().then(function () {
+        FauxtonAPI.navigate('#/_all_dbs');
+        FauxtonAPI.addNotification({
+          msg: 'The database ' + databaseName + ' has been deleted.',
+          clear: true
+        });
+      }).fail(function (rsp, error, msg) {
+        FauxtonAPI.addNotification({
+          msg: 'Could not delete the database, reason ' + msg + '.',
+          type: 'error',
+          clear: true
+        });
+      });
+    },
+
+    serialize: function() {
+      var docLinks = FauxtonAPI.getExtensions('docLinks'),
+          newLinks = FauxtonAPI.getExtensions('sidebar:newLinks'),
+          addLinks = FauxtonAPI.getExtensions('sidebar:links'),
+          extensionList = FauxtonAPI.getExtensions('sidebar:list');
+      return {
+        changes_url: '#' + this.database.url('changes'),
+        permissions_url: '#' + this.database.url('app') + '/permissions',
+        db_url: '#' + this.database.url('index') + '?limit=' + Databases.DocLimit,
+        database: this.collection.database,
+        database_url: '#' + this.database.url('app'), 
+        docLinks: docLinks,
+        docLimit: Databases.DocLimit,
+        addLinks: addLinks,
+        newLinks: newLinks,
+        extensionList: extensionList > 0
+      };
+    },
+
+    buildIndexList: function(collection, selector, design){
+      _.each(_.keys(collection), function(key){
+        var selected = this.ddocID == "_design/"+design;
+        this.insertView("ul.nav." + selector, new Views.IndexItem({
+          ddoc: design,
+          index: key,
+          database: this.collection.database.id,
+          selected: selected && key == this.currView
+        }));
+      }, this);
+    },
+
+    beforeRender: function(manage) {
+
+      var sidebarListViews = FauxtonAPI.getExtensions('sidebar:list');
+      _.each(sidebarListViews, function (view) {
+        var extension = this.insertView('#extension-navs', view);
+        extension.update(this.database, this.collection, this.viewName);
+        extension.render();
+      }, this);
+
+
+      this.collection.each(function(design) {
+        if (design.has('doc')){
+          var ddoc = design.id.replace(/^_design\//,"");
+          if (design.get('doc').views){
+            this.buildIndexList(design.get('doc').views, "views", ddoc);
+          }
+        }
+      }, this);
+    },
+
+    afterRender: function () {
+      if (this.selectedTab) {
+        this.setSelectedTab(this.selectedTab);
+      }
+    },
+
+    setSelectedTab: function (selectedTab) {
+      this.selectedTab = selectedTab;
+      this.$('li').removeClass('active');
+      this.$('#' + selectedTab).parent().addClass('active');
+    }
+  });
+
+  Views.Indexed = FauxtonAPI.View.extend({});
+
+  Views.Changes = FauxtonAPI.View.extend({
+    template: "templates/documents/changes",
+
+    establish: function() {
+      return [ this.model.changes.fetch()];
+    },
+
+    serialize: function () {
+      return {
+        changes: this.model.changes.toJSON(),
+        database: this.model
+      };
+    },
+
+    afterRender: function(){
+      prettyPrint();
+    }
+  });
+
+  Views.DdocInfo = FauxtonAPI.View.extend({
+    template: "templates/documents/ddoc_info",
+
+    initialize: function (options) {
+      this.refreshTime = options.refreshTime || 5000;
+      this.listenTo(this.model, 'change', this.render);
+    },
+
+    serialize: function () {
+      return {
+        view_index: this.model.get('view_index')
+      };
+    },
+
+    afterRender: function () {
+      this.startRefreshInterval();
+    },
+
+    startRefreshInterval: function () {
+      var model = this.model;
+
+      // Interval already set
+      if (this.intervalId) { return ; }
+
+      this.intervalId = setInterval(function () {
+        model.fetch();
+      }, this.refreshTime);
+    },
+
+    stopRefreshInterval: function () {
+      clearInterval(this.intervalId);
+    },
+
+    cleanup: function () {
+      this.stopRefreshInterval();
+    }
+  });
+
+  Documents.Views = Views;
+  return Documents;
+});


Mime
View raw message