datatable in a marionette view, 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             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
600 /* XOSItemView
601       This is for items that will be displayed as table rows.
602       extend with:
603          app - MarionetteApplication
604          template - template (See XOSHelper.html)
605 */
606
607 XOSItemView = Marionette.ItemView.extend({
608              tagName: 'tr',
609              className: 'test-tablerow',
610
611              templateHelpers: function() { return { modelName: this.model.modelName,
612                                                     collectionName: this.model.collectionName,
613                                                     listFields: this.model.listFields,
614                                                     addFields: this.model.addFields,
615                                                     detailFields: this.model.detailFields,
616                                                     foreignFields: this.model.foreignFields,
617                                                     detailLinkFields: this.model.detailLinkFields,
618                                                     inputType: this.model.inputType,
619                                                     model: this.model,
620                                          }},
621 });
622
623 /* XOSListView:
624       extend with:
625          app - MarionetteApplication
626          childView - class of ItemView, probably an XOSItemView
627          template - template (see xosHelper.html)
628          collection - collection that holds these objects
629          title - title to display in template
630 */
631
632 XOSListView = FilteredCompositeView.extend({
633              childViewContainer: 'tbody',
634              parentModel: null,
635
636              events: {"click button.btn-xos-add": "addClicked",
637                       "click button.btn-xos-refresh": "refreshClicked",
638                      },
639
640              _fetchStateChange: function() {
641                  if (this.collection.fetching) {
642                     $("#xos-list-title-spinner").show();
643                  } else {
644                     $("#xos-list-title-spinner").hide();
645                  }
646              },
647
648              addClicked: function(e) {
649                 e.preventDefault();
650                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
651              },
652
653              refreshClicked: function(e) {
654                  e.preventDefault();
655                  this.collection.refresh(refreshRelated=true);
656              },
657
658              initialize: function() {
659                  this.listenTo(this.collection, 'change', this._renderChildren)
660                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
661                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
662                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
663
664                  // Because many of the templates use idToName(), we need to
665                  // listen to the collections that hold the names for the ids
666                  // that we want to display.
667                  for (i in this.collection.foreignCollections) {
668                      foreignName = this.collection.foreignCollections[i];
669                      if (xos[foreignName] == undefined) {
670                          console.log("Failed to find xos class " + foreignName);
671                      }
672                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
673                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
674                  }
675              },
676
677              getAddChildHash: function() {
678                 if (this.parentModel) {
679                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
680                     parentFieldName = parentFieldName || "unknown";
681
682                     /*parentFieldName = "unknown";
683
684                     for (fieldName in this.collection.foreignFields) {
685                         cname = this.collection.foreignFields[fieldName];
686                         if (cname = this.collection.collectionName) {
687                             parentFieldName = fieldName;
688                         }
689                     }*/
690                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
691                 } else {
692                     return null;
693                 }
694              },
695
696              templateHelpers: function() {
697                 return { title: this.title,
698                          addChildHash: this.getAddChildHash(),
699                          foreignFields: this.collection.foreignFields,
700                          listFields: this.collection.listFields,
701                          detailLinkFields: this.collection.detailLinkFields, };
702              },
703 });
704
705 XOSDataTableViewOld = Marionette.View.extend( {
706     //tagName: "table",
707     el: "<div><table></table></div>",
708
709     initialize: function() {
710          //$(this.el).html('<table cellpadding="0" cellspacing="0" border="0" class="display" id="dynamicusersliceinfo' + dtcounter + '"></table><p>scott was here</p>' );
711     },
712
713     render: function() {
714         //this.el = "<div><table></table></div>";
715         console.log(this.el);
716         console.log(this.$el);
717         console.log("render!");
718         actualEntries = [];
719         actualEntries.push(["foo", "bar", "1", "2"]);
720         actualEntries.push(["zoo", "zar", "0", "9"]);
721
722         oTable = $(this.el).find("table").dataTable( {
723             "bJQueryUI": true,
724             "aaData":  actualEntries,
725             "bStateSave": true,
726             "aoColumns": [
727                 { "sTitle": "Slice" },
728                 { "sTitle": "Privilege" , sClass: "alignCenter"},
729                 { "sTitle": "Number of Slivers" , sClass: "alignCenter"},
730                 { "sTitle": "Number of Sites" , sClass: "alignCenter"},
731             ]
732         } );
733         dtcounter = dtcounter + 1;
734
735         return this;
736     },
737
738 });
739
740 XOSDataTableView = Marionette.View.extend( {
741     el: '<div style="overflow: hidden"><table></table></div>',
742
743     filter: undefined,
744
745     initialize: function() {
746     },
747
748     render: function() {
749         var view = this;
750
751         view.aoColumns = [];
752         _.each(this.collection.listFields, function(fieldName) {
753             mRender = undefined;
754             if (fieldName in view.collection.foreignFields) {
755                 var foreignCollection = view.collection.foreignFields[fieldName];
756                 mRender = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
757             } else if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
758                 var collectionName = view.collection.collectionName;
759                 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
760             }
761             view.aoColumns.push( {sTitle: fieldNameToHumanReadable(fieldName), mData: fieldName, mRender: mRender} );
762         });
763
764         oTable = $(this.el).find("table").dataTable( {
765             "bJQueryUI": true,
766             "bStateSave": true,
767             "bServerSide": true,
768             "aoColumns": view.aoColumns,
769
770             fnServerData: function(sSource, aoData, fnCallback, settings) {
771                 var compareColumns = function(sortCols, sortDirs, a, b) {
772                     result = a[sortCols[0]] < b[sortCols[0]];
773                     if (sortDirs[0] == "desc") {
774                         result = -result;
775                     }
776                     return result;
777                 };
778 \r
779                 // function used to populate the DataTable with the current\r
780                 // content of the collection\r
781                 var populateTable = function()\r
782                 {\r
783                   // clear out old row views\r
784                   rows = [];\r
785 \r
786                   sortDirs = [];\r
787                   sortCols = [];\r
788                   _.each(aoData, function(param) {\r
789                       if (param.name == "sSortDir_0") {\r
790                           sortDirs = [param.value];\r
791                       } else if (param.name == "iSortCol_0") {\r
792                           sortCols = [view.aoColumns[param.value].mData];\r
793                       }\r
794                   });\r
795 \r
796                   aaData = view.collection.toJSON();\r
797 \r
798                   if (view.filter) {\r
799                       aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );\r
800                   }\r
801 \r
802                   aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });\r
803 \r
804                   // these 'meta' attributes are set by the collection's parse method\r
805                   var totalSize = view.collection.length;\r
806                   var filteredSize = view.collection.length;\r
807 \r
808                   return fnCallback({iTotalRecords: totalSize,\r
809                          iTotalDisplayRecords: filteredSize,\r
810                          aaData: aaData});\r
811                 };\r
812 \r
813                 aoData.shift(); // ignore sEcho
814                 populateTable();
815             },
816         } );
817
818         return this;
819     },
820
821 });
822
823 idToName = function(id, collectionName, fieldName) {
824     return xos.idToName(id, collectionName, fieldName);
825 };
826
827 /* Constructs lists of <option> html blocks for items in a collection.
828
829    selectedId = the id of an object that should be selected, if any
830    collectionName = name of collection
831    fieldName = name of field within models of collection that will be displayed
832 */
833
834 idToOptions = function(selectedId, collectionName, fieldName) {
835     result=""
836     for (index in xos[collectionName].models) {
837         linkedObject = xos[collectionName].models[index];
838         linkedId = linkedObject["id"];
839         linkedName = linkedObject.attributes[fieldName];
840         if (linkedId == selectedId) {
841             selected = " selected";
842         } else {
843             selected = "";
844         }
845         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
846     }
847     return result;
848 };
849
850 /* Constructs an html <select> and the <option>s to go with it.
851
852    variable = variable name to return to form
853    selectedId = the id of an object that should be selected, if any
854    collectionName = name of collection
855    fieldName = name of field within models of collection that will be displayed
856 */
857
858 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
859     if (readOnly) {
860         readOnly = " readonly";
861     } else {
862         readOnly = "";
863     }
864     result = '<select name="' + variable + '"' + readOnly + '>' +
865              idToOptions(selectedId, collectionName, fieldName) +
866              '</select>';
867     return result;
868 }
869