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