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