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