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