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