Merge pull request #1228 from shackbarth/jsdoc
[xtuple] / lib / backbone-x / source / model_mixin.js
1 /*jshint unused:false, bitwise:false */
2
3 /*global XT:true, XM:true, Backbone:true, _:true */
4
5 (function () {
6   'use strict';
7
8   /**
9     Abstract check for attribute level privilege access.
10
11     @private
12   */
13   var _canDoAttr = function (action, attribute) {
14     var priv = this.privileges &&
15       this.privileges.attribute &&
16       this.privileges.attribute[attribute] &&
17       this.privileges.attribute[attribute][action] ?
18       this.privileges.attribute[attribute][action] : false;
19
20     // If there was a privilege then check our access, otherwise assume we have it
21     var hasPriv = priv ? XT.session.getPrivileges().get(priv) : true;
22
23     // recurse into collections and models to see if they think we can take this action.
24     // First use case is for XM.CharacteristicAssignments. Only implemented at the
25     // moment for "view".
26     var canAct = true;
27     if (action === 'view' && attribute && this.get(attribute) instanceof Backbone.Collection) {
28       canAct = this.get(attribute).model.prototype.canView();
29     } else if (action === 'view' && attribute && this.get(attribute) instanceof Backbone.Model) {
30       canAct = this.get(attribute).canView();
31     }
32
33     return hasPriv && canAct;
34   };
35
36   /**
37     A model mixin used as the base for all models.
38
39     @seealso XM.Model
40     @seealso XM.SimpleModel
41   */
42   XM.ModelMixin = {
43
44     /**
45      * Handler mapping; easily map backbone events to handler functions.
46       Sample usage:
47
48       handlers: {
49         'status:READY_CLEAN': 'onReadyClean',
50         'change:applicationDate': 'dateChanged',
51         'add': 'lineItemAdded'
52       }
53      */
54     handlers: {
55
56     },
57
58     /**
59      * A transient backbone model used to store/manage metadata for this
60      * model.
61      *
62      * @type Backbone.Model
63      */
64     meta: null,
65
66     /**
67       Set to true if you want an id fetched from the server when the `isNew` option
68       is passed on a new model.
69
70       @type {Boolean}
71     */
72     autoFetchId: true,
73
74     /**
75       Are there any binary fields that we might need to worry about transforming?
76       see issue 18661
77     */
78     binaryField: null,
79
80     /**
81       The last error message reported.
82     */
83     lastError: null,
84
85     /**
86       Lock information provide by the server.
87     */
88     lock: null,
89
90     /**
91       Indicate whether a model is lockable.
92       Automatically set when `XT.session` loads
93       the schema.
94     */
95     lockable: false,
96
97     /**
98       A hash structure that defines data access.
99       Automatically set when `XT.session` loads
100       the schema.
101
102       @type {Hash}
103     */
104     privileges: null,
105
106     /**
107       Indicates whether the model is read only.
108
109       @type {Boolean}
110     */
111     readOnly: false,
112
113     /**
114       An array of attribute names designating attributes that are not editable.
115       Use `setReadOnly` to edit this array.
116
117       @seealso `setReadOnly`
118       @seealso `isReadOnly`
119       @type {Array}
120     */
121     readOnlyAttributes: null,
122
123     /**
124       The attribute that is the display name for the model in any case that we
125       want to show just the most obvious field for the user.
126
127       @type {String}
128     */
129     nameAttribute: "name",
130
131     /**
132       Specify the name of a data source model here.
133
134       @type {String}
135     */
136     recordType: null,
137
138     /**
139       An array of required attributes. A `validate` will fail until all the required
140       attributes have values.
141
142       @type {Array}
143     */
144     requiredAttributes: null,
145
146     /**
147       Model's status. You should never modify this directly.
148
149       @seealso `getStatus`
150       @seealse `setStatus`
151       @type {Number}
152       @default `EMPTY`
153     */
154     status: null,
155
156     /**
157       The record version fetched from the server.
158     */
159     etag: null,
160
161
162     // ..........................................................
163     // METHODS
164     //
165
166     /**
167       Allow the mixing in of functionality to models that goes one step
168       deeper than a typical mixin. I can mix in a hash of hashes, arrays,
169       and functions, and those things will be mixed into the pre-existing
170       constructs (instead of overwriting them).
171      */
172     augment: function (hash) {
173       var that = this;
174
175       _.each(hash, function (value, key) {
176         var existingObj = that[key];
177         if (_.isUndefined(existingObj)) {
178           // the target has no value here, so just mix it in
179           that[key] = value;
180
181         } else if (typeof value !== typeof existingObj) {
182           // type mismatch: we're not so clever as to allow this
183           throw new Error("Type mismatch in augment: " + key);
184
185         } else if (_.isArray(value)) {
186           // add array elements (for now we merge duplicates)
187           that[key] = _.union(existingObj, value);
188
189         } else if (_.isFunction(value) && key === 'defaults') {
190           // treat the default function specially: we want to
191           // capture the return values and return the combination
192           // of both
193
194           that[key] = function () {
195             var firstDefaults = existingObj.apply(this, arguments);
196             var secondDefaults = value.apply(this, arguments);
197             return _.extend(firstDefaults, secondDefaults);
198           };
199
200         } else if (_.isFunction(value)) {
201           // for functions, call the super() first, and then the
202           // function that's being mixed in
203
204           that[key] = function () {
205             existingObj.apply(this, arguments);
206             value.apply(this, arguments);
207           };
208
209         } else if (_.isObject(value) &&
210             _.intersection(Object.keys(existingObj), Object.keys(value)).length > 0) {
211           // do not allow overwriting of an object's values
212           throw new Error("Illegal overwrite in augment: " + key);
213
214         } else if (_.isObject(value)) {
215           // mix in objects
216           that[key] = _.extend({}, existingObj, value);
217
218         } else {
219           throw new Error("Do not know how to augment: " + key);
220         }
221       });
222     },
223
224     /**
225       A function that binds events to functions. It can and should only be called
226       once by initialize. Any attempt to call it a second time will throw an error.
227     */
228     bindEvents: function () {
229       // Bind events, but only if we haven't already been here before.
230       // We could silently skip, but then that means any overload done
231       // buy anyone else has do to that check too. That's too error prone
232       // and dangerous because the problems caused by duplicate bindings
233       // are not immediatley apparent and insidiously hard to pin down.
234       if (this._eventsBound) { throw new Error("Events have already been bound."); }
235       this.on('change', this.didChange);
236       this.on('error', this.didError);
237       this.on('destroy', this.didDestroy);
238       this._eventsBound = true;
239     },
240
241     /**
242      * Get the type of an attribute.
243      */
244     getAttributeType: function (attr) {
245       var found = _.findWhere(
246         XT.session.schemas.XM.get(this.recordType.suffix()).columns,
247         { name: attr }
248       );
249       return found && found.type;
250     },
251
252     //
253     // All four of the canVerb functions are defined below as class-level
254     // functions (akin to static functions). Two of those functions are here
255     // as instance functions as well. These just call the class functions.
256     // Notice that canCreate and canRead are missing here. This is on purpose.
257     // Once we have an instance created, there's no reason to ask if we can create
258     // it.
259     //
260     /**
261       Returns whether an attribute can be edited.
262
263       @param {String} Attribute
264       @returns {Boolean}
265     */
266     canEdit: function (attribute) {
267       return _canDoAttr.call(this, "edit", attribute);
268     },
269
270     /**
271       Returns whether the current record can be updated based on privilege
272       settings.
273
274       @returns {Boolean}
275     */
276     canUpdate: function () {
277       return this.getClass().canUpdate(this);
278     },
279
280     /**
281       Returns whether the current record can be deleted based on privilege
282       settings.
283
284       @returns {Boolean}
285     */
286     canDelete: function () {
287       return this.getClass().canDelete(this);
288     },
289
290     /**
291       Returns whether the current record can be deleted based on privilege
292       settings AND whether or not the record is used. Requires a call to the
293       server
294
295       @param {Function} callback. Will be called with boolean response
296     */
297     canDestroy: function (callback) {
298       this.getClass().canDestroy(this, callback);
299     },
300
301     /**
302       Returns whether an attribute can be viewed.
303
304       @param {String} Attribute
305       @returns {Boolean}
306     */
307     canView: function (attribute) {
308       return _canDoAttr.call(this, "view", attribute);
309     },
310
311     /**
312       Reimplemented to handle state change. Calling
313       `destroy` will cause the model to commit to the server
314       immediately.
315
316       @returns {Object|Boolean}
317     */
318     destroy: function (options) {
319       options = options ? _.clone(options) : {};
320       var model = this,
321           result,
322           success = options.success,
323           K = XM.ModelClassMixin;
324
325       this.setStatus(K.DESTROYED_DIRTY);
326       this.setStatus(K.BUSY_DESTROYING);
327       this._wasNew = this.isNew();
328       options.wait = true;
329       options.success = function (resp) {
330         if (success) { success(model, resp, options); }
331       };
332       result = Backbone.Model.prototype.destroy.call(this, options);
333       delete this._wasNew;
334       return result;
335     },
336
337     /**
338       When any attributes change update the status if applicable.
339     */
340     didChange: function (model, options) {
341       options = options || {};
342       var K = XM.ModelClassMixin,
343         status = this.getStatus();
344       if (this.isBusy()) { return; }
345
346       // Mark dirty if we should
347       if (status === K.READY_CLEAN) {
348         this.setStatus(K.READY_DIRTY);
349       }
350     },
351
352     /**
353       Called after confirmation that the model was destroyed on the
354       data source.
355     */
356     didDestroy: function () {
357       var K = XM.ModelClassMixin;
358       this.clear({silent: true});
359       this.setStatus(K.DESTROYED_CLEAN);
360     },
361
362     /**
363       Handle a `sync` response that was an error.
364     */
365     didError: function (model, resp) {
366       model = model || {};
367       this.lastError = resp;
368       XT.log(resp);
369     },
370
371     /**
372       Generate an array of patch objects per:
373       http://tools.ietf.org/html/rfc6902
374
375       @returns {Array}
376     */
377     generatePatches: function () {
378       if (!this._cache) { return []; }
379       var observer = XM.jsonpatch.observe(this._cache);
380       observer.object = this.toJSON();
381       return XM.jsonpatch.generate(observer);
382     },
383
384     /**
385       Called when model is instantiated.
386     */
387     initialize: function (attributes, options) {
388       attributes = attributes || {};
389       options = options || {};
390       var klass,
391         K = XM.ModelClassMixin,
392         status = this.getStatus(),
393         idAttribute = this.idAttribute;
394
395       // Set defaults if not provided
396       this.privileges = this.privileges || {};
397       this.readOnlyAttributes = this.readOnlyAttributes ?
398         this.readOnlyAttributes.slice(0) : [];
399       this.requiredAttributes = this.requiredAttributes ?
400         this.requiredAttributes.slice(0) : [];
401
402       // Validate
403       if (_.isEmpty(this.recordType)) { throw new Error('No record type defined'); }
404
405       if (_.isNull(status)) {
406         this.setStatus(K.EMPTY);
407         this.bindEvents();
408       }
409
410       // Handle options
411       if (options.isNew) {
412         klass = this.getClass();
413         if (!klass.canCreate()) {
414           throw new Error('Insufficent privileges to create a record.');
415         }
416         this.setStatus(K.READY_NEW);
417
418         // Key generator (client based)
419         if (idAttribute === 'uuid' &&
420             !this.get(idAttribute) &&
421             !attributes[idAttribute]) {
422           this.set(idAttribute, XT.generateUUID());
423         }
424
425         // Deprecated key generator (server based)
426         if (this.autoFetchId) {
427           if (options.database) {
428             this.fetchId({database: options.database});
429           } else {
430             // This should throw and error for a call that needs to be fixed.
431             this.fetchId();
432           }
433         }
434       }
435
436       // Set attributes that should be required and read only
437       if (idAttribute &&
438           !_.contains(this.requiredAttributes, idAttribute)) {
439         this.requiredAttributes.push(idAttribute);
440       }
441     },
442
443     lockDidChange: function (model, lock) {
444       var that = this,
445         options = {};
446
447       // Clear any old refresher
448       if (this._keyRefresherInterval) {
449         clearInterval(this._keyRefresherInterval);
450         that._keyRefresherInterval = undefined;
451       }
452
453       if (lock && lock.key && !this._keyRefresherInterval) {
454         options.automatedRefresh = true;
455         options.success = function (renewed) {
456           // If for any reason the lock was not renewed (maybe got disconnected?)
457           // Update the model so it knows.
458           var lock = that.lock;
459           if (lock && !renewed) {
460             lock = _.clone(lock);
461             delete lock.key;
462             that.lock = lock;
463           }
464         };
465
466         // set up a refresher
467         this._keyRefresherInterval = setInterval(function () {
468           that.dispatch('XM.Model', 'renewLock', [lock.key], options);
469         }, 25 * 1000);
470       }
471       this.trigger("lockChange", that);
472     },
473
474     /**
475      * Forward a dispatch request to the data source. Runs a "dispatchable" database function.
476      * Include a `success` callback function in options to handle the result.
477      *
478      * @param {String} Name of the class
479      * @param {String} Function name
480      * @param {Object} Parameters
481      * @param {Object} Options
482      */
483     dispatch: function (name, func, params, options) {
484       options = _.extend({}, options); // clone and set to {} if undefined
485       var dataSource = options.dataSource || XT.dataSource,
486         payload = {
487           nameSpace: name.replace(/\.\w+/i, ''),
488           type: name.suffix(),
489           dispatch: {
490             functionName: func,
491             parameters: params
492           }
493         };
494       return dataSource.request(null, "post", payload, options);
495     },
496
497     /*
498       Reimplemented to handle status changes.
499
500       @param {Object} Options
501       @returns {Object} Request
502     */
503     fetch: function (options) {
504       options = options ? _.clone(options) : {};
505       var model = this,
506         K = XM.ModelClassMixin,
507         success = options.success,
508         klass = this.getClass();
509
510       if (klass.canRead()) {
511         this.setStatus(K.BUSY_FETCHING);
512         options.success = function (resp) {
513           model.setStatus(K.READY_CLEAN, options);
514           if (XT.session.config.debugging) {
515             XT.log('Fetch successful');
516           }
517           if (success) { success(model, resp, options); }
518         };
519         return Backbone.Model.prototype.fetch.call(this, options);
520       }
521       XT.log('Insufficient privileges to fetch');
522       return false;
523     },
524
525     /**
526       Set the id on this record an id from the server. Including the `cascade`
527       option will call ids to be fetched recursively for `HasMany` relations.
528
529       @returns {Object} Request
530     */
531     fetchId: function (options) {
532       options = _.defaults(options ? _.clone(options) : {}, {});
533       var that = this;
534       if (!this.id) {
535         options.success = function (resp) {
536           that.set(that.idAttribute, resp, options);
537         };
538         this.dispatch('XM.Model', 'fetchId', this.recordType, options);
539       }
540     },
541
542     /**
543       Return a matching record id for a passed user `key` and `value`. If none
544       found, returns zero.
545
546       @param {String} Property to search on, typically a user key
547       @param {String} Value to search for
548       @param {Object} Options
549       @param {Function} [options.succss] Callback on success
550       @param {Function} [options.error] Callback on error
551       @returns {Object} Receiver
552     */
553     findExisting: function (key, value, options) {
554       options = options || {};
555       return this.getClass().findExisting.call(this, key, value, options);
556     },
557
558     /**
559       Valid attribute names that can be used on this model based on the
560       data source definition, whether or not they already exist yet on the
561       current instance.
562
563       @returns {Array}
564     */
565     getAttributeNames: function () {
566       return this.getClass().getAttributeNames.call(this);
567     },
568
569     /**
570       Returns the current model prototype class.
571
572       @returns {Object}
573     */
574     getClass: function () {
575       return Object.getPrototypeOf(this).constructor;
576     },
577
578     /**
579       Return the current status.
580
581       @returns {Number}
582     */
583     getStatus: function () {
584       return this.status;
585     },
586
587     /**
588       Return the current status as as string.
589
590       @returns {String}
591     */
592     getStatusString: function () {
593       var ret = [],
594         status = this.getStatus(),
595         prop;
596       for (prop in XM.ModelClassMixin) {
597         if (XM.ModelClassMixin.hasOwnProperty(prop)) {
598           if (prop.match(/[A-Z_]$/) && XM.ModelClassMixin[prop] === status) {
599             ret.push(prop);
600           }
601         }
602       }
603       return ret.join(" ");
604     },
605
606     /**
607       Return the type as defined by the model's orm. Attribute path is supported.
608
609       @parameter {String} Attribute name
610       @returns {String}
611     */
612     getType: function (value) {
613       return this.getClass().getType(value);
614     },
615
616     /**
617       Searches attributes first, if not found then returns either a function call
618       or property value that matches the key. It supports search on an attribute path
619       through a model hierarchy.
620       @param {String} Key
621       @returns {Any}
622       @example
623       // Returns the first name attribute from primary contact model.
624       var firstName = m.getValue('primaryContact.firstName');
625     */
626     getValue: function (key) {
627       var parts,
628         value;
629
630       // Search path
631       if (key.indexOf('.') !== -1) {
632         parts = key.split('.');
633         value = this;
634         _.each(parts, function (part) {
635           value = value instanceof Backbone.Model ? value.getValue(part) : value;
636         });
637         return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
638       }
639
640       // Search attribute, meta, function, propety
641       if (_.has(this.attributes, key)) {
642         return this.attributes[key];
643       } else if (this.meta && _.has(this.meta.attributes, key)) {
644         return this.meta.get(key);
645       } else {
646         return _.isFunction(this[key]) ? this[key]() : this[key];
647       }
648     },
649
650     isBusy: function () {
651       var status = this.getStatus(),
652         K = XM.ModelClassMixin;
653       return status === K.BUSY_FETCHING ||
654              status === K.BUSY_COMMITTING ||
655              status === K.BUSY_DESTROYING;
656     },
657
658     /**
659       Reimplemented. A model is new if the status is `READY_NEW`.
660
661       @returns {Boolean}
662     */
663     isNew: function () {
664       var K = XM.ModelClassMixin;
665       return this.getStatus() === K.READY_NEW || this._wasNew || false;
666     },
667
668     /**
669       Returns true if status is `DESTROYED_CLEAN` or `DESTROYED_DIRTY`.
670
671       @returns {Boolean}
672     */
673     isDestroyed: function () {
674       var status = this.getStatus(),
675         K = XM.ModelClassMixin;
676       return status === K.DESTROYED_CLEAN || status === K.DESTROYED_DIRTY;
677     },
678
679     /**
680       Returns true if status is `READY_NEW` or `READY_DIRTY`.
681
682       @returns {Boolean}
683     */
684     isDirty: function () {
685       var status = this.getStatus(),
686         K = XM.ModelClassMixin;
687       return status === K.READY_NEW ||
688              status === K.READY_DIRTY ||
689              status === K.DESTROYED_DIRTY;
690     },
691
692     /**
693       Returns true if the model is in one of the `READY` statuses
694     */
695     isReady: function () {
696       var status = this.getStatus(),
697         K = XM.ModelClassMixin;
698       return status === K.READY_NEW ||
699              status === K.READY_CLEAN ||
700              status === K.READY_DIRTY;
701     },
702
703     /**
704       Returns true if you have the lock key, or if this model
705       is not lockable. (You can enter the room if you have no
706       key or if there is no lock!). When this value is true and the
707       `isLockable` is true it means the user has a application lock
708       on the object at the database level so that no other users can
709       edit the record.
710
711       This is not to be confused with the `isLocked` function that
712       is used by Backbone-relational to manage events on relations.
713
714       @returns {Boolean}
715     */
716     hasLockKey: function () {
717       return !this.lock || this.lock.key ? true : false;
718     },
719
720     /**
721      * Returns the lock's key if it exists, otherwise null.
722      * @returns {Object}
723      */
724     getLockKey: function () {
725       return this.lock ? this.lock.key : false;
726     },
727
728     /**
729       Return whether the model is in a read-only state. If an attribute name
730       is passed, returns whether that attribute is read-only. It is also
731       capable of checking the read only status of child objects via a search path string.
732
733       <pre><code>
734         // Inquire on the whole model
735         var readOnly = this.isReadOnly();
736
737         // Inquire on a single attribute
738         var readOnly = this.isReadOnly("name");
739
740         // Inquire using a search path
741         var readOnly = this.isReadOnly("contact.firstName");
742       </code></pre>
743
744       @seealso `setReadOnly`
745       @seealso `readOnlyAttributes`
746       @param {String} attribute
747       @returns {Boolean}
748     */
749     isReadOnly: function (value) {
750       var result,
751         parts,
752         isLockedOut = !this.hasLockKey();
753
754       // Search path
755       if (_.isString(value) && value.indexOf('.') !== -1) {
756         parts = value.split('.');
757         result = this;
758         _.each(parts, function (part) {
759           if (result instanceof Backbone.Model) {
760             result = result.getValue(part);
761           } else if (_.isNull(result)) {
762             return result;
763           } else if (!_.isUndefined(result)) {
764             result = result.isReadOnly(part) || !result.hasLockKey();
765           }
766         });
767         return result;
768       }
769
770       if ((!_.isString(value) && !_.isObject(value)) || this.readOnly) {
771         result = this.readOnly;
772       } else {
773         result = _.contains(this.readOnlyAttributes, value);
774       }
775       return result || isLockedOut;
776     },
777
778     /**
779       Return whether an attribute is required.
780
781       @param {String} attribute
782       @returns {Boolean}
783     */
784     isRequired: function (value) {
785       return _.contains(this.requiredAttributes, value);
786     },
787
788
789     /**
790       A utility function that triggers a `notify` event. Useful for passing along
791       information to the interface. Bind to `notify` to use.
792
793       <pre><code>
794         var m = new XM.MyModel();
795         var raiseAlert = function (model, value, options) {
796           alert(value);
797         }
798         m.on('notify', raiseAlert);
799       </code></pre>
800
801       @param {String} Message
802       @param {Object} Options
803       @param {Number} [options.type] Type of notification NOTICE,
804         WARNING, CRITICAL, QUESTION. Default = NOTICE.
805       @param {Object} [options.callback] A callback function to process based on user response.
806     */
807     notify: function (message, options) {
808       options = options ? _.clone(options) : {};
809       if (options.type === undefined) {
810         options.type = XM.ModelClassMixin.NOTICE;
811       }
812       this.trigger('notify', this, message, options);
813     },
814
815     /**
816       Return the original value of an attribute the last time fetch was called.
817
818       @returns {Object}
819     */
820     original: function (attr) {
821       var parts,
822         value;
823
824       // Search path
825       if (attr.indexOf('.') !== -1) {
826         parts = attr.split('.');
827         value = this;
828         _.each(parts, function (part) {
829           value = value instanceof Backbone.Model ? value.original(part) : value;
830         });
831         return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
832       }
833
834       return this._cache ? this._cache[attr] : this.attributes[attr];
835     },
836
837     /**
838       Return all the original values of the attributes the last time fetch was called.
839       Note this returns objects an the original javascript payload format, not relational children.
840
841       @returns {Array}
842     */
843     originalAttributes: function () {
844       return this._cache;
845     },
846
847     /**
848       Checks the object against the schema and converts date strings to date objects.
849
850       @param {Object} Response
851     */
852     parse: function (resp) {
853       var K = XT.Session,
854         schemas = XT.session.getSchemas(),
855         column,
856         parse;
857       parse = function (namespace, typeName, obj) {
858         var type = schemas[namespace].get(typeName),
859           attr;
860         if (!type) { throw new Error(typeName + " not found in schema " + namespace + "."); }
861         for (attr in obj) {
862           if (obj.hasOwnProperty(attr) && obj[attr] !== null) {
863             column = _.findWhere(type.columns, {name: attr}) || {};
864             if (column.category === K.DB_DATE) {
865               obj[attr] = new Date(obj[attr]);
866             }
867           }
868         }
869         return obj;
870       };
871       return parse(this.recordType.prefix(), this.recordType.suffix(), resp);
872     },
873
874     /**
875       Returns the previous status of the model.
876
877       @returns {Boolean} Previous Status
878     */
879     previousStatus: function () {
880       return this._prevStatus;
881     },
882
883     /**
884      * Manage all re-entrant lock actions, namely obtain, renew, and release.
885      *
886      * @param action {String}
887      *
888      * @see XM.Model#obtainLock
889      * @see XM.Model#renewLock
890      * @see XM.Model#releaseLock
891      */
892     _reentrantLockHelper: function (action, params, _options) {
893       var that = this,
894         options = _.extend({ }, _options),
895         userCallback = options.success,
896         methodName = action + 'Lock',
897         eventName = 'lock:' + action;
898
899       this.dispatch("XM.Model", methodName, params, _.extend(options, {
900         success: function (lock) {
901           that.lock = lock;
902           that.lockDidChange(that, lock);
903           that.trigger(eventName, that, { lock: lock });
904
905           if (_.isFunction(userCallback)) {
906             userCallback();
907           }
908         },
909         error: function () {
910           that.trigger('lock:error', that);
911         }
912       }));
913     },
914
915     /**
916       Revert the model to the previous status. Useful for reseting status
917       after a failed validation.
918
919       param {Boolean} - cascade
920     */
921     revertStatus: function (cascade) {
922       var K = XM.ModelClassMixin,
923         prev = this._prevStatus;
924       this.setStatus(this._prevStatus || K.EMPTY);
925       this._prevStatus = prev;
926     },
927
928     /**
929       Reimplemented.
930
931       @retuns {Object} Request
932     */
933     save: function (key, value, options) {
934       options = options ? _.clone(options) : {};
935       var attrs = {},
936         K = XM.ModelClassMixin,
937         success,
938         result;
939
940       // Handle both `"key", value` and `{key: value}` -style arguments.
941       if (_.isObject(key) || _.isEmpty(key)) {
942         attrs = key;
943         options = value ? _.clone(value) : {};
944       } else if (_.isString(key)) {
945         attrs[key] = value;
946       }
947
948       // Only save if we should.
949       if (this.isDirty() || attrs) {
950         this._wasNew = this.isNew();
951         success = options.success;
952         options.wait = true;
953         options.success = function (model, resp, options) {
954           model.setStatus(K.READY_CLEAN, options);
955           if (XT.session.config.debugging) {
956             XT.log('Save successful');
957           }
958           if (success) { success(model, resp, options); }
959         };
960
961         // Handle both `"key", value` and `{key: value}` -style arguments.
962         if (_.isObject(key) || _.isEmpty(key)) { value = options; }
963
964         // Call the super version
965         this.setStatus(K.BUSY_COMMITTING, {cascade: true});
966         result = Backbone.Model.prototype.save.call(this, key, value, options);
967         delete this._wasNew;
968         if (!result) { this.revertStatus(true); }
969         return result;
970       }
971
972       XT.log('No changes to save');
973       return false;
974     },
975
976     /**
977       Overload: Don't allow setting when model is in error or destroyed status, or
978       updating a `READY_CLEAN` record without update privileges.
979
980       @param {String|Object} Key
981       @param {String|Object} Value or Options
982       @param {Object} Options
983     */
984     set: function (key, val, options) {
985       var K = XM.ModelClassMixin,
986         keyIsObject = _.isObject(key),
987         status = this.getStatus(),
988         err;
989
990       // Handle both `"key", value` and `{key: value}` -style arguments.
991       if (keyIsObject) { options = val; }
992       options = options ? options : {};
993
994       switch (status)
995       {
996       case K.READY_CLEAN:
997         // Set error if no update privileges
998         // return;
999         if (!this.canUpdate()) { err = XT.Error.clone('xt1010'); }
1000         break;
1001       case K.READY_DIRTY:
1002       case K.READY_NEW:
1003         break;
1004       case K.ERROR:
1005       case K.DESTROYED_CLEAN:
1006       case K.DESTROYED_DIRTY:
1007         // Set error if attempting to edit a record that is ineligable
1008         err = XT.Error.clone('xt1009', { params: { status: status } });
1009         break;
1010       default:
1011         // If we're not in a `READY` state, silence all events
1012         if (!_.isBoolean(options.silent)) {
1013           options.silent = true;
1014         }
1015       }
1016
1017       // Raise error, if any
1018       if (err) {
1019         this.trigger('invalid', this, err, options);
1020         return false;
1021       }
1022
1023       // Handle both `"key", value` and `{key: value}` -style arguments.
1024       if (keyIsObject) { val = options; }
1025       return Backbone.Model.prototype.set.call(this, key, val, options);
1026     },
1027
1028     /**
1029       Set a field if exists in a schema. Otherwise ignore silently.
1030     */
1031     setIfExists: function (key, val, options) {
1032       var K = XM.ModelClassMixin,
1033         keyIsObject = _.isObject(key),
1034         attributes = this.getAttributeNames();
1035
1036       // Handle both `"key", value` and `{key: value}` -style arguments.
1037       if (keyIsObject) { options = val; }
1038       options = options ? options : {};
1039
1040       if (keyIsObject) {
1041         _.each(key, function (subvalue, subkey) {
1042           if (!_.contains(attributes, subkey)) {
1043             delete key[subkey];
1044           }
1045         });
1046         if (_.isEmpty(key)) {
1047           return false;
1048         }
1049       } else {
1050         if (!_.contains(attributes, key)) {
1051           return false;
1052         }
1053       }
1054
1055       // Handle both `"key", value` and `{key: value}` -style arguments.
1056       if (keyIsObject) { val = options; }
1057       return this.set.call(this, key, val, options);
1058     },
1059
1060     /**
1061       Set a value(s) on attributes if key(s) is/are in schema, otherwise set on
1062       `meta`. If `meta` is null then behaves the same as `setIfExists`.
1063     */
1064     setValue: function (key, val, options) {
1065       var keyIsObject = _.isObject(key),
1066         attributes = this.getAttributeNames(),
1067         that = this,
1068         results;
1069
1070       // If no meta, then forward request.
1071       if (!this.meta) {
1072         return this.setIfExists(key, val, options);
1073       }
1074
1075       // Handle both `"key", value` and `{key: value}` -style arguments.
1076       if (keyIsObject) { options = val; }
1077       options = options ? options : {};
1078
1079       if (keyIsObject) {
1080         _.each(key, function (subvalue, subkey) {
1081           if (!_.contains(attributes, subkey)) {
1082             that.meta.set(subkey, subvalue, options);
1083             delete key[subkey];
1084           }
1085           if (!_.isEmpty(key)) {
1086             that.set(key, options);
1087           }
1088         });
1089       } else {
1090         if (_.contains(attributes, key)) {
1091           this.set(key, val, options);
1092         } else {
1093           this.meta.set(key, val, options);
1094         }
1095       }
1096
1097       return this;
1098     },
1099
1100     /**
1101       Set the entire model, or a specific model attribute to `readOnly`.<br />
1102       Examples:<pre><code>
1103       m.setReadOnly() // sets model to read only
1104       m.setReadOnly(false) // sets model to be editable
1105       m.setReadOnly('name') // sets 'name' attribute to read-only
1106       m.setReadOnly('name', false) // sets 'name' attribute to be editable</code></pre>
1107
1108       Note: Privilege enforcement supercedes read-only settings.
1109
1110       @seealso `isReadOnly`
1111       @seealso `readOnly`
1112       @param {String|Array|Boolean} Attribute string or hash to set, or boolean if setting the model
1113       @param {Boolean} Boolean - default = true.
1114       @returns Receiver
1115     */
1116     setReadOnly: function (key, value) {
1117       value = _.isBoolean(value) ? value : true;
1118       var that = this,
1119         changes = {},
1120         delta,
1121         process = function (key, value) {
1122           if (value && !_.contains(that.readOnlyAttributes, key)) {
1123             that.readOnlyAttributes.push(key);
1124             changes[key] = true;
1125           } else if (!value && _.contains(that.readOnlyAttributes, key)) {
1126             that.readOnlyAttributes = _.without(that.readOnlyAttributes, key);
1127             changes[key] = true;
1128           }
1129         };
1130
1131       // Handle attribute array
1132       if (_.isObject(key)) {
1133         _.each(key, function (attr) {
1134             process(attr, value);
1135             changes[attr] = true;
1136           });
1137
1138       // handle attribute string
1139       } else if (_.isString(key)) {
1140         process(key, value);
1141
1142       // handle model
1143       } else {
1144         key = _.isBoolean(key) ? key : true;
1145         this.readOnly = key;
1146         // Attributes that were already read-only will stay that way
1147         // so only count the attributes that were not affected
1148         delta = _.difference(this.getAttributeNames(), this.readOnlyAttributes);
1149         _.each(delta, function (attr) {
1150           changes[attr] = true;
1151         });
1152       }
1153
1154       // Notify changes
1155       if (!_.isEmpty(changes)) {
1156         this.trigger('readOnlyChange', this, {changes: changes, isReadOnly: value});
1157       }
1158       return this;
1159     },
1160
1161     /**
1162       Set the status on the model. Triggers `statusChange` event.
1163
1164       @param {Number} Status
1165     */
1166     setStatus: function (status, options) {
1167       var K = XM.ModelClassMixin;
1168
1169       if (this.status === status) { return; }
1170       this._prevStatus = this.status;
1171       this.status = status;
1172
1173       // Reset patch cache if applicable
1174       if (status === K.READY_CLEAN && !this.readOnly) {
1175         this._cache = this.toJSON();
1176       }
1177
1178       this.trigger('statusChange', this, status, options);
1179       return this;
1180     },
1181
1182     /**
1183       Sync to xTuple data source.
1184
1185       Accepts options.collection to sync a Backbone collection
1186       of models in lieu of just the current model.
1187     */
1188     sync: function (method, model, options) {
1189       options = options ? _.clone(options) : {};
1190       var dataSource = options.dataSource || XT.dataSource,
1191         key = this.idAttribute,
1192         error = options.error,
1193         K = XM.ModelClassMixin,
1194         that = this,
1195         payload,
1196         result;
1197
1198       options.error = function (resp) {
1199         that.setStatus(K.ERROR);
1200         if (error) { error(model, resp, options); }
1201       };
1202
1203       // Handle a colleciton of models to persist
1204       if (options.collection) {
1205         payload = [];
1206         options.collection.each(function (obj) {
1207           var item = {
1208             nameSpace: obj.recordType.replace(/\.\w+/i, ''),
1209             type: obj.recordType.suffix()
1210           };
1211           item.id = obj.id;
1212
1213           if (obj.binaryField) {
1214             throw "Processing of for arrays of models with binary fields is not supported.";
1215           }
1216
1217           switch (obj.previousStatus())
1218           {
1219           case K.READY_NEW:
1220             item.method = "post";
1221             item.data = obj.toJSON();
1222             item.requery = options.requery;
1223             break;
1224           case K.READY_DIRTY:
1225             item.method = "patch";
1226             item.etag = obj.etag;
1227             item.lock = obj.lock;
1228             item.patches = obj.generatePatches();
1229             item.requery = options.requery;
1230             break;
1231           case K.DESTROYED_DIRTY:
1232             item.method = "delete";
1233             item.etag = obj.etag;
1234             item.lock = obj.lock;
1235             break;
1236           default:
1237             throw "Model in collection syncing from an unsupported state";
1238           }
1239
1240           payload.push(item);
1241         });
1242
1243         // All collections have to go through "post."
1244         method = "post";
1245
1246       // Handle the case of a model only persisting itself
1247       } else {
1248         payload = {};
1249         payload.nameSpace = this.recordType.replace(/\.\w+/i, '');
1250         payload.type = this.recordType.suffix();
1251
1252         // Get an id from... someplace
1253         if (options.id) {
1254           payload.id = options.id;
1255         } else if (options[key]) {
1256           payload.id = options[key];
1257         } else if (model._cache) {
1258           payload.id = model._cache[key];
1259         } else if (model.id) {
1260           payload.id = model.id;
1261         } else if (model.attributes) {
1262           payload.id = model.attributes[key];
1263         } else {
1264           options.error("Cannot find id");
1265           return;
1266         }
1267
1268         switch (method) {
1269         case "create":
1270           payload.data = model.toJSON();
1271           payload.binaryField = model.binaryField; // see issue 18661
1272           payload.requery = options.requery;
1273           break;
1274         case "read":
1275           method = "get";
1276           if (options.context) { payload.context = options.context; }
1277           break;
1278         case "patch":
1279         case "update":
1280           payload.etag = model.etag;
1281           payload.lock = model.lock;
1282           payload.patches = model.generatePatches();
1283           payload.binaryField = model.binaryField;
1284           payload.requery = options.requery;
1285           break;
1286         case "delete":
1287           payload.etag = model.etag;
1288           payload.lock = model.lock;
1289         }
1290
1291         // Translate method
1292         switch (method) {
1293         case "create":
1294           method = "post";
1295           break;
1296         case "read":
1297           method = "get";
1298           break;
1299         case "update":
1300           method = "patch";
1301         }
1302       }
1303
1304       result = dataSource.request(model, method, payload, options);
1305
1306       return result || false;
1307     },
1308
1309     /**
1310       Overload: Convert dates to strings.
1311     */
1312     toJSON: function (options) {
1313       var prop,
1314
1315       json = Backbone.Model.prototype.toJSON.apply(this, arguments);
1316
1317       // Convert dates to strings to avoid conflicts with jsonpatch
1318       for (prop in json) {
1319         if (json.hasOwnProperty(prop) && json[prop] instanceof Date) {
1320           json[prop] = json[prop].toJSON();
1321         }
1322       }
1323
1324       return json;
1325     },
1326
1327     /**
1328       Determine whether this record has been referenced by another. By default
1329       this function inspects foreign key relationships on the database, and is
1330       therefore dependent on foreign key relationships existing where appropriate
1331       to work correctly.
1332
1333       @param {Object} Options
1334       @returns {Object} Request
1335     */
1336     used: function (options) {
1337       return this.getClass().used(this.id, options);
1338     },
1339
1340     /**
1341       Default validation checks `attributes` for:<br />
1342         &#42; Data type integrity.<br />
1343         &#42; Required fields.<br />
1344       <br />
1345       Returns `undefined` if the validation succeeded, or some value, usually
1346       an error message, if it fails.<br />
1347       <br />
1348
1349       @param {Object} Attributes
1350       @param {Object} Options
1351     */
1352     validate: function (attributes, options) {
1353       attributes = attributes || {};
1354       options = options || {};
1355
1356       if (!XT.session.getSchemas()[this.recordType.prefix()].get(this.recordType.suffix())) {
1357         XT.log("Cannot find schema", this.recordType);
1358       }
1359       var i,
1360         S = XT.Session,
1361         attr, value, category, column, params = {},
1362         type = this.recordType.suffix(),
1363         namespace = this.recordType.prefix(),
1364         columns = XT.session.getSchemas()[namespace].get(type).columns,
1365
1366         getColumn = function (attr) {
1367           return _.find(columns, function (column) {
1368             return column.name === attr;
1369           });
1370         };
1371
1372       // Check data type integrity
1373       for (attr in attributes) {
1374         if (attributes.hasOwnProperty(attr) &&
1375             !_.isNull(attributes[attr]) &&
1376             !_.isUndefined(attributes[attr])) {
1377           params.attr = ("_" + attr).loc();
1378
1379           value = attributes[attr];
1380           column = getColumn(attr);
1381           category = column ? column.category : false;
1382           switch (category) {
1383           case S.DB_BYTEA:
1384             if (!_.isObject(value) && !_.isString(value)) { // XXX unscientific
1385               params.type = "_binary".loc();
1386               return XT.Error.clone('xt1003', { params: params });
1387             }
1388             break;
1389           case S.DB_UNKNOWN:
1390           case S.DB_STRING:
1391             if (!_.isString(value)) {
1392               params.type = "_string".loc();
1393               return XT.Error.clone('xt1003', { params: params });
1394             }
1395             break;
1396           case S.DB_NUMBER:
1397             if (!_.isNumber(value)) {
1398               params.type = "_number".loc();
1399               return XT.Error.clone('xt1003', { params: params });
1400             }
1401             break;
1402           case S.DB_DATE:
1403             if (!_.isDate(value)) {
1404               params.type = "_date".loc();
1405               return XT.Error.clone('xt1003', { params: params });
1406             }
1407             break;
1408           case S.DB_BOOLEAN:
1409             if (!_.isBoolean(value)) {
1410               params.type = "_boolean".loc();
1411               return XT.Error.clone('xt1003', { params: params });
1412             }
1413             break;
1414           case S.DB_ARRAY:
1415             if (!_.isArray(value)) {
1416               params.type = "_array".loc();
1417               return XT.Error.clone('xt1003', { params: params });
1418             }
1419             break;
1420           case S.DB_COMPOUND:
1421             if (!_.isObject(value) && !_.isNumber(value)) {
1422               params.type = "_object".loc();
1423               return XT.Error.clone('xt1003', { params: params });
1424             }
1425             break;
1426           default:
1427             return XT.Error.clone('xt1002', { params: params });
1428           }
1429         }
1430       }
1431
1432       // Check required.
1433       for (i = 0; i < this.requiredAttributes.length; i += 1) {
1434         value = attributes[this.requiredAttributes[i]];
1435         if (value === undefined || value === null || value === "") {
1436           params.attr = ("_" + this.requiredAttributes[i]).loc();
1437           return XT.Error.clone('xt1004', { params: params });
1438         }
1439       }
1440
1441       return;
1442     }
1443
1444   };
1445
1446   // ..........................................................
1447   // CLASS METHODS
1448   //
1449
1450   /**
1451     A mixin for use on model classes that includes status constants
1452     and privilege control functions.
1453   */
1454   XM.ModelClassMixin = {
1455     getReportUrl: function (action, modelName, id) {
1456       var reportUrl = "/generate-report?nameSpace=%@&type=%@&id=%@".f(
1457         modelName.prefix(), modelName.suffix(), id);
1458
1459       if (action) {
1460         reportUrl = reportUrl + "&action=" + action;
1461       }
1462       return reportUrl;
1463     },
1464
1465
1466     /**
1467       Use this function to find out whether a user can create records before
1468       instantiating one.
1469
1470       @returns {Boolean}
1471     */
1472     canCreate: function () {
1473       return XM.ModelClassMixin.canDo.call(this, 'create');
1474     },
1475
1476     /**
1477       Use this function to find out whether a user can read this record type
1478       before any have been loaded.
1479
1480       @param {Object} Model
1481       @param {String} Attribute name (optional)
1482       @returns {Boolean}
1483     */
1484     canRead: function (model, attribute) {
1485       return XM.ModelClassMixin.canDo.call(this, 'read', model, attribute);
1486     },
1487
1488     /**
1489       Returns whether a user has access to update a record of this type. If a
1490       record is passed that involves personal privileges, it will validate
1491       whether that particular record is updatable.
1492
1493       @param {Object} Model
1494       @param {String} Attribute name (optional)
1495       @returns {Boolean}
1496     */
1497     canUpdate: function (model) {
1498       return XM.ModelClassMixin.canDo.call(this, 'update', model);
1499     },
1500
1501     /**
1502       Returns whether a user has access to delete a record of this type. If a
1503       record is passed that involves personal privileges, it will validate
1504       whether that particular record is deletable.
1505
1506       @param {Object} Model
1507       @returns {Boolean}
1508     */
1509     canDelete: function (model) {
1510       return XM.ModelClassMixin.canDo.call(this, 'delete', model);
1511     },
1512
1513     /**
1514       Returns whether the current record can be deleted based on privilege
1515       settings AND whether or not the record is used. Requires a call to the
1516       server
1517
1518       @param {Object} Model
1519       @param {Function} callback. Will be called with boolean response
1520     */
1521     canDestroy: function (model, callback) {
1522       var options = {};
1523
1524       if (!XM.ModelClassMixin.canDelete.call(this, model)) {
1525         callback(false);
1526         return;
1527       }
1528
1529       options.success = function (used) {
1530         callback(!used);
1531       };
1532
1533       this.used.call(this, model.id, options);
1534
1535     },
1536
1537     /**
1538       Check privilege on `action`. If `model` is passed, checks personal
1539       privileges on the model where applicable.
1540
1541       @param {String} Action
1542       @param {XM.Model} Model
1543     */
1544     canDo: function (action, model, attribute) {
1545       var privs = this.prototype.privileges,
1546         sessionPrivs = XT.session.privileges,
1547         isGrantedAll = false,
1548         isGrantedPersonal = false,
1549         username = XT.session.details.username,
1550         value,
1551         i,
1552         props,
1553         K = XM.ModelClassMixin,
1554         status = model && model.getStatus ? model.getStatus() : K.READY;
1555
1556       // Need to be in a valid status to "do" anything
1557       if (!(status & K.READY)) { return false; }
1558
1559       // If no privileges, nothing to check.
1560       if (_.isEmpty(privs)) { return true; }
1561
1562       // If we have session prvileges perform the check.
1563       if (sessionPrivs && sessionPrivs.get) {
1564         // Check global privileges.
1565         if (privs.all && privs.all[action]) {
1566           isGrantedAll = this.checkCompoundPrivs(sessionPrivs, privs.all[action]);
1567         }
1568         // update privs are always sufficient for viewing as well
1569         if (!isGrantedAll && privs.all && action === 'read' && privs.all.update) {
1570           isGrantedAll = this.checkCompoundPrivs(sessionPrivs, privs.all.update);
1571         }
1572
1573         // Check personal privileges.
1574         if (!isGrantedAll && privs.personal && privs.personal[action]) {
1575           isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal[action]);
1576         }
1577         // update privs are always sufficient for viewing as well
1578         if (!isGrantedPersonal && privs.personal && action === 'read' && privs.personal.update) {
1579           isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal.update);
1580         }
1581       }
1582
1583       // If only personal privileges, check the personal attribute list to
1584       // see if we can update.
1585       if (!isGrantedAll && isGrantedPersonal && action !== "create" &&
1586           model && model.originalAttributes()) {
1587         i = 0;
1588         props = privs.personal && privs.personal.properties ?
1589                     privs.personal.properties : [];
1590
1591         isGrantedPersonal = false;
1592
1593         // Compare to cached data value in case user attr has been reassigned
1594         while (!isGrantedPersonal && i < props.length) {
1595           value = model.original(props[i]).toLowerCase();
1596           isGrantedPersonal = value === username;
1597           i += 1;
1598         }
1599       }
1600
1601       return isGrantedAll || isGrantedPersonal;
1602     },
1603
1604     checkCompoundPrivs: function (sessionPrivs, privileges) {
1605       if (typeof privileges !== 'string') {
1606         return privileges;
1607       }
1608       var match = _.find(privileges.split(" "), function (priv) {
1609         return sessionPrivs.get(priv);
1610       });
1611       return !!match; // return true if match is truthy
1612     },
1613
1614     /**
1615       Return an array of valid attribute names on the model.
1616
1617       @returns {Array}
1618     */
1619     getAttributeNames: function () {
1620       var recordType = this.recordType || this.prototype.recordType,
1621         namespace = recordType.prefix(),
1622         type = recordType.suffix();
1623       return _.pluck(XT.session.getSchemas()[namespace].get(type).columns, 'name');
1624     },
1625
1626     /**
1627       Return the type as defined by the model's orm. Attribute path is supported.
1628
1629       @parameter {String} Attribute name
1630       @returns {String}
1631     */
1632     getType: function (value) {
1633       var i = 0,
1634         result,
1635         parts,
1636         findType = function (Klass, attr) {
1637           var schema = Klass.prototype.recordType.prefix(),
1638           table = Klass.prototype.recordType.suffix(),
1639           def = XT.session.schemas[schema].get(table),
1640           column = _.findWhere(def.columns, {name: attr});
1641           return column ? column.type : undefined;
1642         };
1643
1644       // Search path
1645       if (_.isString(value) && value.indexOf('.') !== -1) {
1646         parts = value.split('.');
1647         result = this;
1648         _.each(parts, function (part) {
1649           var relation;
1650           i++;
1651           if (i < parts.length) {
1652             relation = _.findWhere(result.prototype.relations, {key: part});
1653             result = _.isString(relation.relatedModel) ?
1654               XT.getObjectByName(relation.relatedModel) : relation.relatedModel;
1655           } else {
1656             result = findType(result, part);
1657           }
1658         });
1659         return result;
1660       }
1661
1662       return findType(this, value);
1663     },
1664
1665     /**
1666       Returns an object from the relational store matching the `name` provided.
1667
1668       @param {String} Name
1669       @returns {Object}
1670     */
1671     getObjectByName: function (name) {
1672       return Backbone.Relational.store.getObjectByName(name);
1673     },
1674
1675     /**
1676       Returns an array of text attribute names on the model.
1677
1678       @returns {Array}
1679     */
1680     getSearchableAttributes: function () {
1681       var recordType = this.prototype.recordType,
1682         namespace = recordType.prefix(),
1683         type = recordType.suffix(),
1684         tbldef = XT.session.getSchemas()[namespace].get(type),
1685         attrs = [],
1686         name,
1687         i;
1688
1689       for (i = 0; i < tbldef.columns.length; i++) {
1690         name = tbldef.columns[i].name;
1691         if (tbldef.columns[i].category === 'S') {
1692           attrs.push(name);
1693         }
1694       }
1695       return attrs;
1696     },
1697
1698     /**
1699       Return a matching record id for a passed user `key` and `value`. If none
1700       found, returns zero.
1701
1702       @param {String} Property to search on, typically a user key
1703       @param {String} Value to search for
1704       @param {Object} Options
1705       @returns {Object} Receiver
1706     */
1707     findExisting: function (key, value, options) {
1708       var recordType = this.recordType || this.prototype.recordType,
1709         params = [ recordType, key, value ];
1710       if (key !== this.idAttribute) { params.push(this.id || ""); }
1711       XM.ModelMixin.dispatch('XM.Model', 'findExisting', params, options);
1712       return this;
1713     },
1714
1715     /**
1716       Determine whether this record has been referenced by another. By default
1717       this function inspects foreign key relationships on the database, and is
1718       therefore dependent on foreign key relationships existing where appropriate
1719       to work correctly.
1720
1721       @param {Number} Id
1722       @param {Object} Options
1723       @returns {Object} Request
1724     */
1725     used: function (id, options) {
1726       return XM.ModelMixin.dispatch('XM.Model', 'used',
1727         [this.prototype.recordType, id], options);
1728     },
1729
1730     // ..........................................................
1731     // CONSTANTS
1732     //
1733
1734     /**
1735       Generic state for records with no local changes.
1736
1737       Use a logical AND (single `&`) to test record status.
1738
1739       @static
1740       @constant
1741       @type Number
1742       @default 0x0001
1743     */
1744     CLEAN:            0x0001, // 1
1745
1746     /**
1747       Generic state for records with local changes.
1748
1749       Use a logical AND (single `&`) to test record status.
1750
1751       @static
1752       @constant
1753       @type Number
1754       @default 0x0002
1755     */
1756     DIRTY:            0x0002, // 2
1757
1758     /**
1759       State for records that are still loaded.
1760
1761       This is the initial state of a new record. It will not be editable until
1762       a record is fetch from the store, or it is initialized with the `isNew`
1763       option.
1764
1765       @static
1766       @constant
1767       @type Number
1768       @default 0x0100
1769     */
1770     EMPTY:            0x0100, // 256
1771
1772     /**
1773       State for records in an error state.
1774
1775       @static
1776       @constant
1777       @type Number
1778       @default 0x1000
1779     */
1780     ERROR:            0x1000, // 4096
1781
1782     /**
1783       Generic state for records that are loaded and ready for use.
1784
1785       Use a logical AND (single `&`) to test record status.
1786
1787       @static
1788       @constant
1789       @type Number
1790       @default 0x0200
1791     */
1792     READY:            0x0200, // 512
1793
1794     /**
1795       State for records that are loaded and ready for use with no local changes.
1796
1797       @static
1798       @constant
1799       @type Number
1800       @default 0x0201
1801     */
1802     READY_CLEAN:      0x0201, // 513
1803
1804
1805     /**
1806       State for records that are loaded and ready for use with local changes.
1807
1808       @static
1809       @constant
1810       @type Number
1811       @default 0x0202
1812     */
1813     READY_DIRTY:      0x0202, // 514
1814
1815
1816     /**
1817       State for records that are new - not yet committed to server.
1818
1819       @static
1820       @constant
1821       @type Number
1822       @default 0x0203
1823     */
1824     READY_NEW:        0x0203, // 515
1825
1826
1827     /**
1828       Generic state for records that have been destroyed.
1829
1830       Use a logical AND (single `&`) to test record status.
1831
1832       @static
1833       @constant
1834       @type Number
1835       @default 0x0400
1836     */
1837     DESTROYED:        0x0400, // 1024
1838
1839
1840     /**
1841       State for records that have been destroyed and committed to server.
1842
1843       @static
1844       @constant
1845       @type Number
1846       @default 0x0401
1847     */
1848     DESTROYED_CLEAN:  0x0401, // 1025
1849
1850     /**
1851       State for records that have been destroyed but not yet committed to
1852       the server.
1853
1854       @static
1855       @constant
1856       @type Number
1857       @default 0x0402
1858     */
1859     DESTROYED_DIRTY:  0x0402, // 1026
1860
1861     /**
1862       Generic state for records that have been submitted to data source.
1863
1864       Use a logical AND (single `&`) to test record status.
1865
1866       @static
1867       @constant
1868       @type Number
1869       @default 0x0800
1870     */
1871     BUSY:             0x0800, // 2048
1872
1873
1874     /**
1875       State for records that are still loading data from the server.
1876
1877       @static
1878       @constant
1879       @type Number
1880       @default 0x0804
1881     */
1882     BUSY_FETCHING:     0x0804, // 2052
1883
1884
1885     /**
1886       State for records that have been modified and submitted to server.
1887
1888       @static
1889       @constant
1890       @type Number
1891       @default 0x0810
1892     */
1893     BUSY_COMMITTING:  0x0810, // 2064
1894
1895     /**
1896       State for records that have been destroyed and submitted to server.
1897
1898       @static
1899       @constant
1900       @type Number
1901       @default 0x0840
1902     */
1903     BUSY_DESTROYING:  0x0840, // 2112
1904
1905     /**
1906       Constant for `notify` message type notice.
1907
1908       @static
1909       @constant
1910       @type Number
1911       @default 0
1912     */
1913     NOTICE:  0,
1914
1915     /**
1916       Constant for `notify` message type warning.
1917
1918       @static
1919       @constant
1920       @type Number
1921       @default 1
1922     */
1923     WARNING:  1,
1924
1925     /**
1926       Constant for `notify` message type critical.
1927
1928       @static
1929       @constant
1930       @type Number
1931       @default 2
1932     */
1933     CRITICAL:  2,
1934
1935     /**
1936       Constant for `notify` message type question.
1937
1938       @static
1939       @constant
1940       @type Number
1941       @default 3
1942     */
1943     QUESTION:  3,
1944
1945     /**
1946       Constant for `notify` message type question.
1947
1948       @static
1949       @constant
1950       @type Number
1951       @default 4
1952     */
1953     YES_NO_CANCEL:  4,
1954
1955     _status: {
1956       CLEAN:            0x0001, // 1
1957       DIRTY:            0x0002, // 2
1958       EMPTY:            0x0100, // 256
1959       ERROR:            0x1000, // 4096
1960       READY:            0x0200, // 512
1961       READY_CLEAN:      0x0201, // 513
1962       READY_DIRTY:      0x0202, // 514
1963       READY_NEW:        0x0203, // 515
1964       DESTROYED:        0x0400, // 1024
1965       DESTROYED_CLEAN:  0x0401, // 1025
1966       DESTROYED_DIRTY:  0x0402, // 1026
1967       BUSY:             0x0800, // 2048
1968       BUSY_FETCHING:    0x0804, // 2052
1969       BUSY_COMMITTING:  0x0810, // 2064
1970       BUSY_DESTROYING:  0x0840, // 2112
1971       NOTICE:           0,
1972       WARNING:          1,
1973       CRITICAL:         2,
1974       QUESTION:         3,
1975       YES_NO_CANCEL:    4,
1976
1977       1     : 'CLEAN',
1978       2     : 'DIRTY',
1979       256   : 'EMPTY',
1980       4096  : 'ERROR',
1981
1982       512   : 'READY',
1983       513   : 'READY_CLEAN',
1984       514   : 'READY_DIRTY',
1985       515   : 'READY_NEW',
1986
1987       1024  : 'DESTROYED',
1988       1025  : 'DESTROYED_CLEAN',
1989       1026  : 'DESTROYED_DIRTY',
1990
1991       2048  : 'BUSY',
1992       2052  : 'BUSY_FETCHING',
1993       2064  : 'BUSY_COMMITTING',
1994       2112  : 'BUSY_DESTROYING'
1995     }
1996   };
1997 })();