1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/changeset/063a1d690724/ changeset: 063a1d690724 user: jgoecks date: 2012-06-28 19:31:20 summary: Update backbone-relational lib. affected #: 1 file diff -r adc8114131fa700da3b5800d7ba4568bd665c9b5 -r 063a1d690724b3485f41c620f664f11c7390ceef static/scripts/libs/backbone-relational.js --- a/static/scripts/libs/backbone-relational.js +++ b/static/scripts/libs/backbone-relational.js @@ -1,4 +1,4 @@ -/** +/** * Backbone-relational.js 0.5.0 * (c) 2011 Paul Uithol * @@ -19,15 +19,15 @@ exports = module.exports = Backbone; } else { - var _ = window._; + _ = window._; Backbone = window.Backbone; exports = window; } - + Backbone.Relational = { showWarnings: true }; - + /** * Semaphore mixin; can be used as both binary and counting. **/ @@ -116,37 +116,87 @@ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel. * Handles lookup for relations. */ - Backbone.Store = function() { + Backbone.Store = function() { this._collections = []; this._reverseRelations = []; + this._subModels = []; + this._modelScopes = [ exports ]; }; _.extend( Backbone.Store.prototype, Backbone.Events, { - _collections: null, - _reverseRelations: null, + addModelScope: function( scope ) { + this._modelScopes.push( scope ); + }, + + /** + * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel' + * for a model later in 'setupSuperModel'. + * + * @param {Backbone.RelationalModel} subModelTypes + * @param {Backbone.RelationalModel} superModelType + */ + addSubModels: function( subModelTypes, superModelType ) { + this._subModels.push({ + 'superModelType': superModelType, + 'subModels': subModelTypes + }); + }, + + /** + * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's + * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'. + * + * @param {Backbone.RelationalModel} modelType + */ + setupSuperModel: function( modelType ) { + _.find( this._subModels, function( subModelDef ) { + return _.find( subModelDef.subModels, function( subModelTypeName, typeValue ) { + var subModelType = this.getObjectByName( subModelTypeName ); + + if ( modelType === subModelType ) { + // Set 'modelType' as a child of the found superModel + subModelDef.superModelType._subModels[ typeValue ] = modelType; + + // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'. + modelType._superModel = subModelDef.superModelType; + modelType._subModelTypeValue = typeValue; + modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute; + return true; + } + }, this ); + }, this ); + }, /** * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to * existing instances of 'model' in the store as well. - * @param {object} relation + * @param {Object} relation * @param {Backbone.RelationalModel} relation.model * @param {String} relation.type * @param {String} relation.key - * @param {String|object} relation.relatedModel + * @param {String|Object} relation.relatedModel */ addReverseRelation: function( relation ) { var exists = _.any( this._reverseRelations, function( rel ) { - return _.all( relation, function( val, key ) { - return val === rel[ key ]; + return _.all( relation, function( val, key ) { + return val === rel[ key ]; + }); }); - }); if ( !exists && relation.model && relation.type ) { this._reverseRelations.push( relation ); - if ( !relation.model.prototype.relations ) { - relation.model.prototype.relations = []; - } - relation.model.prototype.relations.push( relation ); + var addRelation = function( model, relation ) { + if ( !model.prototype.relations ) { + model.prototype.relations = []; + } + model.prototype.relations.push( relation ); + + _.each( model._subModels, function( subModel ) { + addRelation( subModel, relation ); + }, this ); + }; + + addRelation( relation.model, relation ); this.retroFitRelation( relation ); } @@ -154,11 +204,15 @@ /** * Add a 'relation' to all existing instances of 'relation.model' in the store - * @param {object} relation + * @param {Object} relation */ retroFitRelation: function( relation ) { var coll = this.getCollection( relation.model ); coll.each( function( model ) { + if ( !( model instanceof relation.model ) ) { + return; + } + new relation.type( model, relation ); }, this); }, @@ -169,9 +223,17 @@ * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null */ getCollection: function( model ) { - var coll = _.detect( this._collections, function( c ) { - // Check if model is the type itself (a ref to the constructor), or is of type c.model - return model === c.model || model.constructor === c.model; + if ( model instanceof Backbone.RelationalModel ) { + model = model.constructor; + } + + var rootModel = model; + while ( rootModel._superModel ) { + rootModel = rootModel._superModel; + } + + var coll = _.detect( this._collections, function( c ) { + return c.model === rootModel; }); if ( !coll ) { @@ -187,22 +249,32 @@ * @return {Object} */ getObjectByName: function( name ) { - var type = _.reduce( name.split( '.' ), function( memo, val ) { - return memo[ val ]; - }, exports); - return type !== exports ? type: null; + var parts = name.split( '.' ), + type = null; + + _.find( this._modelScopes, function( scope ) { + type = _.reduce( parts, function( memo, val ) { + return memo[ val ]; + }, scope ); + + if ( type && type !== scope ) { + return true; + } + }, this ); + + return type; }, _createCollection: function( type ) { var coll; - // If 'type' is an instance, take it's constructor + // If 'type' is an instance, take its constructor if ( type instanceof Backbone.RelationalModel ) { type = type.constructor; } // Type should inherit from Backbone.RelationalModel. - if ( type.prototype instanceof Backbone.RelationalModel.prototype.constructor ) { + if ( type.prototype instanceof Backbone.RelationalModel ) { coll = new Backbone.Collection(); coll.model = type; @@ -213,7 +285,7 @@ }, /** - * Find an id + * Find the attribute that is to be used as the `id` on a given object * @param type * @param {String|Number|Object|Backbone.RelationalModel} item */ @@ -240,7 +312,18 @@ find: function( type, item ) { var id = this.resolveIdForItem( type, item ); var coll = this.getCollection( type ); - return coll && coll.get( id ); + + // Because the found object could be of any of the type's superModel + // types, only return it if it's actually of the type asked for. + if ( coll ) { + var obj = coll.get( id ); + + if ( obj instanceof type ) { + return obj; + } + } + + return null; }, /** @@ -281,19 +364,19 @@ * are used to regulate addition and removal of models from relations. * * @param {Backbone.RelationalModel} instance - * @param {object} options + * @param {Object} options * @param {string} options.key * @param {Backbone.RelationalModel.constructor} options.relatedModel - * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids. + * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids. * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store. - * @param {object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate + * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs * {Backbone.Relation|String} type ('HasOne' or 'HasMany'). */ Backbone.Relation = function( instance, options ) { this.instance = instance; // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype - options = ( typeof options === 'object' && options ) || {}; + options = _.isObject( options ) ? options : {}; this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation ); this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type : Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type ); @@ -302,7 +385,7 @@ this.key = this.options.key; this.keySource = this.options.keySource || this.key; - this.keyDestination = this.options.keyDestination || this.options.keySource || this.key; + this.keyDestination = this.options.keyDestination || this.keySource || this.key; // 'exports' should be the global object where 'relatedModel' can be found on if given as a string. this.relatedModel = this.options.relatedModel; @@ -340,7 +423,7 @@ _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' ); - if( instance ) { + if ( instance ) { this.initialize(); // When a model in the store is destroyed, check if it is 'this.instance'. @@ -405,28 +488,28 @@ return false; } // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel - if ( !( m.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) { + if ( !( m.prototype instanceof Backbone.RelationalModel ) ) { warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i ); return false; } // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel - if ( !( rm.prototype instanceof Backbone.RelationalModel.prototype.constructor ) ) { + if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) { warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm ); return false; } // Check if this is not a HasMany, and the reverse relation is HasMany as well - if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany.prototype.constructor ) { + if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) { warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this ); return false; } // Check if we're not attempting to create a duplicate relationship - if( i && i._relations.length ) { + if ( i && i._relations.length ) { var exists = _.any( i._relations, function( rel ) { - var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key; - return rel.relatedModel === rm && rel.key === k && - ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key ); - }, this ); + var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key; + return rel.relatedModel === rm && rel.key === k && + ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key ); + }, this ); if ( exists ) { warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists', @@ -451,12 +534,6 @@ this.instance.release(); }, - createModel: function( item ) { - if ( this.options.createModels && typeof( item ) === 'object' ) { - return new this.relatedModel( item ); - } - }, - /** * Determine if a relation (on a different RelationalModel) is the reverse * relation of the current one. @@ -482,12 +559,12 @@ // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array. var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] ); _.each( models , function( related ) { - _.each( related.getRelations(), function( relation ) { - if ( this._isReverseRelation( relation ) ) { - reverseRelations.push( relation ); - } + _.each( related.getRelations(), function( relation ) { + if ( this._isReverseRelation( relation ) ) { + reverseRelations.push( relation ); + } + }, this ); }, this ); - }, this ); return reverseRelations; }, @@ -501,7 +578,7 @@ sanitizeOptions: function( options ) { options = options ? _.clone( options ) : {}; if ( options.silent ) { - options = _.extend( {}, options, { silentChange: true } ); + options.silentChange = true; delete options.silent; } return options; @@ -516,7 +593,7 @@ unsanitizeOptions: function( options ) { options = options ? _.clone( options ) : {}; if ( options.silentChange ) { - options = _.extend( {}, options, { silent: true } ); + options.silent = true; delete options.silentChange; } return options; @@ -551,10 +628,9 @@ this.setRelated( model ); // Notify new 'related' object of the new relation. - var dit = this; - _.each( dit.getReverseRelations(), function( relation ) { - relation.addRelated( dit.instance ); - } ); + _.each( this.getReverseRelations(), function( relation ) { + relation.addRelated( this.instance ); + }, this ); }, findRelated: function( options ) { @@ -565,15 +641,7 @@ model = item; } else if ( item ) { - // Try to find an instance of the appropriate 'relatedModel' in the store, or create it - model = Backbone.Relational.store.find( this.relatedModel, item ); - - if ( model && _.isObject( item ) ) { - model.set( item, options ); - } - else if ( !model ) { - model = this.createModel( item ); - } + model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); } return model; @@ -583,7 +651,7 @@ * If the key is changed, notify old & new reverse relations and initialize the new relation */ onChange: function( model, attr, options ) { - // Don't accept recursive calls to onChange (like onChange->findRelated->createModel->initializeRelations->addRelated->onChange) + // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange) if ( this.isLocked() ) { return; } @@ -691,10 +759,10 @@ // Handle a custom 'collectionType' this.collectionType = this.options.collectionType; - if ( _( this.collectionType ).isString() ) { + if ( _.isString( this.collectionType ) ) { this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType ); } - if ( !this.collectionType.prototype instanceof Backbone.Collection.prototype.constructor ){ + if ( !this.collectionType.prototype instanceof Backbone.Collection ){ throw new Error( 'collectionType must inherit from Backbone.Collection' ); } @@ -710,7 +778,7 @@ }, _getCollectionOptions: function() { - return _.isFunction( this.options.collectionOptions ) ? + return _.isFunction( this.options.collectionOptions ) ? this.options.collectionOptions( this.instance ) : this.options.collectionOptions; }, @@ -737,12 +805,12 @@ if ( this.options.collectionKey ) { var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey; - if (collection[ key ] && collection[ key ] !== this.instance ) { + if ( collection[ key ] && collection[ key ] !== this.instance ) { if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) { console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey ); } } - else if (key) { + else if ( key ) { collection[ key ] = this.instance; } } @@ -768,19 +836,18 @@ // Try to find instances of the appropriate 'relatedModel' in the store _.each( this.keyContents, function( item ) { - var model = Backbone.Relational.store.find( this.relatedModel, item ); + var model = null; + if ( item instanceof this.relatedModel ) { + model = item; + } + else { + model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } ); + } - if ( model && _.isObject( item ) ) { - model.set( item, options ); - } - else if ( !model ) { - model = this.createModel( item ); - } - - if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) { - models.push( model ); - } - }, this ); + if ( model && !this.related.getByCid( model ) && !this.related.get( model ) ) { + models.push( model ); + } + }, this ); } // Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.) @@ -816,7 +883,7 @@ if ( this.related instanceof Backbone.Collection ) { coll = this.related; - coll.reset( [], { silent: true } ); + coll.remove( coll.models ); } else { coll = this._prepareCollection(); @@ -842,9 +909,9 @@ if ( !this.related.getByCid( model ) && !this.related.get( model ) ) { // Check if this new model was specified in 'this.keyContents' var item = _.any( this.keyContents, function( item ) { - var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); - return id && id === model.id; - }, this ); + var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item ); + return id && id === model.id; + }, this ); if ( item ) { this.related.add( model, options ); @@ -860,7 +927,7 @@ //console.debug('handleAddition called; args=%o', arguments); // Make sure the model is in fact a valid model before continuing. // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel) - if( !( model instanceof Backbone.Model ) ) { + if ( !( model instanceof Backbone.Model ) ) { return; } @@ -883,7 +950,7 @@ */ handleRemoval: function( model, coll, options ) { //console.debug('handleRemoval called; args=%o', arguments); - if( !( model instanceof Backbone.Model ) ) { + if ( !( model instanceof Backbone.Model ) ) { return; } @@ -940,6 +1007,9 @@ _deferProcessing: false, _queue: null, + subModelTypeAttribute: 'type', + subModelTypes: null, + constructor: function( attributes, options ) { // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'. // Defer 'processQueue', so that when 'Relation.createModels' is used we: @@ -971,7 +1041,7 @@ this._queue.block(); Backbone.Relational.eventQueue.block(); - Backbone.Model.prototype.constructor.apply( this, arguments ); + Backbone.Model.apply( this, arguments ); // Try to run the global queue holding external events Backbone.Relational.eventQueue.unblock(); @@ -1004,7 +1074,7 @@ _.each( this.relations, function( rel ) { var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); - if ( type && type.prototype instanceof Backbone.Relation.prototype.constructor ) { + if ( type && type.prototype instanceof Backbone.Relation ) { new type( this, rel ); // Also pushes the new Relation into _relations } else { @@ -1016,17 +1086,18 @@ this.release(); this.processQueue(); }, - + /** * When new values are set, notify this model's relations (also if options.silent is set). * (Relation.setRelated locks this model before calling 'set' on it to prevent loops) */ updateRelations: function( options ) { - if( this._isInitialized && !this.isLocked() ) { + if ( this._isInitialized && !this.isLocked() ) { _.each( this._relations, function( rel ) { - var val = this.attributes[ rel.key ]; + // Update from data in `rel.keySource` if set, or `rel.key` otherwise + var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ]; if ( rel.related !== val ) { - this.trigger('relational:change:' + rel.key, this, val, options || {} ); + this.trigger( 'relational:change:' + rel.key, this, val, options || {} ); } }, this ); } @@ -1072,10 +1143,11 @@ /** * Retrieve related objects. * @param key {string} The relation key to fetch models for. - * @param options {object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'. + * @param options {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'. + * @param update {boolean} Whether to force a fetch from the server (updating existing models). * @return {jQuery.when[]} An array of request objects */ - fetchRelated: function( key, options ) { + fetchRelated: function( key, options, update ) { options || ( options = {} ); var setUrl, requests = [], @@ -1083,7 +1155,7 @@ keyContents = rel && rel.keyContents, toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) { var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item ); - return id && !Backbone.Relational.store.find( rel.relatedModel, id ); + return id && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) ); }, this ); if ( toFetch && toFetch.length ) { @@ -1091,13 +1163,13 @@ var models = _.map( toFetch, function( item ) { var model; - if ( typeof( item ) === 'object' ) { - model = new rel.relatedModel( item ); + if ( _.isObject( item ) ) { + model = rel.relatedModel.build( item ); } else { var attrs = {}; attrs[ rel.relatedModel.prototype.idAttribute ] = item; - model = new rel.relatedModel( attrs ); + model = rel.relatedModel.build( attrs ); } return model; @@ -1117,9 +1189,9 @@ error: function() { var args = arguments; _.each( models, function( model ) { - model.trigger( 'destroy', model, model.collection, options ); - options.error && options.error.apply( model, args ); - }); + model.trigger( 'destroy', model, model.collection, options ); + options.error && options.error.apply( model, args ); + }); }, url: setUrl }, @@ -1153,7 +1225,7 @@ // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object var attributes; - if (_.isObject( key ) || key == null) { + if ( _.isObject( key ) || key == null ) { attributes = key; options = value; } @@ -1164,10 +1236,12 @@ var result = Backbone.Model.prototype.set.apply( this, arguments ); - // 'set' is called quite late in 'Backbone.Model.prototype.constructor', but before 'initialize'. // Ideal place to set up relations :) if ( !this._isInitialized && !this.isLocked() ) { + this.constructor.initializeModelHierarchy(); + Backbone.Relational.store.register( this ); + this.initializeRelations(); } // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true} @@ -1227,8 +1301,8 @@ } _.each( this.getRelations(), function( rel ) { - delete attributes[ rel.key ]; - }); + delete attributes[ rel.key ]; + }); return new this.constructor( attributes ); }, @@ -1245,36 +1319,233 @@ this.acquire(); var json = Backbone.Model.prototype.toJSON.call( this ); + if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) { + json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue; + } + _.each( this._relations, function( rel ) { - var value = json[ rel.key ]; + var value = json[ rel.key ]; - if ( rel.options.includeInJSON === true && value && _.isFunction( value.toJSON ) ) { - json[ rel.keyDestination ] = value.toJSON(); - } - else if ( _.isString( rel.options.includeInJSON ) ) { - if ( value instanceof Backbone.Collection ) { - json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON ); + if ( rel.options.includeInJSON === true) { + if ( value && _.isFunction( value.toJSON ) ) { + json[ rel.keyDestination ] = value.toJSON(); + } + else { + json[ rel.keyDestination ] = null; + } } - else if ( value instanceof Backbone.Model ) { - json[ rel.keyDestination ] = value.get( rel.options.includeInJSON ); + else if ( _.isString( rel.options.includeInJSON ) ) { + if ( value instanceof Backbone.Collection ) { + json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON ); + } + else if ( value instanceof Backbone.Model ) { + json[ rel.keyDestination ] = value.get( rel.options.includeInJSON ); + } + else { + json[ rel.keyDestination ] = null; + } } - } - else { - delete json[ rel.key ]; - } + else if ( _.isArray( rel.options.includeInJSON ) ) { + if ( value instanceof Backbone.Collection ) { + var valueSub = []; + value.each( function( model ) { + var curJson = {}; + _.each( rel.options.includeInJSON, function( key ) { + curJson[ key ] = model.get( key ); + }); + valueSub.push( curJson ); + }); + json[ rel.keyDestination ] = valueSub; + } + else if ( value instanceof Backbone.Model ) { + var valueSub = {}; + _.each( rel.options.includeInJSON, function( key ) { + valueSub[ key ] = value.get( key ); + }); + json[ rel.keyDestination ] = valueSub; + } + else { + json[ rel.keyDestination ] = null; + } + } + else { + delete json[ rel.key ]; + } - if ( rel.keyDestination !== rel.key ) { - delete json[ rel.key ]; - } - }, this ); + if ( rel.keyDestination !== rel.key ) { + delete json[ rel.key ]; + } + }); this.release(); return json; } + }, + { + setup: function( superModel ) { + // We don't want to share a relations array with a parent, as this will cause problems with + // reverse relations. + this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 ); + + this._subModels = {}; + this._superModel = null; + + // If this model has 'subModelTypes' itself, remember them in the store + if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) { + Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this ); + } + // The 'subModelTypes' property should not be inherited, so reset it. + else { + this.prototype.subModelTypes = null; + } + + // Initialize all reverseRelations that belong to this new model. + _.each( this.prototype.relations, function( rel ) { + if ( !rel.model ) { + rel.model = this; + } + + if ( rel.reverseRelation && rel.model === this ) { + var preInitialize = true; + if ( _.isString( rel.relatedModel ) ) { + /** + * The related model might not be defined for two reasons + * 1. it never gets defined, e.g. a typo + * 2. it is related to itself + * In neither of these cases do we need to pre-initialize reverse relations. + */ + var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); + preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel ); + } + + var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); + if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) { + new type( null, rel ); + } + } + }, this ); + }, + + /** + * Create a 'Backbone.Model' instance based on 'attributes'. + * @param {Object} attributes + * @param {Object} [options] + * @return {Backbone.Model} + */ + build: function( attributes, options ) { + var model = this; + + // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet. + this.initializeModelHierarchy(); + + // Determine what type of (sub)model should be built if applicable. + // Lookup the proper subModelType in 'this._subModels'. + if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) { + var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ]; + var subModelType = this._subModels[ subModelTypeAttribute ]; + if ( subModelType ) { + model = subModelType; + } + } + + return new model( attributes, options ); + }, + + initializeModelHierarchy: function() { + // If we're here for the first time, try to determine if this modelType has a 'superModel'. + if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) { + Backbone.Relational.store.setupSuperModel( this ); + + // If a superModel has been found, copy relations from the _superModel if they haven't been + // inherited automatically (due to a redefinition of 'relations'). + // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail + // the isUndefined/isNull check next time. + if ( this._superModel ) { + // + if ( this._superModel.prototype.relations ) { + var supermodelRelationsExist = _.any( this.prototype.relations, function( rel ) { + return rel.model && rel.model !== this; + }, this ); + + if ( !supermodelRelationsExist ) { + this.prototype.relations = this._superModel.prototype.relations.concat( this.prototype.relations ); + } + } + } + else { + this._superModel = false; + } + } + + // If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each. + if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) { + _.each( this.prototype.subModelTypes, function( subModelTypeName ) { + var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); + subModelType && subModelType.initializeModelHierarchy(); + }); + } + }, + + /** + * Find an instance of `this` type in 'Backbone.Relational.store'. + * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found. + * - If `attributes` is an object, the model will be updated with `attributes` if found. + * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`). + * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. + * @param {Object} [options] + * @param {Boolean} [options.create=true] + * @return {Backbone.RelationalModel} + */ + findOrCreate: function( attributes, options ) { + // Try to find an instance of 'this' model type in the store + var model = Backbone.Relational.store.find( this, attributes ); + + // If we found an instance, update it with the data in 'item'; if not, create an instance + // (unless 'options.create' is false). + if ( _.isObject( attributes ) ) { + if ( model ) { + model.set( attributes, options ); + } + else if ( !options || ( options && options.create !== false ) ) { + model = this.build( attributes, options ); + } + } + + return model; + } }); _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); /** + * Override Backbone.Collection._prepareModel, so objects will be built using the correct type + * if the collection.model has subModels. + */ + Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel; + Backbone.Collection.prototype._prepareModel = function ( model, options ) { + options || (options = {}); + if ( !( model instanceof Backbone.Model ) ) { + var attrs = model; + options.collection = this; + + if ( typeof this.model.build !== 'undefined' ) { + model = this.model.build( attrs, options ); + } + else { + model = new this.model( attrs, options ); + } + + if ( !model._validate( model.attributes, options ) ) { + model = false; + } + } + else if ( !model.collection ) { + model.collection = this; + } + + return model; + } + + /** * Override Backbone.Collection.add, so objects fetched from the server multiple times will * update the existing Model. Also, trigger 'relational:add'. */ @@ -1289,22 +1560,22 @@ //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options ); _.each( models, function( model ) { - if ( !( model instanceof Backbone.Model ) ) { - // Try to find 'model' in Backbone.store. If it already exists, set the new properties on it. - var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] ); - if ( existingModel ) { - existingModel.set( existingModel.parse ? existingModel.parse( model ) : model, options ); - model = existingModel; + if ( !( model instanceof Backbone.Model ) ) { + // Try to find 'model' in Backbone.store. If it already exists, set the new properties on it. + var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] ); + if ( existingModel ) { + existingModel.set( existingModel.parse ? existingModel.parse( model ) : model, options ); + model = existingModel; + } + else { + model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); + } } - else { - model = Backbone.Collection.prototype._prepareModel.call( this, model, options ); + + if ( model instanceof Backbone.Model && !this.get( model ) && !this.getByCid( model ) ) { + modelsToAdd.push( model ); } - } - - if ( model instanceof Backbone.Model && !this.get( model ) && !this.getByCid( model ) ) { - modelsToAdd.push( model ); - } - }, this ); + }, this ); // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc). @@ -1312,8 +1583,8 @@ add.call( this, modelsToAdd, options ); _.each( modelsToAdd, function( model ) { - this.trigger('relational:add', model, this, options); - }, this ); + this.trigger( 'relational:add', model, this, options ); + }, this ); } return this; @@ -1325,19 +1596,22 @@ var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove; Backbone.Collection.prototype.remove = function( models, options ) { options || (options = {}); - if (!_.isArray( models ) ) { + if ( !_.isArray( models ) ) { models = [ models ]; } + else { + models = models.slice( 0 ); + } //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); _.each( models, function( model ) { - model = this.getByCid( model ) || this.get( model ); + model = this.getByCid( model ) || this.get( model ); - if ( model instanceof Backbone.Model ) { - remove.call( this, model, options ); - this.trigger('relational:remove', model, this, options); - } - }, this ); + if ( model instanceof Backbone.Model ) { + remove.call( this, model, options ); + this.trigger('relational:remove', model, this, options); + } + }, this ); return this; }; @@ -1348,7 +1622,18 @@ var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset; Backbone.Collection.prototype.reset = function( models, options ) { reset.call( this, models, options ); - this.trigger( 'relational:reset', models, options ); + this.trigger( 'relational:reset', this, options ); + + return this; + }; + + /** + * Override 'Backbone.Collection.sort' to trigger 'relational:reset'. + */ + var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort; + Backbone.Collection.prototype.sort = function( options ) { + sort.call( this, options ); + this.trigger( 'relational:reset', this, options ); return this; }; @@ -1372,35 +1657,12 @@ return this; }; - // Override .extend() to check for reverseRelations to initialize. + // Override .extend() to automatically call .setup() Backbone.RelationalModel.extend = function( protoProps, classProps ) { var child = Backbone.Model.extend.apply( this, arguments ); - - var relations = ( protoProps && protoProps.relations ) || []; - _.each( relations, function( rel ) { - if( rel.reverseRelation ) { - rel.model = child; - - var preInitialize = true; - if ( _.isString( rel.relatedModel ) ) { - /** - * The related model might not be defined for two reasons - * 1. it never gets defined, e.g. a typo - * 2. it is related to itself - * In neither of these cases do we need to pre-initialize reverse relations. - */ - var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel ); - preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel.prototype.constructor ); - } - - var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type ); - if ( preInitialize && type && type.prototype instanceof Backbone.Relation.prototype.constructor ) { - new type( null, rel ); - } - } - }); + + child.setup( this ); return child; }; - })(); Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.