help text support for detail view, tenant view warnings about changing 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 SliceSelectorOption = Marionette.ItemView.extend({
8     template: "#xos-sliceselector-option",
9     tagName: "option",
10     attributes: function() {
11         if (this.options.selectedID == this.model.get("id")) {
12             return { value: this.model.get("id"), selected: 1 };
13         } else {
14             return { value: this.model.get("id") };
15         }
16     },
17 });
18
19 SliceSelectorView = Marionette.CompositeView.extend({
20     template: "#xos-sliceselector-select",
21     childViewContainer: "select",
22     childView: SliceSelectorOption,
23     caption: "Slice",
24
25     events: {"change select": "onSliceChanged"},
26
27     childViewOptions: function() {
28         return { selectedID: this.options.selectedID || this.selectedID || null };
29     },
30
31     onSliceChanged: function() {
32         this.sliceChanged(this.$el.find("select").val());
33     },
34
35     sliceChanged: function(id) {
36         console.log("sliceChanged " + id);
37     },
38
39     templateHelpers: function() { return {caption: this.options.caption || this.caption }; },
40 });
41
42 FilteredCompositeView = Marionette.CompositeView.extend( {
43     showCollection: function() {
44       var ChildView;
45       this.collection.each(function(child, index) {
46         if (this.filter && !this.filter(child)) {
47             return;
48         }
49         ChildView = this.getChildView(child);
50         this.addChild(child, ChildView, index);
51       }, this);
52
53     },
54 });
55
56 XOSRouter = Marionette.AppRouter.extend({
57         initialize: function() {\r
58             this.routeStack=[];\r
59         },\r
60 \r
61         onRoute: function(x,y,z) {\r
62              this.routeStack.push(Backbone.history.fragment);\r
63              this.routeStack = this.routeStack.slice(-32);   // limit the size of routeStack to something reasonable\r
64         },\r
65 \r
66         prevPage: function() {\r
67              return this.routeStack.slice(-1)[0];
68         },
69
70         showPreviousURL: function() {
71             prevPage = this.prevPage();
72             //console.log("showPreviousURL");
73             //console.log(this.routeStack);
74             if (prevPage) {
75                 this.navigate("#"+prevPage, {trigger: false, replace: true} );
76             }
77         },
78
79         navigate: function(href, options) {
80             if (options.force) {
81                 Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true});
82             }
83             Marionette.AppRouter.prototype.navigate.call(this, href, options);
84         },
85     });\r
86 \r
87 // XXX - We import backbone multiple times (BAD!) since the import happens\r
88 //   inside of the view's html. The second time it's imported (developer\r
89 //   view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for\r
90 //   now.\r
91 Backbone_Syphon = Backbone.Syphon\r
92 Backbone_Syphon.InputReaders.register('select', function(el) {\r
93     // Modify syphon so that if a select has "syphonall" in the class, then
94     // the value of every option will be returned, regardless of whether of
95     // not it is selected.
96     if (el.hasClass("syphonall")) {
97         result = [];
98         _.each(el.find("option"), function(option) {
99             result.push($(option).val());
100         });
101         return result;
102     }
103     return el.val();
104 });
105
106 XOSApplication = Marionette.Application.extend({
107     detailBoxId: "#detailBox",
108     errorBoxId: "#errorBox",
109     errorCloseButtonId: "#close-error-box",
110     successBoxId: "#successBox",
111     successCloseButtonId: "#close-success-box",
112     errorTemplate: "#xos-error-template",
113     successTemplate: "#xos-success-template",
114     logMessageCount: 0,
115
116     confirmDialog: function(view, event, callback) {
117         $("#xos-confirm-dialog").dialog({
118            autoOpen: false,
119            modal: true,
120            buttons : {
121                 "Confirm" : function() {
122                   $(this).dialog("close");
123                   if (event) {
124                       view.trigger(event);
125                   }
126                   if (callback) {
127                       callback();
128                   }
129                 },
130                 "Cancel" : function() {
131                   $(this).dialog("close");
132                 }
133               }
134             });
135         $("#xos-confirm-dialog").dialog("open");
136     },
137
138     popupErrorDialog: function(responseText) {
139         try {
140             parsed_error=$.parseJSON(responseText);
141             width=300;
142         }
143         catch(err) {
144             parsed_error=undefined;
145             width=640;    // django stacktraces like wide width
146         }
147         console.log(responseText);
148         console.log(parsed_error);
149         if (parsed_error) {
150             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
151         } else {
152             $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
153         }
154
155         $("#xos-error-dialog").dialog({
156             modal: true,
157             width: width,
158             buttons: {
159                 Ok: function() { $(this).dialog("close"); }
160             }
161         });
162     },
163
164     hideLinkedItems: function(result) {
165         var index=0;
166         while (index<4) {
167             this["linkedObjs" + (index+1)].empty();
168             index = index + 1;
169         }
170     },
171
172     hideTabs: function() { $("#tabs").hide(); },
173     showTabs: function() { $("#tabs").show(); },
174
175     createListHandler: function(listViewName, collection_name, regionName, title) {
176         var app=this;
177         return function() {
178             listView = new app[listViewName];
179             app[regionName].show(listView);
180             app.hideLinkedItems();
181             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
182             $("#detail").show();
183             app.hideTabs();
184
185             listButtons = new XOSListButtonView({linkedView: listView});
186             app["rightButtonPanel"].show(listButtons);
187         }
188     },
189
190     createAddHandler: function(detailName, collection_name, regionName, title) {
191         var app=this;
192         return function() {
193             console.log("addHandler");
194
195             app.hideLinkedItems();
196             app.hideTabs();
197
198             model = new xos[collection_name].model();
199             detailViewClass = app[detailName];
200             detailView = new detailViewClass({model: model, collection:xos[collection_name]});
201             app[regionName].show(detailView);
202
203             detailButtons = new XOSDetailButtonView({linkedView: detailView});
204             app["rightButtonPanel"].show(detailButtons);
205         }
206     },
207
208     createAddChildHandler: function(addChildName, collection_name) {
209         var app=this;
210         return function(parent_modelName, parent_fieldName, parent_id) {
211             app.Router.showPreviousURL();
212             model = new xos[collection_name].model();
213             model.attributes[parent_fieldName] = parent_id;
214             model.readOnlyFields.push(parent_fieldName);
215             detailViewClass = app[addChildName];
216             var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
217             detailView.dialog = $("xos-addchild-dialog");
218             app["addChildDetail"].show(detailView);
219             $("#xos-addchild-dialog").dialog({
220                autoOpen: false,
221                modal: true,
222                width: 640,
223                buttons : {
224                     "Save" : function() {
225                       var addDialog = this;
226                       detailView.synchronous = true;
227                       detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); }
228                       detailView.save();
229
230                       //$(this).dialog("close");
231                     },
232                     "Cancel" : function() {
233                       $(this).dialog("close");
234                     }
235                   }
236                 });
237             $("#xos-addchild-dialog").dialog("open");
238         }
239     },
240
241     createDeleteHandler: function(collection_name) {
242         var app=this;
243         return function(model_id) {
244             console.log("deleteCalled");
245             collection = xos[collection_name];
246             model = collection.get(model_id);
247             assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
248             app.Router.showPreviousURL();
249             app.deleteDialog(model);
250         }
251     },
252
253     createDetailHandler: function(detailName, collection_name, regionName, title) {
254         var app=this;
255         showModelId = function(model_id) {
256             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
257
258             collection = xos[collection_name];
259             model = collection.get(model_id);
260             if (model == undefined) {
261                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
262             } else {
263                 detailViewClass = app[detailName];
264                 detailView = new detailViewClass({model: model});
265                 app[regionName].show(detailView);
266                 detailView.showLinkedItems();
267
268                 detailButtons = new XOSDetailButtonView({linkedView: detailView});
269                 app["rightButtonPanel"].show(detailButtons);
270             }
271         }
272         return showModelId;
273     },
274
275     /* error handling callbacks */
276
277     hideError: function() {
278         if (this.logWindowId) {
279         } else {
280             $(this.errorBoxId).hide();
281             $(this.successBoxId).hide();
282         }
283     },
284
285     showSuccess: function(result) {
286          result["statusclass"] = "success";
287          if (this.logTableId) {
288              this.appendLogWindow(result);
289          } else {
290              $(this.successBoxId).show();
291              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
292              var that=this;
293              $(this.successCloseButtonId).unbind().bind('click', function() {
294                  $(that.successBoxId).hide();
295              });
296          }
297     },
298
299     showError: function(result) {
300          result["statusclass"] = "failure";
301          if (this.logTableId) {
302              this.appendLogWindow(result);
303              this.popupErrorDialog(result.responseText);
304          } else {
305              // this is really old stuff
306              $(this.errorBoxId).show();
307              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
308              var that=this;
309              $(this.errorCloseButtonId).unbind().bind('click', function() {
310                  $(that.errorBoxId).hide();
311              });
312          }
313     },
314
315     showInformational: function(result) {
316          result["statusclass"] = "inprog";
317          if (this.logTableId) {
318              return this.appendLogWindow(result);
319          } else {
320              return undefined;
321          }
322     },
323
324     appendLogWindow: function(result) {
325         // compute a new logMessageId for this log message
326         logMessageId = "logMessage" + this.logMessageCount;
327         this.logMessageCount = this.logMessageCount + 1;
328         result["logMessageId"] = logMessageId;
329
330         logMessageTemplate=$("#xos-log-template").html();
331         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
332         newRow = _.template(logMessageTemplate, result);
333         assert(newRow != undefined, "newRow is undefined");
334
335         if (result["infoMsgId"] != undefined) {
336             // We were passed the logMessageId of an informational message,
337             // and the caller wants us to replace that message with our own.
338             // i.e. replace an informational message with a success or an error.
339             $("#"+result["infoMsgId"]).replaceWith(newRow);
340         } else {
341             // Create a brand new log message rather than replacing one.
342             logTableBody = $(this.logTableId + " tbody");
343             logTableBody.prepend(newRow);
344         }
345
346         if (this.statusMsgId) {
347             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
348         }
349
350         limitTableRows(this.logTableId, 5);
351
352         return logMessageId;
353     },
354
355     saveError: function(model, result, xhr, infoMsgId) {
356         console.log("saveError");
357         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
358         result["infoMsgId"] = infoMsgId;
359         this.showError(result);
360     },
361
362     saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
363         console.log("saveSuccess");
364         if (model.addToCollection) {
365             console.log("addToCollection");
366             console.log(model.addToCollection);
367             model.addToCollection.add(model);
368             model.addToCollection.sort();
369             model.addToCollection = undefined;
370         }
371         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
372         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
373         result["infoMsgId"] = infoMsgId;
374         this.showSuccess(result);
375     },
376
377     destroyError: function(model, result, xhr, infoMsgId) {
378         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
379         result["infoMsgId"] = infoMsgId;
380         this.showError(result);
381     },
382
383     destroySuccess: function(model, result, xhr, infoMsgId) {
384         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
385         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
386         result["infoMsgId"] = infoMsgId;
387         this.showSuccess(result);
388     },
389
390     /* end error handling callbacks */
391
392     destroyModel: function(model) {
393          //console.log("destroyModel"); console.log(model);
394          this.hideError();
395          var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
396          var that = this;
397          model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
398                         success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
399     },
400
401     deleteDialog: function(model, afterDelete) {
402         var that=this;
403         assert(model!=undefined, "deleteDialog's model is undefined");
404         //console.log("deleteDialog"); console.log(model);
405         this.confirmDialog(null, null, function() {
406             //console.log("deleteConfirm"); console.log(model);
407             modelName = model.modelName;
408             that.destroyModel(model);
409             if (afterDelete=="list") {
410                 that.navigate("list", modelName);
411             } else if (afterDelete) {
412                 afterDelete();
413             }
414         });
415     },
416 });
417
418 XOSButtonView = Marionette.ItemView.extend({
419             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
420                      "click button.btn-xos-save-leave": "submitLeaveClicked",
421                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
422                      "click button.btn-xos-delete": "deleteClicked",
423                      "click button.btn-xos-add": "addClicked",
424                      "click button.btn-xos-refresh": "refreshClicked",
425                      },
426
427             submitLeaveClicked: function(e) {
428                      this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
429                      },
430
431             submitContinueClicked: function(e) {
432                      this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
433                      },
434
435             submitAddAnotherClicked: function(e) {
436                      this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
437                      },
438
439             submitDeleteClicked: function(e) {
440                      this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
441                      },
442
443             addClicked: function(e) {
444                      this.options.linkedView.addClicked.call(this.options.linkedView, e);
445                      },
446
447             refreshClicked: function(e) {
448                      this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
449                      },
450             });
451
452 XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
453 XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });
454
455 /* XOSDetailView
456       extend with:
457          app - MarionetteApplication
458          template - template (See XOSHelper.html)
459 */
460
461 XOSDetailView = Marionette.ItemView.extend({
462             tagName: "div",
463
464             viewInitializers: [],
465
466             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
467                      "click button.btn-xos-save-leave": "submitLeaveClicked",
468                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
469                      "click button.btn-xos-delete": "deleteClicked",
470                      "change input": "inputChanged"},
471
472             /* inputChanged is watching the onChange events of the input controls. We
473                do this to track when this view is 'dirty', so we can throw up a warning
474                if the user tries to change his slices without saving first.
475             */
476
477             initialize: function() {
478                 this.on("saveSuccess", this.onSaveSuccess);
479                 this.synchronous = false;
480             },
481
482             onShow: function() {
483                 _.each(this.viewInitializers, function(initializer) {
484                     initializer();
485                 });
486             },
487
488             saveSuccess: function(e) {
489                 // always called after a save succeeds
490             },
491
492             afterSave: function(e) {
493                 // if this.synchronous, then called after the save succeeds
494                 // if !this.synchronous, then called after save is initiated
495             },
496
497             onSaveSuccess: function(e) {
498                 this.saveSuccess(e);
499                 if (this.synchronous) {
500                     this.afterSave(e);
501                 }
502             },
503
504             inputChanged: function(e) {
505                 this.dirty = true;
506             },
507
508             submitContinueClicked: function(e) {
509                 console.log("saveContinue");
510                 e.preventDefault();
511                 this.afterSave = function() { };
512                 this.save();
513             },
514
515             submitLeaveClicked: function(e) {
516                 console.log("saveLeave");
517                 e.preventDefault();
518                 if (this.options.noSubmitButton || this.noSubmitButton) {
519                     return;
520                 }
521                 var that=this;
522                 this.afterSave = function() {
523                     that.app.navigate("list", that.model.modelName);
524                 }
525                 this.save();
526             },
527
528             submitAddAnotherClicked: function(e) {
529                 console.log("saveAnother");
530                 console.log(this);
531                 e.preventDefault();
532                 var that=this;
533                 this.afterSave = function() {
534                     console.log("addAnother afterSave");
535                     that.app.navigate("add", that.model.modelName);
536                 }
537                 this.save();
538             },
539
540             save: function() {
541                 this.app.hideError();
542                 var data = Backbone_Syphon.serialize(this);
543                 var that = this;
544                 var isNew = !this.model.id;
545
546                 console.log(data);
547
548                 this.$el.find(".help-inline").remove();
549
550                 /* although model.validate() is called automatically by
551                    model.save, we call it ourselves, so we can throw up our
552                    validation error before creating the infoMsg in the log
553                 */
554                 errors =  this.model.xosValidate(data);
555                 if (errors) {
556                     this.onFormDataInvalid(errors);
557                     return;
558                 }
559
560                 if (isNew) {
561                     this.model.attributes.humanReadableName = "new " + this.model.modelName;
562                     this.model.addToCollection = this.collection;
563                 } else {
564                     this.model.addToCollection = undefined;
565                 }
566
567                 var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
568
569                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
570                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
571                                                                                that.trigger("saveSuccess");
572                                                                              }});
573                 this.dirty = false;
574
575                 if (!this.synchronous) {
576                     this.afterSave();
577                 }
578             },
579
580             deleteClicked: function(e) {
581                 e.preventDefault();
582                 this.app.deleteDialog(this.model, "list");
583             },
584
585             tabClick: function(tabId, regionName) {
586                     region = this.app[regionName];
587                     if (this.currentTabRegion != undefined) {
588                         this.currentTabRegion.$el.hide();
589                     }
590                     if (this.currentTabId != undefined) {
591                         $(this.currentTabId).removeClass('active');
592                     }
593                     this.currentTabRegion = region;
594                     this.currentTabRegion.$el.show();
595
596                     this.currentTabId = tabId;
597                     $(tabId).addClass('active');
598             },
599
600             showTabs: function(tabs) {
601                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
602                 $("#tabs").html(template(tabs));
603                 var that = this;
604
605                 _.each(tabs, function(tab) {
606                     var regionName = tab["region"];
607                     var tabId = '#xos-nav-'+regionName;
608                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
609                 });
610
611                 $("#tabs").show();
612             },
613
614             showLinkedItems: function() {
615                     tabs=[];
616
617                     tabs.push({name: "details", region: "detail"});
618
619                     makeFilter = function(relatedField, relatedId) {
620                         return function(model) { return model.attributes[relatedField] == relatedId; }
621                     };
622
623                     var index=0;
624                     for (relatedName in this.model.collection.relatedCollections) {
625                         var relatedField = this.model.collection.relatedCollections[relatedName];
626                         var relatedId = this.model.id;
627                         regionName = "linkedObjs" + (index+1);
628
629                         relatedListViewClassName = relatedName + "ListView";
630                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
631                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
632                                                                                           filter: makeFilter(relatedField, relatedId),
633                                                                                           parentModel: this.model});
634                         this.app[regionName].show(new relatedListViewClass());
635                         if (this.app.hideTabsByDefault) {
636                             this.app[regionName].$el.hide();
637                         }
638                         tabs.push({name: relatedName, region: regionName});
639                         index = index + 1;
640                     }
641
642                     while (index<4) {
643                         this.app["linkedObjs" + (index+1)].empty();
644                         index = index + 1;
645                     }
646
647                     this.showTabs(tabs);
648                     this.tabClick('#xos-nav-detail', 'detail');
649               },
650
651             onFormDataInvalid: function(errors) {
652                 var self=this;
653                 var markErrors = function(value, key) {
654                     var $inputElement = self.$el.find("[name='" + key + "']");
655                     var $inputContainer = $inputElement.parent();
656                     //$inputContainer.find(".help-inline").remove();
657                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
658                     $inputContainer.append($errorEl).addClass("error");
659                 }
660                 _.each(errors, markErrors);
661             },
662
663              templateHelpers: function() { return { modelName: this.model.modelName,
664                                                     collectionName: this.model.collectionName,
665                                                     addFields: this.model.addFields,
666                                                     listFields: this.model.listFields,
667                                                     detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
668                                                     fieldDisplayNames: this.options.fieldDisplayNames || this.fieldDisplayNames || this.model.fieldDisplayNames || {},
669                                                     foreignFields: this.model.foreignFields,
670                                                     detailLinkFields: this.model.detailLinkFields,
671                                                     inputType: this.model.inputType,
672                                                     model: this.model,
673                                                     detailView: this,
674                                                     choices: this.options.choices || this.choices || this.model.choices || {},
675                                                     helpText: this.options.helpText || this.helpText || this.model.helpText || {},
676                                          }},
677 });
678
679 XOSDetailView_sliver = XOSDetailView.extend( {
680     events: $.extend(XOSDetailView.events,
681         {"change #field_deploymentNetwork": "onDeploymentNetworkChange"}
682     ),
683
684     onShow: function() {
685         // Note that this causes the selects to be updated a second time. The
686         // first time was when the template was originally invoked, and the
687         // selects will all have the full unfiltered set of candidates. Then
688         // onShow will fire, and we'll update them with the filtered values.
689         this.onDeploymentNetworkChange();
690     },
691
692     onDeploymentNetworkChange: function(e) {
693         var deploymentID = this.$el.find("#field_deploymentNetwork").val();
694
695         console.log("onDeploymentNetworkChange");
696         console.log(deploymentID);
697
698         filterFunc = function(model) { return (model.attributes.deployment==deploymentID); }
699         newSelect = idToSelect("node",
700                                this.model.attributes.node,
701                                this.model.foreignFields["node"],
702                                "humanReadableName",
703                                false,
704                                filterFunc);
705         this.$el.find("#field_node").html(newSelect);
706
707         filterFunc = function(model) { for (index in model.attributes.deployments) {
708                                           item=model.attributes.deployments[index];
709                                           if (item.toString()==deploymentID.toString()) return true;
710                                         };
711                                         return false;
712                                      }
713         newSelect = idToSelect("flavor",
714                                this.model.attributes.flavor,
715                                this.model.foreignFields["flavor"],
716                                "humanReadableName",
717                                false,
718                                filterFunc);
719         this.$el.find("#field_flavor").html(newSelect);
720
721         filterFunc = function(model) { for (index in xos.imageDeployments.models) {
722                                            imageDeployment = xos.imageDeployments.models[index];
723                                            if ((imageDeployment.attributes.deployment == deploymentID) && (imageDeployment.attributes.image == model.id)) {
724                                                return true;
725                                            }
726                                        }
727                                        return false;
728                                      };
729         newSelect = idToSelect("image",
730                                this.model.attributes.image,
731                                this.model.foreignFields["image"],
732                                "humanReadableName",
733                                false,
734                                filterFunc);
735         this.$el.find("#field_image").html(newSelect);
736     },
737 });
738
739 /* XOSItemView
740       This is for items that will be displayed as table rows.
741       extend with:
742          app - MarionetteApplication
743          template - template (See XOSHelper.html)
744 */
745
746 XOSItemView = Marionette.ItemView.extend({
747              tagName: 'tr',
748              className: 'test-tablerow',
749
750              templateHelpers: function() { return { modelName: this.model.modelName,
751                                                     collectionName: this.model.collectionName,
752                                                     listFields: this.model.listFields,
753                                                     addFields: this.model.addFields,
754                                                     detailFields: this.model.detailFields,
755                                                     foreignFields: this.model.foreignFields,
756                                                     detailLinkFields: this.model.detailLinkFields,
757                                                     inputType: this.model.inputType,
758                                                     model: this.model,
759                                          }},
760 });
761
762 /* XOSListView:
763       extend with:
764          app - MarionetteApplication
765          childView - class of ItemView, probably an XOSItemView
766          template - template (see xosHelper.html)
767          collection - collection that holds these objects
768          title - title to display in template
769 */
770
771 XOSListView = FilteredCompositeView.extend({
772              childViewContainer: 'tbody',
773              parentModel: null,
774
775              events: {"click button.btn-xos-add": "addClicked",
776                       "click button.btn-xos-refresh": "refreshClicked",
777                      },
778
779              _fetchStateChange: function() {
780                  if (this.collection.fetching) {
781                     $("#xos-list-title-spinner").show();
782                  } else {
783                     $("#xos-list-title-spinner").hide();
784                  }
785              },
786
787              addClicked: function(e) {
788                 e.preventDefault();
789                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
790              },
791
792              refreshClicked: function(e) {
793                  e.preventDefault();
794                  this.collection.refresh(refreshRelated=true);
795              },
796
797              initialize: function() {
798                  this.listenTo(this.collection, 'change', this._renderChildren)
799                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
800                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
801                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
802
803                  // Because many of the templates use idToName(), we need to
804                  // listen to the collections that hold the names for the ids
805                  // that we want to display.
806                  for (i in this.collection.foreignCollections) {
807                      foreignName = this.collection.foreignCollections[i];
808                      if (xos[foreignName] == undefined) {
809                          console.log("Failed to find xos class " + foreignName);
810                      }
811                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
812                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
813                  }
814              },
815
816              getAddChildHash: function() {
817                 if (this.parentModel) {
818                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
819                     parentFieldName = parentFieldName || "unknown";
820
821                     /*parentFieldName = "unknown";
822
823                     for (fieldName in this.collection.foreignFields) {
824                         cname = this.collection.foreignFields[fieldName];
825                         if (cname = this.collection.collectionName) {
826                             parentFieldName = fieldName;
827                         }
828                     }*/
829                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
830                 } else {
831                     return null;
832                 }
833              },
834
835              templateHelpers: function() {
836                 return { title: this.title,
837                          addChildHash: this.getAddChildHash(),
838                          foreignFields: this.collection.foreignFields,
839                          listFields: this.collection.listFields,
840                          detailLinkFields: this.collection.detailLinkFields, };
841              },
842 });
843
844 XOSDataTableView = Marionette.View.extend( {
845     el: '<div style="overflow: hidden">' +
846         '<h3 class="xos-list-title title_placeholder"></h3>' +
847         '<div class="header_placeholder"></div>' +
848         '<table></table>' +
849         '<div class="footer_placeholder"></div>' +
850         '</div>',
851
852     filter: undefined,
853
854      events: {"click button.btn-xos-add": "addClicked",
855               "click button.btn-xos-refresh": "refreshClicked",
856              },
857
858      _fetchStateChange: function() {
859          if (this.collection.fetching) {
860             $("#xos-list-title-spinner").show();
861          } else {
862             $("#xos-list-title-spinner").hide();
863          }
864      },
865
866      addClicked: function(e) {
867         e.preventDefault();
868         this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
869      },
870
871      refreshClicked: function(e) {
872          e.preventDefault();
873          this.collection.refresh(refreshRelated=true);
874      },
875
876
877     initialize: function() {
878         $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
879         $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
880
881         this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
882     },
883
884     render: function() {
885         var view = this;
886         var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};
887
888         view.columnsByIndex = [];
889         view.columnsByFieldName = {};
890         _.each(this.collection.listFields, function(fieldName) {
891             inputType = view.options.inputType || view.inputType || {};
892             mRender = undefined;
893             mSearchText = undefined;
894             sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
895             bSortable = true;
896             if (fieldName=="backend_status") {
897                 mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
898                 sTitle = "";
899                 bSortable = false;
900             } else if (fieldName in view.collection.foreignFields) {
901                 var foreignCollection = view.collection.foreignFields[fieldName];
902                 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
903             } else if (inputType[fieldName] == "spinner") {
904                 mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
905             }
906             if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
907                 var collectionName = view.collection.collectionName;
908                 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
909             }
910             thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
911             view.columnsByIndex.push( thisColumn );
912             view.columnsByFieldName[fieldName] = thisColumn;
913         });
914
915         if (!view.noDeleteColumn) {
916             deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
917             view.columnsByIndex.push(deleteColumn);
918             view.columnsByFieldName["delete"] = deleteColumn;
919         };
920
921         oTable = $(this.el).find("table").dataTable( {
922             "bJQueryUI": true,
923             "bStateSave": true,
924             "bServerSide": true,
925             "bFilter": ! (view.options.disableFilter || view.disableFilter),
926             "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
927             "aoColumns": view.columnsByIndex,
928
929             fnServerData: function(sSource, aoData, fnCallback, settings) {
930                 var compareColumns = function(sortCols, sortDirs, a, b) {
931                     a = a[sortCols[0]];
932                     b = b[sortCols[0]];
933                     result = (a==b) ? 0 : ((a<b) ? -1 : 1);
934                     if (sortDirs[0] == "desc") {
935                         result = -result;
936                     }
937                     return result;
938                 };
939
940                 var searchMatch = function(row, sSearch) {
941                     for (fieldName in row) {
942                         if (fieldName in view.columnsByFieldName) {
943                             try {
944                                 value = row[fieldName].toString();
945                             } catch(e) {
946                                 continue;
947                             }
948                             if (value.indexOf(sSearch) >= 0) {
949                                 return true;
950                             }
951                         }
952                     }
953                     return false;
954                 };
955
956                 //console.log(aoData);
957 \r
958                 // function used to populate the DataTable with the current\r
959                 // content of the collection\r
960                 var populateTable = function()\r
961                 {\r
962                   //console.log("populatetable!");\r
963 \r
964                   // clear out old row views\r
965                   rows = [];\r
966 \r
967                   sSearch = null;\r
968                   iDisplayStart = 0;\r
969                   iDisplayLength = 1000;\r
970                   sortDirs = [];\r
971                   sortCols = [];\r
972                   _.each(aoData, function(param) {\r
973                       if (param.name == "sSortDir_0") {\r
974                           sortDirs = [param.value];\r
975                       } else if (param.name == "iSortCol_0") {\r
976                           sortCols = [view.columnsByIndex[param.value].mData];\r
977                       } else if (param.name == "iDisplayStart") {\r
978                           iDisplayStart = param.value;\r
979                       } else if (param.name == "iDisplayLength") {\r
980                           iDisplayLength = param.value;\r
981                       } else if (param.name == "sSearch") {\r
982                           sSearch = param.value;\r
983                       }\r
984                   });\r
985 \r
986                   aaData = view.collection.toJSON();\r
987 \r
988                   // apply backbone filtering on the models\r
989                   if (view.filter) {\r
990                       aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );\r
991                   }\r
992 \r
993                   var totalSize = aaData.length;\r
994 \r
995                   // turn the ForeignKey fields into human readable things\r
996                   for (rowIndex in aaData) {\r
997                       row = aaData[rowIndex];\r
998                       for (fieldName in row) {\r
999                           if (fieldName in view.columnsByFieldName) {\r
1000                               mSearchText = view.columnsByFieldName[fieldName].mSearchText;\r
1001                               if (mSearchText) {\r
1002                                   row[fieldName] = mSearchText(row[fieldName]);\r
1003                               }\r
1004                           }\r
1005                       }\r
1006                   }\r
1007 \r
1008                   // apply datatables search\r
1009                   if (sSearch) {\r
1010                       aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });\r
1011                   }\r
1012 \r
1013                   var filteredSize = aaData.length;\r
1014 \r
1015                   // apply datatables sort\r
1016                   aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });\r
1017 \r
1018                   // slice it for pagination\r
1019                   if (iDisplayLength >= 0) {\r
1020                       aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);\r
1021                   }\r
1022 \r
1023                   return fnCallback({iTotalRecords: totalSize,\r
1024                          iTotalDisplayRecords: filteredSize,\r
1025                          aaData: aaData});\r
1026                 };\r
1027 \r
1028                 aoData.shift(); // ignore sEcho
1029                 populateTable();
1030
1031                 view.listenTo(view.collection, 'change', populateTable);
1032                 view.listenTo(view.collection, 'add', populateTable);
1033                 view.listenTo(view.collection, 'remove', populateTable);
1034             },
1035         } );
1036
1037         return this;
1038     },
1039
1040      getAddChildHash: function() {
1041         if (this.parentModel) {
1042             parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
1043             parentFieldName = parentFieldName || "unknown";
1044
1045             /*parentFieldName = "unknown";
1046
1047             for (fieldName in this.collection.foreignFields) {
1048                 cname = this.collection.foreignFields[fieldName];
1049                 if (cname = this.collection.collectionName) {
1050                     parentFieldName = fieldName;
1051                 }
1052             }*/
1053             return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
1054         } else {
1055             return null;
1056         }
1057      },
1058
1059 });
1060
1061 idToName = function(id, collectionName, fieldName) {
1062     return xos.idToName(id, collectionName, fieldName);
1063 };
1064
1065 makeIdToName = function(collectionName, fieldName) {
1066     return function(id) { return idToName(id, collectionName, fieldName); }
1067 };
1068
1069 /* Constructs lists of <option> html blocks for items in a collection.
1070
1071    selectedId = the id of an object that should be selected, if any
1072    collectionName = name of collection
1073    fieldName = name of field within models of collection that will be displayed
1074 */
1075
1076 idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
1077     result=""
1078     for (index in xos[collectionName].models) {
1079         linkedObject = xos[collectionName].models[index];
1080         linkedId = linkedObject["id"];
1081         linkedName = linkedObject.attributes[fieldName];
1082         if (linkedId == selectedId) {
1083             selected = " selected";
1084         } else {
1085             selected = "";
1086         }
1087         if ((filterFunc) && (!filterFunc(linkedObject))) {
1088             continue;
1089         }
1090         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
1091     }
1092     return result;
1093 };
1094
1095 /* Constructs an html <select> and the <option>s to go with it.
1096
1097    variable = variable name to return to form
1098    selectedId = the id of an object that should be selected, if any
1099    collectionName = name of collection
1100    fieldName = name of field within models of collection that will be displayed
1101 */
1102
1103 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
1104     if (readOnly) {
1105         readOnly = " readonly";
1106     } else {
1107         readOnly = "";
1108     }
1109     result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
1110              idToOptions(selectedId, collectionName, fieldName, filterFunc) +
1111              '</select>';
1112     return result;
1113 }
1114
1115 choicesToOptions = function(selectedValue, choices) {
1116     result="";
1117     for (index in choices) {
1118         choice = choices[index];
1119         displayName = choice[0];
1120         value = choice[1];
1121         if (value == selectedValue) {
1122             selected = " selected";
1123         } else {
1124             selected = "";
1125         }
1126         result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
1127     }
1128     return result;
1129 }
1130
1131 choicesToSelect = function(variable, selectedValue, choices) {
1132     result = '<select name="' + variable + '" id="field_' + variable + '">' +
1133              choicesToOptions(selectedValue, choices) +
1134              '</select>';
1135     return result;
1136 }