Merge branch 'master' of git://git.planet-lab.org/plstackapi
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xosHelper.js
1 HTMLView = Marionette.ItemView.extend({
2   render: function() {
3       this.$el.append(this.options.html);
4   },
5 });
6
7 XOSRouter = Marionette.AppRouter.extend({
8         initialize: function() {\r
9             this.routeStack=[];\r
10         },\r
11 \r
12         onRoute: function(x,y,z) {\r
13              this.routeStack.push(Backbone.history.fragment);\r
14         },\r
15 \r
16         prevPage: function() {\r
17              return this.routeStack.slice(-2)[0];
18         },
19     });\r
20
21
22
23 XOSApplication = Marionette.Application.extend({
24     detailBoxId: "#detailBox",
25     errorBoxId: "#errorBox",
26     errorCloseButtonId: "#close-error-box",
27     successBoxId: "#successBox",
28     successCloseButtonId: "#close-success-box",
29     errorTemplate: "#xos-error-template",
30     successTemplate: "#xos-success-template",
31     logMessageCount: 0,
32
33     confirmDialog: function(view, event, callback) {
34         $("#xos-confirm-dialog").dialog({
35            autoOpen: false,
36            modal: true,
37            buttons : {
38                 "Confirm" : function() {
39                   $(this).dialog("close");
40                   if (event) {
41                       view.trigger(event);
42                   }
43                   if (callback) {
44                       callback();
45                   }
46                 },
47                 "Cancel" : function() {
48                   $(this).dialog("close");
49                 }
50               }
51             });
52         $("#xos-confirm-dialog").dialog("open");
53     },
54
55     popupErrorDialog: function(responseText) {
56         try {
57             parsed_error=$.parseJSON(responseText);
58             width=300;
59         }
60         catch(err) {
61             parsed_error=undefined;
62             width=640;    // django stacktraces like wide width
63         }
64         if (parsed_error) {
65             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
66         } else {
67             $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
68         }
69
70         $("#xos-error-dialog").dialog({
71             modal: true,
72             width: width,
73             buttons: {
74                 Ok: function() { $(this).dialog("close"); }
75             }
76         });
77     },
78
79     hideLinkedItems: function(result) {
80         var index=0;
81         while (index<4) {
82             this["linkedObjs" + (index+1)].empty();
83             index = index + 1;
84         }
85     },
86
87     listViewShower: function(listViewName, collection_name, regionName, title) {
88         var app=this;
89         return function() {
90             app[regionName].show(new app[listViewName]);
91             app.hideLinkedItems();
92             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
93             $("#detail").show();
94             $("#xos-listview-button-box").show();
95             $("#tabs").hide();
96             $("#xos-detail-button-box").hide();
97         }
98     },
99
100     addShower: function(detailName, collection_name, regionName, title) {
101         var app=this;
102         return function() {
103             model = new xos[collection_name].model();
104             detailViewClass = app[detailName];
105             detailView = new detailViewClass({model: model, collection:xos[collection_name]});
106             app[regionName].show(detailView);
107             $("#xos-detail-button-box").show();
108             $("#xos-listview-button-box").hide();
109         }
110     },
111
112     deleteShower: function(collection_name) {
113         var app=this;
114         return function(model_id) {
115             console.log("deleteCalled");
116             collection = xos[collection_name];
117             model = collection.get(model_id);
118             assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
119             app.deleteDialog(model,"back");
120         }
121     },
122
123     detailShower: function(detailName, collection_name, regionName, title) {
124         var app=this;
125         showModelId = function(model_id) {
126             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
127
128             collection = xos[collection_name];
129             model = collection.get(model_id);
130             if (model == undefined) {
131                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
132             } else {
133                 detailViewClass = app[detailName];
134                 detailView = new detailViewClass({model: model});
135                 app[regionName].show(detailView);
136                 detailView.showLinkedItems();
137                 $("#xos-detail-button-box").show();
138                 $("#xos-listview-button-box").hide();
139             }
140         }
141         return showModelId;
142     },
143
144     /* error handling callbacks */
145
146     hideError: function() {
147         if (this.logWindowId) {
148         } else {
149             $(this.errorBoxId).hide();
150             $(this.successBoxId).hide();
151         }
152     },
153
154     showSuccess: function(result) {
155          result["statusclass"] = "success";
156          if (this.logTableId) {
157              this.appendLogWindow(result);
158          } else {
159              $(this.successBoxId).show();
160              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
161              var that=this;
162              $(this.successCloseButtonId).unbind().bind('click', function() {
163                  $(that.successBoxId).hide();
164              });
165          }
166     },
167
168     showError: function(result) {
169          result["statusclass"] = "failure";
170          if (this.logTableId) {
171              this.appendLogWindow(result);
172              this.popupErrorDialog(result.responseText);
173          } else {
174              // this is really old stuff
175              $(this.errorBoxId).show();
176              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
177              var that=this;
178              $(this.errorCloseButtonId).unbind().bind('click', function() {
179                  $(that.errorBoxId).hide();
180              });
181          }
182     },
183
184     showInformational: function(result) {
185          result["statusclass"] = "inprog";
186          if (this.logTableId) {
187              return this.appendLogWindow(result);
188          } else {
189              return undefined;
190          }
191     },
192
193     appendLogWindow: function(result) {
194         // compute a new logMessageId for this log message
195         logMessageId = "logMessage" + this.logMessageCount;
196         this.logMessageCount = this.logMessageCount + 1;
197         result["logMessageId"] = logMessageId;
198
199         logMessageTemplate=$("#xos-log-template").html();
200         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
201         newRow = _.template(logMessageTemplate, result);
202         assert(newRow != undefined, "newRow is undefined");
203
204         if (result["infoMsgId"] != undefined) {
205             // We were passed the logMessageId of an informational message,
206             // and the caller wants us to replace that message with our own.
207             // i.e. replace an informational message with a success or an error.
208             $("#"+result["infoMsgId"]).replaceWith(newRow);
209         } else {
210             // Create a brand new log message rather than replacing one.
211             logTableBody = $(this.logTableId + " tbody");
212             logTableBody.prepend(newRow);
213         }
214
215         if (this.statusMsgId) {
216             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
217         }
218
219         limitTableRows(this.logTableId, 5);
220
221         return logMessageId;
222     },
223
224     saveError: function(model, result, xhr, infoMsgId) {
225         console.log("saveError");
226         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
227         result["infoMsgId"] = infoMsgId;
228         this.showError(result);
229     },
230
231     saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
232         console.log("saveSuccess");
233         if (model.addToCollection) {
234             console.log("addToCollection");
235             model.addToCollection.add(model);
236             model.addToCollection.sort();
237             model.addToCollection = undefined;
238         }
239         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
240         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
241         result["infoMsgId"] = infoMsgId;
242         this.showSuccess(result);
243     },
244
245     destroyError: function(model, result, xhr, infoMsgId) {
246         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
247         result["infoMsgId"] = infoMsgId;
248         this.showError(result);
249     },
250
251     destroySuccess: function(model, result, xhr, infoMsgId) {
252         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
253         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
254         result["infoMsgId"] = infoMsgId;
255         this.showSuccess(result);
256     },
257
258     /* end error handling callbacks */
259
260     destroyModel: function(model) {
261          //console.log("destroyModel"); console.log(model);
262          this.hideError();
263          var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
264          var that = this;
265          model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
266                         success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
267     },
268
269     deleteDialog: function(model, afterDelete) {
270         var that=this;
271         console.log("XXX");
272         console.log(Backbone.history.fragment);
273         assert(model!=undefined, "deleteDialog's model is undefined");
274         //console.log("deleteDialog"); console.log(model);
275         this.confirmDialog(null, null, function() {
276             //console.log("deleteConfirm"); console.log(model);
277             modelName = model.modelName;
278             that.destroyModel(model);
279             if (afterDelete=="list") {
280                 that.navigate("list", modelName);
281             } else if (afterDelete=="back") {
282                 prevPage = that.Router.prevPage();
283                 if (prevPage) {
284                     that.Router.navigate("#"+prevPage, {trigger: false, replace: true} );
285                 }
286             }
287
288         });
289     },
290 });
291
292 /* XOSDetailView
293       extend with:
294          app - MarionetteApplication
295          template - template (See XOSHelper.html)
296 */
297
298 XOSDetailView = Marionette.ItemView.extend({
299             tagName: "div",
300
301             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
302                      "click button.btn-xos-save-leave": "submitLeaveClicked",
303                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
304                      "click button.btn-xos-delete": "deleteClicked",
305                      "change input": "inputChanged"},
306
307             /*initialize: function() {
308                 this.on('deleteConfirmed', this.deleteConfirmed);
309             },*/
310
311             /* inputChanged is watching the onChange events of the input controls. We
312                do this to track when this view is 'dirty', so we can throw up a warning
313                if the user tries to change his slices without saving first.
314             */
315
316             inputChanged: function(e) {
317                 this.dirty = true;
318             },
319
320             submitContinueClicked: function(e) {
321                 console.log("saveContinue");
322                 e.preventDefault();
323                 this.save();
324             },
325
326             submitLeaveClicked: function(e) {
327                 console.log("saveLeave");
328                 e.preventDefault();
329                 this.save();
330                 this.app.navigate("list", this.model.modelName);
331             },
332
333             submitAddAnotherClicked: function(e) {
334                 console.log("saveAnother");
335                 e.preventDefault();
336                 this.save();
337                 this.app.navigate("add", this.model.modelName);
338             },
339
340             save: function() {
341                 this.app.hideError();
342                 var data = Backbone.Syphon.serialize(this);
343                 var that = this;
344                 var isNew = !this.model.id;
345
346                 this.$el.find(".help-inline").remove();
347
348                 /* although model.validate() is called automatically by
349                    model.save, we call it ourselves, so we can throw up our
350                    validation error before creating the infoMsg in the log
351                 */
352                 errors =  this.model.xosValidate(data);
353                 if (errors) {
354                     this.onFormDataInvalid(errors);
355                     return;
356                 }
357
358                 if (isNew) {
359                     this.model.attributes.humanReadableName = "new " + model.modelName;
360                     this.model.addToCollection = this.collection;
361                 } else {
362                     this.model.addToCollection = undefined;
363                 }
364
365                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
366
367                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
368                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);}});
369                 this.dirty = false;
370             },
371
372             /*destroyModel: function() {
373                  this.app.hideError();
374                  var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
375                  var that = this;
376                  this.model.destroy({error: function(model, result, xhr) { that.app.destroyError(model,result,xhr,infoMsgId);},
377                                      success: function(model, result, xhr) { that.app.destroySuccess(model,result,xhr,infoMsgId);}});
378             },
379
380              deleteClicked: function(e) {
381                  e.preventDefault();
382                  this.app.confirmDialog(this, "deleteConfirmed");
383              },
384
385              deleteConfirmed: function() {
386                  modelName = this.model.modelName;
387                  this.destroyModel();
388                  this.app.navigate("list", modelName);
389              }, */
390
391              deleteClicked: function(e) {
392                  e.preventDefault();
393                  this.app.deleteDialog(this.model, "list");
394              },
395
396             tabClick: function(tabId, regionName) {
397                     region = this.app[regionName];
398                     if (this.currentTabRegion != undefined) {
399                         this.currentTabRegion.$el.hide();
400                     }
401                     if (this.currentTabId != undefined) {
402                         $(this.currentTabId).removeClass('active');
403                     }
404                     this.currentTabRegion = region;
405                     this.currentTabRegion.$el.show();
406
407                     this.currentTabId = tabId;
408                     $(tabId).addClass('active');
409             },
410
411             showTabs: function(tabs) {
412                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
413                 $("#tabs").html(template(tabs));
414                 var that = this;
415
416                 _.each(tabs, function(tab) {
417                     var regionName = tab["region"];
418                     var tabId = '#xos-nav-'+regionName;
419                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
420                 });
421
422                 $("#tabs").show();
423             },
424
425             showLinkedItems: function() {
426                     tabs=[];
427
428                     tabs.push({name: "details", region: "detail"});
429
430                     var index=0;
431                     for (relatedName in this.model.collection.relatedCollections) {
432                         relatedField = this.model.collection.relatedCollections[relatedName];
433                         regionName = "linkedObjs" + (index+1);
434
435                         relatedListViewClassName = relatedName + "ListView";
436                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
437                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});
438                         this.app[regionName].show(new relatedListViewClass());
439                         if (this.app.hideTabsByDefault) {
440                             this.app[regionName].$el.hide();
441                         }
442                         tabs.push({name: relatedName, region: regionName});
443                         index = index + 1;
444                     }
445
446                     while (index<4) {
447                         this.app["linkedObjs" + (index+1)].empty();
448                         index = index + 1;
449                     }
450
451                     this.showTabs(tabs);
452                     this.tabClick('#xos-nav-detail', 'detail');
453               },
454
455             onFormDataInvalid: function(errors) {
456                 var self=this;
457                 var markErrors = function(value, key) {
458                     console.log("name='" + key + "'");
459                     var $inputElement = self.$el.find("[name='" + key + "']");
460                     var $inputContainer = $inputElement.parent();
461                     //$inputContainer.find(".help-inline").remove();
462                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
463                     $inputContainer.append($errorEl).addClass("error");
464                 }
465                 _.each(errors, markErrors);
466             },
467
468 });
469
470 /* XOSItemView
471       This is for items that will be displayed as table rows.
472       extend with:
473          app - MarionetteApplication
474          template - template (See XOSHelper.html)
475          detailClass - class of detail view, probably an XOSDetailView
476 */
477
478 XOSItemView = Marionette.ItemView.extend({
479              tagName: 'tr',
480              className: 'test-tablerow',
481
482              templateHelpers: function() { return { modelName: this.model.modelName,
483                                                     collectionName: this.model.collectionName,
484                                          }},
485 });
486
487 /* XOSListView:
488       extend with:
489          app - MarionetteApplication
490          childView - class of ItemView, probably an XOSItemView
491          template - template (see xosHelper.html)
492          collection - collection that holds these objects
493          title - title to display in template
494 */
495
496 XOSListView = Marionette.CompositeView.extend({
497              childViewContainer: 'tbody',
498
499              events: {"click button.btn-xos-add": "addClicked",
500                       "click button.btn-xos-refresh": "refreshClicked",
501                      },
502
503              _fetchStateChange: function() {
504                  if (this.collection.fetching) {
505                     $("#xos-list-title-spinner").show();
506                  } else {
507                     $("#xos-list-title-spinner").hide();
508                  }
509              },
510
511              addClicked: function(e) {
512                 e.preventDefault();
513                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
514              },
515
516              refreshClicked: function(e) {
517                  e.preventDefault();
518                  this.collection.refresh(refreshRelated=true);
519              },
520
521              initialize: function() {
522                  this.listenTo(this.collection, 'change', this._renderChildren)
523                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
524
525                  // Because many of the templates use idToName(), we need to
526                  // listen to the collections that hold the names for the ids
527                  // that we want to display.
528                  for (i in this.collection.foreignCollections) {
529                      foreignName = this.collection.foreignCollections[i];
530                      if (xos[foreignName] == undefined) {
531                          console.log("Failed to find xos class " + foreignName);
532                      }
533                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
534                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
535                  }
536              },
537
538              templateHelpers: function() {
539                 return { title: this.title };
540              },
541 });
542
543 /* Give an id, the name of a collection, and the name of a field for models
544    within that collection, lookup the id and return the value of the field.
545 */
546
547 idToName = function(id, collectionName, fieldName) {
548     linkedObject = xos[collectionName].get(id);
549     if (linkedObject == undefined) {
550         return "#" + id;
551     } else {
552         return linkedObject.attributes[fieldName];
553     }
554 };
555
556 /* Constructs lists of <option> html blocks for items in a collection.
557
558    selectedId = the id of an object that should be selected, if any
559    collectionName = name of collection
560    fieldName = name of field within models of collection that will be displayed
561 */
562
563 idToOptions = function(selectedId, collectionName, fieldName) {
564     result=""
565     for (index in xos[collectionName].models) {
566         linkedObject = xos[collectionName].models[index];
567         linkedId = linkedObject["id"];
568         linkedName = linkedObject.attributes[fieldName];
569         if (linkedId == selectedId) {
570             selected = " selected";
571         } else {
572             selected = "";
573         }
574         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
575     }
576     return result;
577 };
578
579 /* Constructs an html <select> and the <option>s to go with it.
580
581    variable = variable name to return to form
582    selectedId = the id of an object that should be selected, if any
583    collectionName = name of collection
584    fieldName = name of field within models of collection that will be displayed
585 */
586
587 idToSelect = function(variable, selectedId, collectionName, fieldName) {
588     result = '<select name="' + variable + '">' +
589              idToOptions(selectedId, collectionName, fieldName) +
590              '</select>';
591     return result;
592 }
593