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