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