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