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