display APIException errors properly
[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_deploymentNetwork": "onDeploymentNetworkChange"}
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.onDeploymentNetworkChange();
699     },
700
701     onDeploymentNetworkChange: function(e) {
702         var deploymentID = this.$el.find("#field_deploymentNetwork").val();
703
704         console.log("onDeploymentNetworkChange");
705         console.log(deploymentID);
706
707         filterFunc = function(model) { return (model.attributes.deployment==deploymentID); }
708         newSelect = idToSelect("node",
709                                this.model.attributes.node,
710                                this.model.foreignFields["node"],
711                                "humanReadableName",
712                                false,
713                                filterFunc);
714         this.$el.find("#field_node").html(newSelect);
715
716         filterFunc = function(model) { for (index in model.attributes.deployments) {
717                                           item=model.attributes.deployments[index];
718                                           if (item.toString()==deploymentID.toString()) return true;
719                                         };
720                                         return false;
721                                      }
722         newSelect = idToSelect("flavor",
723                                this.model.attributes.flavor,
724                                this.model.foreignFields["flavor"],
725                                "humanReadableName",
726                                false,
727                                filterFunc);
728         this.$el.find("#field_flavor").html(newSelect);
729
730         filterFunc = function(model) { for (index in xos.imageDeployments.models) {
731                                            imageDeployment = xos.imageDeployments.models[index];
732                                            if ((imageDeployment.attributes.deployment == deploymentID) && (imageDeployment.attributes.image == model.id)) {
733                                                return true;
734                                            }
735                                        }
736                                        return false;
737                                      };
738         newSelect = idToSelect("image",
739                                this.model.attributes.image,
740                                this.model.foreignFields["image"],
741                                "humanReadableName",
742                                false,
743                                filterFunc);
744         this.$el.find("#field_image").html(newSelect);
745     },
746 });
747
748 /* XOSItemView
749       This is for items that will be displayed as table rows.
750       extend with:
751          app - MarionetteApplication
752          template - template (See XOSHelper.html)
753 */
754
755 XOSItemView = Marionette.ItemView.extend({
756              tagName: 'tr',
757              className: 'test-tablerow',
758
759              templateHelpers: function() { return { modelName: this.model.modelName,
760                                                     collectionName: this.model.collectionName,
761                                                     listFields: this.model.listFields,
762                                                     addFields: this.model.addFields,
763                                                     detailFields: this.model.detailFields,
764                                                     foreignFields: this.model.foreignFields,
765                                                     detailLinkFields: this.model.detailLinkFields,
766                                                     inputType: this.model.inputType,
767                                                     model: this.model,
768                                          }},
769 });
770
771 /* XOSListView:
772       extend with:
773          app - MarionetteApplication
774          childView - class of ItemView, probably an XOSItemView
775          template - template (see xosHelper.html)
776          collection - collection that holds these objects
777          title - title to display in template
778 */
779
780 XOSListView = FilteredCompositeView.extend({
781              childViewContainer: 'tbody',
782              parentModel: null,
783
784              events: {"click button.btn-xos-add": "addClicked",
785                       "click button.btn-xos-refresh": "refreshClicked",
786                      },
787
788              _fetchStateChange: function() {
789                  if (this.collection.fetching) {
790                     $("#xos-list-title-spinner").show();
791                  } else {
792                     $("#xos-list-title-spinner").hide();
793                  }
794              },
795
796              addClicked: function(e) {
797                 e.preventDefault();
798                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
799              },
800
801              refreshClicked: function(e) {
802                  e.preventDefault();
803                  this.collection.refresh(refreshRelated=true);
804              },
805
806              initialize: function() {
807                  this.listenTo(this.collection, 'change', this._renderChildren)
808                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
809                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
810                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
811
812                  // Because many of the templates use idToName(), we need to
813                  // listen to the collections that hold the names for the ids
814                  // that we want to display.
815                  for (i in this.collection.foreignCollections) {
816                      foreignName = this.collection.foreignCollections[i];
817                      if (xos[foreignName] == undefined) {
818                          console.log("Failed to find xos class " + foreignName);
819                      }
820                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
821                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
822                  }
823              },
824
825              getAddChildHash: function() {
826                 if (this.parentModel) {
827                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
828                     parentFieldName = parentFieldName || "unknown";
829
830                     /*parentFieldName = "unknown";
831
832                     for (fieldName in this.collection.foreignFields) {
833                         cname = this.collection.foreignFields[fieldName];
834                         if (cname = this.collection.collectionName) {
835                             parentFieldName = fieldName;
836                         }
837                     }*/
838                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
839                 } else {
840                     return null;
841                 }
842              },
843
844              templateHelpers: function() {
845                 return { title: this.title,
846                          addChildHash: this.getAddChildHash(),
847                          foreignFields: this.collection.foreignFields,
848                          listFields: this.collection.listFields,
849                          detailLinkFields: this.collection.detailLinkFields, };
850              },
851 });
852
853 XOSDataTableView = Marionette.View.extend( {
854     el: '<div style="overflow: hidden">' +
855         '<h3 class="xos-list-title title_placeholder"></h3>' +
856         '<div class="header_placeholder"></div>' +
857         '<table></table>' +
858         '<div class="footer_placeholder"></div>' +
859         '</div>',
860
861     filter: undefined,
862
863      events: {"click button.btn-xos-add": "addClicked",
864               "click button.btn-xos-refresh": "refreshClicked",
865              },
866
867      _fetchStateChange: function() {
868          if (this.collection.fetching) {
869             $("#xos-list-title-spinner").show();
870          } else {
871             $("#xos-list-title-spinner").hide();
872          }
873      },
874
875      addClicked: function(e) {
876         e.preventDefault();
877         this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
878      },
879
880      refreshClicked: function(e) {
881          e.preventDefault();
882          this.collection.refresh(refreshRelated=true);
883      },
884
885
886     initialize: function() {
887         $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
888         $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
889
890         this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
891     },
892
893     render: function() {
894         var view = this;
895         var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};
896
897         view.columnsByIndex = [];
898         view.columnsByFieldName = {};
899         _.each(this.collection.listFields, function(fieldName) {
900             inputType = view.options.inputType || view.inputType || {};
901             mRender = undefined;
902             mSearchText = undefined;
903             sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
904             bSortable = true;
905             if (fieldName=="backend_status") {
906                 mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
907                 sTitle = "";
908                 bSortable = false;
909             } else if (fieldName in view.collection.foreignFields) {
910                 var foreignCollection = view.collection.foreignFields[fieldName];
911                 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
912             } else if (inputType[fieldName] == "spinner") {
913                 mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
914             }
915             if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
916                 var collectionName = view.collection.collectionName;
917                 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
918             }
919             thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
920             view.columnsByIndex.push( thisColumn );
921             view.columnsByFieldName[fieldName] = thisColumn;
922         });
923
924         if (!view.noDeleteColumn) {
925             deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
926             view.columnsByIndex.push(deleteColumn);
927             view.columnsByFieldName["delete"] = deleteColumn;
928         };
929
930         oTable = $(this.el).find("table").dataTable( {
931             "bJQueryUI": true,
932             "bStateSave": true,
933             "bServerSide": true,
934             "bFilter": ! (view.options.disableFilter || view.disableFilter),
935             "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
936             "aoColumns": view.columnsByIndex,
937
938             fnServerData: function(sSource, aoData, fnCallback, settings) {
939                 var compareColumns = function(sortCols, sortDirs, a, b) {
940                     a = a[sortCols[0]];
941                     b = b[sortCols[0]];
942                     result = (a==b) ? 0 : ((a<b) ? -1 : 1);
943                     if (sortDirs[0] == "desc") {
944                         result = -result;
945                     }
946                     return result;
947                 };
948
949                 var searchMatch = function(row, sSearch) {
950                     for (fieldName in row) {
951                         if (fieldName in view.columnsByFieldName) {
952                             try {
953                                 value = row[fieldName].toString();
954                             } catch(e) {
955                                 continue;
956                             }
957                             if (value.indexOf(sSearch) >= 0) {
958                                 return true;
959                             }
960                         }
961                     }
962                     return false;
963                 };
964
965                 //console.log(aoData);
966 \r
967                 // function used to populate the DataTable with the current\r
968                 // content of the collection\r
969                 var populateTable = function()\r
970                 {\r
971                   //console.log("populatetable!");\r
972 \r
973                   // clear out old row views\r
974                   rows = [];\r
975 \r
976                   sSearch = null;\r
977                   iDisplayStart = 0;\r
978                   iDisplayLength = 1000;\r
979                   sortDirs = [];\r
980                   sortCols = [];\r
981                   _.each(aoData, function(param) {\r
982                       if (param.name == "sSortDir_0") {\r
983                           sortDirs = [param.value];\r
984                       } else if (param.name == "iSortCol_0") {\r
985                           sortCols = [view.columnsByIndex[param.value].mData];\r
986                       } else if (param.name == "iDisplayStart") {\r
987                           iDisplayStart = param.value;\r
988                       } else if (param.name == "iDisplayLength") {\r
989                           iDisplayLength = param.value;\r
990                       } else if (param.name == "sSearch") {\r
991                           sSearch = param.value;\r
992                       }\r
993                   });\r
994 \r
995                   aaData = view.collection.toJSON();\r
996 \r
997                   // apply backbone filtering on the models\r
998                   if (view.filter) {\r
999                       aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );\r
1000                   }\r
1001 \r
1002                   var totalSize = aaData.length;\r
1003 \r
1004                   // turn the ForeignKey fields into human readable things\r
1005                   for (rowIndex in aaData) {\r
1006                       row = aaData[rowIndex];\r
1007                       for (fieldName in row) {\r
1008                           if (fieldName in view.columnsByFieldName) {\r
1009                               mSearchText = view.columnsByFieldName[fieldName].mSearchText;\r
1010                               if (mSearchText) {\r
1011                                   row[fieldName] = mSearchText(row[fieldName]);\r
1012                               }\r
1013                           }\r
1014                       }\r
1015                   }\r
1016 \r
1017                   // apply datatables search\r
1018                   if (sSearch) {\r
1019                       aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });\r
1020                   }\r
1021 \r
1022                   var filteredSize = aaData.length;\r
1023 \r
1024                   // apply datatables sort\r
1025                   aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });\r
1026 \r
1027                   // slice it for pagination\r
1028                   if (iDisplayLength >= 0) {\r
1029                       aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);\r
1030                   }\r
1031 \r
1032                   return fnCallback({iTotalRecords: totalSize,\r
1033                          iTotalDisplayRecords: filteredSize,\r
1034                          aaData: aaData});\r
1035                 };\r
1036 \r
1037                 aoData.shift(); // ignore sEcho
1038                 populateTable();
1039
1040                 view.listenTo(view.collection, 'change', populateTable);
1041                 view.listenTo(view.collection, 'add', populateTable);
1042                 view.listenTo(view.collection, 'remove', populateTable);
1043             },
1044         } );
1045
1046         return this;
1047     },
1048
1049      getAddChildHash: function() {
1050         if (this.parentModel) {
1051             parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
1052             parentFieldName = parentFieldName || "unknown";
1053
1054             /*parentFieldName = "unknown";
1055
1056             for (fieldName in this.collection.foreignFields) {
1057                 cname = this.collection.foreignFields[fieldName];
1058                 if (cname = this.collection.collectionName) {
1059                     parentFieldName = fieldName;
1060                 }
1061             }*/
1062             return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
1063         } else {
1064             return null;
1065         }
1066      },
1067
1068 });
1069
1070 idToName = function(id, collectionName, fieldName) {
1071     return xos.idToName(id, collectionName, fieldName);
1072 };
1073
1074 makeIdToName = function(collectionName, fieldName) {
1075     return function(id) { return idToName(id, collectionName, fieldName); }
1076 };
1077
1078 /* Constructs lists of <option> html blocks for items in a collection.
1079
1080    selectedId = the id of an object that should be selected, if any
1081    collectionName = name of collection
1082    fieldName = name of field within models of collection that will be displayed
1083 */
1084
1085 idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
1086     result=""
1087     for (index in xos[collectionName].models) {
1088         linkedObject = xos[collectionName].models[index];
1089         linkedId = linkedObject["id"];
1090         linkedName = linkedObject.attributes[fieldName];
1091         if (linkedId == selectedId) {
1092             selected = " selected";
1093         } else {
1094             selected = "";
1095         }
1096         if ((filterFunc) && (!filterFunc(linkedObject))) {
1097             continue;
1098         }
1099         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
1100     }
1101     return result;
1102 };
1103
1104 /* Constructs an html <select> and the <option>s to go with it.
1105
1106    variable = variable name to return to form
1107    selectedId = the id of an object that should be selected, if any
1108    collectionName = name of collection
1109    fieldName = name of field within models of collection that will be displayed
1110 */
1111
1112 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
1113     if (readOnly) {
1114         readOnly = " readonly";
1115     } else {
1116         readOnly = "";
1117     }
1118     result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
1119              idToOptions(selectedId, collectionName, fieldName, filterFunc) +
1120              '</select>';
1121     return result;
1122 }
1123
1124 choicesToOptions = function(selectedValue, choices) {
1125     result="";
1126     for (index in choices) {
1127         choice = choices[index];
1128         displayName = choice[0];
1129         value = choice[1];
1130         if (value == selectedValue) {
1131             selected = " selected";
1132         } else {
1133             selected = "";
1134         }
1135         result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
1136     }
1137     return result;
1138 }
1139
1140 choicesToSelect = function(variable, selectedValue, choices) {
1141     result = '<select name="' + variable + '" id="field_' + variable + '">' +
1142              choicesToOptions(selectedValue, choices) +
1143              '</select>';
1144     return result;
1145 }