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