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