1 /*jshint unused:false */
7 XM.Tuplespace = _.clone(Backbone.Events);
10 @class `XM.Model` is an abstract class designed to operate with `XT.DataSource`.
11 It should be subclassed for any specific implementation. Subclasses should
12 include a `recordType` the data source will use to retrieve the record.
14 To create a new model include `isNew` in the options:
17 XM.MyModel = XM.Model.extend({
18 recordType: 'XM.MyModel'
21 // Instantiate a new model object
22 m = new XM.MyModel(null, {isNew: true});
24 To load an existing record use the `findOrCreate` method and include an id in the attributes:
26 m = XM.MyModel.findOrCreate({id: 1});
31 @description To create a new model include `isNew` in the options:
32 @param {Object} Attributes
33 @param {Object} Options
34 @extends XM.ModelMixin
35 @extends Backbone.RelationalModel
37 XM.Model = Backbone.RelationalModel.extend(XM.ModelMixin);
39 XM.Model = XM.Model.extend(/** @lends XM.Model# */{
44 * Attempt to obtain lock.
47 obtainLock: function (options) {
48 if (this.getLockKey() && _.isFunction(options.success)) {
49 this.trigger('lock:obtain', this, this.lock);
50 return options.success();
53 this.recordType.prefix(),
54 this.recordType.suffix(),
58 this._reentrantLockHelper('obtain', params, options);
62 * Attempt to renew lock.
65 renewLock: function (options) {
66 this._reentrantLockHelper('renew', [ this.lock.key ], options);
73 releaseLock: function (options) {
74 options = options || { };
75 var callback = options.success;
77 if (this.getLockKey()) {
78 this._reentrantLockHelper('release', { key: this.getLockKey() }, options);
79 this.lockDidChange(this, _.omit(this.lock, 'key'));
81 else if (_.isFunction(callback)) {
87 A function that binds events to functions. It can and should only be called
88 once by initialize. Any attempt to call it a second time will throw an error.
90 bindEvents: function () {
92 // Bind events, but only if we haven't already been here before.
93 // We could silently skip, but then that means any overload done
94 // by anyone else has to do that check too. That's too error prone
95 // and dangerous because the problems caused by duplicate bindings
96 // are not immediatley apparent and insidiously hard to pin down.
97 if (this._eventsBound) { throw new Error("Events have already been bound."); }
100 * Bind all events in the optional 'handlers' hash
102 _.each(this.handlers, function (handler, event) {
103 if (!_.isFunction(that[handler])) {
104 console.warn('Handler '+ handler + ' not found: not binding');
107 that.on(event, that[handler]);
110 this.on('change', this.didChange);
111 this.on('error', this.didError);
112 this.on('destroy', this.didDestroy);
115 _.where(this.relations, { type: Backbone.HasMany, includeInJSON: true }),
116 function (relation) {
117 that.on('add:' + relation.key, that.didChange);
118 if (!that.isReadOnly()) {
119 that.on('add:' + relation.key, that.relationAdded);
124 this._eventsBound = true;
128 Reimplemented to handle state change and parent child relationships. Calling
129 `destroy` on a parent will cause the model to commit to the server
130 immediately. Calling destroy on a child relation will simply mark it for
131 deletion on the next save of the parent.
133 @returns {Object|Boolean}
135 destroy: function (options) {
136 options = options ? _.clone(options) : {};
137 var klass = this.getClass(),
138 canDelete = klass.canDelete(this),
139 success = options.success,
140 isNew = this.isNew(),
144 parent = this.getParent(true),
146 findChildren = function (model) {
147 _.each(model.relations, function (relation) {
148 var i, attr = model.attributes[relation.key];
149 if (attr && attr.models &&
150 relation.type === Backbone.HasMany) {
151 for (i = 0; i < attr.models.length; i += 1) {
152 findChildren(attr.models[i]);
154 children = _.union(children, attr.models);
158 if ((parent && parent.canUpdate(this)) ||
159 (!parent && canDelete) ||
160 this.getStatus() === K.READY_NEW) {
161 this._wasNew = isNew; // Hack so prototype call will still work
162 this.setStatus(K.DESTROYED_DIRTY, {cascade: true});
164 // If it's top level commit to the server now.
165 if ((!parent && canDelete) || isNew) {
166 findChildren(this); // Lord Vader ... rise
167 this.setStatus(K.BUSY_DESTROYING, {cascade: true});
169 options.success = function (resp) {
171 // Do not hesitate, show no mercy!
172 for (i = 0; i < children.length; i += 1) {
173 children[i].didDestroy();
175 if (XT.session.config.debugging) {
176 XT.log('Destroy successful');
178 if (success) { success(model, resp, options); }
180 result = Backbone.Model.prototype.destroy.call(this, options);
186 // Otherwise just marked for deletion.
188 success(this, null, options);
192 XT.log('Insufficient privileges to destroy');
196 doEmail: function () {
197 // TODO: a way for an unwatched model to set the scrim
198 XT.dataSource.callRoute("generate-report", this.getReportPayload("email"), {
199 error: function (error) {
200 // TODO: a way for an unwatched model to trigger the notify popup
201 console.log("email error", error);
203 success: function () {
204 console.log("email success");
209 doPrint: function () {
210 XT.dataSource.callRoute("generate-report", this.getReportPayload("print"), {
211 success: function () {
212 console.log("print success");
222 * TODO sync() alone should handle all of this stuff. fetch() is not a
223 * Backbone customization point by design.
225 _fetchHelper: function (_options) {
226 if (!this.getClass().canRead()) {
227 XT.log('Error: insufficient privileges to fetch');
232 options = _.extend({ }, _options),
233 callback = options.success,
238 done = function (resp) {
239 that.setStatus(XM.Model.READY_CLEAN, options);
241 if (_.isFunction(callback)) {
242 callback(that, resp, options);
248 * Handle successful fetch response. Obtain lock if necessary, and invoke
249 * the optional callback.
251 afterFetch = function (resp) {
252 var schema = XT.session.getSchemas()[that.recordType.prefix()],
253 lockable = schema.get(that.recordType.suffix()).lockable;
255 done = _.partial(done, resp);
257 if (lockable && options.obtainLock !== false) {
258 that.obtainLock({ success: done });
265 return _.extend(options, {
273 * Reimplemented to handle status changes and automatically obtain
274 * a pessimistic lock on the record.
276 * @param {Object} Options
277 * @returns {Object} Request
279 fetch: function (_options) {
280 var options = this._fetchHelper(_options);
281 if (!_.isObject(options)) {
285 this.setStatus(XM.Model.BUSY_FETCHING, { cascade: true });
286 return Backbone.Model.prototype.fetch.call(this, options);
290 Set the id on this record an id from the server. Including the `cascade`
291 option will call ids to be fetched recursively for `HasMany` relations.
293 @returns {Object} Request
295 fetchId: function (options) {
296 options = _.defaults(options ? _.clone(options) : {});
297 var that = this, attr;
299 options.success = function (resp) {
300 that.set(that.idAttribute, resp, options);
302 this.dispatch('XM.Model', 'fetchId', this.recordType, options);
305 // Cascade through `HasMany` relations if specified.
306 if (options && options.cascade) {
307 _.each(this.relations, function (relation) {
308 attr = that.attributes[relation.key];
310 if (relation.type === Backbone.HasMany) {
312 _.each(attr.models, function (model) {
313 if (model.fetchId) { model.fetchId(options); }
323 * Retrieve related objects.
324 * @param {String} key The relation key to fetch models for.
325 * @param {Object} options Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
326 * @param {Boolean} update Whether to force a fetch from the server (updating existing models).
327 * @returns {Array} An array of request objects.
329 fetchRelated: function (key, options, refresh) {
330 options = options || {};
334 status = this.getStatus(),
336 rel = this.getRelation(key),
337 keys = rel && (rel.keyIds || [rel.keyId]),
338 toFetch = keys && _.select(keys || [], function (id) {
339 return (id || id === 0) && (refresh || !Backbone.Relational.store.find(rel.relatedModel, id));
342 if (toFetch && toFetch.length) {
343 if (options.max && toFetch.length > options.max) {
344 toFetch.length = options.max;
347 models = _.map(toFetch, function (id) {
348 var model = Backbone.Relational.store.find(rel.relatedModel, id),
353 attrs[rel.relatedModel.prototype.idAttribute] = id;
354 model = rel.relatedModel.findOrCreate(attrs, options);
361 requests = _.map(models, function (model) {
362 var opts = _.defaults(
365 if (_.contains(created, model)) {
366 model.trigger('destroy', model, model.collection, options);
367 if (options.error) { options.error.apply(model, arguments); }
373 // Context option means server will check privilege access of the parent
374 // and the existence of relation on the parent to determine whether user
375 // can see this record instead of usual privilege check on the model.
376 if (status === K.READY_CLEAN || status === K.READY_DIRTY) {
378 recordType: this.recordType,
383 return model.fetch(opts);
386 if (options.success) { options.success(); }
393 Overload: Delete objects marked as destroyed from arrays and
394 convert dates to strings.
396 Add support for 'includeNested' option that will
397 output JSON with nested toOne objects when specified.
399 toJSON: function (options) {
400 var includeNested = options && options.includeNested,
411 // Sort based on the exact order in which items were added.
412 // This is an absolute must for `generatePatch` to work correctly
413 byIdx = function (a, b) {
414 return idx.indexOf(a[idAttr]) - idx.indexOf(b[idAttr]);
416 byIdx2 = function (a, b) {
417 return idx.indexOf(a.attributes[idAttr]) - idx.indexOf(b.attributes[idAttr]);
420 // If include nested, preprocess relations so they will show up nested
422 nested = _.filter(this._relations, function (rel) {
423 return rel.options.isNested;
425 _.each(nested, function (rel) {
426 old[rel.key] = rel.options.includeInJSON;
427 rel.options.includeInJSON = true;
431 json = Backbone.RelationalModel.prototype.toJSON.apply(this, arguments);
433 // Convert dates to strings to avoid conflicts with jsonpatch
435 if (json.hasOwnProperty(prop) && json[prop] instanceof Date) {
436 json[prop] = json[prop].toJSON();
440 // If this Model has already been fully serialized in this branch once, return to avoid loops
441 if (this.isLocked()) {
446 // Exclude relations that by definition don't need to be processed.
447 relations = _.filter(this.relations, function (relation) {
448 return relation.includeInJSON;
451 // Handle "destroyed" records
452 _.each(relations, function (relation) {
453 var K = XM.ModelClassMixin,
460 if (relation.type === Backbone.HasMany) {
461 attr = that.get(key);
462 if (attr && attr.length) {
463 // Sort by create order
464 idx = that._idx[relation.relatedModel.suffix()];
466 Klass = Backbone.Relational.store.getObjectByName(relation.relatedModel);
467 idAttr = Klass.prototype.idAttribute;
468 json[key].sort(byIdx);
470 // We need to sort by index, but we'll change back later.
471 oldComparator = attr.comparator;
472 attr.comparator = byIdx2;
476 for (i = 0; i < attr.models.length; i++) {
477 status = attr.models[i].getStatus();
478 if (status === K.BUSY_COMMITTING) {
479 status = attr.models[i]._prevStatus;
482 // If dirty record has changed from server version.
483 // Deleting will leave a "hole" in the array picked up
484 // by the patch algorithm that signals a change
485 if (status === K.DESTROYED_DIRTY) {
488 // If clean the server never knew about it so remove entirely
489 } else if (status === K.DESTROYED_CLEAN) {
490 json[key].splice(i, 1);
492 // Delete the relation parent data if applicable. Don't need it here.
493 } else if (relation.isNested) {
494 delete json[key][i][relation.reverseRelation.key];
499 if (idx) { attr.comparator = oldComparator; }
505 // Revert relations to previous settings if applicable
507 _.each(nested, function (rel) {
508 rel.options.includeInJSON = old[rel.key];
509 delete rel.options.oldIncludeInJSON;
517 Returns the current model prototype class.
521 getClass: function () {
522 return Backbone.Relational.store.getObjectByName(this.recordType);
526 Return the parent model if one exists. If the `getRoot` parameter is
527 passed, it will return the top level parent of the model hierarchy.
529 @param {Boolean} Get Root
532 getParent: function (getRoot) {
535 relation = _.find(this.relations, function (rel) {
536 if (rel.reverseRelation && rel.isAutoRelation) {
540 parent = relation && relation.key ? this.get(relation.key) : false;
541 if (parent && getRoot) {
542 root = parent.getParent(getRoot);
544 return root || parent;
547 getReportUrl: function (action) {
548 var modelName = this.editableModel || this.recordType,
549 reportUrl = "/generate-report?nameSpace=%@&type=%@&id=%@".f(
550 modelName.prefix(), modelName.suffix(), this.id);
553 reportUrl = reportUrl + "&action=" + action;
558 getReportPayload: function (action) {
559 var modelName = this.editableModel || this.recordType;
561 nameSpace: modelName.prefix(),
562 type: modelName.suffix(),
569 Called when model is instantiated.
571 initialize: function (attributes, options) {
572 attributes = attributes || {};
573 options = options || {};
577 status = this.getStatus(),
578 idAttribute = this.idAttribute;
580 // Set defaults if not provided
581 this.privileges = this.privileges || {};
582 this.readOnlyAttributes = this.readOnlyAttributes ?
583 this.readOnlyAttributes.slice(0) : [];
584 this.requiredAttributes = this.requiredAttributes ?
585 this.requiredAttributes.slice(0) : [];
588 if (_.isEmpty(this.recordType)) { throw new Error('No record type defined'); }
589 if (options.status) {
590 this.setStatus(options.status);
591 } else if (_.isNull(status)) {
592 this.setStatus(K.EMPTY);
598 klass = this.getClass();
599 if (!klass.canCreate()) {
600 throw new Error('Insufficient privileges to create a record of class ' +
603 this.setStatus(K.READY_NEW, {cascade: true});
605 // Key generator (client based)
606 if (idAttribute === 'uuid' &&
607 !this.get(idAttribute) &&
608 !attributes[idAttribute]) {
609 this.set(idAttribute, XT.generateUUID());
612 // Deprecated key generator (server based)
613 if (this.autoFetchId) { this.fetchId(); }
616 // Set attributes that should be required and read only
617 if (this.idAttribute &&
618 !_.contains(this.requiredAttributes, idAttribute)) {
619 this.requiredAttributes.push(this.idAttribute);
623 * Enable ability to listen for events in a global tuple-space, if
626 XM.Tuplespace.listenTo(this, 'all', XM.Tuplespace.trigger);
630 Return whether the model is in a read-only state. If an attribute name
631 is passed, returns whether that attribute is read-only. It is also
632 capable of checking the read only status of child objects via a search path string.
635 // Inquire on the whole model
636 var readOnly = this.isReadOnly();
638 // Inquire on a single attribute
639 var readOnly = this.isReadOnly("name");
641 // Inquire using a search path
642 var readOnly = this.isReadOnly("contact.firstName");
645 @seealso `setReadOnly`
646 @seealso `readOnlyAttributes`
647 @param {String} attribute
650 isReadOnly: function (value) {
651 var parent = this.getParent(true),
652 isLockedOut = parent ? !parent.hasLockKey() : !this.hasLockKey(),
659 if (_.isString(value) && value.indexOf('.') !== -1) {
660 parts = value.split('.');
662 for (i = 0; i < parts.length; i++) {
664 if (result instanceof Backbone.Model && i + 1 < parts.length) {
665 result = result.getValue(part);
666 } else if (_.isNull(result)) {
668 } else if (!_.isUndefined(result)) {
669 result = result.isReadOnly(part) || !result.hasLockKey();
675 if ((!_.isString(value) && !_.isObject(value)) || this.readOnly) {
676 result = this.readOnly;
678 result = _.contains(this.readOnlyAttributes, value);
680 return result || isLockedOut;
684 Recursively checks the object against the schema and converts date strings to
687 @param {Object} Response
689 parse: function (resp, options) {
691 schemas = XT.session.getSchemas(),
694 // A hack to undo damage done by Backbone inside
695 // save function. For use with options that have
696 // collections. See XT.DataSource for the other
698 if (options && options.fixAttributes) {
699 this.attributes = options.fixAttributes;
702 // The normal business.
703 parse = function (namespace, typeName, obj) {
704 var type = schemas[namespace].get(typeName),
709 if (!type) { throw new Error(typeName + " not found in schema."); }
711 if (obj.hasOwnProperty(attr) && obj[attr] !== null) {
712 if (_.isObject(obj[attr])) {
713 rel = _.findWhere(type.relations, {key: attr});
714 typeName = rel ? rel.relatedModel.suffix() : false;
716 if (_.isArray(obj[attr])) {
717 for (i = 0; i < obj[attr].length; i++) {
718 obj[attr][i] = parse(namespace, typeName, obj[attr][i]);
721 obj[attr] = parse(namespace, typeName, obj[attr]);
725 column = _.findWhere(type.columns, {name: attr}) || {};
726 if (column.category === K.DB_DATE) {
727 obj[attr] = new Date(obj[attr]);
736 this._lastParse = parse(this.recordType.prefix(), this.recordType.suffix(), resp);
737 return this._lastParse;
740 relationAdded: function (model, related, options) {
741 var type = model.recordType.suffix(),
745 replaceId = function (model) {
746 idx.splice(idx.indexOf(id), 1, model.id);
747 model.off(evt, replaceId);
749 if (!this._idx[type]) { this._idx[type] = []; }
750 idx = this._idx[type];
753 if (!_.contains(idx, id)) { idx.push(id); }
757 // If no model id, then use a placeholder until we have one
758 id = XT.generateUUID();
760 evt = "change:" + model.idAttribute;
761 model.on(evt, replaceId);
765 Revert a model back to its original state the last time it was fetched.
767 revert: function () {
770 this.clear({silent: true});
771 this.setStatus(K.BUSY_FETCHING);
772 this.set(this._lastParse, {silent: true});
773 this.setStatus(K.READY_CLEAN, {cascade: true});
777 Revert the model to the previous status. Useful for reseting status
778 after a failed validation.
780 param {Boolean} - cascade
782 revertStatus: function (cascade) {
784 prev = this._prevStatus,
787 this.setStatus(this._prevStatus || K.EMPTY);
788 this._prevStatus = prev;
790 // Cascade changes through relations if specified
792 _.each(this.relations, function (relation) {
793 attr = that.attributes[relation.key];
794 if (attr && attr.models &&
795 relation.type === Backbone.HasMany) {
796 _.each(attr.models, function (model) {
797 if (model.revertStatus) {
798 model.revertStatus(cascade);
807 Overload: Don't allow setting when model is in error or destroyed status, or
808 updating a `READY_CLEAN` record without update privileges.
810 @param {String|Object} Key
811 @param {String|Object} Value or Options
812 @param {Objecw} Options
814 set: function (key, val, options) {
816 keyIsObject = _.isObject(key),
817 status = this.getStatus(),
820 // Handle both `"key", value` and `{key: value}` -style arguments.
821 if (keyIsObject) { options = val; }
822 options = options ? options : {};
827 // Set error if no update privileges
828 if (!this.canUpdate()) { err = XT.Error.clone('xt1010'); }
834 case K.DESTROYED_CLEAN:
835 case K.DESTROYED_DIRTY:
836 // Set error if attempting to edit a record that is ineligable
837 err = XT.Error.clone('xt1009', { params: { status: status } });
840 // If we're not in a `READY` state, silence all events
841 if (!_.isBoolean(options.silent)) {
842 options.silent = true;
846 // Raise error, if any
848 this.trigger('invalid', this, err, options);
852 // Handle both `"key", value` and `{key: value}` -style arguments.
853 if (keyIsObject) { val = options; }
854 return Backbone.RelationalModel.prototype.set.call(this, key, val, options);
858 Set the status on the model. Triggers `statusChange` event. Option set to
859 `cascade` will propagate status recursively to all HasMany children.
861 @param {Number} Status
862 @params {Object} Options
863 @params {Boolean} [cascade=false] Cascade changes only through toMany relations
864 @params {Boolean} [propagate=false] Propagate changes through both toMany and toOne relations
866 setStatus: function (status, options) {
874 if (this.isLocked() || this.status === status) { return; }
876 // Reset patch cache if applicable
877 if (status === K.READY_CLEAN && !this.readOnly) {
879 this._cache = this.toJSON();
883 this._prevStatus = this.status;
884 this.status = status;
885 parent = this.getParent();
887 // Cascade changes through relations if specified
888 if (options && (options.cascade || options.propagate)) {
889 _.each(this.relations, function (relation) {
890 attr = that.attributes[relation.key];
891 if (attr && attr.models &&
892 relation.type === Backbone.HasMany) {
893 _.each(attr.models, function (model) {
894 if (model.setStatus) {
895 model.setStatus(status, options);
898 } else if (attr && options.propagate &&
899 relation.type === Backbone.HasOne) {
900 attr.setStatus(status, options);
905 // Percolate changes up to parent when applicable
906 if (parent && (this.isDirty() ||
907 status === K.DESTROYED_DIRTY)) {
908 parentRelation = _.find(this.relations, function (relation) {
909 return relation.isAutoRelation;
911 // #refactor XXX if this is a bona fide Backbone Relational relation,
912 // events will propagate automatically from child to parent.
913 if (parentRelation) {
914 parent.changed[parentRelation.reverseRelation.key] = true;
915 parent.trigger('change', parent, options);
920 // Work around for problem where backbone relational doesn't handle
921 // events consistently on loading.
922 if (status === K.READY_CLEAN) {
923 _handleAddRelated(this);
926 this.trigger('statusChange', this, status, options);
928 * Fire event specifically for the new status.
930 this.trigger('status:' + K._status[status], this, status, options);
938 @retuns {Object} Request
940 save: function (key, value, options) {
941 options = options ? _.clone(options) : {};
943 K = XM.ModelClassMixin,
947 // Can't save unless root
948 if (this.getParent()) {
949 XT.log('You must save on the root level model of this relation');
953 // Handle both `"key", value` and `{key: value}` -style arguments.
954 if (_.isObject(key) || _.isEmpty(key)) {
956 options = value ? _.clone(value) : {};
957 } else if (_.isString(key)) {
961 // Only save if we should.
962 if (this.isDirty() || attrs) {
963 this._wasNew = this.isNew();
964 success = options.success;
966 options.cascade = true; // Cascade status to children
967 options.success = function (model, resp, options) {
968 var namespace = model.recordType.prefix(),
969 schema = XT.session.getSchemas()[namespace],
970 type = model.recordType.suffix(),
971 stype = schema.get(type),
975 if (stype.lockable) {
976 params = [namespace, type, model.id, model.etag];
977 lockOpts.success = function (lock) {
979 model.lockDidChange(model, lock);
980 model.setStatus(K.READY_CLEAN, options);
981 if (success) { success(model, resp, options); }
984 model.dispatch("XM.Model", "obtainLock", params, lockOpts);
985 if (XT.session.config.debugging) { XT.log('Save successful'); }
987 model.setStatus(K.READY_CLEAN, options);
988 if (success) { success(model, resp, options); }
992 // Handle both `"key", value` and `{key: value}` -style arguments.
993 if (_.isObject(key) || _.isEmpty(key)) { value = options; }
995 if (options.collection) {
996 options.collection.each(function (model) {
997 model.setStatus(K.BUSY_COMMITTING, options);
1000 this.setStatus(K.BUSY_COMMITTING, options);
1003 // allow the caller to pass in a different save function to call
1004 result = options.prototypeSave ?
1005 options.prototypeSave.call(this, key, value, options) :
1006 Backbone.Model.prototype.save.call(this, key, value, options);
1007 delete this._wasNew;
1008 if (!result) { this.revertStatus(true); }
1012 XT.log('No changes to save');
1017 Default validation checks `attributes` for:<br />
1018 * Data type integrity.<br />
1019 * Required fields.<br />
1021 Returns `undefined` if the validation succeeded, or some value, usually
1022 an error message, if it fails.<br />
1025 @param {Object} Attributes
1026 @param {Object} Options
1028 validate: function (attributes, options) {
1029 attributes = attributes || {};
1030 options = options || {};
1035 attr, value, category, column,
1036 params = {recordType: this.recordType},
1037 namespace = this.recordType.prefix(),
1038 type = this.recordType.suffix(),
1039 columns = XT.session.getSchemas()[namespace].get(type).columns,
1040 coll = options.collection,
1045 isRelation = function (attr, value, type, options) {
1046 options = options || {};
1049 rel = _.find(that.relations, function (relation) {
1050 return relation.key === attr &&
1051 relation.type === type &&
1052 (!options.nestedOnly || relation.isNested);
1054 return rel ? _.isObject(value) : false;
1056 getColumn = function (attr) {
1057 return _.find(columns, function (column) {
1058 return column.name === attr;
1062 // If we're dealing with a collection, validate each model
1064 for (i = 0; i < coll.length; i++) {
1066 result = model.validate(model.attributes);
1067 if (result) { return result; }
1072 // Check data type integrity
1073 for (attr in attributes) {
1074 if (attributes.hasOwnProperty(attr) &&
1075 !_.isNull(attributes[attr]) &&
1076 !_.isUndefined(attributes[attr])) {
1077 params.attr = ("_" + attr).loc() || "_" + attr;
1079 value = attributes[attr];
1080 column = getColumn(attr);
1081 category = column ? column.category : false;
1084 if (!_.isObject(value) && !_.isString(value)) { // XXX unscientific
1085 params.type = "_binary".loc();
1086 return XT.Error.clone('xt1003', { params: params });
1091 if (!_.isString(value) && !_.isNumber(value) &&
1092 !isRelation(attr, value, Backbone.HasOne)) {
1093 params.type = "_string".loc();
1094 return XT.Error.clone('xt1003', { params: params });
1098 if (!_.isNumber(value) &&
1099 !isRelation(attr, value, Backbone.HasOne)) {
1100 params.type = "_number".loc();
1101 return XT.Error.clone('xt1003', { params: params });
1105 if (!_.isDate(value)) {
1106 params.type = "_date".loc();
1107 return XT.Error.clone('xt1003', { params: params });
1111 if (!_.isBoolean(value)) {
1112 params.type = "_boolean".loc();
1113 return XT.Error.clone('xt1003', { params: params });
1117 isRel = isRelation(attr, value, Backbone.HasMany);
1118 if (!_.isArray(value) && !isRel) {
1119 params.type = "_array".loc();
1120 return XT.Error.clone('xt1003', { params: params });
1122 // Validate children if they're nested, but not if they're not
1123 isRel = isRelation(attr, value, Backbone.HasMany, {nestedOnly: true});
1124 if (isRel && value.models) {
1125 for (i = 0; i < value.models.length; i++) {
1126 model = value.models[i];
1127 if (!(model._prevStatus & XM.Model.DESTROYED)) {
1128 result = model.validate(model.attributes, options);
1129 if (result) { return result; }
1135 if (!_.isObject(value) && !_.isNumber(value)) {
1136 params.type = "_object".loc();
1137 return XT.Error.clone('xt1003', { params: params });
1141 // attribute not in schema
1142 return XT.Error.clone('xt1002', { params: params });
1148 for (i = 0; i < this.requiredAttributes.length; i += 1) {
1149 value = attributes[this.requiredAttributes[i]];
1150 if (value === undefined || value === null || value === "") {
1151 params.attr = ("_" + this.requiredAttributes[i]).loc() ||
1152 "_" + this.requiredAttributes[i];
1153 return XT.Error.clone('xt1004', { params: params });
1162 // ..........................................................
1166 _.extend(XM.Model, XM.ModelClassMixin);
1167 _.extend(XM.Model, /** @lends XM.Model# */{
1170 Overload: Need to handle status here
1172 findOrCreate: function (attributes, options) {
1173 options = options ? options : {};
1174 var parsedAttributes = (_.isObject(attributes) && options.parse && this.prototype.parse) ?
1175 this.prototype.parse(attributes) : attributes;
1177 // Try to find an instance of 'this' model type in the store
1178 var model = Backbone.Relational.store.find(this, parsedAttributes);
1180 // If we found an instance, update it with the data in 'item'; if not, create an instance
1181 // (unless 'options.create' is false).
1182 if (_.isObject(attributes)) {
1183 if (model && options.merge !== false) {
1184 model.setStatus(XM.Model.BUSY_FETCHING);
1185 model.set(attributes, options);
1186 } else if (!model && options.create !== false) {
1187 model = this.build(attributes, options);
1195 Overload: assume that anything calling this function is doing so because it
1196 is building a model for a relation. In that case set the `isFetching` option
1197 is true which will set it in a `BUSY_FETCHING` state when it is created.
1199 build: function (attributes, options) {
1200 options = options ? _.clone(options) : {};
1201 options.isFetching = options.isFetching !== false ? true : false;
1202 options.validate = false;
1203 return Backbone.RelationalModel.build.call(this, attributes, options);
1209 We need to handle the ``isFetching` option at the constructor to make
1210 *sure* the status of the model will be `BUSY_FETCHING` if it needs to be.
1212 var ctor = Backbone.RelationalModel.constructor;
1213 Backbone.RelationalModel.constructor = function (attributes, options) {
1214 ctor.apply(this, arguments);
1215 if (options && options.isFetching) { this.status = XM.Model.BUSY_FETCHING; }
1219 Hack because `add` events don't fire normally when models loaded from the db.
1220 Recursively look for toMany relationships and set sort indexes needed
1221 for `patch` processing.
1223 var _handleAddRelated = function (model) {
1225 // Loop through each model's relation
1226 _.each(model.relations, function (relation) {
1229 // If HasMany, get models and call `relationAdded` on each
1230 // then dive recursively
1231 if (relation.type === Backbone.HasMany) {
1232 coll = model.get(relation.key);
1233 if (coll && coll.length) {
1234 _.each(coll.models, function (item) {
1235 model.relationAdded(item);
1236 _handleAddRelated(item);