2 column picker WIP
[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             viewInitializers: [],
408
409             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
410                      "click button.btn-xos-save-leave": "submitLeaveClicked",
411                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
412                      "click button.btn-xos-delete": "deleteClicked",
413                      "change input": "inputChanged"},
414
415             /* inputChanged is watching the onChange events of the input controls. We
416                do this to track when this view is 'dirty', so we can throw up a warning
417                if the user tries to change his slices without saving first.
418             */
419
420             initialize: function() {
421                 this.on("saveSuccess", this.onAfterSave);
422                 this.synchronous = false;
423             },
424
425             onShow: function() {
426                 _.each(this.viewInitializers, function(initializer) {
427                     initializer();
428                 });
429             },
430
431             afterSave: function(e) {
432             },
433
434             onAfterSave: function(e) {
435                 this.afterSave(e);
436             },
437
438             inputChanged: function(e) {
439                 this.dirty = true;
440             },
441
442             submitContinueClicked: function(e) {
443                 console.log("saveContinue");
444                 e.preventDefault();
445                 this.afterSave = function() { };
446                 this.save();
447             },
448
449             submitLeaveClicked: function(e) {
450                 console.log("saveLeave");
451                 e.preventDefault();
452                 var that=this;
453                 this.afterSave = function() {
454                     that.app.navigate("list", that.model.modelName);
455                 }
456                 this.save();
457             },
458
459             submitAddAnotherClicked: function(e) {
460                 console.log("saveAnother");
461                 console.log(this);
462                 e.preventDefault();
463                 var that=this;
464                 this.afterSave = function() {
465                     console.log("addAnother afterSave");
466                     that.app.navigate("add", that.model.modelName);
467                 }
468                 this.save();
469             },
470
471             save: function() {
472                 this.app.hideError();
473                 var data = Backbone.Syphon.serialize(this);
474                 var that = this;
475                 var isNew = !this.model.id;
476
477                 this.$el.find(".help-inline").remove();
478
479                 /* although model.validate() is called automatically by
480                    model.save, we call it ourselves, so we can throw up our
481                    validation error before creating the infoMsg in the log
482                 */
483                 errors =  this.model.xosValidate(data);
484                 if (errors) {
485                     this.onFormDataInvalid(errors);
486                     return;
487                 }
488
489                 if (isNew) {
490                     this.model.attributes.humanReadableName = "new " + model.modelName;
491                     this.model.addToCollection = this.collection;
492                 } else {
493                     this.model.addToCollection = undefined;
494                 }
495
496                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
497
498                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
499                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
500                                                                                if (that.synchronous) {
501                                                                                    that.trigger("saveSuccess");
502                                                                                }
503                                                                              }});
504                 this.dirty = false;
505
506                 if (!this.synchronous) {
507                     this.afterSave();
508                 }
509             },
510
511             deleteClicked: function(e) {
512                 e.preventDefault();
513                 this.app.deleteDialog(this.model, "list");
514             },
515
516             tabClick: function(tabId, regionName) {
517                     region = this.app[regionName];
518                     if (this.currentTabRegion != undefined) {
519                         this.currentTabRegion.$el.hide();
520                     }
521                     if (this.currentTabId != undefined) {
522                         $(this.currentTabId).removeClass('active');
523                     }
524                     this.currentTabRegion = region;
525                     this.currentTabRegion.$el.show();
526
527                     this.currentTabId = tabId;
528                     $(tabId).addClass('active');
529             },
530
531             showTabs: function(tabs) {
532                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
533                 $("#tabs").html(template(tabs));
534                 var that = this;
535
536                 _.each(tabs, function(tab) {
537                     var regionName = tab["region"];
538                     var tabId = '#xos-nav-'+regionName;
539                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
540                 });
541
542                 $("#tabs").show();
543             },
544
545             showLinkedItems: function() {
546                     tabs=[];
547
548                     tabs.push({name: "details", region: "detail"});
549
550                     makeFilter = function(relatedField, relatedId) {
551                         return function(model) { return model.attributes[relatedField] == relatedId; }
552                     };
553
554                     var index=0;
555                     for (relatedName in this.model.collection.relatedCollections) {
556                         var relatedField = this.model.collection.relatedCollections[relatedName];
557                         var relatedId = this.model.id;
558                         regionName = "linkedObjs" + (index+1);
559
560                         relatedListViewClassName = relatedName + "ListView";
561                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
562                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
563                                                                                           filter: makeFilter(relatedField, relatedId),
564                                                                                           parentModel: this.model});
565                         this.app[regionName].show(new relatedListViewClass());
566                         if (this.app.hideTabsByDefault) {
567                             this.app[regionName].$el.hide();
568                         }
569                         tabs.push({name: relatedName, region: regionName});
570                         index = index + 1;
571                     }
572
573                     while (index<4) {
574                         this.app["linkedObjs" + (index+1)].empty();
575                         index = index + 1;
576                     }
577
578                     this.showTabs(tabs);
579                     this.tabClick('#xos-nav-detail', 'detail');
580               },
581
582             onFormDataInvalid: function(errors) {
583                 var self=this;
584                 var markErrors = function(value, key) {
585                     var $inputElement = self.$el.find("[name='" + key + "']");
586                     var $inputContainer = $inputElement.parent();
587                     //$inputContainer.find(".help-inline").remove();
588                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
589                     $inputContainer.append($errorEl).addClass("error");
590                 }
591                 _.each(errors, markErrors);
592             },
593
594              templateHelpers: function() { return { modelName: this.model.modelName,
595                                                     collectionName: this.model.collectionName,
596                                                     addFields: this.model.addFields,
597                                                     listFields: this.model.listFields,
598                                                     detailFields: this.model.detailFields,
599                                                     foreignFields: this.model.foreignFields,
600                                                     detailLinkFields: this.model.detailLinkFields,
601                                                     inputType: this.model.inputType,
602                                                     model: this.model,
603                                                     detailView: this,
604                                          }},
605 });
606
607 XOSDetailView_sliver = XOSDetailView.extend( {
608     events: $.extend(XOSDetailView.events,
609         {"change #field_deploymentNetwork": "onDeploymentNetworkChange"}
610     ),
611
612     onShow: function() {
613         // Note that this causes the selects to be updated a second time. The
614         // first time was when the template was originally invoked, and the
615         // selects will all have the full unfiltered set of candidates. Then
616         // onShow will fire, and we'll update them with the filtered values.
617         this.onDeploymentNetworkChange();
618     },
619
620     onDeploymentNetworkChange: function(e) {
621         var deploymentID = this.$el.find("#field_deploymentNetwork").val();
622
623         console.log("onDeploymentNetworkChange");
624         console.log(deploymentID);
625
626         filterFunc = function(model) { return (model.attributes.deployment==deploymentID); }
627         newSelect = idToSelect("node",
628                                this.model.attributes.node,
629                                this.model.foreignFields["node"],
630                                "humanReadableName",
631                                false,
632                                filterFunc);
633         this.$el.find("#field_node").html(newSelect);
634
635         filterFunc = function(model) { for (index in model.attributes.deployments) {
636                                           item=model.attributes.deployments[index];
637                                           if (item.toString()==deploymentID.toString()) return true;
638                                         };
639                                         return false;
640                                      }
641         newSelect = idToSelect("flavor",
642                                this.model.attributes.flavor,
643                                this.model.foreignFields["flavor"],
644                                "humanReadableName",
645                                false,
646                                filterFunc);
647         this.$el.find("#field_flavor").html(newSelect);
648
649         filterFunc = function(model) { for (index in xos.imageDeployments.models) {
650                                            imageDeployment = xos.imageDeployments.models[index];
651                                            if ((imageDeployment.attributes.deployment == deploymentID) && (imageDeployment.attributes.image == model.id)) {
652                                                return true;
653                                            }
654                                        }
655                                        return false;
656                                      };
657         newSelect = idToSelect("image",
658                                this.model.attributes.image,
659                                this.model.foreignFields["image"],
660                                "humanReadableName",
661                                false,
662                                filterFunc);
663         this.$el.find("#field_image").html(newSelect);
664     },
665 });
666
667 /* XOSItemView
668       This is for items that will be displayed as table rows.
669       extend with:
670          app - MarionetteApplication
671          template - template (See XOSHelper.html)
672 */
673
674 XOSItemView = Marionette.ItemView.extend({
675              tagName: 'tr',
676              className: 'test-tablerow',
677
678              templateHelpers: function() { return { modelName: this.model.modelName,
679                                                     collectionName: this.model.collectionName,
680                                                     listFields: this.model.listFields,
681                                                     addFields: this.model.addFields,
682                                                     detailFields: this.model.detailFields,
683                                                     foreignFields: this.model.foreignFields,
684                                                     detailLinkFields: this.model.detailLinkFields,
685                                                     inputType: this.model.inputType,
686                                                     model: this.model,
687                                          }},
688 });
689
690 /* XOSListView:
691       extend with:
692          app - MarionetteApplication
693          childView - class of ItemView, probably an XOSItemView
694          template - template (see xosHelper.html)
695          collection - collection that holds these objects
696          title - title to display in template
697 */
698
699 XOSListView = FilteredCompositeView.extend({
700              childViewContainer: 'tbody',
701              parentModel: null,
702
703              events: {"click button.btn-xos-add": "addClicked",
704                       "click button.btn-xos-refresh": "refreshClicked",
705                      },
706
707              _fetchStateChange: function() {
708                  if (this.collection.fetching) {
709                     $("#xos-list-title-spinner").show();
710                  } else {
711                     $("#xos-list-title-spinner").hide();
712                  }
713              },
714
715              addClicked: function(e) {
716                 e.preventDefault();
717                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
718              },
719
720              refreshClicked: function(e) {
721                  e.preventDefault();
722                  this.collection.refresh(refreshRelated=true);
723              },
724
725              initialize: function() {
726                  this.listenTo(this.collection, 'change', this._renderChildren)
727                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
728                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
729                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
730
731                  // Because many of the templates use idToName(), we need to
732                  // listen to the collections that hold the names for the ids
733                  // that we want to display.
734                  for (i in this.collection.foreignCollections) {
735                      foreignName = this.collection.foreignCollections[i];
736                      if (xos[foreignName] == undefined) {
737                          console.log("Failed to find xos class " + foreignName);
738                      }
739                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
740                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
741                  }
742              },
743
744              getAddChildHash: function() {
745                 if (this.parentModel) {
746                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
747                     parentFieldName = parentFieldName || "unknown";
748
749                     /*parentFieldName = "unknown";
750
751                     for (fieldName in this.collection.foreignFields) {
752                         cname = this.collection.foreignFields[fieldName];
753                         if (cname = this.collection.collectionName) {
754                             parentFieldName = fieldName;
755                         }
756                     }*/
757                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
758                 } else {
759                     return null;
760                 }
761              },
762
763              templateHelpers: function() {
764                 return { title: this.title,
765                          addChildHash: this.getAddChildHash(),
766                          foreignFields: this.collection.foreignFields,
767                          listFields: this.collection.listFields,
768                          detailLinkFields: this.collection.detailLinkFields, };
769              },
770 });
771
772 XOSDataTableView = Marionette.View.extend( {
773     el: '<div style="overflow: hidden">' +
774         '<h3 class="xos-list-title title_placeholder"></h3>' +
775         '<div class="header_placeholder"></div>' +
776         '<table></table>' +
777         '<div class="footer_placeholder"></div>' +
778         '</div>',
779
780     filter: undefined,
781
782      events: {"click button.btn-xos-add": "addClicked",
783               "click button.btn-xos-refresh": "refreshClicked",
784              },
785
786      _fetchStateChange: function() {
787          if (this.collection.fetching) {
788             $("#xos-list-title-spinner").show();
789          } else {
790             $("#xos-list-title-spinner").hide();
791          }
792      },
793
794      addClicked: function(e) {
795         e.preventDefault();
796         this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
797      },
798
799      refreshClicked: function(e) {
800          e.preventDefault();
801          this.collection.refresh(refreshRelated=true);
802      },
803
804
805     initialize: function() {
806         $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
807         $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
808
809         this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
810     },
811
812     render: function() {
813         var view = this;
814
815         view.columnsByIndex = [];
816         view.columnsByFieldName = {};
817         _.each(this.collection.listFields, function(fieldName) {
818             mRender = undefined;
819             mSearchText = undefined;
820             sTitle = fieldNameToHumanReadable(fieldName);
821             bSortable = true;
822             if (fieldName=="backend_status") {
823                 mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
824                 sTitle = "";
825                 bSortable = false;
826             } else if (fieldName in view.collection.foreignFields) {
827                 var foreignCollection = view.collection.foreignFields[fieldName];
828                 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
829             }
830             if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
831                 var collectionName = view.collection.collectionName;
832                 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
833             }
834             thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
835             view.columnsByIndex.push( thisColumn );
836             view.columnsByFieldName[fieldName] = thisColumn;
837         });
838
839         deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
840         view.columnsByIndex.push(deleteColumn);
841         view.columnsByFieldName["delete"] = deleteColumn;
842
843         oTable = $(this.el).find("table").dataTable( {
844             "bJQueryUI": true,
845             "bStateSave": true,
846             "bServerSide": true,
847             "aoColumns": view.columnsByIndex,
848
849             fnServerData: function(sSource, aoData, fnCallback, settings) {
850                 var compareColumns = function(sortCols, sortDirs, a, b) {
851                     a = a[sortCols[0]];
852                     b = b[sortCols[0]];
853                     result = (a==b) ? 0 : ((a<b) ? -1 : 1);
854                     if (sortDirs[0] == "desc") {
855                         result = -result;
856                     }
857                     return result;
858                 };
859
860                 var searchMatch = function(row, sSearch) {
861                     for (fieldName in row) {
862                         if (fieldName in view.columnsByFieldName) {
863                             try {
864                                 value = row[fieldName].toString();
865                             } catch(e) {
866                                 continue;
867                             }
868                             if (value.indexOf(sSearch) >= 0) {
869                                 return true;
870                             }
871                         }
872                     }
873                     return false;
874                 };
875
876                 //console.log(aoData);
877 \r
878                 // function used to populate the DataTable with the current\r
879                 // content of the collection\r
880                 var populateTable = function()\r
881                 {\r
882                   console.log("populatetable!");\r
883 \r
884                   // clear out old row views\r
885                   rows = [];\r
886 \r
887                   sSearch = null;\r
888                   iDisplayStart = 0;\r
889                   iDisplayLength = 1000;\r
890                   sortDirs = [];\r
891                   sortCols = [];\r
892                   _.each(aoData, function(param) {\r
893                       if (param.name == "sSortDir_0") {\r
894                           sortDirs = [param.value];\r
895                       } else if (param.name == "iSortCol_0") {\r
896                           sortCols = [view.columnsByIndex[param.value].mData];\r
897                       } else if (param.name == "iDisplayStart") {\r
898                           iDisplayStart = param.value;\r
899                       } else if (param.name == "iDisplayLength") {\r
900                           iDisplayLength = param.value;\r
901                       } else if (param.name == "sSearch") {\r
902                           sSearch = param.value;\r
903                       }\r
904                   });\r
905 \r
906                   aaData = view.collection.toJSON();\r
907 \r
908                   // apply backbone filtering on the models\r
909                   if (view.filter) {\r
910                       aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );\r
911                   }\r
912 \r
913                   var totalSize = aaData.length;\r
914 \r
915                   // turn the ForeignKey fields into human readable things\r
916                   for (rowIndex in aaData) {\r
917                       row = aaData[rowIndex];\r
918                       for (fieldName in row) {\r
919                           if (fieldName in view.columnsByFieldName) {\r
920                               mSearchText = view.columnsByFieldName[fieldName].mSearchText;\r
921                               if (mSearchText) {\r
922                                   row[fieldName] = mSearchText(row[fieldName]);\r
923                               }\r
924                           }\r
925                       }\r
926                   }\r
927 \r
928                   // apply datatables search\r
929                   if (sSearch) {\r
930                       aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });\r
931                   }\r
932 \r
933                   var filteredSize = aaData.length;\r
934 \r
935                   // apply datatables sort\r
936                   aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });\r
937 \r
938                   // slice it for pagination\r
939                   aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);\r
940 \r
941                   return fnCallback({iTotalRecords: totalSize,\r
942                          iTotalDisplayRecords: filteredSize,\r
943                          aaData: aaData});\r
944                 };\r
945 \r
946                 aoData.shift(); // ignore sEcho
947                 populateTable();
948
949                 view.listenTo(view.collection, 'change', populateTable);
950                 view.listenTo(view.collection, 'add', populateTable);
951                 view.listenTo(view.collection, 'remove', populateTable);
952             },
953         } );
954
955         return this;
956     },
957
958      getAddChildHash: function() {
959         if (this.parentModel) {
960             parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
961             parentFieldName = parentFieldName || "unknown";
962
963             /*parentFieldName = "unknown";
964
965             for (fieldName in this.collection.foreignFields) {
966                 cname = this.collection.foreignFields[fieldName];
967                 if (cname = this.collection.collectionName) {
968                     parentFieldName = fieldName;
969                 }
970             }*/
971             return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
972         } else {
973             return null;
974         }
975      },
976
977 });
978
979 idToName = function(id, collectionName, fieldName) {
980     return xos.idToName(id, collectionName, fieldName);
981 };
982
983 makeIdToName = function(collectionName, fieldName) {
984     return function(id) { return idToName(id, collectionName, fieldName); }
985 };
986
987 /* Constructs lists of <option> html blocks for items in a collection.
988
989    selectedId = the id of an object that should be selected, if any
990    collectionName = name of collection
991    fieldName = name of field within models of collection that will be displayed
992 */
993
994 idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
995     result=""
996     for (index in xos[collectionName].models) {
997         linkedObject = xos[collectionName].models[index];
998         linkedId = linkedObject["id"];
999         linkedName = linkedObject.attributes[fieldName];
1000         if (linkedId == selectedId) {
1001             selected = " selected";
1002         } else {
1003             selected = "";
1004         }
1005         if ((filterFunc) && (!filterFunc(linkedObject))) {
1006             continue;
1007         }
1008         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
1009     }
1010     return result;
1011 };
1012
1013 /* Constructs an html <select> and the <option>s to go with it.
1014
1015    variable = variable name to return to form
1016    selectedId = the id of an object that should be selected, if any
1017    collectionName = name of collection
1018    fieldName = name of field within models of collection that will be displayed
1019 */
1020
1021 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
1022     if (readOnly) {
1023         readOnly = " readonly";
1024     } else {
1025         readOnly = "";
1026     }
1027     result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
1028              idToOptions(selectedId, collectionName, fieldName, filterFunc) +
1029              '</select>';
1030     return result;
1031 }
1032