blob: 831d5e3d0afafeaa03949858475e03cb69ac0e22 [file] [log] [blame]
// 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 = FauxtonAPI.addon();
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.utils.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 && 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.utils.safeURLName(ddoc);
}else{
return app.utils.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.utils.safeURLName(ddoc);
}
});
Documents.ViewRow = Backbone.Model.extend({
// this is a hack so that backbone.collections doesn't group
// these by id and reduce the number of items returned.
idAttribute: "_id",
docType: function() {
if (!this.id) return "reduction";
return this.id.match(/^_design/) ? "design doc" : "doc";
},
documentation: function(){
return "docs";
},
url: function(context) {
return this.collection.database.url(context) + "/" + this.safeID();
},
isEditable: function() {
return this.docType() != "reduction";
},
safeID: function() {
var id = this.id || this.get("id");
return app.utils.safeURLName(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, params) {
if (params) {
this.params = params;
} else {
this.params = {reduce: false};
}
this.params.limit = num;
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.utils.safeURLName(this.design),
view = app.utils.safeURLName(this.view);
var url = [startOfUrl, this.database.safeID(), "_design", design, this.idxType, view];
return url.join("/") + query;
},
urlNextPage: function (num, lastId) {
if (!lastId) {
lastDoc = this.last();
}
var id = lastDoc.get("id");
if (id) {
this.params.startkey_docid = id;
}
this.params.startkey = JSON.stringify(lastDoc.get('key'));
this.params.limit = num + 1;
return this.url('app');
},
urlPreviousPage: function (num, params) {
if (params) {
this.params = params;
} else {
this.params = {reduce: false};
}
this.params.limit = num;
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() {
if (this.params.reduce) { return "unknown_reduce";}
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;
});