chiropractor 1.0.0

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