Free cookie consent management tool by TermsFeed Policy Generator

source: branches/1888_OaaS/HeuristicLab.Services.Optimization.Web/Content/backbone/backbone-relational.js @ 17169

Last change on this file since 17169 was 9305, checked in by fschoepp, 12 years ago

#1888:

  • Added an Update / GetExperiment... methods to the controller for updating and querying experiments.
  • The AlgorithmConverter class now properly converts from/to JSON format.
  • Integrated backbone js as MVC provider for JavaScript + jquery.
  • Added experiment.model.js + experiment.view.js + experiment.controller.js containing the MVC impl. for the Experiment pages.
  • Added new methods to the ExperimentController usable by the backbone js model implementation.
  • Added the experiment dialog from HL 3.3.7 (variate experiment parameters). It's capable of variating the algorithm parameters.
File size: 54.6 KB
Line 
1/* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
2/**
3 * Backbone-relational.js 0.7.1
4 * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
5 *
6 * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
7 * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
8 * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
9 */
10( function( undefined ) {
11  "use strict";
12 
13  /**
14   * CommonJS shim
15   **/
16  var _, Backbone, exports;
17  if ( typeof window === 'undefined' ) {
18    _ = require( 'underscore' );
19    Backbone = require( 'backbone' );
20    exports = module.exports = Backbone;
21  }
22  else {
23    _ = window._;
24    Backbone = window.Backbone;
25    exports = window;
26  }
27
28  Backbone.Relational = {
29    showWarnings: true
30  };
31
32  /**
33   * Semaphore mixin; can be used as both binary and counting.
34   **/
35  Backbone.Semaphore = {
36    _permitsAvailable: null,
37    _permitsUsed: 0,
38   
39    acquire: function() {
40      if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
41        throw new Error( 'Max permits acquired' );
42      }
43      else {
44        this._permitsUsed++;
45      }
46    },
47   
48    release: function() {
49      if ( this._permitsUsed === 0 ) {
50        throw new Error( 'All permits released' );
51      }
52      else {
53        this._permitsUsed--;
54      }
55    },
56   
57    isLocked: function() {
58      return this._permitsUsed > 0;
59    },
60   
61    setAvailablePermits: function( amount ) {
62      if ( this._permitsUsed > amount ) {
63        throw new Error( 'Available permits cannot be less than used permits' );
64      }
65      this._permitsAvailable = amount;
66    }
67  };
68 
69  /**
70   * A BlockingQueue that accumulates items while blocked (via 'block'),
71   * and processes them when unblocked (via 'unblock').
72   * Process can also be called manually (via 'process').
73   */
74  Backbone.BlockingQueue = function() {
75    this._queue = [];
76  };
77  _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
78    _queue: null,
79   
80    add: function( func ) {
81      if ( this.isBlocked() ) {
82        this._queue.push( func );
83      }
84      else {
85        func();
86      }
87    },
88   
89    process: function() {
90      while ( this._queue && this._queue.length ) {
91        this._queue.shift()();
92      }
93    },
94   
95    block: function() {
96      this.acquire();
97    },
98   
99    unblock: function() {
100      this.release();
101      if ( !this.isBlocked() ) {
102        this.process();
103      }
104    },
105   
106    isBlocked: function() {
107      return this.isLocked();
108    }
109  });
110  /**
111   * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'update:<key>')
112   * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
113   */
114  Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
115 
116  /**
117   * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
118   * Handles lookup for relations.
119   */
120  Backbone.Store = function() {
121    this._collections = [];
122    this._reverseRelations = [];
123    this._subModels = [];
124    this._modelScopes = [ exports ];
125  };
126  _.extend( Backbone.Store.prototype, Backbone.Events, {
127    addModelScope: function( scope ) {
128      this._modelScopes.push( scope );
129    },
130
131    /**
132     * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
133     * for a model later in 'setupSuperModel'.
134     *
135     * @param {Backbone.RelationalModel} subModelTypes
136     * @param {Backbone.RelationalModel} superModelType
137     */
138    addSubModels: function( subModelTypes, superModelType ) {
139      this._subModels.push({
140        'superModelType': superModelType,
141        'subModels': subModelTypes
142      });
143    },
144
145    /**
146     * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
147     * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
148     *
149     * @param {Backbone.RelationalModel} modelType
150     */
151    setupSuperModel: function( modelType ) {
152      _.find( this._subModels || [], function( subModelDef ) {
153        return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {
154          var subModelType = this.getObjectByName( subModelTypeName );
155
156          if ( modelType === subModelType ) {
157            // Set 'modelType' as a child of the found superModel
158            subModelDef.superModelType._subModels[ typeValue ] = modelType;
159
160            // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
161            modelType._superModel = subModelDef.superModelType;
162            modelType._subModelTypeValue = typeValue;
163            modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
164            return true;
165          }
166        }, this );
167      }, this );
168    },
169   
170    /**
171     * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
172     * existing instances of 'model' in the store as well.
173     * @param {Object} relation
174     * @param {Backbone.RelationalModel} relation.model
175     * @param {String} relation.type
176     * @param {String} relation.key
177     * @param {String|Object} relation.relatedModel
178     */
179    addReverseRelation: function( relation ) {
180      var exists = _.any( this._reverseRelations || [], function( rel ) {
181          return _.all( relation || [], function( val, key ) {
182              return val === rel[ key ];
183            });
184        });
185     
186      if ( !exists && relation.model && relation.type ) {
187        this._reverseRelations.push( relation );
188       
189        var addRelation = function( model, relation ) {
190          if ( !model.prototype.relations ) {
191            model.prototype.relations = [];
192          }
193          model.prototype.relations.push( relation );
194         
195          _.each( model._subModels || [], function( subModel ) {
196              addRelation( subModel, relation );
197            }, this );
198        };
199       
200        addRelation( relation.model, relation );
201       
202        this.retroFitRelation( relation );
203      }
204    },
205   
206    /**
207     * Add a 'relation' to all existing instances of 'relation.model' in the store
208     * @param {Object} relation
209     */
210    retroFitRelation: function( relation ) {
211      var coll = this.getCollection( relation.model );
212      coll.each( function( model ) {
213        if ( !( model instanceof relation.model ) ) {
214          return;
215        }
216
217        new relation.type( model, relation );
218      }, this);
219    },
220   
221    /**
222     * Find the Store's collection for a certain type of model.
223     * @param {Backbone.RelationalModel} model
224     * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
225     */
226    getCollection: function( model ) {
227      if ( model instanceof Backbone.RelationalModel ) {
228        model = model.constructor;
229      }
230     
231      var rootModel = model;
232      while ( rootModel._superModel ) {
233        rootModel = rootModel._superModel;
234      }
235     
236      var coll = _.detect( this._collections, function( c ) {
237        return c.model === rootModel;
238      });
239     
240      if ( !coll ) {
241        coll = this._createCollection( rootModel );
242      }
243     
244      return coll;
245    },
246   
247    /**
248     * Find a type on the global object by name. Splits name on dots.
249     * @param {String} name
250     * @return {Object}
251     */
252    getObjectByName: function( name ) {
253      var parts = name.split( '.' ),
254        type = null;
255
256      _.find( this._modelScopes || [], function( scope ) {
257        type = _.reduce( parts || [], function( memo, val ) {
258          return memo ? memo[ val ] : undefined;
259        }, scope );
260
261        if ( type && type !== scope ) {
262          return true;
263        }
264      }, this );
265
266      return type;
267    },
268   
269    _createCollection: function( type ) {
270      var coll;
271     
272      // If 'type' is an instance, take its constructor
273      if ( type instanceof Backbone.RelationalModel ) {
274        type = type.constructor;
275      }
276     
277      // Type should inherit from Backbone.RelationalModel.
278      if ( type.prototype instanceof Backbone.RelationalModel ) {
279        coll = new Backbone.Collection();
280        coll.model = type;
281       
282        this._collections.push( coll );
283      }
284     
285      return coll;
286    },
287
288    /**
289     * Find the attribute that is to be used as the `id` on a given object
290     * @param type
291     * @param {String|Number|Object|Backbone.RelationalModel} item
292     * @return {String|Number}
293     */
294    resolveIdForItem: function( type, item ) {
295      var id = _.isString( item ) || _.isNumber( item ) ? item : null;
296
297      if ( id === null ) {
298        if ( item instanceof Backbone.RelationalModel ) {
299          id = item.id;
300        }
301        else if ( _.isObject( item ) ) {
302          id = item[ type.prototype.idAttribute ];
303        }
304      }
305
306      // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
307      if ( !id && id !== 0 ) {
308        id = null;
309      }
310
311      return id;
312    },
313
314    /**
315     *
316     * @param type
317     * @param {String|Number|Object|Backbone.RelationalModel} item
318     */
319    find: function( type, item ) {
320      var id = this.resolveIdForItem( type, item );
321      var coll = this.getCollection( type );
322     
323      // Because the found object could be of any of the type's superModel
324      // types, only return it if it's actually of the type asked for.
325      if ( coll ) {
326        var obj = coll.get( id );
327
328        if ( obj instanceof type ) {
329          return obj;
330        }
331      }
332
333      return null;
334    },
335   
336    /**
337     * Add a 'model' to it's appropriate collection. Retain the original contents of 'model.collection'.
338     * @param {Backbone.RelationalModel} model
339     */
340    register: function( model ) {
341      var coll = this.getCollection( model );
342
343      if ( coll ) {
344        if ( coll.get( model ) ) {
345          throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );
346        }
347
348        var modelColl = model.collection;
349        coll.add( model );
350        model.bind( 'destroy', this.unregister, this );
351        model.collection = modelColl;
352      }
353    },
354   
355    /**
356     * Explicitly update a model's id in it's store collection
357     * @param {Backbone.RelationalModel} model
358     */
359    update: function( model ) {
360      var coll = this.getCollection( model );
361      coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
362    },
363   
364    /**
365     * Remove a 'model' from the store.
366     * @param {Backbone.RelationalModel} model
367     */
368    unregister: function( model ) {
369      model.unbind( 'destroy', this.unregister );
370      var coll = this.getCollection( model );
371      coll && coll.remove( model );
372    }
373  });
374  Backbone.Relational.store = new Backbone.Store();
375 
376  /**
377   * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events
378   * are used to regulate addition and removal of models from relations.
379   *
380   * @param {Backbone.RelationalModel} instance
381   * @param {Object} options
382   * @param {string} options.key
383   * @param {Backbone.RelationalModel.constructor} options.relatedModel
384   * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
385   * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
386   * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
387   *    the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
388   *    {Backbone.Relation|String} type ('HasOne' or 'HasMany').
389   */
390  Backbone.Relation = function( instance, options ) {
391    this.instance = instance;
392    // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
393    options = _.isObject( options ) ? options : {};
394    this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
395    this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
396      Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
397    this.model = options.model || this.instance.constructor;
398    this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
399   
400    this.key = this.options.key;
401    this.keySource = this.options.keySource || this.key;
402    this.keyDestination = this.options.keyDestination || this.keySource || this.key;
403
404    // 'exports' should be the global object where 'relatedModel' can be found on if given as a string.
405    this.relatedModel = this.options.relatedModel;
406    if ( _.isString( this.relatedModel ) ) {
407      this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
408    }
409
410    if ( !this.checkPreconditions() ) {
411      return;
412    }
413
414    if ( instance ) {
415      var contentKey = this.keySource;
416      if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) {
417        contentKey = this.key;
418      }
419
420      this.keyContents = this.instance.get( contentKey );
421
422      // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
423      if ( this.keySource !== this.key ) {
424        this.instance.unset( this.keySource, { silent: true } );
425      }
426
427      // Add this Relation to instance._relations
428      this.instance._relations.push( this );
429    }
430
431    // Add the reverse relation on 'relatedModel' to the store's reverseRelations
432    if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
433      Backbone.Relational.store.addReverseRelation( _.defaults( {
434          isAutoRelation: true,
435          model: this.relatedModel,
436          relatedModel: this.model,
437          reverseRelation: this.options // current relation is the 'reverseRelation' for it's own reverseRelation
438        },
439        this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
440      ) );
441    }
442
443    _.bindAll( this, '_modelRemovedFromCollection', '_relatedModelAdded', '_relatedModelRemoved' );
444
445    if ( instance ) {
446      this.initialize();
447
448      if ( options.autoFetch ) {
449        this.instance.fetchRelated( options.key, _.isObject( options.autoFetch ) ? options.autoFetch : {} );
450      }
451
452      // When a model in the store is destroyed, check if it is 'this.instance'.
453      Backbone.Relational.store.getCollection( this.instance )
454        .bind( 'relational:remove', this._modelRemovedFromCollection );
455
456      // When 'relatedModel' are created or destroyed, check if it affects this relation.
457      Backbone.Relational.store.getCollection( this.relatedModel )
458        .bind( 'relational:add', this._relatedModelAdded )
459        .bind( 'relational:remove', this._relatedModelRemoved );
460    }
461  };
462  // Fix inheritance :\
463  Backbone.Relation.extend = Backbone.Model.extend;
464  // Set up all inheritable **Backbone.Relation** properties and methods.
465  _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, {
466    options: {
467      createModels: true,
468      includeInJSON: true,
469      isAutoRelation: false,
470      autoFetch: false
471    },
472   
473    instance: null,
474    key: null,
475    keyContents: null,
476    relatedModel: null,
477    reverseRelation: null,
478    related: null,
479   
480    _relatedModelAdded: function( model, coll, options ) {
481      // Allow 'model' to set up it's relations, before calling 'tryAddRelated'
482      // (which can result in a call to 'addRelated' on a relation of 'model')
483      var dit = this;
484      model.queue( function() {
485        dit.tryAddRelated( model, options );
486      });
487    },
488   
489    _relatedModelRemoved: function( model, coll, options ) {
490      this.removeRelated( model, options );
491    },
492   
493    _modelRemovedFromCollection: function( model ) {
494      if ( model === this.instance ) {
495        this.destroy();
496      }
497    },
498   
499    /**
500     * Check several pre-conditions.
501     * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
502     */
503    checkPreconditions: function() {
504      var i = this.instance,
505        k = this.key,
506        m = this.model,
507        rm = this.relatedModel,
508        warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
509
510      if ( !m || !k || !rm ) {
511        warn && console.warn( 'Relation=%o; no model, key or relatedModel (%o, %o, %o)', this, m, k, rm );
512        return false;
513      }
514      // Check if the type in 'model' inherits from Backbone.RelationalModel
515      if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
516        warn && console.warn( 'Relation=%o; model does not inherit from Backbone.RelationalModel (%o)', this, i );
517        return false;
518      }
519      // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
520      if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
521        warn && console.warn( 'Relation=%o; relatedModel does not inherit from Backbone.RelationalModel (%o)', this, rm );
522        return false;
523      }
524      // Check if this is not a HasMany, and the reverse relation is HasMany as well
525      if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
526        warn && console.warn( 'Relation=%o; relation is a HasMany, and the reverseRelation is HasMany as well.', this );
527        return false;
528      }
529
530      // Check if we're not attempting to create a duplicate relationship
531      if ( i && i._relations.length ) {
532        var exists = _.any( i._relations || [], function( rel ) {
533            var hasReverseRelation = this.reverseRelation.key && rel.reverseRelation.key;
534            return rel.relatedModel === rm && rel.key === k &&
535              ( !hasReverseRelation || this.reverseRelation.key === rel.reverseRelation.key );
536          }, this );
537
538        if ( exists ) {
539          warn && console.warn( 'Relation=%o between instance=%o.%s and relatedModel=%o.%s already exists',
540            this, i, k, rm, this.reverseRelation.key );
541          return false;
542        }
543      }
544
545      return true;
546    },
547
548    /**
549     * Set the related model(s) for this relation
550     * @param {Backbone.Model|Backbone.Collection} related
551     * @param {Object} [options]
552     */
553    setRelated: function( related, options ) {
554      this.related = related;
555
556      this.instance.acquire();
557      this.instance.attributes[ this.key ] = related;
558      this.instance.release();
559    },
560   
561    /**
562     * Determine if a relation (on a different RelationalModel) is the reverse
563     * relation of the current one.
564     * @param {Backbone.Relation} relation
565     * @return {Boolean}
566     */
567    _isReverseRelation: function( relation ) {
568      if ( relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
569          this.key === relation.reverseRelation.key ) {
570        return true;
571      }
572      return false;
573    },
574   
575    /**
576     * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
577     * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
578     *    If not specified, 'this.related' is used.
579     * @return {Backbone.Relation[]}
580     */
581    getReverseRelations: function( model ) {
582      var reverseRelations = [];
583      // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
584      var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
585      _.each( models || [], function( related ) {
586          _.each( related.getRelations() || [], function( relation ) {
587              if ( this._isReverseRelation( relation ) ) {
588                reverseRelations.push( relation );
589              }
590            }, this );
591        }, this );
592     
593      return reverseRelations;
594    },
595   
596    /**
597     * Rename options.silent to options.silentChange, so events propagate properly.
598     * (for example in HasMany, from 'addRelated'->'handleAddition')
599     * @param {Object} [options]
600     * @return {Object}
601     */
602    sanitizeOptions: function( options ) {
603      options = options ? _.clone( options ) : {};
604      if ( options.silent ) {
605        options.silentChange = true;
606        delete options.silent;
607      }
608      return options;
609    },
610
611    /**
612     * Rename options.silentChange to options.silent, so events are silenced as intended in Backbone's
613     * original functions.
614     * @param {Object} [options]
615     * @return {Object}
616     */
617    unsanitizeOptions: function( options ) {
618      options = options ? _.clone( options ) : {};
619      if ( options.silentChange ) {
620        options.silent = true;
621        delete options.silentChange;
622      }
623      return options;
624    },
625   
626    // Cleanup. Get reverse relation, call removeRelated on each.
627    destroy: function() {
628      Backbone.Relational.store.getCollection( this.instance )
629        .unbind( 'relational:remove', this._modelRemovedFromCollection );
630     
631      Backbone.Relational.store.getCollection( this.relatedModel )
632        .unbind( 'relational:add', this._relatedModelAdded )
633        .unbind( 'relational:remove', this._relatedModelRemoved );
634     
635      _.each( this.getReverseRelations() || [], function( relation ) {
636          relation.removeRelated( this.instance );
637        }, this );
638    }
639  });
640 
641  Backbone.HasOne = Backbone.Relation.extend({
642    options: {
643      reverseRelation: { type: 'HasMany' }
644    },
645   
646    initialize: function() {
647      _.bindAll( this, 'onChange' );
648
649      this.instance.bind( 'relational:change:' + this.key, this.onChange );
650
651      var model = this.findRelated( { silent: true } );
652      this.setRelated( model );
653
654      // Notify new 'related' object of the new relation.
655      _.each( this.getReverseRelations() || [], function( relation ) {
656          relation.addRelated( this.instance );
657        }, this );
658    },
659   
660    findRelated: function( options ) {
661      var item = this.keyContents;
662      var model = null;
663     
664      if ( item instanceof this.relatedModel ) {
665        model = item;
666      }
667      else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
668        model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
669      }
670     
671      return model;
672    },
673   
674    /**
675     * If the key is changed, notify old & new reverse relations and initialize the new relation
676     */
677    onChange: function( model, attr, options ) {
678      // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
679      if ( this.isLocked() ) {
680        return;
681      }
682      this.acquire();
683      options = this.sanitizeOptions( options );
684     
685      // 'options._related' is set by 'addRelated'/'removeRelated'. If it is set, the change
686      // is the result of a call from a relation. If it's not, the change is the result of
687      // a 'set' call on this.instance.
688      var changed = _.isUndefined( options._related );
689      var oldRelated = changed ? this.related : options._related;
690     
691      if ( changed ) { 
692        this.keyContents = attr;
693       
694        // Set new 'related'
695        if ( attr instanceof this.relatedModel ) {
696          this.related = attr;
697        }
698        else if ( attr ) {
699          var related = this.findRelated( options );
700          this.setRelated( related );
701        }
702        else {
703          this.setRelated( null );
704        }
705      }
706     
707      // Notify old 'related' object of the terminated relation
708      if ( oldRelated && this.related !== oldRelated ) {
709        _.each( this.getReverseRelations( oldRelated ) || [], function( relation ) {
710            relation.removeRelated( this.instance, options );
711          }, this );
712      }
713     
714      // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
715      // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
716      // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
717      _.each( this.getReverseRelations() || [], function( relation ) {
718          relation.addRelated( this.instance, options );
719        }, this);
720     
721      // Fire the 'update:<key>' event if 'related' was updated
722      if ( !options.silentChange && this.related !== oldRelated ) {
723        var dit = this;
724        Backbone.Relational.eventQueue.add( function() {
725          dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
726        });
727      }
728      this.release();
729    },
730   
731    /**
732     * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
733     */
734    tryAddRelated: function( model, options ) {
735      if ( this.related ) {
736        return;
737      }
738      options = this.sanitizeOptions( options );
739     
740      var item = this.keyContents;
741      if ( item || item === 0 ) { // since 0 can be a valid `id` as well
742        var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
743        if ( !_.isNull( id ) && model.id === id ) {
744          this.addRelated( model, options );
745        }
746      }
747    },
748   
749    addRelated: function( model, options ) {
750      if ( model !== this.related ) {
751        var oldRelated = this.related || null;
752        this.setRelated( model );
753        this.onChange( this.instance, model, { _related: oldRelated } );
754      }
755    },
756   
757    removeRelated: function( model, options ) {
758      if ( !this.related ) {
759        return;
760      }
761     
762      if ( model === this.related ) {
763        var oldRelated = this.related || null;
764        this.setRelated( null );
765        this.onChange( this.instance, model, { _related: oldRelated } );
766      }
767    }
768  });
769 
770  Backbone.HasMany = Backbone.Relation.extend({
771    collectionType: null,
772   
773    options: {
774      reverseRelation: { type: 'HasOne' },
775      collectionType: Backbone.Collection,
776      collectionKey: true,
777      collectionOptions: {}
778    },
779   
780    initialize: function() {
781      _.bindAll( this, 'onChange', 'handleAddition', 'handleRemoval', 'handleReset' );
782      this.instance.bind( 'relational:change:' + this.key, this.onChange );
783     
784      // Handle a custom 'collectionType'
785      this.collectionType = this.options.collectionType;
786      if ( _.isString( this.collectionType ) ) {
787        this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
788      }
789      if ( !this.collectionType.prototype instanceof Backbone.Collection ){
790        throw new Error( 'collectionType must inherit from Backbone.Collection' );
791      }
792
793      // Handle cases where a model/relation is created with a collection passed straight into 'attributes'
794      if ( this.keyContents instanceof Backbone.Collection ) {
795        this.setRelated( this._prepareCollection( this.keyContents ) );
796      }
797      else {
798        this.setRelated( this._prepareCollection() );
799      }
800
801      this.findRelated( { silent: true } );
802    },
803   
804    _getCollectionOptions: function() {
805      return _.isFunction( this.options.collectionOptions ) ?
806        this.options.collectionOptions( this.instance ) :
807        this.options.collectionOptions;
808    },
809
810    /**
811     * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
812     * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
813     * @param {Backbone.Collection} [collection]
814     */
815    _prepareCollection: function( collection ) {
816      if ( this.related ) {
817        this.related
818          .unbind( 'relational:add', this.handleAddition )
819          .unbind( 'relational:remove', this.handleRemoval )
820          .unbind( 'relational:reset', this.handleReset )
821      }
822
823      if ( !collection || !( collection instanceof Backbone.Collection ) ) {
824        collection = new this.collectionType( [], this._getCollectionOptions() );
825      }
826
827      collection.model = this.relatedModel;
828     
829      if ( this.options.collectionKey ) {
830        var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
831       
832        if ( collection[ key ] && collection[ key ] !== this.instance ) {
833          if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
834            console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
835          }
836        }
837        else if ( key ) {
838          collection[ key ] = this.instance;
839        }
840      }
841     
842      collection
843        .bind( 'relational:add', this.handleAddition )
844        .bind( 'relational:remove', this.handleRemoval )
845        .bind( 'relational:reset', this.handleReset );
846     
847      return collection;
848    },
849   
850    findRelated: function( options ) {
851      if ( this.keyContents ) {
852        var models = [];
853
854        if ( this.keyContents instanceof Backbone.Collection ) {
855          models = this.keyContents.models;
856        }
857        else {
858          // Handle cases the an API/user supplies just an Object/id instead of an Array
859          this.keyContents = _.isArray( this.keyContents ) ? this.keyContents : [ this.keyContents ];
860
861          // Try to find instances of the appropriate 'relatedModel' in the store
862          _.each( this.keyContents || [], function( item ) {
863              var model = null;
864              if ( item instanceof this.relatedModel ) {
865                model = item;
866              }
867              else if ( item || item === 0 ) { // since 0 can be a valid `id` as well
868                model = this.relatedModel.findOrCreate( item, { create: this.options.createModels } );
869              }
870
871              if ( model && !this.related.get( model ) ) {
872                models.push( model );
873              }
874            }, this );
875        }
876
877        // Add all found 'models' in on go, so 'add' will only be called once (and thus 'sort', etc.)
878        if ( models.length ) {
879          options = this.unsanitizeOptions( options );
880          this.related.add( models, options );
881        }
882      }
883    },
884   
885    /**
886     * If the key is changed, notify old & new reverse relations and initialize the new relation
887     */
888    onChange: function( model, attr, options ) {
889      options = this.sanitizeOptions( options );
890      this.keyContents = attr;
891     
892      // Replace 'this.related' by 'attr' if it is a Backbone.Collection
893      if ( attr instanceof Backbone.Collection ) {
894        this._prepareCollection( attr );
895        this.related = attr;
896      }
897      // Otherwise, 'attr' should be an array of related object ids.
898      // Re-use the current 'this.related' if it is a Backbone.Collection, and remove any current entries.
899      // Otherwise, create a new collection.
900      else {
901        var oldIds = {}, newIds = {};
902
903        if (!_.isArray( attr ) && attr !== undefined) {
904          attr = [ attr ];
905        }
906
907        _.each( attr, function( attributes ) {
908          newIds[ attributes.id ] = true;
909        });
910
911        var coll = this.related;
912        if ( coll instanceof Backbone.Collection ) {
913          // Make sure to operate on a copy since we're removing while iterating
914          _.each( coll.models.slice(0) , function( model ) {
915            // When fetch is called with the 'keepNewModels' option, we don't want to remove
916            // client-created new models when the fetch is completed.
917            if ( !options.keepNewModels || !model.isNew() ) {
918              oldIds[ model.id ] = true;
919              coll.remove( model, { silent: (model.id in newIds) } );
920            }
921          });
922        } else {
923          coll = this._prepareCollection();
924        }
925
926        _.each( attr, function( attributes ) {
927          var model = this.relatedModel.findOrCreate( attributes, { create: this.options.createModels } );
928          if (model) {
929            coll.add( model, { silent: (attributes.id in oldIds)} );
930          }
931        }, this);
932
933        this.setRelated( coll );
934
935      }
936     
937      var dit = this;
938      Backbone.Relational.eventQueue.add( function() {
939        !options.silentChange && dit.instance.trigger( 'update:' + dit.key, dit.instance, dit.related, options );
940      });
941    },
942   
943    tryAddRelated: function( model, options ) {
944      options = this.sanitizeOptions( options );
945      if ( !this.related.get( model ) ) {
946        // Check if this new model was specified in 'this.keyContents'
947        var item = _.any( this.keyContents || [], function( item ) {
948            var id = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
949            return !_.isNull( id ) && id === model.id;
950          }, this );
951       
952        if ( item ) {
953          this.related.add( model, options );
954        }
955      }
956    },
957   
958    /**
959     * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
960     * (should be 'HasOne', must set 'this.instance' as their related).
961     */
962    handleAddition: function( model, coll, options ) {
963      //console.debug('handleAddition called; args=%o', arguments);
964      // Make sure the model is in fact a valid model before continuing.
965      // (it can be invalid as a result of failing validation in Backbone.Collection._prepareModel)
966      if ( !( model instanceof Backbone.Model ) ) {
967        return;
968      }
969     
970      options = this.sanitizeOptions( options );
971     
972      _.each( this.getReverseRelations( model ) || [], function( relation ) {
973          relation.addRelated( this.instance, options );
974        }, this );
975
976      // Only trigger 'add' once the newly added model is initialized (so, has it's relations set up)
977      var dit = this;
978      Backbone.Relational.eventQueue.add( function() {
979        !options.silentChange && dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
980      });
981    },
982   
983    /**
984     * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
985     * (should be 'HasOne', which should be nullified)
986     */
987    handleRemoval: function( model, coll, options ) {
988      //console.debug('handleRemoval called; args=%o', arguments);
989      if ( !( model instanceof Backbone.Model ) ) {
990        return;
991      }
992
993      options = this.sanitizeOptions( options );
994     
995      _.each( this.getReverseRelations( model ) || [], function( relation ) {
996          relation.removeRelated( this.instance, options );
997        }, this );
998     
999      var dit = this;
1000      Backbone.Relational.eventQueue.add( function() {
1001        !options.silentChange && dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
1002      });
1003    },
1004
1005    handleReset: function( coll, options ) {
1006      options = this.sanitizeOptions( options );
1007
1008      var dit = this;
1009      Backbone.Relational.eventQueue.add( function() {
1010        !options.silentChange && dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
1011      });
1012    },
1013   
1014    addRelated: function( model, options ) {
1015      var dit = this;
1016      options = this.unsanitizeOptions( options );
1017      model.queue( function() { // Queued to avoid errors for adding 'model' to the 'this.related' set twice
1018        if ( dit.related && !dit.related.get( model ) ) {
1019          dit.related.add( model, options );
1020        }
1021      });
1022    },
1023   
1024    removeRelated: function( model, options ) {
1025      options = this.unsanitizeOptions( options );
1026      if ( this.related.get( model ) ) {
1027        this.related.remove( model, options );
1028      }
1029    }
1030  });
1031 
1032  /**
1033   * A type of Backbone.Model that also maintains relations to other models and collections.
1034   * New events when compared to the original:
1035   *  - 'add:<key>' (model, related collection, options)
1036   *  - 'remove:<key>' (model, related collection, options)
1037   *  - 'update:<key>' (model, related model or collection, options)
1038   */
1039  Backbone.RelationalModel = Backbone.Model.extend({
1040    relations: null, // Relation descriptions on the prototype
1041    _relations: null, // Relation instances
1042    _isInitialized: false,
1043    _deferProcessing: false,
1044    _queue: null,
1045   
1046    subModelTypeAttribute: 'type',
1047    subModelTypes: null,
1048   
1049    constructor: function( attributes, options ) {
1050      // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
1051      // Defer 'processQueue', so that when 'Relation.createModels' is used we:
1052      // a) Survive 'Backbone.Collection.add'; this takes care we won't error on "can't add model to a set twice"
1053      //    (by creating a model from properties, having the model add itself to the collection via one of
1054      //    it's relations, then trying to add it to the collection).
1055      // b) Trigger 'HasMany' collection events only after the model is really fully set up.
1056      // Example that triggers both a and b: "p.get('jobs').add( { company: c, person: p } )".
1057      var dit = this;
1058      if ( options && options.collection ) {
1059        this._deferProcessing = true;
1060       
1061        var processQueue = function( model ) {
1062          if ( model === dit ) {
1063            dit._deferProcessing = false;
1064            dit.processQueue();
1065            options.collection.unbind( 'relational:add', processQueue );
1066          }
1067        };
1068        options.collection.bind( 'relational:add', processQueue );
1069       
1070        // So we do process the queue eventually, regardless of whether this model really gets added to 'options.collection'.
1071        _.defer( function() {
1072          processQueue( dit );
1073        });
1074      }
1075     
1076      this._queue = new Backbone.BlockingQueue();
1077      this._queue.block();
1078      Backbone.Relational.eventQueue.block();
1079     
1080      Backbone.Model.apply( this, arguments );
1081     
1082      // Try to run the global queue holding external events
1083      Backbone.Relational.eventQueue.unblock();
1084    },
1085   
1086    /**
1087     * Override 'trigger' to queue 'change' and 'change:*' events
1088     */
1089    trigger: function( eventName ) {
1090      if ( eventName.length > 5 && 'change' === eventName.substr( 0, 6 ) ) {
1091        var dit = this, args = arguments;
1092        Backbone.Relational.eventQueue.add( function() {
1093            Backbone.Model.prototype.trigger.apply( dit, args );
1094          });
1095      }
1096      else {
1097        Backbone.Model.prototype.trigger.apply( this, arguments );
1098      }
1099     
1100      return this;
1101    },
1102   
1103    /**
1104     * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
1105     * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
1106     */
1107    initializeRelations: function() {
1108      this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
1109      this._relations = [];
1110     
1111      _.each( this.relations || [], function( rel ) {
1112          var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1113          if ( type && type.prototype instanceof Backbone.Relation ) {
1114            new type( this, rel ); // Also pushes the new Relation into _relations
1115          }
1116          else {
1117            Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid type!', rel );
1118          }
1119        }, this );
1120     
1121      this._isInitialized = true;
1122      this.release();
1123      this.processQueue();
1124    },
1125
1126    /**
1127     * When new values are set, notify this model's relations (also if options.silent is set).
1128     * (Relation.setRelated locks this model before calling 'set' on it to prevent loops)
1129     */
1130    updateRelations: function( options ) {
1131      if ( this._isInitialized && !this.isLocked() ) {
1132        _.each( this._relations || [], function( rel ) {
1133          // Update from data in `rel.keySource` if set, or `rel.key` otherwise
1134          var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
1135          if ( rel.related !== val ) {
1136            this.trigger( 'relational:change:' + rel.key, this, val, options || {} );
1137          }
1138        }, this );
1139      }
1140    },
1141   
1142    /**
1143     * Either add to the queue (if we're not initialized yet), or execute right away.
1144     */
1145    queue: function( func ) {
1146      this._queue.add( func );
1147    },
1148   
1149    /**
1150     * Process _queue
1151     */
1152    processQueue: function() {
1153      if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) {
1154        this._queue.unblock();
1155      }
1156    },
1157   
1158    /**
1159     * Get a specific relation.
1160     * @param key {string} The relation key to look for.
1161     * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
1162     */
1163    getRelation: function( key ) {
1164      return _.detect( this._relations, function( rel ) {
1165        if ( rel.key === key ) {
1166          return true;
1167        }
1168      }, this );
1169    },
1170   
1171    /**
1172     * Get all of the created relations.
1173     * @return {Backbone.Relation[]}
1174     */
1175    getRelations: function() {
1176      return this._relations;
1177    },
1178   
1179    /**
1180     * Retrieve related objects.
1181     * @param key {string} The relation key to fetch models for.
1182     * @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1183     * @param [update=false] {boolean} Whether to force a fetch from the server (updating existing models).
1184     * @return {jQuery.when[]} An array of request objects
1185     */
1186    fetchRelated: function( key, options, update ) {
1187      options || ( options = {} );
1188      var setUrl,
1189        requests = [],
1190        rel = this.getRelation( key ),
1191        keyContents = rel && rel.keyContents,
1192        toFetch = keyContents && _.select( _.isArray( keyContents ) ? keyContents : [ keyContents ], function( item ) {
1193          var id = Backbone.Relational.store.resolveIdForItem( rel.relatedModel, item );
1194          return !_.isNull( id ) && ( update || !Backbone.Relational.store.find( rel.relatedModel, id ) );
1195        }, this );
1196     
1197      if ( toFetch && toFetch.length ) {
1198        // Create a model for each entry in 'keyContents' that is to be fetched
1199        var models = _.map( toFetch, function( item ) {
1200          var model;
1201
1202          if ( _.isObject( item ) ) {
1203            model = rel.relatedModel.findOrCreate( item );
1204          }
1205          else {
1206            var attrs = {};
1207            attrs[ rel.relatedModel.prototype.idAttribute ] = item;
1208            model = rel.relatedModel.findOrCreate( attrs );
1209          }
1210
1211          return model;
1212        }, this );
1213       
1214        // Try if the 'collection' can provide a url to fetch a set of models in one request.
1215        if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
1216          setUrl = rel.related.url( models );
1217        }
1218       
1219        // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
1220        // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
1221        // the one supplied for the default fetch action (without args to 'url').
1222        if ( setUrl && setUrl !== rel.related.url() ) {
1223          var opts = _.defaults(
1224            {
1225              error: function() {
1226                var args = arguments;
1227                _.each( models || [], function( model ) {
1228                    model.trigger( 'destroy', model, model.collection, options );
1229                    options.error && options.error.apply( model, args );
1230                  });
1231              },
1232              url: setUrl
1233            },
1234            options,
1235            { add: true }
1236          );
1237
1238          requests = [ rel.related.fetch( opts ) ];
1239        }
1240        else {
1241          requests = _.map( models || [], function( model ) {
1242            var opts = _.defaults(
1243              {
1244                error: function() {
1245                  model.trigger( 'destroy', model, model.collection, options );
1246                  options.error && options.error.apply( model, arguments );
1247                }
1248              },
1249              options
1250            );
1251            return model.fetch( opts );
1252          }, this );
1253        }
1254      }
1255     
1256      return requests;
1257    },
1258   
1259    set: function( key, value, options ) {
1260      Backbone.Relational.eventQueue.block();
1261     
1262      // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
1263      var attributes;
1264      if ( _.isObject( key ) || key == null ) {
1265        attributes = key;
1266        options = value;
1267      }
1268      else {
1269        attributes = {};
1270        attributes[ key ] = value;
1271      }
1272     
1273      var result = Backbone.Model.prototype.set.apply( this, arguments );
1274     
1275      // Ideal place to set up relations :)
1276      if ( !this._isInitialized && !this.isLocked() ) {
1277        this.constructor.initializeModelHierarchy();
1278
1279        Backbone.Relational.store.register( this );
1280
1281        this.initializeRelations();
1282      }
1283      // Update the 'idAttribute' in Backbone.store if; we don't want it to miss an 'id' update due to {silent:true}
1284      else if ( attributes && this.idAttribute in attributes ) {
1285        Backbone.Relational.store.update( this );
1286      }
1287     
1288      if ( attributes ) {
1289        this.updateRelations( options );
1290      }
1291     
1292      // Try to run the global queue holding external events
1293      Backbone.Relational.eventQueue.unblock();
1294     
1295      return result;
1296    },
1297   
1298    unset: function( attribute, options ) {
1299      Backbone.Relational.eventQueue.block();
1300     
1301      var result = Backbone.Model.prototype.unset.apply( this, arguments );
1302      this.updateRelations( options );
1303     
1304      // Try to run the global queue holding external events
1305      Backbone.Relational.eventQueue.unblock();
1306     
1307      return result;
1308    },
1309   
1310    clear: function( options ) {
1311      Backbone.Relational.eventQueue.block();
1312     
1313      var result = Backbone.Model.prototype.clear.apply( this, arguments );
1314      this.updateRelations( options );
1315     
1316      // Try to run the global queue holding external events
1317      Backbone.Relational.eventQueue.unblock();
1318     
1319      return result;
1320    },
1321
1322    clone: function() {
1323      var attributes = _.clone( this.attributes );
1324      if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
1325        attributes[ this.idAttribute ] = null;
1326      }
1327
1328      _.each( this.getRelations() || [], function( rel ) {
1329          delete attributes[ rel.key ];
1330        });
1331
1332      return new this.constructor( attributes );
1333    },
1334   
1335    /**
1336     * Convert relations to JSON, omits them when required
1337     */
1338    toJSON: function(options) {
1339      // If this Model has already been fully serialized in this branch once, return to avoid loops
1340      if ( this.isLocked() ) {
1341        return this.id;
1342      }
1343     
1344      this.acquire();
1345      var json = Backbone.Model.prototype.toJSON.call( this, options );
1346     
1347      if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
1348        json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
1349      }
1350     
1351      _.each( this._relations || [], function( rel ) {
1352          var value = json[ rel.key ];
1353
1354          if ( rel.options.includeInJSON === true) {
1355            if ( value && _.isFunction( value.toJSON ) ) {
1356              json[ rel.keyDestination ] = value.toJSON( options );
1357            }
1358            else {
1359              json[ rel.keyDestination ] = null;
1360            }
1361          }
1362          else if ( _.isString( rel.options.includeInJSON ) ) {
1363            if ( value instanceof Backbone.Collection ) {
1364              json[ rel.keyDestination ] = value.pluck( rel.options.includeInJSON );
1365            }
1366            else if ( value instanceof Backbone.Model ) {
1367              json[ rel.keyDestination ] = value.get( rel.options.includeInJSON );
1368            }
1369            else {
1370              json[ rel.keyDestination ] = null;
1371            }
1372          }
1373          else if ( _.isArray( rel.options.includeInJSON ) ) {
1374            if ( value instanceof Backbone.Collection ) {
1375              var valueSub = [];
1376              value.each( function( model ) {
1377                var curJson = {};
1378                _.each( rel.options.includeInJSON, function( key ) {
1379                  curJson[ key ] = model.get( key );
1380                });
1381                valueSub.push( curJson );
1382              });
1383              json[ rel.keyDestination ] = valueSub;
1384            }
1385            else if ( value instanceof Backbone.Model ) {
1386              var valueSub = {};
1387              _.each( rel.options.includeInJSON, function( key ) {
1388                valueSub[ key ] = value.get( key );
1389              });
1390              json[ rel.keyDestination ] = valueSub;
1391            }
1392            else {
1393              json[ rel.keyDestination ] = null;
1394            }
1395          }
1396          else {
1397            delete json[ rel.key ];
1398          }
1399
1400          if ( rel.keyDestination !== rel.key ) {
1401            delete json[ rel.key ];
1402          }
1403        });
1404     
1405      this.release();
1406      return json;
1407    }
1408  },
1409  {
1410    setup: function( superModel ) {
1411      // We don't want to share a relations array with a parent, as this will cause problems with
1412      // reverse relations.
1413      this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
1414
1415      this._subModels = {};
1416      this._superModel = null;
1417
1418      // If this model has 'subModelTypes' itself, remember them in the store
1419      if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
1420        Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
1421      }
1422      // The 'subModelTypes' property should not be inherited, so reset it.
1423      else {
1424        this.prototype.subModelTypes = null;
1425      }
1426
1427      // Initialize all reverseRelations that belong to this new model.
1428      _.each( this.prototype.relations || [], function( rel ) {
1429          if ( !rel.model ) {
1430            rel.model = this;
1431          }
1432
1433          if ( rel.reverseRelation && rel.model === this ) {       
1434            var preInitialize = true;
1435            if ( _.isString( rel.relatedModel ) ) {
1436              /**
1437               * The related model might not be defined for two reasons
1438               *  1. it never gets defined, e.g. a typo
1439               *  2. it is related to itself
1440               * In neither of these cases do we need to pre-initialize reverse relations.
1441               */
1442              var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1443              preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1444            }
1445
1446            var type = !_.isString( rel.type ) ? rel.type : Backbone[ rel.type ] || Backbone.Relational.store.getObjectByName( rel.type );
1447            if ( preInitialize && type && type.prototype instanceof Backbone.Relation ) {
1448              new type( null, rel );
1449            }
1450          }
1451        }, this );
1452     
1453      return this;
1454    },
1455
1456    /**
1457     * Create a 'Backbone.Model' instance based on 'attributes'.
1458     * @param {Object} attributes
1459     * @param {Object} [options]
1460     * @return {Backbone.Model}
1461     */
1462    build: function( attributes, options ) {
1463      var model = this;
1464
1465      // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
1466      this.initializeModelHierarchy();
1467
1468      // Determine what type of (sub)model should be built if applicable.
1469      // Lookup the proper subModelType in 'this._subModels'.
1470      if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
1471        var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
1472        var subModelType = this._subModels[ subModelTypeAttribute ];
1473        if ( subModelType ) {
1474          model = subModelType;
1475        }
1476      }
1477     
1478      return new model( attributes, options );
1479    },
1480
1481    initializeModelHierarchy: function() {
1482      // If we're here for the first time, try to determine if this modelType has a 'superModel'.
1483      if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
1484        Backbone.Relational.store.setupSuperModel( this );
1485
1486        // If a superModel has been found, copy relations from the _superModel if they haven't been
1487        // inherited automatically (due to a redefinition of 'relations').
1488        // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
1489        // the isUndefined/isNull check next time.
1490        if ( this._superModel ) {
1491          //
1492          if ( this._superModel.prototype.relations ) {
1493            var supermodelRelationsExist = _.any( this.prototype.relations || [], function( rel ) {
1494              return rel.model && rel.model !== this;
1495            }, this );
1496
1497            if ( !supermodelRelationsExist ) {
1498              this.prototype.relations = this._superModel.prototype.relations.concat( this.prototype.relations );
1499            }
1500          }
1501        }
1502        else {
1503          this._superModel = false;
1504        }
1505      }
1506
1507      // 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.
1508      if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
1509        _.each( this.prototype.subModelTypes || [], function( subModelTypeName ) {
1510          var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
1511          subModelType && subModelType.initializeModelHierarchy();
1512        });
1513      }
1514    },
1515
1516    /**
1517     * Find an instance of `this` type in 'Backbone.Relational.store'.
1518     * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
1519     * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1520     *   Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
1521     * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1522     * @param {Object} [options]
1523     * @param {Boolean} [options.create=true]
1524     * @param {Boolean} [options.update=true]
1525     * @return {Backbone.RelationalModel}
1526     */
1527    findOrCreate: function( attributes, options ) {
1528      options || ( options = {} );
1529      var parsedAttributes = (_.isObject( attributes ) && this.prototype.parse) ? this.prototype.parse( attributes ) : attributes;
1530      // Try to find an instance of 'this' model type in the store
1531      var model = Backbone.Relational.store.find( this, parsedAttributes );
1532
1533      // If we found an instance, update it with the data in 'item' (unless 'options.update' is false).
1534      // If not, create an instance (unless 'options.create' is false).
1535      if ( _.isObject( attributes ) ) {
1536        if ( model && options.update !== false ) {
1537          model.set( parsedAttributes, options );
1538        }
1539        else if ( !model && options.create !== false ) {
1540          model = this.build( attributes, options );
1541        }
1542      }
1543
1544      return model;
1545    }
1546  });
1547  _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1548 
1549  /**
1550   * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
1551   * if the collection.model has subModels.
1552   */
1553  Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
1554  Backbone.Collection.prototype._prepareModel = function ( attrs, options ) {
1555    var model;
1556   
1557    if ( attrs instanceof Backbone.Model ) {
1558      if ( !attrs.collection ) {
1559        attrs.collection = this;
1560      }
1561      model = attrs;
1562    }
1563    else {
1564      options || (options = {});
1565      options.collection = this;
1566     
1567      if ( typeof this.model.findOrCreate !== 'undefined' ) {
1568        model = this.model.findOrCreate( attrs, options );
1569      }
1570      else {
1571        model = new this.model( attrs, options );
1572      }
1573     
1574      if ( !model._validate( attrs, options ) ) {
1575        model = false;
1576      }
1577    }
1578   
1579    return model;
1580  };
1581
1582 
1583  /**
1584   * Override Backbone.Collection.add, so objects fetched from the server multiple times will
1585   * update the existing Model. Also, trigger 'relational:add'.
1586   */
1587  var add = Backbone.Collection.prototype.__add = Backbone.Collection.prototype.add;
1588  Backbone.Collection.prototype.add = function( models, options ) {
1589    options || (options = {});
1590    if ( !_.isArray( models ) ) {
1591      models = [ models ];
1592    }
1593
1594    var modelsToAdd = [];
1595
1596    //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1597    _.each( models || [], function( model ) {
1598      if ( !( model instanceof Backbone.Model ) ) {
1599        // `_prepareModel` attempts to find `model` in Backbone.store through `findOrCreate`,
1600        // and sets the new properties on it if is found. Otherwise, a new model is instantiated.
1601        model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1602      }
1603
1604        if ( model instanceof Backbone.Model && !this.get( model ) ) {
1605          modelsToAdd.push( model );
1606        }
1607      }, this );
1608
1609    // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
1610    if ( modelsToAdd.length ) {
1611      add.call( this, modelsToAdd, options );
1612
1613      _.each( modelsToAdd || [], function( model ) {
1614        this.trigger( 'relational:add', model, this, options );
1615      }, this );
1616    }
1617   
1618    return this;
1619  };
1620 
1621  /**
1622   * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1623   */
1624  var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
1625  Backbone.Collection.prototype.remove = function( models, options ) {
1626    options || (options = {});
1627    if ( !_.isArray( models ) ) {
1628      models = [ models ];
1629    }
1630    else {
1631      models = models.slice( 0 );
1632    }
1633
1634    //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1635    _.each( models || [], function( model ) {
1636        model = this.get( model );
1637
1638        if ( model instanceof Backbone.Model ) {
1639          remove.call( this, model, options );
1640          this.trigger('relational:remove', model, this, options);
1641        }
1642      }, this );
1643   
1644    return this;
1645  };
1646
1647  /**
1648   * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
1649   */
1650  var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
1651  Backbone.Collection.prototype.reset = function( models, options ) {
1652    reset.call( this, models, options );
1653    this.trigger( 'relational:reset', this, options );
1654
1655    return this;
1656  };
1657
1658  /**
1659   * Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
1660   */
1661  var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
1662  Backbone.Collection.prototype.sort = function( options ) {
1663    sort.call( this, options );
1664    this.trigger( 'relational:reset', this, options );
1665
1666    return this;
1667  };
1668 
1669  /**
1670   * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
1671   * are ready.
1672   */
1673  var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
1674  Backbone.Collection.prototype.trigger = function( eventName ) {
1675    if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
1676      var dit = this, args = arguments;
1677     
1678      if (eventName === 'add') {
1679        args = _.toArray( args );
1680        // the fourth argument in case of a regular add is the option object.
1681        // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
1682        if (_.isObject( args[3] ) ) {
1683          args[3] = _.clone( args[3] );
1684        }
1685      }
1686     
1687      Backbone.Relational.eventQueue.add( function() {
1688          trigger.apply( dit, args );
1689        });
1690    }
1691    else {
1692      trigger.apply( this, arguments );
1693    }
1694   
1695    return this;
1696  };
1697
1698  // Override .extend() to automatically call .setup()
1699  Backbone.RelationalModel.extend = function( protoProps, classProps ) {
1700    var child = Backbone.Model.extend.apply( this, arguments );
1701   
1702    child.setup( this );
1703
1704    return child;
1705  };
1706})();
Note: See TracBrowser for help on using the repository browser.