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