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