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