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