readonly fields, wip
[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         attrs.inputType = attrs.inputType || {};
324         attrs.foreignFields = attrs.foreignFields || {};
325         attrs.readOnlyFields = attrs.readOnlyFields || [];
326
327         if (!attrs.collectionName) {
328             attrs.collectionName = modelName + "s";
329         }
330         collectionName = attrs.collectionName;
331
332         modelAttrs = {}
333         collectionAttrs = {}
334
335         for (key in attrs) {
336             value = attrs[key];
337             if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "addFields", "detailFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections"])>=0) {
338                 modelAttrs[key] = value;
339                 collectionAttrs[key] = value;
340             }
341             if ($.inArray(key, ["validate", "preSave", "readOnlyFields"])) {
342                 modelAttrs[key] = value;
343             }
344         }
345
346         if (xosdefaults && xosdefaults[modelName]) {
347             modelAttrs["defaults"] = xosdefaults[modelName];
348         }
349
350         if (xosvalidators && xosvalidators[modelName]) {
351             modelAttrs["validators"] = xosvalidators[modelName];
352         }
353
354         lib[modelName] = XOSModel.extend(modelAttrs);
355
356         collectionAttrs["model"] = lib[modelName];
357
358         lib[collectionClassName] = XOSCollection.extend(collectionAttrs);
359         lib[collectionName] = new lib[collectionClassName]();
360
361         lib.allCollectionNames.push(collectionName);
362         lib.allCollections.push(lib[collectionName]);
363     };
364
365     function xoslib() {
366         this.allCollectionNames = [];
367         this.allCollections = [];
368
369         /* Give an id, the name of a collection, and the name of a field for models
370            within that collection, lookup the id and return the value of the field.
371         */
372
373         this.idToName = function(id, collectionName, fieldName) {
374             linkedObject = xos[collectionName].get(id);
375             if (linkedObject == undefined) {
376                 return "#" + id;
377             } else {
378                 return linkedObject.attributes[fieldName];
379             }
380         };
381
382         define_model(this, {urlRoot: SLIVER_API,
383                             relatedCollections: {"networkSlivers": "sliver"},
384                             foreignCollections: ["slices", "deployments", "images", "nodes", "users"],
385                             foreignFields: {"creator": "users", "image": "images", "node": "nodes", "deploymentNetwork": "deployments", "slice": "slices"},
386                             modelName: "sliver",
387                             addFields: ["slice", "deploymentNetwork", "image", "node"],
388                             detailFields: ["name", "instance_id", "instance_name", "slice", "deploymentNetwork", "image", "node", "creator"],
389                             preSave: function() { if (!this.attributes.name && this.attributes.slice) { this.attributes.name = xos.idToName(this.attributes.slice, "slices", "name"); } },
390                             });
391
392         define_model(this, {urlRoot: SLICE_API,
393                            relatedCollections: {"slivers": "slice", "sliceDeployments": "slice", "slicePrivileges": "slice", "networks": "owner"},
394                            foreignCollections: ["services", "sites"],
395                            foreignFields: {"service": "services", "site": "sites"},
396                            detailFields: ["name", "site", "enabled", "description", "url", "max_slivers"],
397                            inputType: {"enabled": "checkbox"},
398                            modelName: "slice",
399                            xosValidate: function(attrs, options) {
400                                errors = XOSModel.prototype.xosValidate(this, attrs, options);
401                                // validate that slice.name starts with site.login_base
402                                site = attrs.site || this.site;
403                                if ((site!=undefined) && (attrs.name!=undefined)) {
404                                    site = xos.sites.get(site);
405                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
406                                         errors = errors || {};
407                                         errors["name"] = "must start with " + site.attributes.login_base + "_";
408                                    }
409                                }
410                                return errors;
411                              },
412                            });
413
414         define_model(this, {urlRoot: SLICEDEPLOYMENT_API,
415                            foreignCollections: ["slices", "deployments"],
416                            modelName: "sliceDeployment",
417                            foreignFields: {"slice": "slices", "deployment": "deployments"},
418                            detailFields: ["slice", "deployment", "tenant_id"],
419                            });
420
421         define_model(this, {urlRoot: SLICEPRIVILEGE_API,
422                             foreignCollections: ["slices", "users", "sliceRoles"],
423                             modelName: "slicePrivilege",
424                             foreignFields: {"user": "users", "slice": "slices", "role": "sliceRoles"},
425                             detailFields: ["user", "slice", "role"],
426                             });
427
428         define_model(this, {urlRoot: SLICEROLE_API,
429                             modelName: "sliceRole",
430                             detailFields: ["role"],
431                             });
432
433         define_model(this, {urlRoot: NODE_API,
434                             foreignCollections: ["sites", "deployments"],
435                             modelName: "node",
436                             foreignFields: {"site": "sites", "deployment": "deployments"},
437                             detailFields: ["name", "site", "deployment"],
438                             });
439
440         define_model(this, {urlRoot: SITE_API,
441                             relatedCollections: {"users": "site", "slices": "site", "nodes": "site"},
442                             modelName: "site",
443                             detailFields: ["name", "abbreviated_name", "url", "enabled", "is_public", "login_base"],
444                             inputType: {"enabled": "checkbox", "is_public": "checkbox"},
445                             });
446
447         define_model(this, {urlRoot: USER_API,
448                             relatedCollections: {"slicePrivileges": "user", "slices": "owner", "userDeployments": "user"},
449                             foreignCollections: ["sites"],
450                             modelName: "user",
451                             foreignFields: {"site": "sites"},
452                             detailFields: ["username", "firstname", "lastname", "phone", "user_url", "site"],
453                             });
454
455         define_model(this, {urlRoot: USERDEPLOYMENT_API,
456                             foreignCollections: ["users","deployments"],
457                             modelName: "userDeployment",
458                             foreignFields: {"deployment": "deployments", "user": "users"},
459                             detailFields: ["user", "deployment", "kuser_id"],
460                             });
461
462         define_model(this, { urlRoot: DEPLOYMENT_API,
463                              relatedCollections: {"nodes": "deployment", "slivers": "deploymentNetwork", "networkDeployments": "deployment", "userDeployments": "deployment"},
464                              modelName: "deployment",
465                              detailFields: ["name", "backend_type", "admin_tenant"],
466                              });
467
468         define_model(this, {urlRoot: IMAGE_API,
469                             model: this.image,
470                             modelName: "image",
471                             detailFields: ["name", "disk_format", "admin_tenant"],
472                             });
473
474         define_model(this, {urlRoot: NETWORKTEMPLATE_API,
475                             modelName: "networkTemplate",
476                             detailFields: ["name", "description", "visibility", "translation", "sharedNetworkName", "sharedNetworkId"],
477                             });
478
479         define_model(this, {urlRoot: NETWORK_API,
480                             relatedCollections: {"networkDeployments": "network", "networkSlivers": "network"},
481                             foreignCollections: ["slices", "networkTemplates"],
482                             modelName: "network",
483                             foreignFields: {"template": "networkTemplates", "owner": "slices"},
484                             detailFields: ["name", "template", "ports", "labels", "owner"],
485                             });
486
487         define_model(this, {urlRoot: NETWORKSLIVER_API,
488                             modelName: "networkSliver",
489                             foreignFields: {"network": "networks", "sliver": "slivers"},
490                             detailFields: ["network", "sliver", "ip", "port_id"],
491                             });
492
493         define_model(this, {urlRoot: NETWORKDEPLOYMENT_API,
494                             modelName: "networkDeployment",
495                             foreignFields: {"network": "networks", "deployment": "deployments"},
496                             detailFields: ["network", "deployment", "net_id"],
497                             });
498
499         define_model(this, {urlRoot: SERVICE_API,
500                             modelName: "service",
501                             detailFields: ["name", "description", "versionNumber"],
502                             });
503
504         // enhanced REST
505         define_model(this, {urlRoot: SLICEPLUS_API,
506                             relatedCollections: {'slivers': "slice"},
507                             modelName: "slicePlus",
508                             collectionName: "slicesPlus"});
509
510         this.listObjects = function() { return this.allCollectionNames; };
511
512         this.getCollectionStatus = function() {
513             stats = {isLoaded: 0, failedLoad: 0, startedLoad: 0};
514             for (index in this.allCollections) {
515                 collection = this.allCollections[index];
516                 if (collection.isLoaded) {
517                     stats["isLoaded"] = stats["isLoaded"] + 1;
518                 }
519                 if (collection.failedLoad) {
520                     stats["failedLoad"] = stats["failedLoad"] + 1;
521                 }
522                 if (collection.startedLoad) {
523                     stats["startedLoad"] = stats["startedLoad"] + 1;
524                 }
525             }
526             stats["completedLoad"] = stats["failedLoad"] + stats["isLoaded"];
527             return stats;
528         };
529     };
530
531     xos = new xoslib();
532
533     function getCookie(name) {
534         var cookieValue = null;\r
535         if (document.cookie && document.cookie != '') {\r
536             var cookies = document.cookie.split(';');\r
537             for (var i = 0; i < cookies.length; i++) {\r
538                 var cookie = jQuery.trim(cookies[i]);\r
539                 // Does this cookie string begin with the name we want?\r
540                 if (cookie.substring(0, name.length + 1) == (name + '=')) {\r
541                     cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\r
542                     break;\r
543                 }\r
544             }\r
545         }\r
546         return cookieValue;\r
547     }
548
549     (function() {
550       var _sync = Backbone.sync;\r
551       Backbone.sync = function(method, model, options){\r
552         options.beforeSend = function(xhr){\r
553           var token = getCookie("csrftoken");\r
554           xhr.setRequestHeader('X-CSRFToken', token);\r
555         };\r
556         return _sync(method, model, options);\r
557       };\r
558     })();
559 }