Merge branch 'master' of ssh://git.planet-lab.org/git/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     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             /* If a 'validate' method is supplied, then it will be called
74                automatically on save. Unfortunately, save calls neither the
75                'error' nor the 'success' callback if the validator fails.
76
77                For now, we're calling our validator 'xosValidate' so this
78                autoamtic validation doesn't occur.
79             */
80
81             xosValidate: function(attrs, options) {
82                 errors = {};
83                 foundErrors = false;
84                 _.each(this.validators, function(validatorList, fieldName) {
85                     _.each(validatorList, function(validator) {
86                         if (fieldName in attrs) {
87                             validatorResult = validateField(validator, attrs[fieldName], this)
88                             if (validatorResult != true) {
89                                 errors[fieldName] = validatorResult;
90                                 foundErrors = true;
91                             }
92                         }
93                     });
94                 });
95                 if (foundErrors) {
96                     return errors;
97                 }
98                 // backbone.js semantics -- on successful validate, return nothing
99             },
100
101             /* uncommenting this would make validate() call xosValidate()
102             validate: function(attrs, options) {
103                 r = this.xosValidate(attrs, options);
104                 console.log("validate");
105                 console.log(r);
106                 return r;
107             }, */
108     });
109
110     XOSCollection = Backbone.Collection.extend({
111         objects: function() {
112                     return this.models.map(function(element) { return element.attributes; });
113                  },
114
115         initialize: function(){
116           this.isLoaded = false;
117           this.failedLoad = false;
118           this.startedLoad = false;
119           this.sortVar = 'name';\r
120           this.sortOrder = 'asc';\r
121           this.on( "sort", this.sorted );\r
122         },\r
123 \r
124         relatedCollections: [],\r
125         foreignCollections: [],\r
126 \r
127         sorted: function() {\r
128             //console.log("sorted " + this.modelName);\r
129         },\r
130 \r
131         simpleComparator: function( model ){\r
132           parts=this.sortVar.split(".");\r
133           result = model.get(parts[0]);\r
134           for (index=1; index<parts.length; ++index) {\r
135               result=result[parts[index]];\r
136           }\r
137           return result;\r
138         },\r
139 \r
140         comparator: function (left, right) {\r
141             var l = this.simpleComparator(left);\r
142             var r = this.simpleComparator(right);\r
143 \r
144             if (l === void 0) return -1;\r
145             if (r === void 0) return 1;\r
146 \r
147             if (this.sortOrder=="desc") {\r
148                 return l < r ? 1 : l > r ? -1 : 0;\r
149             } else {\r
150                 return l < r ? -1 : l > r ? 1 : 0;\r
151             }\r
152         },\r
153 \r
154         fetchSuccess: function(collection, response, options) {\r
155             //console.log("fetch succeeded " + collection.modelName);\r
156             this.failedLoad = false;\r
157             this.fetching = false;\r
158             if (!this.isLoaded) {\r
159                 this.isLoaded = true;\r
160                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
161             }\r
162             this.trigger("fetchStateChange");\r
163             if (options["orig_success"]) {\r
164                 options["orig_success"](collection, response, options);\r
165             }\r
166         },\r
167 \r
168         fetchFailure: function(collection, response, options) {\r
169             //console.log("fetch failed " + collection.modelName);\r
170             this.fetching = false;\r
171             if ((!this.isLoaded) && (!this.failedLoad)) {\r
172                 this.failedLoad=true;\r
173                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
174             }\r
175             this.trigger("fetchStateChange");\r
176             if (options["orig_failure"]) {\r
177                 options["orig_failure"](collection, response, options);\r
178             }\r
179         },\r
180 \r
181         fetch: function(options) {\r
182             var self=this;\r
183             this.fetching=true;\r
184             //console.log("fetch " + this.modelName);\r
185             if (!this.startedLoad) {\r
186                 this.startedLoad=true;\r
187                 Backbone.trigger("xoslib:collectionLoadChange", this);\r
188             }\r
189             this.trigger("fetchStateChange");\r
190             if (options == undefined) {\r
191                 options = {};\r
192             }\r
193             options["orig_success"] = options["success"];\r
194             options["orig_failure"] = options["failure"];\r
195             options["success"] = function(collection, response, options) { self.fetchSuccess.call(self, collection, response, options); };\r
196             options["failure"] = this.fetchFailure;\r
197             Backbone.Collection.prototype.fetch.call(this, options);\r
198         },\r
199 \r
200         startPolling: function() {\r
201             if (!this._polling) {\r
202                 var collection=this;
203                 setInterval(function() { collection.fetch(); }, 10000);
204                 this._polling=true;
205                 this.fetch();
206             }
207         },
208
209         refresh: function(refreshRelated) {
210             if (!this.fetching) {
211                 this.fetch();
212             }
213             if (refreshRelated) {
214                 for (related in this.relatedCollections) {
215                     related = xos[related];
216                     if (!related.fetching) {
217                         related.fetch();
218                     }
219                 }
220             }
221         },
222
223         maybeFetch: function(options){
224                 // Helper function to fetch only if this collection has not been fetched before.
225             if(this._fetched){
226                     // If this has already been fetched, call the success, if it exists
227                 options.success && options.success();
228                 console.log("alreadyFetched");
229                 return;
230             }
231
232                 // when the original success function completes mark this collection as fetched
233             var self = this,
234             successWrapper = function(success){
235                 return function(){
236                     self._fetched = true;
237                     success && success.apply(this, arguments);
238                 };
239             };
240             options.success = successWrapper(options.success);
241             console.log("call fetch");
242             this.fetch(options);
243         },
244
245         getOrFetch: function(id, options){
246                 // Helper function to use this collection as a cache for models on the server
247             var model = this.get(id);
248
249             if(model){
250                 options.success && options.success(model);
251                 return;
252             }
253
254             model = new this.model({
255                 resource_uri: id
256             });
257
258             model.fetch(options);
259         },
260
261         filterBy: function(fieldName, value) {
262              filtered = this.filter(function(obj) {
263                  return obj.get(fieldName) == value;
264                  });
265              return new this.constructor(filtered);
266         },
267
268         /* from backbone-tastypie.js */
269         url: function( models ) {
270                     var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
271                     url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
272
273                     // Build a url to retrieve a set of models. This assume the last part of each model's idAttribute
274                     // (set to 'resource_uri') contains the model's id.
275                     if ( models && models.length ) {
276                             var ids = _.map( models, function( model ) {
277                                             var parts = _.compact( model.id.split('/') );
278                                             return parts[ parts.length - 1 ];
279                                     });
280                             url += 'set/' + ids.join(';') + '/';
281                     }
282
283                     url && ( url += "?no_hyperlinks=1" );
284
285                     return url;
286             },
287
288         listMethods: function() {
289                 var res = [];\r
290                 for(var m in this) {\r
291                     if(typeof this[m] == "function") {\r
292                         res.push(m)\r
293                     }\r
294                 }\r
295                 return res;\r
296             },
297     });
298
299     function define_model(lib, attrs) {
300         modelName = attrs.modelName;
301         modelClassName = modelName;
302         collectionClassName = modelName + "Collection";
303
304         if (!attrs.collectionName) {
305             attrs.collectionName = modelName + "s";
306         }
307         collectionName = attrs.collectionName;
308
309         modelAttrs = {}
310         collectionAttrs = {}
311
312         for (key in attrs) {
313             value = attrs[key];
314             if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "validate"])>=0) {
315                 modelAttrs[key] = value;
316             }
317             if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "relatedCollections", "foreignCollections"])>=0) {
318                 collectionAttrs[key] = value;
319             }
320         }
321
322         if (xosdefaults && xosdefaults[modelName]) {
323             modelAttrs["defaults"] = xosdefaults[modelName];
324         }
325
326         if (xosvalidators && xosvalidators[modelName]) {
327             modelAttrs["validators"] = xosvalidators[modelName];
328         }
329
330         lib[modelName] = XOSModel.extend(modelAttrs);
331
332         collectionAttrs["model"] = lib[modelName];
333
334         lib[collectionClassName] = XOSCollection.extend(collectionAttrs);
335         lib[collectionName] = new lib[collectionClassName]();
336
337         lib.allCollectionNames.push(collectionName);
338         lib.allCollections.push(lib[collectionName]);
339     };
340
341     function xoslib() {
342         this.allCollectionNames = [];
343         this.allCollections = [];
344
345         define_model(this, {urlRoot: SLIVER_API,
346                             relatedCollections: {"networkSlivers": "sliver"},
347                             foreignCollections: ["slices", "deployments", "images", "nodes", "users"],
348                             modelName: "sliver"});
349
350         define_model(this, {urlRoot: SLICE_API,
351                            relatedCollections: {"slivers": "slice", "sliceDeployments": "slice", "slicePrivileges": "slice", "networks": "owner"},
352                            foreignCollections: ["services", "sites"],
353                            modelName: "slice",
354                            xosValidate: function(attrs, options) {
355                                errors = XOSModel.prototype.xosValidate(this, attrs, options);
356                                // validate that slice.name starts with site.login_base
357                                site = attrs.site || this.site;
358                                if ((site!=undefined) && (attrs.name!=undefined)) {
359                                    site = xos.sites.get(site);
360                                    if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
361                                         errors = errors || {};
362                                         errors["name"] = "must start with " + site.attributes.login_base + "_";
363                                    }
364                                }
365                                return errors;
366                              },
367                            });
368
369         define_model(this, {urlRoot: SLICEDEPLOYMENT_API,
370                            foreignCollections: ["slices", "deployments"],
371                            modelName: "sliceDeployment"});
372
373         define_model(this, {urlRoot: SLICEPRIVILEGE_API,
374                             foreignCollections: ["slices", "users", "sliceRoles"],
375                             modelName: "slicePrivilege"});
376
377         define_model(this, {urlRoot: SLICEROLE_API,
378                             modelName: "sliceRole"});
379
380         define_model(this, {urlRoot: NODE_API,
381                             foreignCollections: ["sites", "deployments"],
382                             modelName: "node"});
383
384         define_model(this, {urlRoot: SITE_API,
385                             relatedCollections: {"users": "site", "slices": "site", "nodes": "site"},
386                             modelName: "site"});
387
388         define_model(this, {urlRoot: USER_API,
389                             relatedCollections: {"slicePrivileges": "user", "slices": "owner", "userDeployments": "user"},
390                             foreignCollections: ["sites"],
391                             modelName: "user"});
392
393         define_model(this, {urlRoot: USERDEPLOYMENT_API,
394                             foreignCollections: ["users","deployments"],
395                             modelName: "userDeployment"});
396
397         define_model(this, { urlRoot: DEPLOYMENT_API,
398                              relatedCollections: {"nodes": "deployment", "slivers": "deploymentNetwork", "networkDeployments": "deployment", "userDeployments": "deployment"},
399                              modelName: "deployment"});
400
401         define_model(this, {urlRoot: IMAGE_API,
402                             model: this.image,
403                             modelName: "image"});
404
405         define_model(this, {urlRoot: NETWORKTEMPLATE_API,
406                             modelName: "networkTemplate"});
407
408         define_model(this, {urlRoot: NETWORK_API,
409                             relatedCollections: {"networkDeployments": "network", "networkSlivers": "network"},
410                             foreignCollections: ["slices", "networkTemplates"],
411                             modelName: "network"});
412
413         define_model(this, {urlRoot: NETWORKSLIVER_API,
414                             modelName: "networkSliver"});
415
416         define_model(this, {urlRoot: NETWORKDEPLOYMENT_API,
417                             modelName: "networkDeployment"});
418
419         define_model(this, {urlRoot: SERVICE_API,
420                             modelName: "service"});
421
422         // enhanced REST
423         define_model(this, {urlRoot: SLICEPLUS_API,
424                             relatedCollections: {'slivers': "slice"},
425                             modelName: "slicePlus",
426                             collectionName: "slicesPlus"});
427
428         this.listObjects = function() { return this.allCollectionNames; };
429
430         this.getCollectionStatus = function() {
431             stats = {isLoaded: 0, failedLoad: 0, startedLoad: 0};
432             for (index in this.allCollections) {
433                 collection = this.allCollections[index];
434                 if (collection.isLoaded) {
435                     stats["isLoaded"] = stats["isLoaded"] + 1;
436                 }
437                 if (collection.failedLoad) {
438                     stats["failedLoad"] = stats["failedLoad"] + 1;
439                 }
440                 if (collection.startedLoad) {
441                     stats["startedLoad"] = stats["startedLoad"] + 1;
442                 }
443             }
444             stats["completedLoad"] = stats["failedLoad"] + stats["isLoaded"];
445             return stats;
446         };
447     };
448
449     xos = new xoslib();
450
451     function getCookie(name) {
452         var cookieValue = null;\r
453         if (document.cookie && document.cookie != '') {\r
454             var cookies = document.cookie.split(';');\r
455             for (var i = 0; i < cookies.length; i++) {\r
456                 var cookie = jQuery.trim(cookies[i]);\r
457                 // Does this cookie string begin with the name we want?\r
458                 if (cookie.substring(0, name.length + 1) == (name + '=')) {\r
459                     cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\r
460                     break;\r
461                 }\r
462             }\r
463         }\r
464         return cookieValue;\r
465     }
466
467     (function() {
468       var _sync = Backbone.sync;\r
469       Backbone.sync = function(method, model, options){\r
470         options.beforeSend = function(xhr){\r
471           var token = getCookie("csrftoken");\r
472           xhr.setRequestHeader('X-CSRFToken', token);\r
473         };\r
474         return _sync(method, model, options);\r
475       };\r
476     })();
477 }