humanReadableNames in logPanel
[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         limitTableRows(this.logTableId, 5);
120
121         return logMessageId;
122     },
123
124     hideLinkedItems: function(result) {
125         var index=0;
126         while (index<4) {\r
127             this["linkedObjs" + (index+1)].empty();\r
128             index = index + 1;\r
129         }\r
130     },\r
131 \r
132     listViewShower: function(listViewName, collection_name, regionName, title) {\r
133         var app=this;\r
134         return function() {\r
135             app[regionName].show(new app[listViewName]);\r
136             app.hideLinkedItems();\r
137             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));\r
138             $("#detail").show();\r
139             $("#xos-listview-button-box").show();\r
140             $("#tabs").hide();\r
141             $("#xos-detail-button-box").hide();\r
142         }\r
143     },\r
144 \r
145     addShower: function(detailName, collection_name, regionName, title) {\r
146         var app=this;\r
147         return function() {\r
148             model = new xos[collection_name].model();\r
149             detailViewClass = app[detailName];\r
150             detailView = new detailViewClass({model: model, collection:xos[collection_name]});\r
151             app[regionName].show(detailView);\r
152             $("#xos-detail-button-box").show();\r
153             $("#xos-listview-button-box").hide();\r
154         }\r
155     },\r
156 \r
157     detailShower: function(detailName, collection_name, regionName, title) {\r
158         var app=this;\r
159         showModelId = function(model_id) {\r
160             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));\r
161 \r
162             collection = xos[collection_name];\r
163             model = collection.get(model_id);\r
164             if (model == undefined) {\r
165                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));\r
166             } else {\r
167                 detailViewClass = app[detailName];\r
168                 detailView = new detailViewClass({model: model});\r
169                 app[regionName].show(detailView);\r
170                 detailView.showLinkedItems();\r
171                 $("#xos-detail-button-box").show();\r
172                 $("#xos-listview-button-box").hide();\r
173             }\r
174         }\r
175         return showModelId;\r
176     },\r
177 });
178
179 /* XOSDetailView
180       extend with:
181          app - MarionetteApplication
182          template - template (See XOSHelper.html)
183 */
184
185 XOSDetailView = Marionette.ItemView.extend({
186             tagName: "div",
187
188             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
189                      "click button.btn-xos-save-leave": "submitLeaveClicked",
190                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
191                      "click button.btn-xos-delete": "deleteClicked",
192                      "change input": "inputChanged"},
193
194             initialize: function() {
195                 this.on('deleteConfirmed', this.deleteConfirmed);
196             },
197
198             /* inputChanged is watching the onChange events of the input controls. We
199                do this to track when this view is 'dirty', so we can throw up a warning\r
200                if the user tries to change his slices without saving first.\r
201             */\r
202 \r
203             inputChanged: function(e) {\r
204                 this.dirty = true;\r
205             },\r
206 \r
207             saveError: function(model, result, xhr, infoMsgId) {\r
208                 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;\r
209                 result["infoMsgId"] = infoMsgId;\r
210                 this.app.showError(result);\r
211             },\r
212 \r
213             saveSuccess: function(model, result, xhr, infoMsgId) {\r
214                 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};\r
215                 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;\r
216                 result["infoMsgId"] = infoMsgId;\r
217                 this.app.showSuccess(result);\r
218             },
219
220             destroyError: function(model, result, xhr, infoMsgId) {
221                 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;\r
222                 result["infoMsgId"] = infoMsgId;\r
223                 this.app.showError(result);\r
224             },\r
225 \r
226             destroySuccess: function(model, result, xhr, infoMsgId) {\r
227                 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};\r
228                 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;\r
229                 result["infoMsgId"] = infoMsgId;\r
230                 this.app.showSuccess(result);\r
231             },
232
233             submitContinueClicked: function(e) {
234                 console.log("saveContinue");
235                 e.preventDefault();
236                 this.save();
237             },
238
239             submitLeaveClicked: function(e) {
240                 console.log("saveLeave");
241                 e.preventDefault();
242                 this.save();
243                 this.app.navigate("list", this.model.modelName);
244             },
245
246             submitAddAnotherClicked: function(e) {
247                 console.log("saveAnother");
248                 e.preventDefault();
249                 this.save();
250                 this.app.navigate("add", this.model.modelName);
251             },
252
253             save: function() {
254                 this.app.hideError();
255                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );\r
256                 var data = Backbone.Syphon.serialize(this);\r
257                 var that = this;\r
258                 var isNew = !this.model.id;\r
259                 this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},\r
260                                        success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId);}});\r
261                 if (isNew) {\r
262                     console.log(this.model);\r
263                     this.collection.add(this.model);\r
264                     this.collection.sort();\r
265                 }\r
266                 this.dirty = false;\r
267             },
268
269             destroyModel: function() {
270                  this.app.hideError();
271                  var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
272                  var that = this;
273                  this.model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
274                                      success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
275             },
276
277              deleteClicked: function(e) {
278                  e.preventDefault();
279 \r                 this.app.confirmDialog(this, "deleteConfirmed");
280 \r             },
281 \r
282 \r             deleteConfirmed: function() {
283 \r                 modelName = this.model.modelName;
284 \r                 this.destroyModel();
285 \r                 this.app.navigate("list", modelName);
286 \r             },
287 \r
288             tabClick: function(tabId, regionName) {
289                     region = this.app[regionName];\r
290                     if (this.currentTabRegion != undefined) {\r
291                         this.currentTabRegion.$el.hide();\r
292                     }\r
293                     if (this.currentTabId != undefined) {\r
294                         $(this.currentTabId).removeClass('active');\r
295                     }\r
296                     this.currentTabRegion = region;\r
297                     this.currentTabRegion.$el.show();\r
298 \r
299                     this.currentTabId = tabId;\r
300                     $(tabId).addClass('active');\r
301             },
302
303             showTabs: function(tabs) {
304                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
305                 $("#tabs").html(template(tabs));
306                 var that = this;
307
308                 _.each(tabs, function(tab) {
309                     var regionName = tab["region"];
310                     var tabId = '#xos-nav-'+regionName;
311                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
312                 });
313
314                 $("#tabs").show();
315             },
316
317             showLinkedItems: function() {
318                     tabs=[];
319
320                     tabs.push({name: "details", region: "detail"});
321
322                     var index=0;
323                     for (relatedName in this.model.collection.relatedCollections) {\r
324                         relatedField = this.model.collection.relatedCollections[relatedName];\r
325                         regionName = "linkedObjs" + (index+1);\r
326 \r
327                         relatedListViewClassName = relatedName + "ListView";\r
328                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");\r
329                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});\r
330                         this.app[regionName].show(new relatedListViewClass());\r
331                         if (this.app.hideTabsByDefault) {\r
332                             this.app[regionName].$el.hide();\r
333                         }\r
334                         tabs.push({name: relatedName, region: regionName});\r
335                         index = index + 1;\r
336                     }\r
337 \r
338                     while (index<4) {\r
339                         this.app["linkedObjs" + (index+1)].empty();\r
340                         index = index + 1;\r
341                     }\r
342 \r
343                     this.showTabs(tabs);\r
344                     this.tabClick('#xos-nav-detail', 'detail');\r
345               },\r
346 \r
347 });\r
348
349 /* XOSItemView
350       This is for items that will be displayed as table rows.
351       extend with:
352          app - MarionetteApplication
353          template - template (See XOSHelper.html)
354          detailClass - class of detail view, probably an XOSDetailView
355 */
356
357 XOSItemView = Marionette.ItemView.extend({
358              tagName: 'tr',
359              className: 'test-tablerow',
360
361              events: {"click": "changeItem"},
362
363              changeItem: function(e) {\r
364                     this.app.hideError();\r
365                     e.preventDefault();\r
366                     e.stopPropagation();\r
367 \r
368                     this.app.navigateToModel(this.app, this.detailClass, this.detailNavLink, this.model);\r
369              },\r
370 });
371
372 /* XOSListView:
373       extend with:
374          app - MarionetteApplication
375          childView - class of ItemView, probably an XOSItemView
376          template - template (see xosHelper.html)
377          collection - collection that holds these objects
378          title - title to display in template
379 */
380
381 XOSListView = Marionette.CompositeView.extend({
382              childViewContainer: 'tbody',\r
383 \r
384              events: {"click button.btn-xos-add": "addClicked",\r
385                       "click button.btn-xos-refresh": "refreshClicked",\r
386                      },\r
387 \r
388              _fetchStateChange: function() {\r
389                  if (this.collection.fetching) {\r
390                     $("#xos-list-title-spinner").show();\r
391                  } else {\r
392                     $("#xos-list-title-spinner").hide();\r
393                  }\r
394              },\r
395 \r
396              addClicked: function(e) {
397                 e.preventDefault();
398                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
399              },
400 \r
401 \r             refreshClicked: function(e) {
402 \r                 e.preventDefault();
403 \r                 this.collection.refresh(refreshRelated=true);
404 \r             },
405 \r
406 \r             initialize: function() {
407 \r                 this.listenTo(this.collection, 'change', this._renderChildren)
408                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
409
410                  // Because many of the templates use idToName(), we need to
411                  // listen to the collections that hold the names for the ids
412                  // that we want to display.
413                  for (i in this.collection.foreignCollections) {
414                      foreignName = this.collection.foreignCollections[i];
415                      if (xos[foreignName] == undefined) {
416                          console.log("Failed to find xos class " + foreignName);
417                      }
418                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
419                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
420                  }
421              },
422
423              templateHelpers: function() {
424                 return { title: this.title };
425              },\r
426 });
427
428 /* Give an id, the name of a collection, and the name of a field for models
429    within that collection, lookup the id and return the value of the field.
430 */
431
432 idToName = function(id, collectionName, fieldName) {
433     linkedObject = xos[collectionName].get(id);
434     if (linkedObject == undefined) {
435         return "#" + id;
436     } else {
437         return linkedObject.attributes[fieldName];
438     }
439 };
440
441 /* Constructs lists of <option> html blocks for items in a collection.
442
443    selectedId = the id of an object that should be selected, if any
444    collectionName = name of collection
445    fieldName = name of field within models of collection that will be displayed
446 */
447
448 idToOptions = function(selectedId, collectionName, fieldName) {
449     result=""
450     for (index in xos[collectionName].models) {
451         linkedObject = xos[collectionName].models[index];
452         linkedId = linkedObject["id"];
453         linkedName = linkedObject.attributes[fieldName];
454         if (linkedId == selectedId) {
455             selected = " selected";
456         } else {
457             selected = "";
458         }
459         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
460     }
461     return result;
462 };
463
464 /* Constructs an html <select> and the <option>s to go with it.
465
466    variable = variable name to return to form
467    selectedId = the id of an object that should be selected, if any
468    collectionName = name of collection
469    fieldName = name of field within models of collection that will be displayed
470 */
471
472 idToSelect = function(variable, selectedId, collectionName, fieldName) {
473     result = '<select name="' + variable + '">' +
474              idToOptions(selectedId, collectionName, fieldName) +
475              '</select>';
476     return result;
477 }
478