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