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