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