1 /*jshint unused:false, bitwise:false */
3 /*global XT:true, XM:true, Backbone:true, _:true */
9 Abstract check for attribute level privilege access.
13 var _canDoAttr = function (action, attribute) {
14 var priv = this.privileges &&
15 this.privileges.attribute &&
16 this.privileges.attribute[attribute] &&
17 this.privileges.attribute[attribute][action] ?
18 this.privileges.attribute[attribute][action] : false;
20 // If there was a privilege then check our access, otherwise assume we have it
21 var hasPriv = priv ? XT.session.getPrivileges().get(priv) : true;
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
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();
33 return hasPriv && canAct;
37 A model mixin used as the base for all models.
40 @seealso XM.SimpleModel
45 * Handler mapping; easily map backbone events to handler functions.
49 'status:READY_CLEAN': 'onReadyClean',
50 'change:applicationDate': 'dateChanged',
51 'add': 'lineItemAdded'
59 * A transient backbone model used to store/manage metadata for this
62 * @type Backbone.Model
67 Set to true if you want an id fetched from the server when the `isNew` option
68 is passed on a new model.
75 Are there any binary fields that we might need to worry about transforming?
81 The last error message reported.
86 Lock information provide by the server.
91 Indicate whether a model is lockable.
92 Automatically set when `XT.session` loads
98 A hash structure that defines data access.
99 Automatically set when `XT.session` loads
107 Indicates whether the model is read only.
114 An array of attribute names designating attributes that are not editable.
115 Use `setReadOnly` to edit this array.
117 @seealso `setReadOnly`
118 @seealso `isReadOnly`
121 readOnlyAttributes: null,
124 The attribute that is the display name for the model in any case that we
125 want to show just the most obvious field for the user.
129 nameAttribute: "name",
132 Specify the name of a data source model here.
139 An array of required attributes. A `validate` will fail until all the required
140 attributes have values.
144 requiredAttributes: null,
147 Model's status. You should never modify this directly.
157 The record version fetched from the server.
162 // ..........................................................
167 Allow the mixing in of functionality to models that goes one step
168 deeper than a typical mixin. I can mix in a hash of hashes, arrays,
169 and functions, and those things will be mixed into the pre-existing
170 constructs (instead of overwriting them).
172 augment: function (hash) {
175 _.each(hash, function (value, key) {
176 var existingObj = that[key];
177 if (_.isUndefined(existingObj)) {
178 // the target has no value here, so just mix it in
181 } else if (typeof value !== typeof existingObj) {
182 // type mismatch: we're not so clever as to allow this
183 throw new Error("Type mismatch in augment: " + key);
185 } else if (_.isArray(value)) {
186 // add array elements (for now we merge duplicates)
187 that[key] = _.union(existingObj, value);
189 } else if (_.isFunction(value) && key === 'defaults') {
190 // treat the default function specially: we want to
191 // capture the return values and return the combination
194 that[key] = function () {
195 var firstDefaults = existingObj.apply(this, arguments);
196 var secondDefaults = value.apply(this, arguments);
197 return _.extend(firstDefaults, secondDefaults);
200 } else if (_.isFunction(value)) {
201 // for functions, call the super() first, and then the
202 // function that's being mixed in
204 that[key] = function () {
205 existingObj.apply(this, arguments);
206 value.apply(this, arguments);
209 } else if (_.isObject(value) &&
210 _.intersection(Object.keys(existingObj), Object.keys(value)).length > 0) {
211 // do not allow overwriting of an object's values
212 throw new Error("Illegal overwrite in augment: " + key);
214 } else if (_.isObject(value)) {
216 that[key] = _.extend({}, existingObj, value);
219 throw new Error("Do not know how to augment: " + key);
225 A function that binds events to functions. It can and should only be called
226 once by initialize. Any attempt to call it a second time will throw an error.
228 bindEvents: function () {
229 // Bind events, but only if we haven't already been here before.
230 // We could silently skip, but then that means any overload done
231 // buy anyone else has do to that check too. That's too error prone
232 // and dangerous because the problems caused by duplicate bindings
233 // are not immediatley apparent and insidiously hard to pin down.
234 if (this._eventsBound) { throw new Error("Events have already been bound."); }
235 this.on('change', this.didChange);
236 this.on('error', this.didError);
237 this.on('destroy', this.didDestroy);
238 this._eventsBound = true;
242 * Get the type of an attribute.
244 getAttributeType: function (attr) {
245 var found = _.findWhere(
246 XT.session.schemas.XM.get(this.recordType.suffix()).columns,
249 return found && found.type;
253 // All four of the canVerb functions are defined below as class-level
254 // functions (akin to static functions). Two of those functions are here
255 // as instance functions as well. These just call the class functions.
256 // Notice that canCreate and canRead are missing here. This is on purpose.
257 // Once we have an instance created, there's no reason to ask if we can create
261 Returns whether an attribute can be edited.
263 @param {String} Attribute
266 canEdit: function (attribute) {
267 return _canDoAttr.call(this, "edit", attribute);
271 Returns whether the current record can be updated based on privilege
276 canUpdate: function () {
277 return this.getClass().canUpdate(this);
281 Returns whether the current record can be deleted based on privilege
286 canDelete: function () {
287 return this.getClass().canDelete(this);
291 Returns whether the current record can be deleted based on privilege
292 settings AND whether or not the record is used. Requires a call to the
295 @param {Function} callback. Will be called with boolean response
297 canDestroy: function (callback) {
298 this.getClass().canDestroy(this, callback);
302 Returns whether an attribute can be viewed.
304 @param {String} Attribute
307 canView: function (attribute) {
308 return _canDoAttr.call(this, "view", attribute);
312 Reimplemented to handle state change. Calling
313 `destroy` will cause the model to commit to the server
316 @returns {Object|Boolean}
318 destroy: function (options) {
319 options = options ? _.clone(options) : {};
322 success = options.success,
323 K = XM.ModelClassMixin;
325 this.setStatus(K.DESTROYED_DIRTY);
326 this.setStatus(K.BUSY_DESTROYING);
327 this._wasNew = this.isNew();
329 options.success = function (resp) {
330 if (success) { success(model, resp, options); }
332 result = Backbone.Model.prototype.destroy.call(this, options);
338 When any attributes change update the status if applicable.
340 didChange: function (model, options) {
341 options = options || {};
342 var K = XM.ModelClassMixin,
343 status = this.getStatus();
344 if (this.isBusy()) { return; }
346 // Mark dirty if we should
347 if (status === K.READY_CLEAN) {
348 this.setStatus(K.READY_DIRTY);
353 Called after confirmation that the model was destroyed on the
356 didDestroy: function () {
357 var K = XM.ModelClassMixin;
358 this.clear({silent: true});
359 this.setStatus(K.DESTROYED_CLEAN);
363 Handle a `sync` response that was an error.
365 didError: function (model, resp) {
367 this.lastError = resp;
372 Generate an array of patch objects per:
373 http://tools.ietf.org/html/rfc6902
377 generatePatches: function () {
378 if (!this._cache) { return []; }
379 var observer = XM.jsonpatch.observe(this._cache);
380 observer.object = this.toJSON();
381 return XM.jsonpatch.generate(observer);
385 Called when model is instantiated.
387 initialize: function (attributes, options) {
388 attributes = attributes || {};
389 options = options || {};
391 K = XM.ModelClassMixin,
392 status = this.getStatus(),
393 idAttribute = this.idAttribute;
395 // Set defaults if not provided
396 this.privileges = this.privileges || {};
397 this.readOnlyAttributes = this.readOnlyAttributes ?
398 this.readOnlyAttributes.slice(0) : [];
399 this.requiredAttributes = this.requiredAttributes ?
400 this.requiredAttributes.slice(0) : [];
403 if (_.isEmpty(this.recordType)) { throw new Error('No record type defined'); }
405 if (_.isNull(status)) {
406 this.setStatus(K.EMPTY);
412 klass = this.getClass();
413 if (!klass.canCreate()) {
414 throw new Error('Insufficent privileges to create a record.');
416 this.setStatus(K.READY_NEW);
418 // Key generator (client based)
419 if (idAttribute === 'uuid' &&
420 !this.get(idAttribute) &&
421 !attributes[idAttribute]) {
422 this.set(idAttribute, XT.generateUUID());
425 // Deprecated key generator (server based)
426 if (this.autoFetchId) {
427 if (options.database) {
428 this.fetchId({database: options.database});
430 // This should throw and error for a call that needs to be fixed.
436 // Set attributes that should be required and read only
438 !_.contains(this.requiredAttributes, idAttribute)) {
439 this.requiredAttributes.push(idAttribute);
443 lockDidChange: function (model, lock) {
447 // Clear any old refresher
448 if (this._keyRefresherInterval) {
449 clearInterval(this._keyRefresherInterval);
450 that._keyRefresherInterval = undefined;
453 if (lock && lock.key && !this._keyRefresherInterval) {
454 options.automatedRefresh = true;
455 options.success = function (renewed) {
456 // If for any reason the lock was not renewed (maybe got disconnected?)
457 // Update the model so it knows.
458 var lock = that.lock;
459 if (lock && !renewed) {
460 lock = _.clone(lock);
466 // set up a refresher
467 this._keyRefresherInterval = setInterval(function () {
468 that.dispatch('XM.Model', 'renewLock', [lock.key], options);
471 this.trigger("lockChange", that);
475 * Forward a dispatch request to the data source. Runs a "dispatchable" database function.
476 * Include a `success` callback function in options to handle the result.
478 * @param {String} Name of the class
479 * @param {String} Function name
480 * @param {Object} Parameters
481 * @param {Object} Options
483 dispatch: function (name, func, params, options) {
484 options = _.extend({}, options); // clone and set to {} if undefined
485 var dataSource = options.dataSource || XT.dataSource,
487 nameSpace: name.replace(/\.\w+/i, ''),
494 return dataSource.request(null, "post", payload, options);
498 Reimplemented to handle status changes.
500 @param {Object} Options
501 @returns {Object} Request
503 fetch: function (options) {
504 options = options ? _.clone(options) : {};
506 K = XM.ModelClassMixin,
507 success = options.success,
508 klass = this.getClass();
510 if (klass.canRead()) {
511 this.setStatus(K.BUSY_FETCHING);
512 options.success = function (resp) {
513 model.setStatus(K.READY_CLEAN, options);
514 if (XT.session.config.debugging) {
515 XT.log('Fetch successful');
517 if (success) { success(model, resp, options); }
519 return Backbone.Model.prototype.fetch.call(this, options);
521 XT.log('Insufficient privileges to fetch');
526 Set the id on this record an id from the server. Including the `cascade`
527 option will call ids to be fetched recursively for `HasMany` relations.
529 @returns {Object} Request
531 fetchId: function (options) {
532 options = _.defaults(options ? _.clone(options) : {}, {});
535 options.success = function (resp) {
536 that.set(that.idAttribute, resp, options);
538 this.dispatch('XM.Model', 'fetchId', this.recordType, options);
543 Return a matching record id for a passed user `key` and `value`. If none
546 @param {String} Property to search on, typically a user key
547 @param {String} Value to search for
548 @param {Object} Options
549 @param {Function} [options.succss] Callback on success
550 @param {Function} [options.error] Callback on error
551 @returns {Object} Receiver
553 findExisting: function (key, value, options) {
554 options = options || {};
555 return this.getClass().findExisting.call(this, key, value, options);
559 Valid attribute names that can be used on this model based on the
560 data source definition, whether or not they already exist yet on the
565 getAttributeNames: function () {
566 return this.getClass().getAttributeNames.call(this);
570 Returns the current model prototype class.
574 getClass: function () {
575 return Object.getPrototypeOf(this).constructor;
579 Return the current status.
583 getStatus: function () {
588 Return the current status as as string.
592 getStatusString: function () {
594 status = this.getStatus(),
596 for (prop in XM.ModelClassMixin) {
597 if (XM.ModelClassMixin.hasOwnProperty(prop)) {
598 if (prop.match(/[A-Z_]$/) && XM.ModelClassMixin[prop] === status) {
603 return ret.join(" ");
607 Return the type as defined by the model's orm. Attribute path is supported.
609 @parameter {String} Attribute name
612 getType: function (value) {
613 return this.getClass().getType(value);
617 Searches attributes first, if not found then returns either a function call
618 or property value that matches the key. It supports search on an attribute path
619 through a model hierarchy.
623 // Returns the first name attribute from primary contact model.
624 var firstName = m.getValue('primaryContact.firstName');
626 getValue: function (key) {
631 if (key.indexOf('.') !== -1) {
632 parts = key.split('.');
634 _.each(parts, function (part) {
635 value = value instanceof Backbone.Model ? value.getValue(part) : value;
637 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
640 // Search attribute, meta, function, propety
641 if (_.has(this.attributes, key)) {
642 return this.attributes[key];
643 } else if (this.meta && _.has(this.meta.attributes, key)) {
644 return this.meta.get(key);
646 return _.isFunction(this[key]) ? this[key]() : this[key];
650 isBusy: function () {
651 var status = this.getStatus(),
652 K = XM.ModelClassMixin;
653 return status === K.BUSY_FETCHING ||
654 status === K.BUSY_COMMITTING ||
655 status === K.BUSY_DESTROYING;
659 Reimplemented. A model is new if the status is `READY_NEW`.
664 var K = XM.ModelClassMixin;
665 return this.getStatus() === K.READY_NEW || this._wasNew || false;
669 Returns true if status is `DESTROYED_CLEAN` or `DESTROYED_DIRTY`.
673 isDestroyed: function () {
674 var status = this.getStatus(),
675 K = XM.ModelClassMixin;
676 return status === K.DESTROYED_CLEAN || status === K.DESTROYED_DIRTY;
680 Returns true if status is `READY_NEW` or `READY_DIRTY`.
684 isDirty: function () {
685 var status = this.getStatus(),
686 K = XM.ModelClassMixin;
687 return status === K.READY_NEW ||
688 status === K.READY_DIRTY ||
689 status === K.DESTROYED_DIRTY;
693 Returns true if the model is in one of the `READY` statuses
695 isReady: function () {
696 var status = this.getStatus(),
697 K = XM.ModelClassMixin;
698 return status === K.READY_NEW ||
699 status === K.READY_CLEAN ||
700 status === K.READY_DIRTY;
704 Returns true if you have the lock key, or if this model
705 is not lockable. (You can enter the room if you have no
706 key or if there is no lock!). When this value is true and the
707 `isLockable` is true it means the user has a application lock
708 on the object at the database level so that no other users can
711 This is not to be confused with the `isLocked` function that
712 is used by Backbone-relational to manage events on relations.
716 hasLockKey: function () {
717 return !this.lock || this.lock.key ? true : false;
721 * Returns the lock's key if it exists, otherwise null.
724 getLockKey: function () {
725 return this.lock ? this.lock.key : false;
729 Return whether the model is in a read-only state. If an attribute name
730 is passed, returns whether that attribute is read-only. It is also
731 capable of checking the read only status of child objects via a search path string.
734 // Inquire on the whole model
735 var readOnly = this.isReadOnly();
737 // Inquire on a single attribute
738 var readOnly = this.isReadOnly("name");
740 // Inquire using a search path
741 var readOnly = this.isReadOnly("contact.firstName");
744 @seealso `setReadOnly`
745 @seealso `readOnlyAttributes`
746 @param {String} attribute
749 isReadOnly: function (value) {
752 isLockedOut = !this.hasLockKey();
755 if (_.isString(value) && value.indexOf('.') !== -1) {
756 parts = value.split('.');
758 _.each(parts, function (part) {
759 if (result instanceof Backbone.Model) {
760 result = result.getValue(part);
761 } else if (_.isNull(result)) {
763 } else if (!_.isUndefined(result)) {
764 result = result.isReadOnly(part) || !result.hasLockKey();
770 if ((!_.isString(value) && !_.isObject(value)) || this.readOnly) {
771 result = this.readOnly;
773 result = _.contains(this.readOnlyAttributes, value);
775 return result || isLockedOut;
779 Return whether an attribute is required.
781 @param {String} attribute
784 isRequired: function (value) {
785 return _.contains(this.requiredAttributes, value);
790 A utility function that triggers a `notify` event. Useful for passing along
791 information to the interface. Bind to `notify` to use.
794 var m = new XM.MyModel();
795 var raiseAlert = function (model, value, options) {
798 m.on('notify', raiseAlert);
801 @param {String} Message
802 @param {Object} Options
803 @param {Number} [options.type] Type of notification NOTICE,
804 WARNING, CRITICAL, QUESTION. Default = NOTICE.
805 @param {Object} [options.callback] A callback function to process based on user response.
807 notify: function (message, options) {
808 options = options ? _.clone(options) : {};
809 if (options.type === undefined) {
810 options.type = XM.ModelClassMixin.NOTICE;
812 this.trigger('notify', this, message, options);
816 Return the original value of an attribute the last time fetch was called.
820 original: function (attr) {
825 if (attr.indexOf('.') !== -1) {
826 parts = attr.split('.');
828 _.each(parts, function (part) {
829 value = value instanceof Backbone.Model ? value.original(part) : value;
831 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
834 return this._cache ? this._cache[attr] : this.attributes[attr];
838 Return all the original values of the attributes the last time fetch was called.
839 Note this returns objects an the original javascript payload format, not relational children.
843 originalAttributes: function () {
848 Checks the object against the schema and converts date strings to date objects.
850 @param {Object} Response
852 parse: function (resp) {
854 schemas = XT.session.getSchemas(),
857 parse = function (namespace, typeName, obj) {
858 var type = schemas[namespace].get(typeName),
860 if (!type) { throw new Error(typeName + " not found in schema " + namespace + "."); }
862 if (obj.hasOwnProperty(attr) && obj[attr] !== null) {
863 column = _.findWhere(type.columns, {name: attr}) || {};
864 if (column.category === K.DB_DATE) {
865 obj[attr] = new Date(obj[attr]);
871 return parse(this.recordType.prefix(), this.recordType.suffix(), resp);
875 Returns the previous status of the model.
877 @returns {Boolean} Previous Status
879 previousStatus: function () {
880 return this._prevStatus;
884 * Manage all re-entrant lock actions, namely obtain, renew, and release.
886 * @param action {String}
888 * @see XM.Model#obtainLock
889 * @see XM.Model#renewLock
890 * @see XM.Model#releaseLock
892 _reentrantLockHelper: function (action, params, _options) {
894 options = _.extend({ }, _options),
895 userCallback = options.success,
896 methodName = action + 'Lock',
897 eventName = 'lock:' + action;
899 this.dispatch("XM.Model", methodName, params, _.extend(options, {
900 success: function (lock) {
902 that.lockDidChange(that, lock);
903 that.trigger(eventName, that, { lock: lock });
905 if (_.isFunction(userCallback)) {
910 that.trigger('lock:error', that);
916 Revert the model to the previous status. Useful for reseting status
917 after a failed validation.
919 param {Boolean} - cascade
921 revertStatus: function (cascade) {
922 var K = XM.ModelClassMixin,
923 prev = this._prevStatus;
924 this.setStatus(this._prevStatus || K.EMPTY);
925 this._prevStatus = prev;
931 @retuns {Object} Request
933 save: function (key, value, options) {
934 options = options ? _.clone(options) : {};
936 K = XM.ModelClassMixin,
940 // Handle both `"key", value` and `{key: value}` -style arguments.
941 if (_.isObject(key) || _.isEmpty(key)) {
943 options = value ? _.clone(value) : {};
944 } else if (_.isString(key)) {
948 // Only save if we should.
949 if (this.isDirty() || attrs) {
950 this._wasNew = this.isNew();
951 success = options.success;
953 options.success = function (model, resp, options) {
954 model.setStatus(K.READY_CLEAN, options);
955 if (XT.session.config.debugging) {
956 XT.log('Save successful');
958 if (success) { success(model, resp, options); }
961 // Handle both `"key", value` and `{key: value}` -style arguments.
962 if (_.isObject(key) || _.isEmpty(key)) { value = options; }
964 // Call the super version
965 this.setStatus(K.BUSY_COMMITTING, {cascade: true});
966 result = Backbone.Model.prototype.save.call(this, key, value, options);
968 if (!result) { this.revertStatus(true); }
972 XT.log('No changes to save');
977 Overload: Don't allow setting when model is in error or destroyed status, or
978 updating a `READY_CLEAN` record without update privileges.
980 @param {String|Object} Key
981 @param {String|Object} Value or Options
982 @param {Object} Options
984 set: function (key, val, options) {
985 var K = XM.ModelClassMixin,
986 keyIsObject = _.isObject(key),
987 status = this.getStatus(),
990 // Handle both `"key", value` and `{key: value}` -style arguments.
991 if (keyIsObject) { options = val; }
992 options = options ? options : {};
997 // Set error if no update privileges
999 if (!this.canUpdate()) { err = XT.Error.clone('xt1010'); }
1005 case K.DESTROYED_CLEAN:
1006 case K.DESTROYED_DIRTY:
1007 // Set error if attempting to edit a record that is ineligable
1008 err = XT.Error.clone('xt1009', { params: { status: status } });
1011 // If we're not in a `READY` state, silence all events
1012 if (!_.isBoolean(options.silent)) {
1013 options.silent = true;
1017 // Raise error, if any
1019 this.trigger('invalid', this, err, options);
1023 // Handle both `"key", value` and `{key: value}` -style arguments.
1024 if (keyIsObject) { val = options; }
1025 return Backbone.Model.prototype.set.call(this, key, val, options);
1029 Set a field if exists in a schema. Otherwise ignore silently.
1031 setIfExists: function (key, val, options) {
1032 var K = XM.ModelClassMixin,
1033 keyIsObject = _.isObject(key),
1034 attributes = this.getAttributeNames();
1036 // Handle both `"key", value` and `{key: value}` -style arguments.
1037 if (keyIsObject) { options = val; }
1038 options = options ? options : {};
1041 _.each(key, function (subvalue, subkey) {
1042 if (!_.contains(attributes, subkey)) {
1046 if (_.isEmpty(key)) {
1050 if (!_.contains(attributes, key)) {
1055 // Handle both `"key", value` and `{key: value}` -style arguments.
1056 if (keyIsObject) { val = options; }
1057 return this.set.call(this, key, val, options);
1061 Set a value(s) on attributes if key(s) is/are in schema, otherwise set on
1062 `meta`. If `meta` is null then behaves the same as `setIfExists`.
1064 setValue: function (key, val, options) {
1065 var keyIsObject = _.isObject(key),
1066 attributes = this.getAttributeNames(),
1070 // If no meta, then forward request.
1072 return this.setIfExists(key, val, options);
1075 // Handle both `"key", value` and `{key: value}` -style arguments.
1076 if (keyIsObject) { options = val; }
1077 options = options ? options : {};
1080 _.each(key, function (subvalue, subkey) {
1081 if (!_.contains(attributes, subkey)) {
1082 that.meta.set(subkey, subvalue, options);
1085 if (!_.isEmpty(key)) {
1086 that.set(key, options);
1090 if (_.contains(attributes, key)) {
1091 this.set(key, val, options);
1093 this.meta.set(key, val, options);
1101 Set the entire model, or a specific model attribute to `readOnly`.<br />
1102 Examples:<pre><code>
1103 m.setReadOnly() // sets model to read only
1104 m.setReadOnly(false) // sets model to be editable
1105 m.setReadOnly('name') // sets 'name' attribute to read-only
1106 m.setReadOnly('name', false) // sets 'name' attribute to be editable</code></pre>
1108 Note: Privilege enforcement supercedes read-only settings.
1110 @seealso `isReadOnly`
1112 @param {String|Array|Boolean} Attribute string or hash to set, or boolean if setting the model
1113 @param {Boolean} Boolean - default = true.
1116 setReadOnly: function (key, value) {
1117 value = _.isBoolean(value) ? value : true;
1121 process = function (key, value) {
1122 if (value && !_.contains(that.readOnlyAttributes, key)) {
1123 that.readOnlyAttributes.push(key);
1124 changes[key] = true;
1125 } else if (!value && _.contains(that.readOnlyAttributes, key)) {
1126 that.readOnlyAttributes = _.without(that.readOnlyAttributes, key);
1127 changes[key] = true;
1131 // Handle attribute array
1132 if (_.isObject(key)) {
1133 _.each(key, function (attr) {
1134 process(attr, value);
1135 changes[attr] = true;
1138 // handle attribute string
1139 } else if (_.isString(key)) {
1140 process(key, value);
1144 key = _.isBoolean(key) ? key : true;
1145 this.readOnly = key;
1146 // Attributes that were already read-only will stay that way
1147 // so only count the attributes that were not affected
1148 delta = _.difference(this.getAttributeNames(), this.readOnlyAttributes);
1149 _.each(delta, function (attr) {
1150 changes[attr] = true;
1155 if (!_.isEmpty(changes)) {
1156 this.trigger('readOnlyChange', this, {changes: changes, isReadOnly: value});
1162 Set the status on the model. Triggers `statusChange` event.
1164 @param {Number} Status
1166 setStatus: function (status, options) {
1167 var K = XM.ModelClassMixin;
1169 if (this.status === status) { return; }
1170 this._prevStatus = this.status;
1171 this.status = status;
1173 // Reset patch cache if applicable
1174 if (status === K.READY_CLEAN && !this.readOnly) {
1175 this._cache = this.toJSON();
1178 this.trigger('statusChange', this, status, options);
1183 Sync to xTuple data source.
1185 Accepts options.collection to sync a Backbone collection
1186 of models in lieu of just the current model.
1188 sync: function (method, model, options) {
1189 options = options ? _.clone(options) : {};
1190 var dataSource = options.dataSource || XT.dataSource,
1191 key = this.idAttribute,
1192 error = options.error,
1193 K = XM.ModelClassMixin,
1198 options.error = function (resp) {
1199 that.setStatus(K.ERROR);
1200 if (error) { error(model, resp, options); }
1203 // Handle a colleciton of models to persist
1204 if (options.collection) {
1206 options.collection.each(function (obj) {
1208 nameSpace: obj.recordType.replace(/\.\w+/i, ''),
1209 type: obj.recordType.suffix()
1213 if (obj.binaryField) {
1214 throw "Processing of for arrays of models with binary fields is not supported.";
1217 switch (obj.previousStatus())
1220 item.method = "post";
1221 item.data = obj.toJSON();
1222 item.requery = options.requery;
1225 item.method = "patch";
1226 item.etag = obj.etag;
1227 item.lock = obj.lock;
1228 item.patches = obj.generatePatches();
1229 item.requery = options.requery;
1231 case K.DESTROYED_DIRTY:
1232 item.method = "delete";
1233 item.etag = obj.etag;
1234 item.lock = obj.lock;
1237 throw "Model in collection syncing from an unsupported state";
1243 // All collections have to go through "post."
1246 // Handle the case of a model only persisting itself
1249 payload.nameSpace = this.recordType.replace(/\.\w+/i, '');
1250 payload.type = this.recordType.suffix();
1252 // Get an id from... someplace
1254 payload.id = options.id;
1255 } else if (options[key]) {
1256 payload.id = options[key];
1257 } else if (model._cache) {
1258 payload.id = model._cache[key];
1259 } else if (model.id) {
1260 payload.id = model.id;
1261 } else if (model.attributes) {
1262 payload.id = model.attributes[key];
1264 options.error("Cannot find id");
1270 payload.data = model.toJSON();
1271 payload.binaryField = model.binaryField; // see issue 18661
1272 payload.requery = options.requery;
1276 if (options.context) { payload.context = options.context; }
1280 payload.etag = model.etag;
1281 payload.lock = model.lock;
1282 payload.patches = model.generatePatches();
1283 payload.binaryField = model.binaryField;
1284 payload.requery = options.requery;
1287 payload.etag = model.etag;
1288 payload.lock = model.lock;
1304 result = dataSource.request(model, method, payload, options);
1306 return result || false;
1310 Overload: Convert dates to strings.
1312 toJSON: function (options) {
1315 json = Backbone.Model.prototype.toJSON.apply(this, arguments);
1317 // Convert dates to strings to avoid conflicts with jsonpatch
1318 for (prop in json) {
1319 if (json.hasOwnProperty(prop) && json[prop] instanceof Date) {
1320 json[prop] = json[prop].toJSON();
1328 Determine whether this record has been referenced by another. By default
1329 this function inspects foreign key relationships on the database, and is
1330 therefore dependent on foreign key relationships existing where appropriate
1333 @param {Object} Options
1334 @returns {Object} Request
1336 used: function (options) {
1337 return this.getClass().used(this.id, options);
1341 Default validation checks `attributes` for:<br />
1342 * Data type integrity.<br />
1343 * Required fields.<br />
1345 Returns `undefined` if the validation succeeded, or some value, usually
1346 an error message, if it fails.<br />
1349 @param {Object} Attributes
1350 @param {Object} Options
1352 validate: function (attributes, options) {
1353 attributes = attributes || {};
1354 options = options || {};
1356 if (!XT.session.getSchemas()[this.recordType.prefix()].get(this.recordType.suffix())) {
1357 XT.log("Cannot find schema", this.recordType);
1361 attr, value, category, column, params = {},
1362 type = this.recordType.suffix(),
1363 namespace = this.recordType.prefix(),
1364 columns = XT.session.getSchemas()[namespace].get(type).columns,
1366 getColumn = function (attr) {
1367 return _.find(columns, function (column) {
1368 return column.name === attr;
1372 // Check data type integrity
1373 for (attr in attributes) {
1374 if (attributes.hasOwnProperty(attr) &&
1375 !_.isNull(attributes[attr]) &&
1376 !_.isUndefined(attributes[attr])) {
1377 params.attr = ("_" + attr).loc();
1379 value = attributes[attr];
1380 column = getColumn(attr);
1381 category = column ? column.category : false;
1384 if (!_.isObject(value) && !_.isString(value)) { // XXX unscientific
1385 params.type = "_binary".loc();
1386 return XT.Error.clone('xt1003', { params: params });
1391 if (!_.isString(value)) {
1392 params.type = "_string".loc();
1393 return XT.Error.clone('xt1003', { params: params });
1397 if (!_.isNumber(value)) {
1398 params.type = "_number".loc();
1399 return XT.Error.clone('xt1003', { params: params });
1403 if (!_.isDate(value)) {
1404 params.type = "_date".loc();
1405 return XT.Error.clone('xt1003', { params: params });
1409 if (!_.isBoolean(value)) {
1410 params.type = "_boolean".loc();
1411 return XT.Error.clone('xt1003', { params: params });
1415 if (!_.isArray(value)) {
1416 params.type = "_array".loc();
1417 return XT.Error.clone('xt1003', { params: params });
1421 if (!_.isObject(value) && !_.isNumber(value)) {
1422 params.type = "_object".loc();
1423 return XT.Error.clone('xt1003', { params: params });
1427 return XT.Error.clone('xt1002', { params: params });
1433 for (i = 0; i < this.requiredAttributes.length; i += 1) {
1434 value = attributes[this.requiredAttributes[i]];
1435 if (value === undefined || value === null || value === "") {
1436 params.attr = ("_" + this.requiredAttributes[i]).loc();
1437 return XT.Error.clone('xt1004', { params: params });
1446 // ..........................................................
1451 A mixin for use on model classes that includes status constants
1452 and privilege control functions.
1454 XM.ModelClassMixin = {
1455 getReportUrl: function (action, modelName, id) {
1456 var reportUrl = "/generate-report?nameSpace=%@&type=%@&id=%@".f(
1457 modelName.prefix(), modelName.suffix(), id);
1460 reportUrl = reportUrl + "&action=" + action;
1467 Use this function to find out whether a user can create records before
1472 canCreate: function () {
1473 return XM.ModelClassMixin.canDo.call(this, 'create');
1477 Use this function to find out whether a user can read this record type
1478 before any have been loaded.
1480 @param {Object} Model
1481 @param {String} Attribute name (optional)
1484 canRead: function (model, attribute) {
1485 return XM.ModelClassMixin.canDo.call(this, 'read', model, attribute);
1489 Returns whether a user has access to update a record of this type. If a
1490 record is passed that involves personal privileges, it will validate
1491 whether that particular record is updatable.
1493 @param {Object} Model
1494 @param {String} Attribute name (optional)
1497 canUpdate: function (model) {
1498 return XM.ModelClassMixin.canDo.call(this, 'update', model);
1502 Returns whether a user has access to delete a record of this type. If a
1503 record is passed that involves personal privileges, it will validate
1504 whether that particular record is deletable.
1506 @param {Object} Model
1509 canDelete: function (model) {
1510 return XM.ModelClassMixin.canDo.call(this, 'delete', model);
1514 Returns whether the current record can be deleted based on privilege
1515 settings AND whether or not the record is used. Requires a call to the
1518 @param {Object} Model
1519 @param {Function} callback. Will be called with boolean response
1521 canDestroy: function (model, callback) {
1524 if (!XM.ModelClassMixin.canDelete.call(this, model)) {
1529 options.success = function (used) {
1533 this.used.call(this, model.id, options);
1538 Check privilege on `action`. If `model` is passed, checks personal
1539 privileges on the model where applicable.
1541 @param {String} Action
1542 @param {XM.Model} Model
1544 canDo: function (action, model, attribute) {
1545 var privs = this.prototype.privileges,
1546 sessionPrivs = XT.session.privileges,
1547 isGrantedAll = false,
1548 isGrantedPersonal = false,
1549 username = XT.session.details.username,
1553 K = XM.ModelClassMixin,
1554 status = model && model.getStatus ? model.getStatus() : K.READY;
1556 // Need to be in a valid status to "do" anything
1557 if (!(status & K.READY)) { return false; }
1559 // If no privileges, nothing to check.
1560 if (_.isEmpty(privs)) { return true; }
1562 // If we have session prvileges perform the check.
1563 if (sessionPrivs && sessionPrivs.get) {
1564 // Check global privileges.
1565 if (privs.all && privs.all[action]) {
1566 isGrantedAll = this.checkCompoundPrivs(sessionPrivs, privs.all[action]);
1568 // update privs are always sufficient for viewing as well
1569 if (!isGrantedAll && privs.all && action === 'read' && privs.all.update) {
1570 isGrantedAll = this.checkCompoundPrivs(sessionPrivs, privs.all.update);
1573 // Check personal privileges.
1574 if (!isGrantedAll && privs.personal && privs.personal[action]) {
1575 isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal[action]);
1577 // update privs are always sufficient for viewing as well
1578 if (!isGrantedPersonal && privs.personal && action === 'read' && privs.personal.update) {
1579 isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal.update);
1583 // If only personal privileges, check the personal attribute list to
1584 // see if we can update.
1585 if (!isGrantedAll && isGrantedPersonal && action !== "create" &&
1586 model && model.originalAttributes()) {
1588 props = privs.personal && privs.personal.properties ?
1589 privs.personal.properties : [];
1591 isGrantedPersonal = false;
1593 // Compare to cached data value in case user attr has been reassigned
1594 while (!isGrantedPersonal && i < props.length) {
1595 value = model.original(props[i]).toLowerCase();
1596 isGrantedPersonal = value === username;
1601 return isGrantedAll || isGrantedPersonal;
1604 checkCompoundPrivs: function (sessionPrivs, privileges) {
1605 if (typeof privileges !== 'string') {
1608 var match = _.find(privileges.split(" "), function (priv) {
1609 return sessionPrivs.get(priv);
1611 return !!match; // return true if match is truthy
1615 Return an array of valid attribute names on the model.
1619 getAttributeNames: function () {
1620 var recordType = this.recordType || this.prototype.recordType,
1621 namespace = recordType.prefix(),
1622 type = recordType.suffix();
1623 return _.pluck(XT.session.getSchemas()[namespace].get(type).columns, 'name');
1627 Return the type as defined by the model's orm. Attribute path is supported.
1629 @parameter {String} Attribute name
1632 getType: function (value) {
1636 findType = function (Klass, attr) {
1637 var schema = Klass.prototype.recordType.prefix(),
1638 table = Klass.prototype.recordType.suffix(),
1639 def = XT.session.schemas[schema].get(table),
1640 column = _.findWhere(def.columns, {name: attr});
1641 return column ? column.type : undefined;
1645 if (_.isString(value) && value.indexOf('.') !== -1) {
1646 parts = value.split('.');
1648 _.each(parts, function (part) {
1651 if (i < parts.length) {
1652 relation = _.findWhere(result.prototype.relations, {key: part});
1653 result = _.isString(relation.relatedModel) ?
1654 XT.getObjectByName(relation.relatedModel) : relation.relatedModel;
1656 result = findType(result, part);
1662 return findType(this, value);
1666 Returns an object from the relational store matching the `name` provided.
1668 @param {String} Name
1671 getObjectByName: function (name) {
1672 return Backbone.Relational.store.getObjectByName(name);
1676 Returns an array of text attribute names on the model.
1680 getSearchableAttributes: function () {
1681 var recordType = this.prototype.recordType,
1682 namespace = recordType.prefix(),
1683 type = recordType.suffix(),
1684 tbldef = XT.session.getSchemas()[namespace].get(type),
1689 for (i = 0; i < tbldef.columns.length; i++) {
1690 name = tbldef.columns[i].name;
1691 if (tbldef.columns[i].category === 'S') {
1699 Return a matching record id for a passed user `key` and `value`. If none
1700 found, returns zero.
1702 @param {String} Property to search on, typically a user key
1703 @param {String} Value to search for
1704 @param {Object} Options
1705 @returns {Object} Receiver
1707 findExisting: function (key, value, options) {
1708 var recordType = this.recordType || this.prototype.recordType,
1709 params = [ recordType, key, value ];
1710 if (key !== this.idAttribute) { params.push(this.id || ""); }
1711 XM.ModelMixin.dispatch('XM.Model', 'findExisting', params, options);
1716 Determine whether this record has been referenced by another. By default
1717 this function inspects foreign key relationships on the database, and is
1718 therefore dependent on foreign key relationships existing where appropriate
1722 @param {Object} Options
1723 @returns {Object} Request
1725 used: function (id, options) {
1726 return XM.ModelMixin.dispatch('XM.Model', 'used',
1727 [this.prototype.recordType, id], options);
1730 // ..........................................................
1735 Generic state for records with no local changes.
1737 Use a logical AND (single `&`) to test record status.
1747 Generic state for records with local changes.
1749 Use a logical AND (single `&`) to test record status.
1759 State for records that are still loaded.
1761 This is the initial state of a new record. It will not be editable until
1762 a record is fetch from the store, or it is initialized with the `isNew`
1770 EMPTY: 0x0100, // 256
1773 State for records in an error state.
1780 ERROR: 0x1000, // 4096
1783 Generic state for records that are loaded and ready for use.
1785 Use a logical AND (single `&`) to test record status.
1792 READY: 0x0200, // 512
1795 State for records that are loaded and ready for use with no local changes.
1802 READY_CLEAN: 0x0201, // 513
1806 State for records that are loaded and ready for use with local changes.
1813 READY_DIRTY: 0x0202, // 514
1817 State for records that are new - not yet committed to server.
1824 READY_NEW: 0x0203, // 515
1828 Generic state for records that have been destroyed.
1830 Use a logical AND (single `&`) to test record status.
1837 DESTROYED: 0x0400, // 1024
1841 State for records that have been destroyed and committed to server.
1848 DESTROYED_CLEAN: 0x0401, // 1025
1851 State for records that have been destroyed but not yet committed to
1859 DESTROYED_DIRTY: 0x0402, // 1026
1862 Generic state for records that have been submitted to data source.
1864 Use a logical AND (single `&`) to test record status.
1871 BUSY: 0x0800, // 2048
1875 State for records that are still loading data from the server.
1882 BUSY_FETCHING: 0x0804, // 2052
1886 State for records that have been modified and submitted to server.
1893 BUSY_COMMITTING: 0x0810, // 2064
1896 State for records that have been destroyed and submitted to server.
1903 BUSY_DESTROYING: 0x0840, // 2112
1906 Constant for `notify` message type notice.
1916 Constant for `notify` message type warning.
1926 Constant for `notify` message type critical.
1936 Constant for `notify` message type question.
1946 Constant for `notify` message type question.
1958 EMPTY: 0x0100, // 256
1959 ERROR: 0x1000, // 4096
1960 READY: 0x0200, // 512
1961 READY_CLEAN: 0x0201, // 513
1962 READY_DIRTY: 0x0202, // 514
1963 READY_NEW: 0x0203, // 515
1964 DESTROYED: 0x0400, // 1024
1965 DESTROYED_CLEAN: 0x0401, // 1025
1966 DESTROYED_DIRTY: 0x0402, // 1026
1967 BUSY: 0x0800, // 2048
1968 BUSY_FETCHING: 0x0804, // 2052
1969 BUSY_COMMITTING: 0x0810, // 2064
1970 BUSY_DESTROYING: 0x0840, // 2112
1983 513 : 'READY_CLEAN',
1984 514 : 'READY_DIRTY',
1988 1025 : 'DESTROYED_CLEAN',
1989 1026 : 'DESTROYED_DIRTY',
1992 2052 : 'BUSY_FETCHING',
1993 2064 : 'BUSY_COMMITTING',
1994 2112 : 'BUSY_DESTROYING'