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