fix issue with delete dialog
[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,
34            modal: true,
35            buttons : {
36                 "Confirm" : function() {
37                   $(this).dialog("close");
38                   if (event) {
39                       view.trigger(event);
40                   }
41                   if (callback) {
42                       callback();
43                   }
44                 },
45                 "Cancel" : function() {
46                   $(this).dialog("close");
47                 }
48               }
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) {
80             this["linkedObjs" + (index+1)].empty();
81             index = index + 1;
82         }
83     },
84
85     listViewShower: function(listViewName, collection_name, regionName, title) {
86         var app=this;
87         return function() {
88             app[regionName].show(new app[listViewName]);
89             app.hideLinkedItems();
90             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
91             $("#detail").show();
92             $("#xos-listview-button-box").show();
93             $("#tabs").hide();
94             $("#xos-detail-button-box").hide();
95         }
96     },
97
98     addShower: function(detailName, collection_name, regionName, title) {
99         var app=this;
100         return function() {
101             model = new xos[collection_name].model();
102             detailViewClass = app[detailName];
103             detailView = new detailViewClass({model: model, collection:xos[collection_name]});
104             app[regionName].show(detailView);
105             $("#xos-detail-button-box").show();
106             $("#xos-listview-button-box").hide();
107         }
108     },
109
110     detailShower: function(detailName, collection_name, regionName, title) {
111         var app=this;
112         showModelId = function(model_id) {
113             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
114
115             collection = xos[collection_name];
116             model = collection.get(model_id);
117             if (model == undefined) {
118                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
119             } else {
120                 detailViewClass = app[detailName];
121                 detailView = new detailViewClass({model: model});
122                 app[regionName].show(detailView);
123                 detailView.showLinkedItems();
124                 $("#xos-detail-button-box").show();
125                 $("#xos-listview-button-box").hide();
126             }
127         }
128         return showModelId;
129     },
130
131     /* error handling callbacks */
132
133     hideError: function() {
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     },
154
155     showError: function(result) {
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     },
170
171     showInformational: function(result) {
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     },
210
211     saveError: function(model, result, xhr, infoMsgId) {
212         console.log("saveError");
213         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
214         result["infoMsgId"] = infoMsgId;
215         this.showError(result);
216     },
217
218     saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
219         console.log("saveSuccess");
220         if (addToCollection) {
221             addToCollection.add(model);
222             addToCollection.sort();
223         }
224         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
225         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
226         result["infoMsgId"] = infoMsgId;
227         this.showSuccess(result);
228     },
229
230     destroyError: function(model, result, xhr, infoMsgId) {
231         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
232         result["infoMsgId"] = infoMsgId;
233         this.showError(result);
234     },
235
236     destroySuccess: function(model, result, xhr, infoMsgId) {
237         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
238         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
239         result["infoMsgId"] = infoMsgId;
240         this.showSuccess(result);
241     },
242
243     /* end error handling callbacks */
244
245     destroyModel: function(model) {
246          //console.log("destroyModel"); console.log(model);
247          this.hideError();
248          var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
249          var that = this;
250          model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
251                         success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
252     },
253
254     deleteDialog: function(model, navToListAfterDelete) {
255         var that=this;
256         //console.log("deleteDialog"); console.log(model);
257         this.confirmDialog(null, null, function() {
258             //console.log("deleteConfirm"); console.log(model);
259             modelName = model.modelName;
260             that.destroyModel(model);
261             if (navToListAfterDelete) {
262                 that.navigate("list", modelName);
263             }
264
265         });
266     },
267 });
268
269 /* XOSDetailView
270       extend with:
271          app - MarionetteApplication
272          template - template (See XOSHelper.html)
273 */
274
275 XOSDetailView = Marionette.ItemView.extend({
276             tagName: "div",
277
278             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
279                      "click button.btn-xos-save-leave": "submitLeaveClicked",
280                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
281                      "click button.btn-xos-delete": "deleteClicked",
282                      "change input": "inputChanged"},
283
284             /*initialize: function() {
285                 this.on('deleteConfirmed', this.deleteConfirmed);
286             },*/
287
288             /* inputChanged is watching the onChange events of the input controls. We
289                do this to track when this view is 'dirty', so we can throw up a warning
290                if the user tries to change his slices without saving first.
291             */
292
293             inputChanged: function(e) {
294                 this.dirty = true;
295             },
296
297             submitContinueClicked: function(e) {
298                 console.log("saveContinue");
299                 e.preventDefault();
300                 this.save();
301             },
302
303             submitLeaveClicked: function(e) {
304                 console.log("saveLeave");
305                 e.preventDefault();
306                 this.save();
307                 this.app.navigate("list", this.model.modelName);
308             },
309
310             submitAddAnotherClicked: function(e) {
311                 console.log("saveAnother");
312                 e.preventDefault();
313                 this.save();
314                 this.app.navigate("add", this.model.modelName);
315             },
316
317             save: function() {
318                 this.app.hideError();
319                 var data = Backbone.Syphon.serialize(this);
320                 var that = this;
321                 var isNew = !this.model.id;
322
323                 this.$el.find(".help-inline").remove();
324
325                 /* although model.validate() is called automatically by
326                    model.save, we call it ourselves, so we can throw up our
327                    validation error before creating the infoMsg in the log
328                 */
329                 errors =  this.model.xosValidate(data);
330                 if (errors) {
331                     this.onFormDataInvalid(errors);
332                     return;
333                 }
334
335                 if (isNew) {
336                     this.model.attributes.humanReadableName = "new " + model.modelName;
337                     this.model.addToCollection = this.collection;
338                 } else {
339                     this.model.addToCollection = undefined;
340                 }
341
342                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
343
344                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
345                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);}});
346                 this.dirty = false;
347             },
348
349             /*destroyModel: function() {
350                  this.app.hideError();
351                  var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
352                  var that = this;
353                  this.model.destroy({error: function(model, result, xhr) { that.app.destroyError(model,result,xhr,infoMsgId);},
354                                      success: function(model, result, xhr) { that.app.destroySuccess(model,result,xhr,infoMsgId);}});
355             },
356
357              deleteClicked: function(e) {
358                  e.preventDefault();
359                  this.app.confirmDialog(this, "deleteConfirmed");
360              },
361
362              deleteConfirmed: function() {
363                  modelName = this.model.modelName;
364                  this.destroyModel();
365                  this.app.navigate("list", modelName);
366              }, */
367
368              deleteClicked: function(e) {
369                  e.preventDefault();
370                  this.app.deleteDialog(this.model, true);
371              },
372
373             tabClick: function(tabId, regionName) {
374                     region = this.app[regionName];
375                     if (this.currentTabRegion != undefined) {
376                         this.currentTabRegion.$el.hide();
377                     }
378                     if (this.currentTabId != undefined) {
379                         $(this.currentTabId).removeClass('active');
380                     }
381                     this.currentTabRegion = region;
382                     this.currentTabRegion.$el.show();
383
384                     this.currentTabId = tabId;
385                     $(tabId).addClass('active');
386             },
387
388             showTabs: function(tabs) {
389                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
390                 $("#tabs").html(template(tabs));
391                 var that = this;
392
393                 _.each(tabs, function(tab) {
394                     var regionName = tab["region"];
395                     var tabId = '#xos-nav-'+regionName;
396                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
397                 });
398
399                 $("#tabs").show();
400             },
401
402             showLinkedItems: function() {
403                     tabs=[];
404
405                     tabs.push({name: "details", region: "detail"});
406
407                     var index=0;
408                     for (relatedName in this.model.collection.relatedCollections) {
409                         relatedField = this.model.collection.relatedCollections[relatedName];
410                         regionName = "linkedObjs" + (index+1);
411
412                         relatedListViewClassName = relatedName + "ListView";
413                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
414                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});
415                         this.app[regionName].show(new relatedListViewClass());
416                         if (this.app.hideTabsByDefault) {
417                             this.app[regionName].$el.hide();
418                         }
419                         tabs.push({name: relatedName, region: regionName});
420                         index = index + 1;
421                     }
422
423                     while (index<4) {
424                         this.app["linkedObjs" + (index+1)].empty();
425                         index = index + 1;
426                     }
427
428                     this.showTabs(tabs);
429                     this.tabClick('#xos-nav-detail', 'detail');
430               },
431
432             onFormDataInvalid: function(errors) {
433                 var self=this;
434                 var markErrors = function(value, key) {
435                     console.log("name='" + key + "'");
436                     var $inputElement = self.$el.find("[name='" + key + "']");
437                     var $inputContainer = $inputElement.parent();
438                     //$inputContainer.find(".help-inline").remove();
439                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
440                     $inputContainer.append($errorEl).addClass("error");
441                 }
442                 _.each(errors, markErrors);
443             },
444
445 });
446
447 /* XOSItemView
448       This is for items that will be displayed as table rows.
449       extend with:
450          app - MarionetteApplication
451          template - template (See XOSHelper.html)
452          detailClass - class of detail view, probably an XOSDetailView
453 */
454
455 XOSItemView = Marionette.ItemView.extend({
456              tagName: 'tr',
457              className: 'test-tablerow',
458
459              templateHelpers: function() { return { modelName: this.model.modelName,
460                                                     collectionName: this.model.collectionName,
461                                          }},
462 });
463
464 /* XOSListView:
465       extend with:
466          app - MarionetteApplication
467          childView - class of ItemView, probably an XOSItemView
468          template - template (see xosHelper.html)
469          collection - collection that holds these objects
470          title - title to display in template
471 */
472
473 XOSListView = Marionette.CompositeView.extend({
474              childViewContainer: 'tbody',
475
476              events: {"click button.btn-xos-add": "addClicked",
477                       "click button.btn-xos-refresh": "refreshClicked",
478                      },
479
480              _fetchStateChange: function() {
481                  if (this.collection.fetching) {
482                     $("#xos-list-title-spinner").show();
483                  } else {
484                     $("#xos-list-title-spinner").hide();
485                  }
486              },
487
488              addClicked: function(e) {
489                 e.preventDefault();
490                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
491              },
492
493              refreshClicked: function(e) {
494                  e.preventDefault();
495                  this.collection.refresh(refreshRelated=true);
496              },
497
498              initialize: function() {
499                  this.listenTo(this.collection, 'change', this._renderChildren)
500                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
501
502                  // Because many of the templates use idToName(), we need to
503                  // listen to the collections that hold the names for the ids
504                  // that we want to display.
505                  for (i in this.collection.foreignCollections) {
506                      foreignName = this.collection.foreignCollections[i];
507                      if (xos[foreignName] == undefined) {
508                          console.log("Failed to find xos class " + foreignName);
509                      }
510                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
511                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
512                  }
513              },
514
515              templateHelpers: function() {
516                 return { title: this.title };
517              },
518 });
519
520 /* Give an id, the name of a collection, and the name of a field for models
521    within that collection, lookup the id and return the value of the field.
522 */
523
524 idToName = function(id, collectionName, fieldName) {
525     linkedObject = xos[collectionName].get(id);
526     if (linkedObject == undefined) {
527         return "#" + id;
528     } else {
529         return linkedObject.attributes[fieldName];
530     }
531 };
532
533 /* Constructs lists of <option> html blocks for items in a collection.
534
535    selectedId = the id of an object that should be selected, if any
536    collectionName = name of collection
537    fieldName = name of field within models of collection that will be displayed
538 */
539
540 idToOptions = function(selectedId, collectionName, fieldName) {
541     result=""
542     for (index in xos[collectionName].models) {
543         linkedObject = xos[collectionName].models[index];
544         linkedId = linkedObject["id"];
545         linkedName = linkedObject.attributes[fieldName];
546         if (linkedId == selectedId) {
547             selected = " selected";
548         } else {
549             selected = "";
550         }
551         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
552     }
553     return result;
554 };
555
556 /* Constructs an html <select> and the <option>s to go with it.
557
558    variable = variable name to return to form
559    selectedId = the id of an object that should be selected, if any
560    collectionName = name of collection
561    fieldName = name of field within models of collection that will be displayed
562 */
563
564 idToSelect = function(variable, selectedId, collectionName, fieldName) {
565     result = '<select name="' + variable + '">' +
566              idToOptions(selectedId, collectionName, fieldName) +
567              '</select>';
568     return result;
569 }
570