/**
* @namespace models.client
*/
"use strict";
var Backbone = require("backbone");
var _ = require("lodash");
var $ = require("jquery");
var Promise = require("bluebird");
var Cocktail = require("backbone.cocktail");
var BaseMixin = require("../BaseMixin");
function createReplaceMixin(parentPrototype) {
return {
/**
* Fetch model state from the server
* http://backbonejs.org/#Model-fetch
*
* @method fetch
* @return {Bluebird.Promise}
*/
fetch: function() {
if (this.parent) {
throw new Error("This is a child model. Use fetchParent() to get a new parent");
}
if (this._type === "model" && !this.get("id")) {
throw new Error("fetch() without id makes no sense on a model");
}
var url = _.result(this, "url");
var op = Promise.cast($.get(url))
.bind(this)
.catch(function(err) {
if (err && err.status === 404) {
throw new NotFound("404 response from API", url, err);
}
throw err;
})
.then(function(res) {
return new this.constructor(res, { parent: this.parent });
});
this.trigger("replace", op);
return op;
},
fetchParent: function() {
if (this.parent) return this.parent.fetchParent();
return this.fetch();
},
};
}
function disabledMethod(name) {
/*jshint validthis:true */
this[name] = function() {
throw new Error("Do not mutate existing instances. Create new instances if you need to mutate anything.");
};
}
/**
* Not found error class for 404 api errors
*
* @class Base.NotFound
* @constructor
* @param {String} message
* @param {String} url
* @param {Object} xhr The ajax request
*/
function NotFound(message, url, xhr) {
this.name = "NotFound";
this.url = url;
this.xhr = xhr;
this.message = message;
}
NotFound.prototype = new Error();
/**
* Base class for client models
*
* http://backbonejs.org/#Model
*
* @class Base
* @extends Backbone.Model
* @uses models.client.ReplaceMixin
* @uses models.BaseMixin
* @see http://backbonejs.org/#Model
*/
var Base = Backbone.Model.extend({
_type: "model",
constructor: function(attrs, opts) {
this.parent = opts && opts.parent;
Backbone.Model.apply(this, arguments);
["set", "clear", "unset"].forEach(disabledMethod.bind(this));
},
isOperating: function() {
console.error("Deprecated isOperating() call");
return false;
},
createdBy: function() {
var User = require("./User");
return new User(this.get("createdBy"));
},
push: function(attr, value) {
var array = this.get(attr);
array.push(value);
this.set(attr, array);
},
/**
*
* Use unique_id from {{#crossLink "models.server.Base"}}{{/crossLink}} as
* the model id.
*
* We need this to be able to put models of diffent type to a single
* Backbone collection
*
* http://backbonejs.org/#Model-idAttribute
*
* @property idAttribute
* @type String
*/
idAttribute: "unique_id",
/**
* @method uniqueId
* @return {String}
*/
getUniqueId: function() {
var type = this.get("type");
var id = this.get("id");
if (!id || !type) throw new Error("bad unique id");
return type + "-" + id;
},
/**
* Return relation data for given key or throw if it's not loaded
*
* @method rel
* @param {String} key
* @return {Object|Array} Relation data
*/
rel: function(key){
var data = this.get(key);
if (!data) {
var err = new Error("Relation for key '" + key + "' is not loaded for '" + this.get("type") + "' model");
err.model = this;
throw err;
}
return data;
},
/**
* Save model to server
* http://backbonejs.org/#Model-save
*
* @method save
* @return {Bluebird.Promise} with the new saved model
*/
save: function() {
if (!this.isNew()) throw new Error("Only new models can be saved!");
return Promise.cast($.post(_.result(this, "url"), this.toJSON()))
.bind(this)
.then(function(res) {
return new this.constructor(res, { parent: this.parent });
});
},
/**
* Promise of the saving operation instantiated by Base#save(). Available
* only when the operation is ongoing.
*
* @property saving
* @type Bluebird.Promise|null
*/
saving: null,
/**
* Call when not using this model anymore. Unbinds all event listeners.
*
* @method dispose
*/
dispose: function() {
this.off();
},
/**
* Return true if the another model was created within 60 seconds of this
* one by the same user
*
* @method wasCreatedInVicinity
* @return {Boolean}
*/
wasCreatedInVicinityOf: function(another) {
if (this.get("createdById") !== another.get("createdById")) return false;
var diff = Math.abs(another.createdAt().getTime() - this.createdAt().getTime());
return diff < 60*1000;
},
}, {
NotFound: NotFound,
/**
* Create instance of models.client.Base.Collection with this model class
* as the `model` property
*
* @static
* @method collection
* @param {Array} [models] Array of models.client.Base models
* @param {Object} [options] http://backbonejs.org/#Collection-constructor
* @return {models.client.Base.Collection}
*/
collection: function(models, options) {
var Klass = this.Collection.extend({
model: this
});
return new Klass(models, options);
},
});
/**
* Base class for client model collections
*
* http://backbonejs.org/#Collection
*
* @class Base.Collection
* @extends Backbone.Collection
* @uses models.client.ReplaceMixin
*/
Base.Collection = Backbone.Collection.extend({
_type: "collection",
constructor: function() {
Backbone.Collection.apply(this, arguments);
[
"add",
"remove",
"push",
"pop",
"sort",
"unshift",
"shift"
].forEach(disabledMethod.bind(this));
},
/**
* http://backbonejs.org/#Collection-model
*
* @property model
* @type models.client.Base
*/
model: Base
});
Cocktail.mixin(Base, BaseMixin);
_.extend(Base.prototype, createReplaceMixin(Backbone.Model.prototype));
_.extend(Base.Collection.prototype, createReplaceMixin(Backbone.Collection.prototype));
_.extend(Base, Backbone.Events);
module.exports = Base;