1 /*jshint unused:false, bitwise:false */
3 /*global XT:true, XM:true, Backbone:true, _:true */
8 Object.observe = undefined;
11 Abstract check for attribute level privilege access.
15 var _canDoAttr = function (action, attribute) {
16 var privObject = this.privileges &&
17 this.privileges.attribute &&
18 this.privileges.attribute[attribute];
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) {
29 var priv = privObject &&
30 !_.isUndefined(privObject[action]) ?
31 privObject[action] : undefined;
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;
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
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();
46 return hasPriv && canAct;
50 A model mixin used as the base for all models.
53 @seealso XM.SimpleModel
58 * Handler mapping; easily map backbone events to handler functions.
62 'status:READY_CLEAN': 'onReadyClean',
63 'change:applicationDate': 'dateChanged',
64 'add': 'lineItemAdded'
72 * A transient backbone model used to store/manage metadata for this
75 * @type Backbone.Model
80 Set to true if you want an id fetched from the server when the `isNew` option
81 is passed on a new model.
88 Are there any binary fields that we might need to worry about transforming?
94 The last error message reported.
99 Lock information provide by the server.
104 Indicate whether a model is lockable.
105 Automatically set when `XT.session` loads
111 Indicate whether a model should be kept track of in the
117 A hash structure that defines data access.
118 Automatically set when `XT.session` loads
126 Indicates whether the model is read only.
133 An array of attribute names designating attributes that are not editable.
134 Use `setReadOnly` to edit this array.
136 @seealso `setReadOnly`
137 @seealso `isReadOnly`
140 readOnlyAttributes: null,
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.
148 nameAttribute: "name",
151 Specify the name of a data source model here.
158 An array of required attributes. A `validate` will fail until all the required
159 attributes have values.
163 requiredAttributes: null,
166 Model's status. You should never modify this directly.
176 The record version fetched from the server.
181 // ..........................................................
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).
191 augment: function (hash) {
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
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);
204 } else if (_.isArray(value)) {
205 // add array elements (for now we merge duplicates)
206 that[key] = _.union(existingObj, value);
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
213 that[key] = function () {
214 var firstDefaults = existingObj.apply(this, arguments);
215 var secondDefaults = value.apply(this, arguments);
216 return _.extend(firstDefaults, secondDefaults);
219 } else if (_.isFunction(value)) {
220 // for functions, call the super() first, and then the
221 // function that's being mixed in
223 that[key] = function () {
224 existingObj.apply(this, arguments);
225 value.apply(this, arguments);
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);
233 } else if (_.isObject(value)) {
235 that[key] = _.extend({}, existingObj, value);
238 throw new Error("Do not know how to augment: " + key);
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.
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;
261 * Get the type of an attribute.
263 getAttributeType: function (attr) {
264 var found = _.findWhere(
265 XT.session.schemas.XM.get(this.recordType.suffix()).columns,
268 return found && found.type;
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
280 Returns whether an attribute can be edited.
282 @param {String} Attribute
285 canEdit: function (attribute) {
286 return _canDoAttr.call(this, "update", attribute);
290 Returns whether the current record can be updated based on privilege
295 canUpdate: function () {
296 return this.getClass().canUpdate(this);
300 Returns whether the current record can be deleted based on privilege
305 canDelete: function () {
306 return this.getClass().canDelete(this);
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
314 @param {Function} callback. Will be called with boolean response
316 canDestroy: function (callback) {
317 this.getClass().canDestroy(this, callback);
321 Returns whether an attribute can be viewed.
323 @param {String} Attribute
326 canView: function (attribute) {
327 return _canDoAttr.call(this, "view", attribute);
331 Reimplemented to handle state change. Calling
332 `destroy` will cause the model to commit to the server
335 @returns {Object|Boolean}
337 destroy: function (options) {
338 options = options ? _.clone(options) : {};
341 success = options.success,
342 K = XM.ModelClassMixin;
344 this.setStatus(K.DESTROYED_DIRTY);
345 this.setStatus(K.BUSY_DESTROYING);
346 this._wasNew = this.isNew();
348 options.success = function (resp) {
349 if (success) { success(model, resp, options); }
351 result = Backbone.Model.prototype.destroy.call(this, options);
357 When any attributes change update the status if applicable.
359 didChange: function (model, options) {
360 options = options || {};
361 var K = XM.ModelClassMixin,
362 status = this.getStatus();
363 if (this.isBusy()) { return; }
365 // Mark dirty if we should
366 if (status === K.READY_CLEAN) {
367 this.setStatus(K.READY_DIRTY);
372 Called after confirmation that the model was destroyed on the
375 didDestroy: function () {
376 var K = XM.ModelClassMixin;
377 this.clear({silent: true});
378 this.setStatus(K.DESTROYED_CLEAN);
382 Handle a `sync` response that was an error.
384 didError: function (model, resp) {
386 this.lastError = resp;
391 Generate an array of patch objects per:
392 http://tools.ietf.org/html/rfc6902
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);
404 Called when model is instantiated.
406 initialize: function (attributes, options) {
407 attributes = attributes || {};
408 options = options || {};
410 K = XM.ModelClassMixin,
411 status = this.getStatus(),
412 idAttribute = this.idAttribute;
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) : [];
422 if (_.isEmpty(this.recordType)) { throw new Error('No record type defined'); }
424 if (_.isNull(status)) {
425 this.setStatus(K.EMPTY);
431 klass = this.getClass();
432 if (!klass.canCreate()) {
433 throw new Error('Insufficent privileges to create a record.');
435 this.setStatus(K.READY_NEW);
437 // Key generator (client based)
438 if (idAttribute === 'uuid' &&
439 !this.get(idAttribute) &&
440 !attributes[idAttribute]) {
441 this.set(idAttribute, XT.generateUUID());
444 // Deprecated key generator (server based)
445 if (this.autoFetchId) {
446 if (options.database) {
447 this.fetchId({database: options.database});
449 // This should throw and error for a call that needs to be fixed.
455 // Set attributes that should be required and read only
457 !_.contains(this.requiredAttributes, idAttribute)) {
458 this.requiredAttributes.push(idAttribute);
462 lockDidChange: function (model, lock) {
466 // Clear any old refresher
467 if (this._keyRefresherInterval) {
468 clearInterval(this._keyRefresherInterval);
469 that._keyRefresherInterval = undefined;
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);
485 // set up a refresher
486 this._keyRefresherInterval = setInterval(function () {
487 that.dispatch('XM.Model', 'renewLock', [lock.key], options);
490 this.trigger("lockChange", that);
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.
497 * @param {String} Name of the class
498 * @param {String} Function name
499 * @param {Object} Parameters
500 * @param {Object} Options
502 dispatch: function (name, func, params, options) {
503 options = _.extend({}, options); // clone and set to {} if undefined
504 var dataSource = options.dataSource || XT.dataSource,
506 nameSpace: name.replace(/\.\w+/i, ''),
513 return dataSource.request(null, "post", payload, options);
517 Reimplemented to handle status changes.
519 @param {Object} Options
520 @returns {Object} Request
522 fetch: function (options) {
523 options = options ? _.clone(options) : {};
525 K = XM.ModelClassMixin,
526 success = options.success,
527 klass = this.getClass();
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');
536 if (success) { success(model, resp, options); }
538 return Backbone.Model.prototype.fetch.call(this, options);
540 XT.log('Insufficient privileges to fetch');
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.
548 @returns {Object} Request
550 fetchId: function (options) {
551 options = _.defaults(options ? _.clone(options) : {}, {});
554 options.success = function (resp) {
555 that.set(that.idAttribute, resp, options);
557 this.dispatch('XM.Model', 'fetchId', this.recordType, options);
562 Return a matching record id for a passed user `key` and `value`. If none
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
572 findExisting: function (key, value, options) {
573 options = options || {};
574 return this.getClass().findExisting.call(this, key, value, options);
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
584 getAttributeNames: function () {
585 return this.getClass().getAttributeNames.call(this);
589 Returns the current model prototype class.
593 getClass: function () {
594 return Object.getPrototypeOf(this).constructor;
598 Return the current status.
602 getStatus: function () {
607 Return the current status as as string.
611 getStatusString: function () {
613 status = this.getStatus(),
615 for (prop in XM.ModelClassMixin) {
616 if (XM.ModelClassMixin.hasOwnProperty(prop)) {
617 if (prop.match(/[A-Z_]$/) && XM.ModelClassMixin[prop] === status) {
622 return ret.join(" ");
626 Return the type as defined by the model's orm. Attribute path is supported.
628 @parameter {String} Attribute name
631 getType: function (value) {
632 return this.getClass().getType(value);
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.
642 // Returns the first name attribute from primary contact model.
643 var firstName = m.getValue('primaryContact.firstName');
645 getValue: function (key) {
650 if (key.indexOf('.') !== -1) {
651 parts = key.split('.');
653 _.each(parts, function (part) {
654 value = value instanceof Backbone.Model ? value.getValue(part) : value;
656 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
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);
665 return _.isFunction(this[key]) ? this[key]() : this[key];
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;
678 Reimplemented. A model is new if the status is `READY_NEW`.
683 var K = XM.ModelClassMixin;
684 return this.getStatus() === K.READY_NEW || this._wasNew || false;
688 Returns true if status is `DESTROYED_CLEAN` or `DESTROYED_DIRTY`.
692 isDestroyed: function () {
693 var status = this.getStatus(),
694 K = XM.ModelClassMixin;
695 return status === K.DESTROYED_CLEAN || status === K.DESTROYED_DIRTY;
699 Returns true if status is `READY_NEW` or `READY_DIRTY`.
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;
712 Returns true if the model is in one of the `READY` statuses
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;
723 Returns true if the model is `READY_CLEAN`
725 isReadyClean: function () {
726 return this.getStatus() === XM.Model.READY_CLEAN;
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
737 This is not to be confused with the `isLocked` function that
738 is used by Backbone-relational to manage events on relations.
742 hasLockKey: function () {
743 return !this.lock || this.lock.key ? true : false;
747 * Returns the lock's key if it exists, otherwise null.
750 getLockKey: function () {
751 return this.lock ? this.lock.key : false;
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.
760 // Inquire on the whole model
761 var readOnly = this.isReadOnly();
763 // Inquire on a single attribute
764 var readOnly = this.isReadOnly("name");
766 // Inquire using a search path
767 var readOnly = this.isReadOnly("contact.firstName");
770 @seealso `setReadOnly`
771 @seealso `readOnlyAttributes`
772 @param {String} attribute
775 isReadOnly: function (value) {
778 isLockedOut = !this.hasLockKey();
781 if (_.isString(value) && value.indexOf('.') !== -1) {
782 parts = value.split('.');
784 _.each(parts, function (part) {
785 if (result instanceof Backbone.Model) {
786 result = result.getValue(part);
787 } else if (_.isNull(result)) {
789 } else if (!_.isUndefined(result)) {
790 result = result.isReadOnly(part) || !result.hasLockKey();
796 if ((!_.isString(value) && !_.isObject(value)) || this.readOnly) {
797 result = this.readOnly;
799 result = _.contains(this.readOnlyAttributes, value);
801 return result || isLockedOut;
805 Return whether an attribute is required.
807 @param {String} attribute
810 isRequired: function (value) {
811 return _.contains(this.requiredAttributes, value);
816 A utility function that triggers a `notify` event. Useful for passing along
817 information to the interface. Bind to `notify` to use.
820 var m = new XM.MyModel();
821 var raiseAlert = function (model, value, options) {
824 m.on('notify', raiseAlert);
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
836 notify: function (message, options) {
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;
844 this.trigger('notify', this, message, options);
848 Return the original value of an attribute the last time fetch was called.
852 original: function (attr) {
857 if (attr.indexOf('.') !== -1) {
858 parts = attr.split('.');
860 _.each(parts, function (part) {
861 value = value instanceof Backbone.Model ? value.original(part) : value;
863 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
866 return this._cache ? this._cache[attr] : this.attributes[attr];
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.
875 originalAttributes: function () {
880 Checks the object against the schema and converts date strings to date objects.
882 @param {Object} Response
884 parse: function (resp) {
886 schemas = XT.session.getSchemas(),
889 parse = function (namespace, typeName, obj) {
890 var type = schemas[namespace].get(typeName),
892 if (!type) { throw new Error(typeName + " not found in schema " + namespace + "."); }
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]);
903 return parse(this.recordType.prefix(), this.recordType.suffix(), resp);
907 Returns the previous status of the model.
909 @returns {Boolean} Previous Status
911 previousStatus: function () {
912 return this._prevStatus;
916 * Manage all re-entrant lock actions, namely obtain, renew, and release.
918 * @param action {String}
920 * @see XM.Model#obtainLock
921 * @see XM.Model#renewLock
922 * @see XM.Model#releaseLock
924 _reentrantLockHelper: function (action, params, _options) {
926 options = _.extend({ }, _options),
927 userCallback = options.success,
928 methodName = action + 'Lock',
929 eventName = 'lock:' + action;
931 this.dispatch("XM.Model", methodName, params, _.extend(options, {
932 success: function (lock) {
934 that.lockDidChange(that, lock);
935 that.trigger(eventName, that, { lock: lock });
937 if (_.isFunction(userCallback)) {
942 that.trigger('lock:error', that);
948 Revert the model to the previous status. Useful for reseting status
949 after a failed validation.
951 param {Boolean} - cascade
953 revertStatus: function (cascade) {
954 var K = XM.ModelClassMixin,
955 prev = this._prevStatus;
956 this.setStatus(this._prevStatus || K.EMPTY);
957 this._prevStatus = prev;
963 @retuns {Object} Request
965 save: function (key, value, options) {
966 options = options ? _.clone(options) : {};
968 K = XM.ModelClassMixin,
972 // Handle both `"key", value` and `{key: value}` -style arguments.
973 if (_.isObject(key) || _.isEmpty(key)) {
975 options = value ? _.clone(value) : {};
976 } else if (_.isString(key)) {
980 // Only save if we should.
981 if (this.isDirty() || attrs) {
982 this._wasNew = this.isNew();
983 success = options.success;
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');
990 if (success) { success(model, resp, options); }
993 // Handle both `"key", value` and `{key: value}` -style arguments.
994 if (_.isObject(key) || _.isEmpty(key)) { value = options; }
996 // Call the super version
997 this.setStatus(K.BUSY_COMMITTING, {cascade: true});
998 result = Backbone.Model.prototype.save.call(this, key, value, options);
1000 if (!result) { this.revertStatus(true); }
1004 XT.log('No changes to save');
1009 Overload: Don't allow setting when model is in error or destroyed status, or
1010 updating a `READY_CLEAN` record without update privileges.
1012 @param {String|Object} Key
1013 @param {String|Object} Value or Options
1014 @param {Object} Options
1016 set: function (key, val, options) {
1017 var K = XM.ModelClassMixin,
1018 keyIsObject = _.isObject(key),
1019 status = this.getStatus(),
1022 // Handle both `"key", value` and `{key: value}` -style arguments.
1023 if (keyIsObject) { options = val; }
1024 options = options ? options : {};
1029 // Set error if no update privileges
1031 if (!this.canUpdate()) { err = XT.Error.clone('xt1010'); }
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 } });
1043 // If we're not in a `READY` state, silence all events
1044 if (!_.isBoolean(options.silent)) {
1045 options.silent = true;
1049 // Raise error, if any
1051 this.trigger('invalid', this, err, options);
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);
1061 Set a field if exists in a schema. Otherwise ignore silently.
1063 setIfExists: function (key, val, options) {
1064 var K = XM.ModelClassMixin,
1065 keyIsObject = _.isObject(key),
1066 attributes = this.getAttributeNames();
1068 // Handle both `"key", value` and `{key: value}` -style arguments.
1069 if (keyIsObject) { options = val; }
1070 options = options ? options : {};
1073 _.each(key, function (subvalue, subkey) {
1074 if (!_.contains(attributes, subkey)) {
1078 if (_.isEmpty(key)) {
1082 if (!_.contains(attributes, key)) {
1087 // Handle both `"key", value` and `{key: value}` -style arguments.
1088 if (keyIsObject) { val = options; }
1089 return this.set.call(this, key, val, options);
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`.
1096 setValue: function (key, val, options) {
1097 var keyIsObject = _.isObject(key),
1098 attributes = this.getAttributeNames(),
1102 // If no meta, then forward request.
1104 return this.setIfExists(key, val, options);
1107 // Handle both `"key", value` and `{key: value}` -style arguments.
1108 if (keyIsObject) { options = val; }
1109 options = options ? options : {};
1112 _.each(key, function (subvalue, subkey) {
1113 if (!_.contains(attributes, subkey)) {
1114 that.meta.set(subkey, subvalue, options);
1118 if (!_.isEmpty(key)) {
1119 that.set(key, options);
1122 if (_.contains(attributes, key)) {
1123 this.set(key, val, options);
1125 this.meta.set(key, val, options);
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>
1140 Note: Privilege enforcement supercedes read-only settings.
1142 @seealso `isReadOnly`
1144 @param {String|Array|Boolean} Attribute string or hash to set, or boolean if setting the model
1145 @param {Boolean} Boolean - default = true.
1148 setReadOnly: function (key, value) {
1149 value = _.isBoolean(value) ? value : true;
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;
1163 // Handle attribute array
1164 if (_.isObject(key)) {
1165 _.each(key, function (attr) {
1166 process(attr, value);
1167 changes[attr] = true;
1170 // handle attribute string
1171 } else if (_.isString(key)) {
1172 process(key, value);
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;
1187 if (!_.isEmpty(changes)) {
1188 this.trigger('readOnlyChange', this, {changes: changes, isReadOnly: value});
1194 Set the status on the model. Triggers `statusChange` event.
1196 @param {Number} Status
1198 setStatus: function (status, options) {
1199 var K = XM.ModelClassMixin;
1201 if (this.status === status) { return; }
1202 this._prevStatus = this.status;
1203 this.status = status;
1205 // Reset patch cache if applicable
1206 if (status === K.READY_CLEAN && !this.readOnly) {
1207 this._cache = this.toJSON();
1210 this.trigger('statusChange', this, status, options);
1215 Sync to xTuple data source.
1217 Accepts options.collection to sync a Backbone collection
1218 of models in lieu of just the current model.
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,
1229 success = options.success;
1231 options.error = function (resp) {
1232 that.setStatus(K.ERROR);
1233 if (error) { error(model, resp, options); }
1236 options.success = function (model, resp, options) {
1237 if (_.isFunction(success)) {
1238 success(model, resp, options);
1240 that.trigger('sync', model, resp, options);
1243 // Handle a colleciton of models to persist
1244 if (options.collection) {
1245 delete options.validate; // Don't let this pass through...
1247 options.collection.each(function (obj) {
1249 nameSpace: obj.recordType.replace(/\.\w+/i, ''),
1250 type: obj.recordType.suffix()
1254 if (obj.binaryField) {
1255 throw "Processing of for arrays of models with binary fields is not supported.";
1258 switch (obj.previousStatus())
1261 item.method = "post";
1262 item.data = obj.toJSON();
1263 item.requery = options.requery;
1266 item.method = "patch";
1267 item.etag = obj.etag;
1268 item.lock = obj.lock;
1269 item.patches = obj.generatePatches();
1270 item.requery = options.requery;
1272 case K.DESTROYED_DIRTY:
1273 item.method = "delete";
1274 item.etag = obj.etag;
1275 item.lock = obj.lock;
1278 throw "Model in collection syncing from an unsupported state";
1284 // All collections have to go through "post."
1287 // Handle the case of a model only persisting itself
1290 payload.nameSpace = this.recordType.replace(/\.\w+/i, '');
1291 payload.type = this.recordType.suffix();
1293 // Get an id from... someplace
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];
1305 options.error("Cannot find id");
1311 payload.data = model.toJSON();
1312 payload.binaryField = model.binaryField; // see issue 18661
1313 payload.requery = options.requery;
1317 if (options.context) { payload.context = options.context; }
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;
1328 payload.etag = model.etag;
1329 payload.lock = model.lock;
1345 result = dataSource.request(model, method, payload, options);
1346 //this.trigger('request', this, result, options);
1348 return result || false;
1352 Overload: Convert dates to strings.
1354 toJSON: function (options) {
1357 json = Backbone.Model.prototype.toJSON.apply(this, arguments);
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();
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
1375 @param {Object} Options
1376 @returns {Object} Request
1378 used: function (options) {
1379 return this.getClass().used(this.id, options);
1383 Default validation checks `attributes` for:<br />
1384 * Data type integrity.<br />
1385 * Required fields.<br />
1387 Returns `undefined` if the validation succeeded, or some value, usually
1388 an error message, if it fails.<br />
1391 @param {Object} Attributes
1392 @param {Object} Options
1394 validate: function (attributes, options) {
1395 attributes = attributes || {};
1396 options = options || {};
1398 if (!XT.session.getSchemas()[this.recordType.prefix()].get(this.recordType.suffix())) {
1399 XT.log("Cannot find schema", this.recordType);
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,
1408 getColumn = function (attr) {
1409 return _.find(columns, function (column) {
1410 return column.name === attr;
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();
1422 value = attributes[attr];
1423 column = getColumn(attr);
1424 category = column ? column.category : false;
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 });
1435 if (!_.isString(value)) {
1436 params.type = "_string".loc();
1437 return XT.Error.clone('xt1003', { params: params });
1441 if (!_.isNumber(value)) {
1442 params.type = "_number".loc();
1443 return XT.Error.clone('xt1003', { params: params });
1447 if (!_.isDate(value)) {
1448 params.type = "_date".loc();
1449 return XT.Error.clone('xt1003', { params: params });
1453 if (!_.isBoolean(value)) {
1454 params.type = "_boolean".loc();
1455 return XT.Error.clone('xt1003', { params: params });
1459 if (!_.isArray(value)) {
1460 params.type = "_array".loc();
1461 return XT.Error.clone('xt1003', { params: params });
1465 if (!_.isObject(value) && !_.isNumber(value)) {
1466 params.type = "_object".loc();
1467 return XT.Error.clone('xt1003', { params: params });
1471 return XT.Error.clone('xt1002', { params: params });
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 });
1490 // ..........................................................
1495 A mixin for use on model classes that includes status constants
1496 and privilege control functions.
1498 XM.ModelClassMixin = {
1499 getReportUrl: function (action, modelName, id) {
1500 var reportUrl = "/generate-report?nameSpace=%@&type=%@&id=%@".f(
1501 modelName.prefix(), modelName.suffix(), id);
1504 reportUrl = reportUrl + "&action=" + action;
1511 Use this function to find out whether a user can create records before
1516 canCreate: function () {
1517 return XM.ModelClassMixin.canDo.call(this, 'create');
1521 Use this function to find out whether a user can read this record type
1522 before any have been loaded.
1524 @param {Object} Model
1525 @param {String} Attribute name (optional)
1528 canRead: function (model, attribute) {
1529 return XM.ModelClassMixin.canDo.call(this, 'read', model, attribute);
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.
1537 @param {Object} Model
1538 @param {String} Attribute name (optional)
1541 canUpdate: function (model) {
1542 return XM.ModelClassMixin.canDo.call(this, 'update', model);
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.
1550 @param {Object} Model
1553 canDelete: function (model) {
1554 return XM.ModelClassMixin.canDo.call(this, 'delete', model);
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
1562 @param {Object} Model
1563 @param {Function} callback. Will be called with boolean response
1565 canDestroy: function (model, callback) {
1568 if (!XM.ModelClassMixin.canDelete.call(this, model)) {
1573 options.success = function (used) {
1577 this.used.call(this, model.id, options);
1582 Check privilege on `action`. If `model` is passed, checks personal
1583 privileges on the model where applicable.
1585 @param {String} Action
1586 @param {XM.Model} Model
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,
1597 K = XM.ModelClassMixin,
1598 status = model && model.getStatus ? model.getStatus() : K.READY;
1600 // Need to be in a valid status to "do" anything
1601 if (!(status & K.READY)) { return false; }
1603 // If no privileges, nothing to check.
1604 if (_.isEmpty(privs)) { return true; }
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]);
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);
1617 // Check personal privileges.
1618 if (!isGrantedAll && privs.personal && privs.personal[action]) {
1619 isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal[action]);
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);
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()) {
1632 props = privs.personal && privs.personal.properties ?
1633 privs.personal.properties : [];
1635 isGrantedPersonal = false;
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;
1645 return isGrantedAll || isGrantedPersonal;
1648 checkCompoundPrivs: function (sessionPrivs, privileges) {
1649 if (typeof privileges !== 'string') {
1652 var match = _.find(privileges.split(" "), function (priv) {
1653 return sessionPrivs.get(priv);
1655 return !!match; // return true if match is truthy
1659 Return an array of valid attribute names on the model.
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');
1671 Return the type as defined by the model's orm. Attribute path is supported.
1673 @parameter {String} Attribute name
1676 getType: function (value) {
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;
1689 if (_.isString(value) && value.indexOf('.') !== -1) {
1690 parts = value.split('.');
1692 _.each(parts, function (part) {
1695 if (i < parts.length) {
1696 relation = _.findWhere(result.prototype.relations, {key: part});
1698 result = _.isString(relation.relatedModel) ?
1699 XT.getObjectByName(relation.relatedModel) : relation.relatedModel;
1704 result = findType(result, part);
1710 return findType(this, value);
1714 Returns an object from the relational store matching the `name` provided.
1716 @param {String} Name
1719 getObjectByName: function (name) {
1720 return Backbone.Relational.store.getObjectByName(name);
1724 Returns an array of text attribute names on the model.
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),
1737 for (i = 0; i < tbldef.columns.length; i++) {
1738 name = tbldef.columns[i].name;
1739 if (tbldef.columns[i].category === 'S') {
1747 Return a matching record id for a passed user `key` and `value`. If none
1748 found, returns zero.
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
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);
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
1770 @param {Object} Options
1771 @returns {Object} Request
1773 used: function (id, options) {
1774 return XM.ModelMixin.dispatch('XM.Model', 'used',
1775 [this.prototype.recordType, id], options);
1778 // ..........................................................
1783 Generic state for records with no local changes.
1785 Use a logical AND (single `&`) to test record status.
1795 Generic state for records with local changes.
1797 Use a logical AND (single `&`) to test record status.
1807 State for records that are still loaded.
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`
1818 EMPTY: 0x0100, // 256
1821 State for records in an error state.
1828 ERROR: 0x1000, // 4096
1831 Generic state for records that are loaded and ready for use.
1833 Use a logical AND (single `&`) to test record status.
1840 READY: 0x0200, // 512
1843 State for records that are loaded and ready for use with no local changes.
1850 READY_CLEAN: 0x0201, // 513
1854 State for records that are loaded and ready for use with local changes.
1861 READY_DIRTY: 0x0202, // 514
1865 State for records that are new - not yet committed to server.
1872 READY_NEW: 0x0203, // 515
1876 Generic state for records that have been destroyed.
1878 Use a logical AND (single `&`) to test record status.
1885 DESTROYED: 0x0400, // 1024
1889 State for records that have been destroyed and committed to server.
1896 DESTROYED_CLEAN: 0x0401, // 1025
1899 State for records that have been destroyed but not yet committed to
1907 DESTROYED_DIRTY: 0x0402, // 1026
1910 Generic state for records that have been submitted to data source.
1912 Use a logical AND (single `&`) to test record status.
1919 BUSY: 0x0800, // 2048
1923 State for records that are still loading data from the server.
1930 BUSY_FETCHING: 0x0804, // 2052
1934 State for records that have been modified and submitted to server.
1941 BUSY_COMMITTING: 0x0810, // 2064
1944 State for records that have been destroyed and submitted to server.
1951 BUSY_DESTROYING: 0x0840, // 2112
1954 Constant for `notify` message type notice.
1964 Constant for `notify` message type warning.
1974 Constant for `notify` message type critical.
1984 Constant for `notify` message type question.
1994 Constant for `notify` message type question with cancel option.
2004 Constant for `notify` message type ok/cancel.
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
2041 513 : 'READY_CLEAN',
2042 514 : 'READY_DIRTY',
2046 1025 : 'DESTROYED_CLEAN',
2047 1026 : 'DESTROYED_DIRTY',
2050 2052 : 'BUSY_FETCHING',
2051 2064 : 'BUSY_COMMITTING',
2052 2112 : 'BUSY_DESTROYING'