add SiteDeployment to xoslib
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xos-backbone.js
1 if (! window.XOSLIB_LOADED ) {
2     window.XOSLIB_LOADED=true;
3
4     SLIVER_API = "/plstackapi/slivers/";
5     SLICE_API = "/plstackapi/slices/";
6     SLICEROLE_API = "/plstackapi/slice_roles/";
7     NODE_API = "/plstackapi/nodes/";
8     SITE_API = "/plstackapi/sites/";
9     SITEDEPLOYMENT_API = "/plstackapi/sitedeployments/";
10     USER_API = "/plstackapi/users/";
11     USERDEPLOYMENT_API = "/plstackapi/user_deployments/";
12     DEPLOYMENT_API = "/plstackapi/deployments/";
13     IMAGE_API = "/plstackapi/images/";
14     IMAGEDEPLOYMENTS_API = "/plstackapi/imagedeployments/";
15     NETWORKTEMPLATE_API = "/plstackapi/networktemplates/";
16     NETWORK_API = "/plstackapi/networks/";
17     NETWORKSLIVER_API = "/plstackapi/networkslivers/";
18     SERVICE_API = "/plstackapi/services/";
19     SLICEPRIVILEGE_API = "/plstackapi/slice_privileges/";
20     NETWORKDEPLOYMENT_API = "/plstackapi/networkdeployments/";
21     FLAVOR_API = "/plstackapi/flavors/";
22     CONTROLLER_API = "/plstackapi/controllers/";
23
24     /* changed as a side effect of the big rename
25     SLICEDEPLOYMENT_API = "/plstackapi/slice_deployments/";
26     USERDEPLOYMENT_API = "/plstackapi/user_deployments/";
27     */
28
29     SLICEDEPLOYMENT_API = "/plstackapi/slicedeployments/";
30     USERDEPLOYMENT_API = "/plstackapi/userdeployments/";
31
32     SLICEPLUS_API = "/xoslib/slicesplus/";
33
34     XOSModel = Backbone.Model.extend({
35         /* from backbone-tastypie.js */
36         //idAttribute: 'resource_uri',
37
38         /* from backbone-tastypie.js */
39         url: function() {
40                     var url = this.attributes.resource_uri;
41
42                     if (!url) {
43                         if (this.id) {
44                             url = this.urlRoot + this.id;
45                         } else {
46                             // this happens when creating a new model.
47                             url = this.urlRoot;
48                         }
49                     }
50
51                     if (!url) {
52                         // XXX I'm not sure this does anything useful
53                         url = ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );
54                         url = url || this.urlRoot;
55                     }
56
57                     // remove any existing query parameters
58                     url && ( url.indexOf("?") > -1 ) && ( url = url.split("?")[0] );
59
60                     url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
61
62                     url && ( url += "?no_hyperlinks=1" );
63
64                     return url;
65             },
66
67             listMethods: function() {
68                 var res = [];\r
69                 for(var m in this) {\r
70                     if(typeof this[m] == "function") {\r
71                         res.push(m)\r
72                     }\r
73                 }\r
74                 return res;\r
75             },
76
77             save: function(attributes, options) {
78                 if (this.preSave) {
79                     this.preSave();
80                 }
81                 return Backbone.Model.prototype.save.call(this, attributes, options);
82             },
83
84             getChoices: function(fieldName, excludeChosen) {
85                 choices=[];
86                 if (fieldName in this.m2mFields) {
87                     for (index in xos[this.m2mFields[fieldName]].models) {
88                         candidate = xos[this.m2mFields[fieldName]].models[index];
89                         if (excludeChosen && idInArray(candidate.id, this.attributes[fieldName])) {
90                             continue;
91                         }
92                         choices.push(candidate.id);
93                     }
94                 }
95                 return choices;
96             },
97
98             /* If a 'validate' method is supplied, then it will be called
99                automatically on save. Unfortunately, save calls neither the
100                'error' nor the 'success' callback if the validator fails.
101
102                For now, we're calling our validator 'xosValidate' so this
103                autoamtic validation doesn't occur.
104             */
105
106             xosValidate: function(attrs, options) {
107                 errors = {};
108                 foundErrors = false;
109                 _.each(this.validators, function(validatorList, fieldName) {
110                     _.each(validatorList, function(validator) {
111                         if (fieldName in attrs) {
112                             validatorResult = validateField(validator, attrs[fieldName], this)
113                             if (validatorResult != true) {
114                                 errors[fieldName] = validatorResult;
115                                 foundErrors = true;
116                             }
117                         }
118                     });
119                 });
120                 if (foundErrors) {
121                     return errors;
122                 }
123                 // backbone.js semantics -- on successful validate, return nothing
124             },
125
126             /* uncommenting this would make validate() call xosValidate()
127             validate: function(attrs, options) {
128                 r = this.xosValidate(attrs, options);
129                 console.log("validate");
130                 console.log(r);
131                 return r;
132             }, */
133     });
134
135     XOSCollection = Backbone.Collection.extend({
136         objects: function() {
137                     return this.models.map(function(element) { return element.attributes; });
138                  },
139
140         initialize: function(){
141           this.isLoaded = false;
142           this.failedLoad = false;
143           this.startedLoad = false;
144           this.sortVar = 'name';\r
145           this.sortOrder = 'asc';\r
146           this.on( "sort", this.sorted );\r
147         },\r
148 \r
149         relatedCollections: [],\r
150         foreignCollections: [],\r
151 \r
152         sorted: function() {\r
153             //console.log("sorted " + this.modelName);\r
154         },\r
155 \r
156         simpleComparator: function( model ){\r
157           parts=this.sortVar.split(".");\r
158           result = model.get(parts[0]);\r
159           for (index=1; index<parts.length; ++index) {\r
160               result=result[parts[index]];\r
161           }\r
162           return result;\r
163         },\r
164 \r
165         comparator: function (left, right) {\r
166             var l = this.simpleComparator(left);\r
167             var r = this.simpleComparator(right);\r
168 \r
169             if (l === void 0) return -1;\r
170             if (r === void 0) return 1;\r
171 \r
172             if (this.sortOrder=="desc") {\r
173                 return l < r ? 1 : l > r ? -1 : 0;\r
174             } else {\r
175                 return l < r ? -1 : l > r ? 1 : 0;\r
176             }\r
177         },\r
178 \r
179         fetchSuccess: function(collection, response, options) {\r
180             //console.log("fetch succeeded " + collection.modelName);\r
181             this.failedLoad = false;\r
182             this.fetching = false;\r
183             if (!this.isLoaded) {\r
184                 this.isLoaded = true;\r
185                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
186             }\r
187             this.trigger("fetchStateChange");\r
188             if (options["orig_success"]) {\r
189                 options["orig_success"](collection, response, options);\r
190             }\r
191         },\r
192 \r
193         fetchFailure: function(collection, response, options) {\r
194             //console.log("fetch failed " + collection.modelName);\r
195             this.fetching = false;\r
196             if ((!this.isLoaded) && (!this.failedLoad)) {\r
197                 this.failedLoad=true;\r
198                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
199             }\r
200             this.trigger("fetchStateChange");\r
201             if (options["orig_failure"]) {\r
202                 options["orig_failure"](collection, response, options);\r
203             }\r
204         },\r
205 \r
206         fetch: function(options) {\r
207             var self=this;\r
208             this.fetching=true;\r
209             //console.log("fetch " + this.modelName);\r
210             if (!this.startedLoad) {\r
211                 this.startedLoad=true;\r
212                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
213             }\r
214             this.trigger("fetchStateChange");\r
215             if (options == undefined) {\r
216                 options = {};\r
217             }\r
218             options["orig_success"] = options["success"];\r
219             options["orig_failure"] = options["failure"];\r
220             options["success"] = function(collection, response, options) { self.fetchSuccess.call(self, collection, response, options); };\r
221             options["failure"] = this.fetchFailure;\r
222             Backbone.Collection.prototype.fetch.call(this, options);\r
223         },\r
224 \r
225         startPolling: function() {\r
226             if (!this._polling) {\r
227                 var collection=this;
228                 setInterval(function() { collection.fetch(); }, 10000);
229                 this._polling=true;
230                 this.fetch();
231             }
232         },
233
234         refresh: function(refreshRelated) {
235             if (!this.fetching) {
236                 this.fetch();
237             }
238             if (refreshRelated) {
239                 for (related in this.relatedCollections) {
240                     related = xos[related];
241                     if (!related.fetching) {
242                         related.fetch();
243                     }
244                 }
245             }
246         },
247
248         maybeFetch: function(options){
249                 // Helper function to fetch only if this collection has not been fetched before.
250             if(this._fetched){
251                     // If this has already been fetched, call the success, if it exists
252                 options.success && options.success();
253                 console.log("alreadyFetched");
254                 return;
255             }
256
257                 // when the original success function completes mark this collection as fetched
258             var self = this,
259             successWrapper = function(success){
260                 return function(){
261                     self._fetched = true;
262                     success && success.apply(this, arguments);
263                 };
264             };
265             options.success = successWrapper(options.success);
266             console.log("call fetch");
267             this.fetch(options);
268         },
269
270         getOrFetch: function(id, options){
271                 // Helper function to use this collection as a cache for models on the server
272             var model = this.get(id);
273
274             if(model){
275                 options.success && options.success(model);
276                 return;
277             }
278
279             model = new this.model({
280                 resource_uri: id
281             });
282
283             model.fetch(options);
284         },
285
286         /* filterBy: note that this yields a new collection. If you pass that
287               collection to a CompositeView, then the CompositeView won't get
288               any events that trigger on the original collection.
289
290               Using this function is probably wrong, and I wrote
291               FilteredCompositeView() to replace it.
292         */
293
294         filterBy: function(fieldName, value) {
295              filtered = this.filter(function(obj) {
296                  return obj.get(fieldName) == value;
297                  });
298              return new this.constructor(filtered);
299         },
300
301         /* from backbone-tastypie.js */
302         url: function( models ) {
303                     var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
304                     url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
305
306                     // Build a url to retrieve a set of models. This assume the last part of each model's idAttribute
307                     // (set to 'resource_uri') contains the model's id.
308                     if ( models && models.length ) {
309                             var ids = _.map( models, function( model ) {
310                                             var parts = _.compact( model.id.split('/') );
311                                             return parts[ parts.length - 1 ];
312                                     });
313                             url += 'set/' + ids.join(';') + '/';
314                     }
315
316                     url && ( url += "?no_hyperlinks=1" );
317
318                     return url;
319             },
320
321         listMethods: function() {
322                 var res = [];\r
323                 for(var m in this) {\r
324                     if(typeof this[m] == "function") {\r
325                         res.push(m)\r
326                     }\r
327                 }\r
328                 return res;\r
329             },
330     });
331
332     function define_model(lib, attrs) {
333         modelName = attrs.modelName;
334         modelClassName = modelName;
335         collectionClassName = modelName + "Collection";
336
337         if (!attrs.addFields) {
338             attrs.addFields = attrs.detailFields;
339         }
340
341         attrs.inputType = attrs.inputType || {};
342         attrs.foreignFields = attrs.foreignFields || {};
343         attrs.m2mFields = attrs.m2mFields || {};
344         attrs.readOnlyFields = attrs.readOnlyFields || [];
345         attrs.detailLinkFields = attrs.detailLinkFields || ["id","name"];
346
347         if (!attrs.collectionName) {
348             attrs.collectionName = modelName + "s";
349         }
350         collectionName = attrs.collectionName;
351
352         modelAttrs = {}
353         collectionAttrs = {}
354
355         for (key in attrs) {
356             value = attrs[key];
357             if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections"])>=0) {
358                 modelAttrs[key] = value;
359                 collectionAttrs[key] = value;
360             }
361             if ($.inArray(key, ["validate", "preSave", "readOnlyFields"])) {
362                 modelAttrs[key] = value;
363             }
364         }
365
366         if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
367             modelAttrs["defaults"] = xosdefaults[modelName];
368         }
369
370         if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) {
371             modelAttrs["validators"] = xosvalidators[modelName];
372         }
373
374         lib[modelName] = XOSModel.extend(modelAttrs);
375
376         collectionAttrs["model"] = lib[modelName];
377
378         lib[collectionClassName] = XOSCollection.extend(collectionAttrs);
379         lib[collectionName] = new lib[collectionClassName]();
380
381         lib.allCollectionNames.push(collectionName);
382         lib.allCollections.push(lib[collectionName]);
383     };
384
385     function xoslib() {
386         this.allCollectionNames = [];
387         this.allCollections = [];
388
389         /* Give an id, the name of a collection, and the name of a field for models
390            within that collection, lookup the id and return the value of the field.
391         */
392
393         this.idToName = function(id, collectionName, fieldName) {
394             linkedObject = xos[collectionName].get(id);
395             if (linkedObject == undefined) {
396                 return "#" + id;
397             } else {
398                 return linkedObject.attributes[fieldName];
399             }
400         };
401
402         /* defining the models
403
404            modelName          - name of the model.
405
406            relatedCollections - collections which should be drawn as an inline
407                                 list when the detail view is displayed.
408                                 Format: <collection>:<collectionFieldName> where
409                                 <collectionFieldName> is the name of the field
410                                 in the collection that points back to the
411                                 collection in the detail view.
412
413            foreignCollections - collections which are used in idToName() calls
414                                 when presenting the data to the user. Used to
415                                 create a listento event. Somewhat
416                                 redundant with foreignFields.
417
418            foreignFields -      <localFieldName>:<collection>. Used to
419                                 automatically map ids into humanReadableNames
420                                 when presenting data to the user.
421
422            m2mfields -          <localFieldName>:<colleciton>. Used to
423                                 populate choices in picker lists. Simalar to
424                                 foreignFields.
425
426            listFields -         fields to display in lists
427
428            detailFields -       fields to display in detail views
429
430            addFields -          fields to display in popup add windows
431
432            inputType -          by default, "detailFields" will be displayed
433                                 as text input controls. This will let you display
434                                 a checkbox or a picker instead.
435         */
436
437         define_model(this, {urlRoot: SLIVER_API,
438                             relatedCollections: {"networkSlivers": "sliver"},
439                             foreignCollections: ["slices", "deployments", "images", "nodes", "users", "flavors"],
440                             foreignFields: {"creator": "users", "image": "images", "node": "nodes", "deploymentNetwork": "deployments", "slice": "slices", "flavor": "flavors"},
441                             modelName: "sliver",
442                             listFields: ["backend_status", "id", "name", "instance_id", "instance_name", "slice", "deploymentNetwork", "image", "node", "flavor"],
443                             addFields: ["slice", "deploymentNetwork", "flavor", "image", "node"],
444                             detailFields: ["backend_status", "name", "instance_id", "instance_name", "slice", "deploymentNetwork", "flavor", "image", "node", "creator"],
445                             preSave: function() { if (!this.attributes.name && this.attributes.slice) { this.attributes.name = xos.idToName(this.attributes.slice, "slices", "name"); } },
446                             });
447
448         define_model(this, {urlRoot: SLICE_API,
449                            relatedCollections: {"slivers": "slice", "slicePrivileges": "slice", "networks": "owner"},
450                            foreignCollections: ["services", "sites"],
451                            foreignFields: {"service": "services", "site": "sites"},
452                            listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_slivers", "service"],
453                            detailFields: ["backend_status", "name", "site", "enabled", "description", "slice_url", "max_slivers"],
454                            inputType: {"enabled": "checkbox"},
455                            modelName: "slice",
456                            xosValidate: function(attrs, options) {
457                                errors = XOSModel.prototype.xosValidate(this, attrs, options);
458                                // validate that slice.name starts with site.login_base
459                                site = attrs.site || this.site;
460                                if ((site!=undefined) && (attrs.name!=undefined)) {
461                                    site = xos.sites.get(site);
462                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
463                                         errors = errors || {};
464                                         errors["name"] = "must start with " + site.attributes.login_base + "_";
465                                    }
466                                }
467                                return errors;
468                              },
469                            });
470
471         define_model(this, {urlRoot: SLICEPRIVILEGE_API,
472                             foreignCollections: ["slices", "users", "sliceRoles"],
473                             modelName: "slicePrivilege",
474                             foreignFields: {"user": "users", "slice": "slices", "role": "sliceRoles"},
475                             listFields: ["backend_status", "id", "user", "slice", "role"],
476                             detailFields: ["backend_status", "user", "slice", "role"],
477                             });
478
479         define_model(this, {urlRoot: SLICEROLE_API,
480                             modelName: "sliceRole",
481                             listFields: ["backend_status", "id", "role"],
482                             detailFields: ["backend_status", "role"],
483                             });
484
485         define_model(this, {urlRoot: NODE_API,
486                             foreignCollections: ["sites", "deployments"],
487                             modelName: "node",
488                             foreignFields: {"site": "sites", "deployment": "deployments"},
489                             listFields: ["backend_status", "id", "name", "site", "deployment"],
490                             detailFields: ["backend_status", "name", "site", "deployment"],
491                             });
492
493         define_model(this, {urlRoot: SITE_API,
494                             relatedCollections: {"users": "site", "slices": "site", "nodes": "site", "siteDeployments": "site"},
495                             modelName: "site",
496                             listFields: ["backend_status", "id", "name", "site_url", "enabled", "login_base", "is_public", "abbreviated_name"],
497                             detailFields: ["backend_status", "name", "abbreviated_name", "url", "enabled", "is_public", "login_base"],
498                             inputType: {"enabled": "checkbox", "is_public": "checkbox"},
499                             });
500
501         define_model(this, {urlRoot: SITEDEPLOYMENT_API,
502                             foreignCollections: ["sites", "deployments", "controllers"],
503                             foreignFields: {"site": "sites", "deployment": "deployments", "controller": "controllers"},
504                             modelName: "siteDeployment",
505                             listFields: ["backend_status", "id", "site", "deployment", "controller", "availability_zone"],
506                             detailFields: ["backend_status", "site", "deployment", "controller", "availability_zone"],
507                             inputType: {"enabled": "checkbox", "is_public": "checkbox"},
508                             });
509
510         define_model(this, {urlRoot: USER_API,
511                             relatedCollections: {"slicePrivileges": "user", "slices": "owner"},
512                             foreignCollections: ["sites"],
513                             modelName: "user",
514                             foreignFields: {"site": "sites"},
515                             listFields: ["backend_status", "id", "username", "firstname", "lastname", "phone", "user_url", "site"],
516                             detailFields: ["backend_status", "username", "firstname", "lastname", "phone", "user_url", "site"],
517                             });
518
519         define_model(this, { urlRoot: DEPLOYMENT_API,
520                              relatedCollections: {"nodes": "deployment", "slivers": "deploymentNetwork"},
521                              m2mFields: {"flavors": "flavors", "sites": "sites", "images": "images"},
522                              modelName: "deployment",
523                              listFields: ["backend_status", "id", "name", "backend_type", "admin_tenant"],
524                              detailFields: ["backend_status", "name", "backend_type", "admin_tenant", "flavors", "sites", "images"],
525                              inputType: {"flavors": "picker", "sites": "picker", "images": "picker"},
526                              });
527
528         define_model(this, {urlRoot: IMAGE_API,
529                             model: this.image,
530                             modelName: "image",
531                             listFields: ["backend_status", "id", "name", "disk_format", "container_format", "path"],
532                             detailFields: ["backend_status", "name", "disk_format", "admin_tenant"],
533                             });
534
535         define_model(this, {urlRoot: NETWORKTEMPLATE_API,
536                             modelName: "networkTemplate",
537                             listFields: ["backend_status", "id", "name", "visibility", "translation", "sharedNetworkName", "sharedNetworkId"],
538                             detailFields: ["backend_status", "name", "description", "visibility", "translation", "sharedNetworkName", "sharedNetworkId"],
539                             });
540
541         define_model(this, {urlRoot: NETWORK_API,
542                             relatedCollections: {"networkSlivers": "network"},
543                             foreignCollections: ["slices", "networkTemplates"],
544                             modelName: "network",
545                             foreignFields: {"template": "networkTemplates", "owner": "slices"},
546                             listFields: ["backend_status", "id", "name", "template", "ports", "labels", "owner"],
547                             detailFields: ["backend_status", "name", "template", "ports", "labels", "owner"],
548                             });
549
550         define_model(this, {urlRoot: NETWORKSLIVER_API,
551                             modelName: "networkSliver",
552                             foreignFields: {"network": "networks", "sliver": "slivers"},
553                             listFields: ["backend_status", "id", "network", "sliver", "ip", "port_id"],
554                             detailFields: ["backend_status", "network", "sliver", "ip", "port_id"],
555                             });
556
557         define_model(this, {urlRoot: SERVICE_API,
558                             modelName: "service",
559                             listFields: ["backend_status", "id", "name", "enabled", "versionNumber", "published"],
560                             detailFields: ["backend_status", "name", "description", "versionNumber"],
561                             });
562
563         define_model(this, {urlRoot: FLAVOR_API,
564                             modelName: "flavor",
565                             m2mFields: {"deployments": "deployments"},
566                             listFields: ["backend_status", "id", "name", "flavor", "order", "default"],
567                             detailFields: ["backend_status", "name", "description", "flavor", "order", "default", "deployments"],
568                             inputType: {"default": "checkbox", "deployments": "picker"},
569                             });
570
571         define_model(this, {urlRoot: CONTROLLER_API,
572                             modelName: "controller",
573                             listFields: ["backend_status", "id", "name", "version", "backend_type"],
574                             detailFields: ["backend_status", "id", "name", "version", "backend_type", "auth_url", "admin_user", "admin_password", "admin_tenant"],
575                             });
576
577         /* DELETED in site-controller branch
578
579         define_model(this, {urlRoot: NETWORKDEPLOYMENT_API,
580                             modelName: "networkDeployment",
581                             foreignFields: {"network": "networks", "deployment": "deployments"},
582                             listFields: ["backend_status", "id", "network", "deployment", "net_id"],
583                             detailFields: ["backend_status", "network", "deployment", "net_id"],
584                             });
585
586         define_model(this, {urlRoot: SLICEDEPLOYMENT_API,
587                            foreignCollections: ["slices", "deployments"],
588                            modelName: "sliceDeployment",
589                            foreignFields: {"slice": "slices", "deployment": "deployments"},
590                            listFields: ["backend_status", "id", "slice", "deployment", "tenant_id"],
591                            detailFields: ["backend_status", "slice", "deployment", "tenant_id"],
592                            });
593
594         define_model(this, {urlRoot: USERDEPLOYMENT_API,
595                             foreignCollections: ["users","deployments"],
596                             modelName: "userDeployment",
597                             foreignFields: {"deployment": "deployments", "user": "users"},
598                             listFields: ["backend_status", "id", "user", "deployment", "kuser_id"],
599                             detailFields: ["backend_status", "user", "deployment", "kuser_id"],
600                             });
601
602         END stuff deleted in site-controller branch */
603
604         /* not deleted, but obsolete since it has degenerated to a ManyToMany with no other fields
605
606         define_model(this, {urlRoot: IMAGEDEPLOYMENTS_API,
607                             modelName: "imageDeployment",
608                             foreignCollections: ["images", "deployments"],
609                             listFields: ["backend_status", "id", "image", "deployment", "glance_image_id"],
610                             detailFields: ["backend_status", "image", "deployment", "glance_image_id"],
611                             });
612
613         */
614
615         // enhanced REST
616         // XXX this really needs to somehow be combined with Slice, to avoid duplication
617         define_model(this, {urlRoot: SLICEPLUS_API,
618                             relatedCollections: {'slivers': "slice"},
619                             modelName: "slicePlus",
620                             collectionName: "slicesPlus"});
621
622         this.listObjects = function() { return this.allCollectionNames; };
623
624         this.getCollectionStatus = function() {
625             stats = {isLoaded: 0, failedLoad: 0, startedLoad: 0};
626             for (index in this.allCollections) {
627                 collection = this.allCollections[index];
628                 if (collection.isLoaded) {
629                     stats["isLoaded"] = stats["isLoaded"] + 1;
630                 }
631                 if (collection.failedLoad) {
632                     stats["failedLoad"] = stats["failedLoad"] + 1;
633                 }
634                 if (collection.startedLoad) {
635                     stats["startedLoad"] = stats["startedLoad"] + 1;
636                 }
637             }
638             stats["completedLoad"] = stats["failedLoad"] + stats["isLoaded"];
639             return stats;
640         };
641     };
642
643     xos = new xoslib();
644
645     function getCookie(name) {
646         var cookieValue = null;\r
647         if (document.cookie && document.cookie != '') {\r
648             var cookies = document.cookie.split(';');\r
649             for (var i = 0; i < cookies.length; i++) {\r
650                 var cookie = jQuery.trim(cookies[i]);\r
651                 // Does this cookie string begin with the name we want?\r
652                 if (cookie.substring(0, name.length + 1) == (name + '=')) {\r
653                     cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\r
654                     break;\r
655                 }\r
656             }\r
657         }\r
658         return cookieValue;\r
659     }
660
661     (function() {
662       var _sync = Backbone.sync;\r
663       Backbone.sync = function(method, model, options){\r
664         options.beforeSend = function(xhr){\r
665           var token = getCookie("csrftoken");\r
666           xhr.setRequestHeader('X-CSRFToken', token);\r
667         };\r
668         return _sync(method, model, options);\r
669       };\r
670     })();
671 }