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 !_.isUndefined(this.privileges.attribute[attribute][action]) ?
18 this.privileges.attribute[attribute][action] : undefined;
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;
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 Indicate whether a model should be kept track of in the
104 A hash structure that defines data access.
105 Automatically set when `XT.session` loads
113 Indicates whether the model is read only.
120 An array of attribute names designating attributes that are not editable.
121 Use `setReadOnly` to edit this array.
123 @seealso `setReadOnly`
124 @seealso `isReadOnly`
127 readOnlyAttributes: null,
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.
135 nameAttribute: "name",
138 Specify the name of a data source model here.
145 An array of required attributes. A `validate` will fail until all the required
146 attributes have values.
150 requiredAttributes: null,
153 Model's status. You should never modify this directly.
163 The record version fetched from the server.
168 // ..........................................................
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).
178 augment: function (hash) {
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
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);
191 } else if (_.isArray(value)) {
192 // add array elements (for now we merge duplicates)
193 that[key] = _.union(existingObj, value);
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
200 that[key] = function () {
201 var firstDefaults = existingObj.apply(this, arguments);
202 var secondDefaults = value.apply(this, arguments);
203 return _.extend(firstDefaults, secondDefaults);
206 } else if (_.isFunction(value)) {
207 // for functions, call the super() first, and then the
208 // function that's being mixed in
210 that[key] = function () {
211 existingObj.apply(this, arguments);
212 value.apply(this, arguments);
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);
220 } else if (_.isObject(value)) {
222 that[key] = _.extend({}, existingObj, value);
225 throw new Error("Do not know how to augment: " + key);
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.
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;
248 * Get the type of an attribute.
250 getAttributeType: function (attr) {
251 var found = _.findWhere(
252 XT.session.schemas.XM.get(this.recordType.suffix()).columns,
255 return found && found.type;
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
267 Returns whether an attribute can be edited.
269 @param {String} Attribute
272 canEdit: function (attribute) {
273 return _canDoAttr.call(this, "update", attribute);
277 Returns whether the current record can be updated based on privilege
282 canUpdate: function () {
283 return this.getClass().canUpdate(this);
287 Returns whether the current record can be deleted based on privilege
292 canDelete: function () {
293 return this.getClass().canDelete(this);
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
301 @param {Function} callback. Will be called with boolean response
303 canDestroy: function (callback) {
304 this.getClass().canDestroy(this, callback);
308 Returns whether an attribute can be viewed.
310 @param {String} Attribute
313 canView: function (attribute) {
314 return _canDoAttr.call(this, "view", attribute);
318 Reimplemented to handle state change. Calling
319 `destroy` will cause the model to commit to the server
322 @returns {Object|Boolean}
324 destroy: function (options) {
325 options = options ? _.clone(options) : {};
328 success = options.success,
329 K = XM.ModelClassMixin;
331 this.setStatus(K.DESTROYED_DIRTY);
332 this.setStatus(K.BUSY_DESTROYING);
333 this._wasNew = this.isNew();
335 options.success = function (resp) {
336 if (success) { success(model, resp, options); }
338 result = Backbone.Model.prototype.destroy.call(this, options);
344 When any attributes change update the status if applicable.
346 didChange: function (model, options) {
347 options = options || {};
348 var K = XM.ModelClassMixin,
349 status = this.getStatus();
350 if (this.isBusy()) { return; }
352 // Mark dirty if we should
353 if (status === K.READY_CLEAN) {
354 this.setStatus(K.READY_DIRTY);
359 Called after confirmation that the model was destroyed on the
362 didDestroy: function () {
363 var K = XM.ModelClassMixin;
364 this.clear({silent: true});
365 this.setStatus(K.DESTROYED_CLEAN);
369 Handle a `sync` response that was an error.
371 didError: function (model, resp) {
373 this.lastError = resp;
378 Generate an array of patch objects per:
379 http://tools.ietf.org/html/rfc6902
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);
391 Called when model is instantiated.
393 initialize: function (attributes, options) {
394 attributes = attributes || {};
395 options = options || {};
397 K = XM.ModelClassMixin,
398 status = this.getStatus(),
399 idAttribute = this.idAttribute;
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) : [];
409 if (_.isEmpty(this.recordType)) { throw new Error('No record type defined'); }
411 if (_.isNull(status)) {
412 this.setStatus(K.EMPTY);
418 klass = this.getClass();
419 if (!klass.canCreate()) {
420 throw new Error('Insufficent privileges to create a record.');
422 this.setStatus(K.READY_NEW);
424 // Key generator (client based)
425 if (idAttribute === 'uuid' &&
426 !this.get(idAttribute) &&
427 !attributes[idAttribute]) {
428 this.set(idAttribute, XT.generateUUID());
431 // Deprecated key generator (server based)
432 if (this.autoFetchId) {
433 if (options.database) {
434 this.fetchId({database: options.database});
436 // This should throw and error for a call that needs to be fixed.
442 // Set attributes that should be required and read only
444 !_.contains(this.requiredAttributes, idAttribute)) {
445 this.requiredAttributes.push(idAttribute);
449 lockDidChange: function (model, lock) {
453 // Clear any old refresher
454 if (this._keyRefresherInterval) {
455 clearInterval(this._keyRefresherInterval);
456 that._keyRefresherInterval = undefined;
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);
472 // set up a refresher
473 this._keyRefresherInterval = setInterval(function () {
474 that.dispatch('XM.Model', 'renewLock', [lock.key], options);
477 this.trigger("lockChange", that);
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.
484 * @param {String} Name of the class
485 * @param {String} Function name
486 * @param {Object} Parameters
487 * @param {Object} Options
489 dispatch: function (name, func, params, options) {
490 options = _.extend({}, options); // clone and set to {} if undefined
491 var dataSource = options.dataSource || XT.dataSource,
493 nameSpace: name.replace(/\.\w+/i, ''),
500 return dataSource.request(null, "post", payload, options);
504 Reimplemented to handle status changes.
506 @param {Object} Options
507 @returns {Object} Request
509 fetch: function (options) {
510 options = options ? _.clone(options) : {};
512 K = XM.ModelClassMixin,
513 success = options.success,
514 klass = this.getClass();
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');
523 if (success) { success(model, resp, options); }
525 return Backbone.Model.prototype.fetch.call(this, options);
527 XT.log('Insufficient privileges to fetch');
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.
535 @returns {Object} Request
537 fetchId: function (options) {
538 options = _.defaults(options ? _.clone(options) : {}, {});
541 options.success = function (resp) {
542 that.set(that.idAttribute, resp, options);
544 this.dispatch('XM.Model', 'fetchId', this.recordType, options);
549 Return a matching record id for a passed user `key` and `value`. If none
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
559 findExisting: function (key, value, options) {
560 options = options || {};
561 return this.getClass().findExisting.call(this, key, value, options);
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
571 getAttributeNames: function () {
572 return this.getClass().getAttributeNames.call(this);
576 Returns the current model prototype class.
580 getClass: function () {
581 return Object.getPrototypeOf(this).constructor;
585 Return the current status.
589 getStatus: function () {
594 Return the current status as as string.
598 getStatusString: function () {
600 status = this.getStatus(),
602 for (prop in XM.ModelClassMixin) {
603 if (XM.ModelClassMixin.hasOwnProperty(prop)) {
604 if (prop.match(/[A-Z_]$/) && XM.ModelClassMixin[prop] === status) {
609 return ret.join(" ");
613 Return the type as defined by the model's orm. Attribute path is supported.
615 @parameter {String} Attribute name
618 getType: function (value) {
619 return this.getClass().getType(value);
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.
629 // Returns the first name attribute from primary contact model.
630 var firstName = m.getValue('primaryContact.firstName');
632 getValue: function (key) {
637 if (key.indexOf('.') !== -1) {
638 parts = key.split('.');
640 _.each(parts, function (part) {
641 value = value instanceof Backbone.Model ? value.getValue(part) : value;
643 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
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);
652 return _.isFunction(this[key]) ? this[key]() : this[key];
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;
665 Reimplemented. A model is new if the status is `READY_NEW`.
670 var K = XM.ModelClassMixin;
671 return this.getStatus() === K.READY_NEW || this._wasNew || false;
675 Returns true if status is `DESTROYED_CLEAN` or `DESTROYED_DIRTY`.
679 isDestroyed: function () {
680 var status = this.getStatus(),
681 K = XM.ModelClassMixin;
682 return status === K.DESTROYED_CLEAN || status === K.DESTROYED_DIRTY;
686 Returns true if status is `READY_NEW` or `READY_DIRTY`.
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;
699 Returns true if the model is in one of the `READY` statuses
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;
710 Returns true if the model is `READY_CLEAN`
712 isReadyClean: function () {
713 return this.getStatus() === XM.Model.READY_CLEAN;
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
724 This is not to be confused with the `isLocked` function that
725 is used by Backbone-relational to manage events on relations.
729 hasLockKey: function () {
730 return !this.lock || this.lock.key ? true : false;
734 * Returns the lock's key if it exists, otherwise null.
737 getLockKey: function () {
738 return this.lock ? this.lock.key : false;
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.
747 // Inquire on the whole model
748 var readOnly = this.isReadOnly();
750 // Inquire on a single attribute
751 var readOnly = this.isReadOnly("name");
753 // Inquire using a search path
754 var readOnly = this.isReadOnly("contact.firstName");
757 @seealso `setReadOnly`
758 @seealso `readOnlyAttributes`
759 @param {String} attribute
762 isReadOnly: function (value) {
765 isLockedOut = !this.hasLockKey();
768 if (_.isString(value) && value.indexOf('.') !== -1) {
769 parts = value.split('.');
771 _.each(parts, function (part) {
772 if (result instanceof Backbone.Model) {
773 result = result.getValue(part);
774 } else if (_.isNull(result)) {
776 } else if (!_.isUndefined(result)) {
777 result = result.isReadOnly(part) || !result.hasLockKey();
783 if ((!_.isString(value) && !_.isObject(value)) || this.readOnly) {
784 result = this.readOnly;
786 result = _.contains(this.readOnlyAttributes, value);
788 return result || isLockedOut;
792 Return whether an attribute is required.
794 @param {String} attribute
797 isRequired: function (value) {
798 return _.contains(this.requiredAttributes, value);
803 A utility function that triggers a `notify` event. Useful for passing along
804 information to the interface. Bind to `notify` to use.
807 var m = new XM.MyModel();
808 var raiseAlert = function (model, value, options) {
811 m.on('notify', raiseAlert);
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
823 notify: function (message, options) {
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;
831 this.trigger('notify', this, message, options);
835 Return the original value of an attribute the last time fetch was called.
839 original: function (attr) {
844 if (attr.indexOf('.') !== -1) {
845 parts = attr.split('.');
847 _.each(parts, function (part) {
848 value = value instanceof Backbone.Model ? value.original(part) : value;
850 return value || (_.isBoolean(value) || _.isNumber(value) ? value : "");
853 return this._cache ? this._cache[attr] : this.attributes[attr];
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.
862 originalAttributes: function () {
867 Checks the object against the schema and converts date strings to date objects.
869 @param {Object} Response
871 parse: function (resp) {
873 schemas = XT.session.getSchemas(),
876 parse = function (namespace, typeName, obj) {
877 var type = schemas[namespace].get(typeName),
879 if (!type) { throw new Error(typeName + " not found in schema " + namespace + "."); }
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]);
890 return parse(this.recordType.prefix(), this.recordType.suffix(), resp);
894 Returns the previous status of the model.
896 @returns {Boolean} Previous Status
898 previousStatus: function () {
899 return this._prevStatus;
903 * Manage all re-entrant lock actions, namely obtain, renew, and release.
905 * @param action {String}
907 * @see XM.Model#obtainLock
908 * @see XM.Model#renewLock
909 * @see XM.Model#releaseLock
911 _reentrantLockHelper: function (action, params, _options) {
913 options = _.extend({ }, _options),
914 userCallback = options.success,
915 methodName = action + 'Lock',
916 eventName = 'lock:' + action;
918 this.dispatch("XM.Model", methodName, params, _.extend(options, {
919 success: function (lock) {
921 that.lockDidChange(that, lock);
922 that.trigger(eventName, that, { lock: lock });
924 if (_.isFunction(userCallback)) {
929 that.trigger('lock:error', that);
935 Revert the model to the previous status. Useful for reseting status
936 after a failed validation.
938 param {Boolean} - cascade
940 revertStatus: function (cascade) {
941 var K = XM.ModelClassMixin,
942 prev = this._prevStatus;
943 this.setStatus(this._prevStatus || K.EMPTY);
944 this._prevStatus = prev;
950 @retuns {Object} Request
952 save: function (key, value, options) {
953 options = options ? _.clone(options) : {};
955 K = XM.ModelClassMixin,
959 // Handle both `"key", value` and `{key: value}` -style arguments.
960 if (_.isObject(key) || _.isEmpty(key)) {
962 options = value ? _.clone(value) : {};
963 } else if (_.isString(key)) {
967 // Only save if we should.
968 if (this.isDirty() || attrs) {
969 this._wasNew = this.isNew();
970 success = options.success;
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');
977 if (success) { success(model, resp, options); }
980 // Handle both `"key", value` and `{key: value}` -style arguments.
981 if (_.isObject(key) || _.isEmpty(key)) { value = options; }
983 // Call the super version
984 this.setStatus(K.BUSY_COMMITTING, {cascade: true});
985 result = Backbone.Model.prototype.save.call(this, key, value, options);
987 if (!result) { this.revertStatus(true); }
991 XT.log('No changes to save');
996 Overload: Don't allow setting when model is in error or destroyed status, or
997 updating a `READY_CLEAN` record without update privileges.
999 @param {String|Object} Key
1000 @param {String|Object} Value or Options
1001 @param {Object} Options
1003 set: function (key, val, options) {
1004 var K = XM.ModelClassMixin,
1005 keyIsObject = _.isObject(key),
1006 status = this.getStatus(),
1009 // Handle both `"key", value` and `{key: value}` -style arguments.
1010 if (keyIsObject) { options = val; }
1011 options = options ? options : {};
1016 // Set error if no update privileges
1018 if (!this.canUpdate()) { err = XT.Error.clone('xt1010'); }
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 } });
1030 // If we're not in a `READY` state, silence all events
1031 if (!_.isBoolean(options.silent)) {
1032 options.silent = true;
1036 // Raise error, if any
1038 this.trigger('invalid', this, err, options);
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);
1048 Set a field if exists in a schema. Otherwise ignore silently.
1050 setIfExists: function (key, val, options) {
1051 var K = XM.ModelClassMixin,
1052 keyIsObject = _.isObject(key),
1053 attributes = this.getAttributeNames();
1055 // Handle both `"key", value` and `{key: value}` -style arguments.
1056 if (keyIsObject) { options = val; }
1057 options = options ? options : {};
1060 _.each(key, function (subvalue, subkey) {
1061 if (!_.contains(attributes, subkey)) {
1065 if (_.isEmpty(key)) {
1069 if (!_.contains(attributes, key)) {
1074 // Handle both `"key", value` and `{key: value}` -style arguments.
1075 if (keyIsObject) { val = options; }
1076 return this.set.call(this, key, val, options);
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`.
1083 setValue: function (key, val, options) {
1084 var keyIsObject = _.isObject(key),
1085 attributes = this.getAttributeNames(),
1089 // If no meta, then forward request.
1091 return this.setIfExists(key, val, options);
1094 // Handle both `"key", value` and `{key: value}` -style arguments.
1095 if (keyIsObject) { options = val; }
1096 options = options ? options : {};
1099 _.each(key, function (subvalue, subkey) {
1100 if (!_.contains(attributes, subkey)) {
1101 that.meta.set(subkey, subvalue, options);
1105 if (!_.isEmpty(key)) {
1106 that.set(key, options);
1109 if (_.contains(attributes, key)) {
1110 this.set(key, val, options);
1112 this.meta.set(key, val, options);
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>
1127 Note: Privilege enforcement supercedes read-only settings.
1129 @seealso `isReadOnly`
1131 @param {String|Array|Boolean} Attribute string or hash to set, or boolean if setting the model
1132 @param {Boolean} Boolean - default = true.
1135 setReadOnly: function (key, value) {
1136 value = _.isBoolean(value) ? value : true;
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;
1150 // Handle attribute array
1151 if (_.isObject(key)) {
1152 _.each(key, function (attr) {
1153 process(attr, value);
1154 changes[attr] = true;
1157 // handle attribute string
1158 } else if (_.isString(key)) {
1159 process(key, value);
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;
1174 if (!_.isEmpty(changes)) {
1175 this.trigger('readOnlyChange', this, {changes: changes, isReadOnly: value});
1181 Set the status on the model. Triggers `statusChange` event.
1183 @param {Number} Status
1185 setStatus: function (status, options) {
1186 var K = XM.ModelClassMixin;
1188 if (this.status === status) { return; }
1189 this._prevStatus = this.status;
1190 this.status = status;
1192 // Reset patch cache if applicable
1193 if (status === K.READY_CLEAN && !this.readOnly) {
1194 this._cache = this.toJSON();
1197 this.trigger('statusChange', this, status, options);
1202 Sync to xTuple data source.
1204 Accepts options.collection to sync a Backbone collection
1205 of models in lieu of just the current model.
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,
1216 success = options.success;
1218 options.error = function (resp) {
1219 that.setStatus(K.ERROR);
1220 if (error) { error(model, resp, options); }
1223 options.success = function (model, resp, options) {
1224 if (_.isFunction(success)) {
1225 success(model, resp, options);
1227 that.trigger('sync', model, resp, options);
1230 // Handle a colleciton of models to persist
1231 if (options.collection) {
1232 delete options.validate; // Don't let this pass through...
1234 options.collection.each(function (obj) {
1236 nameSpace: obj.recordType.replace(/\.\w+/i, ''),
1237 type: obj.recordType.suffix()
1241 if (obj.binaryField) {
1242 throw "Processing of for arrays of models with binary fields is not supported.";
1245 switch (obj.previousStatus())
1248 item.method = "post";
1249 item.data = obj.toJSON();
1250 item.requery = options.requery;
1253 item.method = "patch";
1254 item.etag = obj.etag;
1255 item.lock = obj.lock;
1256 item.patches = obj.generatePatches();
1257 item.requery = options.requery;
1259 case K.DESTROYED_DIRTY:
1260 item.method = "delete";
1261 item.etag = obj.etag;
1262 item.lock = obj.lock;
1265 throw "Model in collection syncing from an unsupported state";
1271 // All collections have to go through "post."
1274 // Handle the case of a model only persisting itself
1277 payload.nameSpace = this.recordType.replace(/\.\w+/i, '');
1278 payload.type = this.recordType.suffix();
1280 // Get an id from... someplace
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];
1292 options.error("Cannot find id");
1298 payload.data = model.toJSON();
1299 payload.binaryField = model.binaryField; // see issue 18661
1300 payload.requery = options.requery;
1304 if (options.context) { payload.context = options.context; }
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;
1315 payload.etag = model.etag;
1316 payload.lock = model.lock;
1332 result = dataSource.request(model, method, payload, options);
1333 //this.trigger('request', this, result, options);
1335 return result || false;
1339 Overload: Convert dates to strings.
1341 toJSON: function (options) {
1344 json = Backbone.Model.prototype.toJSON.apply(this, arguments);
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();
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
1362 @param {Object} Options
1363 @returns {Object} Request
1365 used: function (options) {
1366 return this.getClass().used(this.id, options);
1370 Default validation checks `attributes` for:<br />
1371 * Data type integrity.<br />
1372 * Required fields.<br />
1374 Returns `undefined` if the validation succeeded, or some value, usually
1375 an error message, if it fails.<br />
1378 @param {Object} Attributes
1379 @param {Object} Options
1381 validate: function (attributes, options) {
1382 attributes = attributes || {};
1383 options = options || {};
1385 if (!XT.session.getSchemas()[this.recordType.prefix()].get(this.recordType.suffix())) {
1386 XT.log("Cannot find schema", this.recordType);
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,
1395 getColumn = function (attr) {
1396 return _.find(columns, function (column) {
1397 return column.name === attr;
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();
1409 value = attributes[attr];
1410 column = getColumn(attr);
1411 category = column ? column.category : false;
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 });
1422 if (!_.isString(value)) {
1423 params.type = "_string".loc();
1424 return XT.Error.clone('xt1003', { params: params });
1428 if (!_.isNumber(value)) {
1429 params.type = "_number".loc();
1430 return XT.Error.clone('xt1003', { params: params });
1434 if (!_.isDate(value)) {
1435 params.type = "_date".loc();
1436 return XT.Error.clone('xt1003', { params: params });
1440 if (!_.isBoolean(value)) {
1441 params.type = "_boolean".loc();
1442 return XT.Error.clone('xt1003', { params: params });
1446 if (!_.isArray(value)) {
1447 params.type = "_array".loc();
1448 return XT.Error.clone('xt1003', { params: params });
1452 if (!_.isObject(value) && !_.isNumber(value)) {
1453 params.type = "_object".loc();
1454 return XT.Error.clone('xt1003', { params: params });
1458 return XT.Error.clone('xt1002', { params: params });
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 });
1477 // ..........................................................
1482 A mixin for use on model classes that includes status constants
1483 and privilege control functions.
1485 XM.ModelClassMixin = {
1486 getReportUrl: function (action, modelName, id) {
1487 var reportUrl = "/generate-report?nameSpace=%@&type=%@&id=%@".f(
1488 modelName.prefix(), modelName.suffix(), id);
1491 reportUrl = reportUrl + "&action=" + action;
1498 Use this function to find out whether a user can create records before
1503 canCreate: function () {
1504 return XM.ModelClassMixin.canDo.call(this, 'create');
1508 Use this function to find out whether a user can read this record type
1509 before any have been loaded.
1511 @param {Object} Model
1512 @param {String} Attribute name (optional)
1515 canRead: function (model, attribute) {
1516 return XM.ModelClassMixin.canDo.call(this, 'read', model, attribute);
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.
1524 @param {Object} Model
1525 @param {String} Attribute name (optional)
1528 canUpdate: function (model) {
1529 return XM.ModelClassMixin.canDo.call(this, 'update', model);
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.
1537 @param {Object} Model
1540 canDelete: function (model) {
1541 return XM.ModelClassMixin.canDo.call(this, 'delete', model);
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
1549 @param {Object} Model
1550 @param {Function} callback. Will be called with boolean response
1552 canDestroy: function (model, callback) {
1555 if (!XM.ModelClassMixin.canDelete.call(this, model)) {
1560 options.success = function (used) {
1564 this.used.call(this, model.id, options);
1569 Check privilege on `action`. If `model` is passed, checks personal
1570 privileges on the model where applicable.
1572 @param {String} Action
1573 @param {XM.Model} Model
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,
1584 K = XM.ModelClassMixin,
1585 status = model && model.getStatus ? model.getStatus() : K.READY;
1587 // Need to be in a valid status to "do" anything
1588 if (!(status & K.READY)) { return false; }
1590 // If no privileges, nothing to check.
1591 if (_.isEmpty(privs)) { return true; }
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]);
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);
1604 // Check personal privileges.
1605 if (!isGrantedAll && privs.personal && privs.personal[action]) {
1606 isGrantedPersonal = this.checkCompoundPrivs(sessionPrivs, privs.personal[action]);
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);
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()) {
1619 props = privs.personal && privs.personal.properties ?
1620 privs.personal.properties : [];
1622 isGrantedPersonal = false;
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;
1632 return isGrantedAll || isGrantedPersonal;
1635 checkCompoundPrivs: function (sessionPrivs, privileges) {
1636 if (typeof privileges !== 'string') {
1639 var match = _.find(privileges.split(" "), function (priv) {
1640 return sessionPrivs.get(priv);
1642 return !!match; // return true if match is truthy
1646 Return an array of valid attribute names on the model.
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');
1658 Return the type as defined by the model's orm. Attribute path is supported.
1660 @parameter {String} Attribute name
1663 getType: function (value) {
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;
1676 if (_.isString(value) && value.indexOf('.') !== -1) {
1677 parts = value.split('.');
1679 _.each(parts, function (part) {
1682 if (i < parts.length) {
1683 relation = _.findWhere(result.prototype.relations, {key: part});
1685 result = _.isString(relation.relatedModel) ?
1686 XT.getObjectByName(relation.relatedModel) : relation.relatedModel;
1691 result = findType(result, part);
1697 return findType(this, value);
1701 Returns an object from the relational store matching the `name` provided.
1703 @param {String} Name
1706 getObjectByName: function (name) {
1707 return Backbone.Relational.store.getObjectByName(name);
1711 Returns an array of text attribute names on the model.
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),
1724 for (i = 0; i < tbldef.columns.length; i++) {
1725 name = tbldef.columns[i].name;
1726 if (tbldef.columns[i].category === 'S') {
1734 Return a matching record id for a passed user `key` and `value`. If none
1735 found, returns zero.
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
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);
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
1757 @param {Object} Options
1758 @returns {Object} Request
1760 used: function (id, options) {
1761 return XM.ModelMixin.dispatch('XM.Model', 'used',
1762 [this.prototype.recordType, id], options);
1765 // ..........................................................
1770 Generic state for records with no local changes.
1772 Use a logical AND (single `&`) to test record status.
1782 Generic state for records with local changes.
1784 Use a logical AND (single `&`) to test record status.
1794 State for records that are still loaded.
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`
1805 EMPTY: 0x0100, // 256
1808 State for records in an error state.
1815 ERROR: 0x1000, // 4096
1818 Generic state for records that are loaded and ready for use.
1820 Use a logical AND (single `&`) to test record status.
1827 READY: 0x0200, // 512
1830 State for records that are loaded and ready for use with no local changes.
1837 READY_CLEAN: 0x0201, // 513
1841 State for records that are loaded and ready for use with local changes.
1848 READY_DIRTY: 0x0202, // 514
1852 State for records that are new - not yet committed to server.
1859 READY_NEW: 0x0203, // 515
1863 Generic state for records that have been destroyed.
1865 Use a logical AND (single `&`) to test record status.
1872 DESTROYED: 0x0400, // 1024
1876 State for records that have been destroyed and committed to server.
1883 DESTROYED_CLEAN: 0x0401, // 1025
1886 State for records that have been destroyed but not yet committed to
1894 DESTROYED_DIRTY: 0x0402, // 1026
1897 Generic state for records that have been submitted to data source.
1899 Use a logical AND (single `&`) to test record status.
1906 BUSY: 0x0800, // 2048
1910 State for records that are still loading data from the server.
1917 BUSY_FETCHING: 0x0804, // 2052
1921 State for records that have been modified and submitted to server.
1928 BUSY_COMMITTING: 0x0810, // 2064
1931 State for records that have been destroyed and submitted to server.
1938 BUSY_DESTROYING: 0x0840, // 2112
1941 Constant for `notify` message type notice.
1951 Constant for `notify` message type warning.
1961 Constant for `notify` message type critical.
1971 Constant for `notify` message type question.
1981 Constant for `notify` message type question with cancel option.
1991 Constant for `notify` message type ok/cancel.
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
2028 513 : 'READY_CLEAN',
2029 514 : 'READY_DIRTY',
2033 1025 : 'DESTROYED_CLEAN',
2034 1026 : 'DESTROYED_DIRTY',
2037 2052 : 'BUSY_FETCHING',
2038 2064 : 'BUSY_COMMITTING',
2039 2112 : 'BUSY_DESTROYING'