fix wrong parentfieldName in add link, fix filters using incorrect attributes
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xosHelper.js
1 HTMLView = Marionette.ItemView.extend({
2   render: function() {
3       this.$el.append(this.options.html);
4   },
5 });
6
7 FilteredCompositeView = Marionette.CompositeView.extend( {
8     showCollection: function() {
9       var ChildView;
10       this.collection.each(function(child, index) {
11         if (this.filter && !this.filter(child)) {
12             return;
13         }
14         ChildView = this.getChildView(child);
15         this.addChild(child, ChildView, index);
16       }, this);
17
18     },
19 });
20
21 XOSRouter = Marionette.AppRouter.extend({
22         initialize: function() {\r
23             this.routeStack=[];\r
24         },\r
25 \r
26         onRoute: function(x,y,z) {\r
27              this.routeStack.push(Backbone.history.fragment);\r
28         },\r
29 \r
30         prevPage: function() {\r
31              return this.routeStack.slice(-1)[0];
32         },
33
34         showPreviousURL: function() {
35             prevPage = this.prevPage();
36             console.log("showPreviousURL");
37             console.log(this.routeStack);
38             if (prevPage) {
39                 this.navigate("#"+prevPage, {trigger: false, replace: true} );
40             }
41         },
42     });\r
43
44
45
46 XOSApplication = Marionette.Application.extend({
47     detailBoxId: "#detailBox",
48     errorBoxId: "#errorBox",
49     errorCloseButtonId: "#close-error-box",
50     successBoxId: "#successBox",
51     successCloseButtonId: "#close-success-box",
52     errorTemplate: "#xos-error-template",
53     successTemplate: "#xos-success-template",
54     logMessageCount: 0,
55
56     confirmDialog: function(view, event, callback) {
57         $("#xos-confirm-dialog").dialog({
58            autoOpen: false,
59            modal: true,
60            buttons : {
61                 "Confirm" : function() {
62                   $(this).dialog("close");
63                   if (event) {
64                       view.trigger(event);
65                   }
66                   if (callback) {
67                       callback();
68                   }
69                 },
70                 "Cancel" : function() {
71                   $(this).dialog("close");
72                 }
73               }
74             });
75         $("#xos-confirm-dialog").dialog("open");
76     },
77
78     popupErrorDialog: function(responseText) {
79         try {
80             parsed_error=$.parseJSON(responseText);
81             width=300;
82         }
83         catch(err) {
84             parsed_error=undefined;
85             width=640;    // django stacktraces like wide width
86         }
87         if (parsed_error) {
88             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
89         } else {
90             $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
91         }
92
93         $("#xos-error-dialog").dialog({
94             modal: true,
95             width: width,
96             buttons: {
97                 Ok: function() { $(this).dialog("close"); }
98             }
99         });
100     },
101
102     hideLinkedItems: function(result) {
103         var index=0;
104         while (index<4) {
105             this["linkedObjs" + (index+1)].empty();
106             index = index + 1;
107         }
108     },
109
110     createListHandler: function(listViewName, collection_name, regionName, title) {
111         var app=this;
112         return function() {
113             app[regionName].show(new app[listViewName]);
114             app.hideLinkedItems();
115             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
116             $("#detail").show();
117             $("#xos-listview-button-box").show();
118             $("#tabs").hide();
119             $("#xos-detail-button-box").hide();
120         }
121     },
122
123     createAddHandler: function(detailName, collection_name, regionName, title) {
124         var app=this;
125         return function() {
126             model = new xos[collection_name].model();
127             detailViewClass = app[detailName];
128             detailView = new detailViewClass({model: model, collection:xos[collection_name]});
129             app[regionName].show(detailView);
130             $("#xos-detail-button-box").show();
131             $("#xos-listview-button-box").hide();
132         }
133     },
134
135     createAddChildHandler: function(addChildName, collection_name) {
136         var app=this;
137         return function(parent_modelName, parent_fieldName, parent_id) {
138             app.Router.showPreviousURL();
139             console.log("acs");
140             console.log(parent_modelName);
141             console.log(parent_fieldName);
142             console.log(parent_id);
143             model = new xos[collection_name].model();
144             model.attributes[parent_fieldName] = parent_id;
145             model.readOnlyFields.push(parent_fieldName);
146             console.log(model);
147             detailViewClass = app[addChildName];
148             var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
149             detailView.dialog = $("xos-addchild-dialog");
150             app["addChildDetail"].show(detailView);
151             $("#xos-addchild-dialog").dialog({
152                autoOpen: false,
153                modal: true,
154                width: 640,
155                buttons : {
156                     "Save" : function() {
157                       var addDialog = this;
158                       detailView.synchronous = true;
159                       detailView.afterSave = function() { console.log("afterSave"); $(addDialog).dialog("close"); }
160                       detailView.save();
161
162                       //$(this).dialog("close");
163                     },
164                     "Cancel" : function() {
165                       $(this).dialog("close");
166                     }
167                   }
168                 });
169             $("#xos-addchild-dialog").dialog("open");
170         }
171     },
172
173     createDeleteHandler: function(collection_name) {
174         var app=this;
175         return function(model_id) {
176             console.log("deleteCalled");
177             collection = xos[collection_name];
178             model = collection.get(model_id);
179             assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
180             app.Router.showPreviousURL();
181             app.deleteDialog(model);
182         }
183     },
184
185     createDetailHandler: function(detailName, collection_name, regionName, title) {
186         var app=this;
187         showModelId = function(model_id) {
188             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
189
190             collection = xos[collection_name];
191             model = collection.get(model_id);
192             if (model == undefined) {
193                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
194             } else {
195                 detailViewClass = app[detailName];
196                 detailView = new detailViewClass({model: model});
197                 app[regionName].show(detailView);
198                 detailView.showLinkedItems();
199                 $("#xos-detail-button-box").show();
200                 $("#xos-listview-button-box").hide();
201             }
202         }
203         return showModelId;
204     },
205
206     /* error handling callbacks */
207
208     hideError: function() {
209         if (this.logWindowId) {
210         } else {
211             $(this.errorBoxId).hide();
212             $(this.successBoxId).hide();
213         }
214     },
215
216     showSuccess: function(result) {
217          result["statusclass"] = "success";
218          if (this.logTableId) {
219              this.appendLogWindow(result);
220          } else {
221              $(this.successBoxId).show();
222              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
223              var that=this;
224              $(this.successCloseButtonId).unbind().bind('click', function() {
225                  $(that.successBoxId).hide();
226              });
227          }
228     },
229
230     showError: function(result) {
231          result["statusclass"] = "failure";
232          if (this.logTableId) {
233              this.appendLogWindow(result);
234              this.popupErrorDialog(result.responseText);
235          } else {
236              // this is really old stuff
237              $(this.errorBoxId).show();
238              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
239              var that=this;
240              $(this.errorCloseButtonId).unbind().bind('click', function() {
241                  $(that.errorBoxId).hide();
242              });
243          }
244     },
245
246     showInformational: function(result) {
247          result["statusclass"] = "inprog";
248          if (this.logTableId) {
249              return this.appendLogWindow(result);
250          } else {
251              return undefined;
252          }
253     },
254
255     appendLogWindow: function(result) {
256         // compute a new logMessageId for this log message
257         logMessageId = "logMessage" + this.logMessageCount;
258         this.logMessageCount = this.logMessageCount + 1;
259         result["logMessageId"] = logMessageId;
260
261         logMessageTemplate=$("#xos-log-template").html();
262         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
263         newRow = _.template(logMessageTemplate, result);
264         assert(newRow != undefined, "newRow is undefined");
265
266         if (result["infoMsgId"] != undefined) {
267             // We were passed the logMessageId of an informational message,
268             // and the caller wants us to replace that message with our own.
269             // i.e. replace an informational message with a success or an error.
270             $("#"+result["infoMsgId"]).replaceWith(newRow);
271         } else {
272             // Create a brand new log message rather than replacing one.
273             logTableBody = $(this.logTableId + " tbody");
274             logTableBody.prepend(newRow);
275         }
276
277         if (this.statusMsgId) {
278             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
279         }
280
281         limitTableRows(this.logTableId, 5);
282
283         return logMessageId;
284     },
285
286     saveError: function(model, result, xhr, infoMsgId) {
287         console.log("saveError");
288         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
289         result["infoMsgId"] = infoMsgId;
290         this.showError(result);
291     },
292
293     saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
294         console.log("saveSuccess");
295         if (model.addToCollection) {
296             console.log("addToCollection");
297             console.log(model.addToCollection);
298             model.addToCollection.add(model);
299             model.addToCollection.sort();
300             model.addToCollection = undefined;
301         }
302         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
303         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
304         result["infoMsgId"] = infoMsgId;
305         this.showSuccess(result);
306     },
307
308     destroyError: function(model, result, xhr, infoMsgId) {
309         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
310         result["infoMsgId"] = infoMsgId;
311         this.showError(result);
312     },
313
314     destroySuccess: function(model, result, xhr, infoMsgId) {
315         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
316         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
317         result["infoMsgId"] = infoMsgId;
318         this.showSuccess(result);
319     },
320
321     /* end error handling callbacks */
322
323     destroyModel: function(model) {
324          //console.log("destroyModel"); console.log(model);
325          this.hideError();
326          var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
327          var that = this;
328          model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
329                         success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
330     },
331
332     deleteDialog: function(model, afterDelete) {
333         var that=this;
334         assert(model!=undefined, "deleteDialog's model is undefined");
335         //console.log("deleteDialog"); console.log(model);
336         this.confirmDialog(null, null, function() {
337             //console.log("deleteConfirm"); console.log(model);
338             modelName = model.modelName;
339             that.destroyModel(model);
340             if (afterDelete=="list") {
341                 that.navigate("list", modelName);
342             }
343         });
344     },
345 });
346
347 /* XOSDetailView
348       extend with:
349          app - MarionetteApplication
350          template - template (See XOSHelper.html)
351 */
352
353 XOSDetailView = Marionette.ItemView.extend({
354             tagName: "div",
355
356             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
357                      "click button.btn-xos-save-leave": "submitLeaveClicked",
358                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
359                      "click button.btn-xos-delete": "deleteClicked",
360                      "change input": "inputChanged"},
361
362             /* inputChanged is watching the onChange events of the input controls. We
363                do this to track when this view is 'dirty', so we can throw up a warning
364                if the user tries to change his slices without saving first.
365             */
366
367             initialize: function() {
368                 this.on("saveSuccess", this.onAfterSave);
369                 this.synchronous = false;
370             },
371
372             afterSave: function(e) {
373             },
374
375             onAfterSave: function(e) {
376                 this.afterSave(e);
377             },
378
379             inputChanged: function(e) {
380                 this.dirty = true;
381             },
382
383             submitContinueClicked: function(e) {
384                 console.log("saveContinue");
385                 e.preventDefault();
386                 this.afterSave = function() { };
387                 this.save();
388             },
389
390             submitLeaveClicked: function(e) {
391                 console.log("saveLeave");
392                 e.preventDefault();
393                 var that=this;
394                 this.afterSave = function() {
395                     that.app.navigate("list", that.model.modelName);
396                 }
397                 this.save();
398             },
399
400             submitAddAnotherClicked: function(e) {
401                 console.log("saveAnother");
402                 e.preventDefault();
403                 var that=this;
404                 this.afterSave = function() {
405                     that.app.navigate("add", that.model.modelName);
406                 }
407                 this.save();
408             },
409
410             save: function() {
411                 this.app.hideError();
412                 var data = Backbone.Syphon.serialize(this);
413                 var that = this;
414                 var isNew = !this.model.id;
415
416                 this.$el.find(".help-inline").remove();
417
418                 /* although model.validate() is called automatically by
419                    model.save, we call it ourselves, so we can throw up our
420                    validation error before creating the infoMsg in the log
421                 */
422                 errors =  this.model.xosValidate(data);
423                 if (errors) {
424                     this.onFormDataInvalid(errors);
425                     return;
426                 }
427
428                 if (isNew) {
429                     this.model.attributes.humanReadableName = "new " + model.modelName;
430                     this.model.addToCollection = this.collection;
431                 } else {
432                     this.model.addToCollection = undefined;
433                 }
434
435                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
436
437                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
438                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
439                                                                                if (that.synchronous) {
440                                                                                    that.trigger("saveSuccess");
441                                                                                }
442                                                                              }});
443                 this.dirty = false;
444
445                 if (!this.synchronous) {
446                     this.afterSave();
447                 }
448             },
449
450             deleteClicked: function(e) {
451                 e.preventDefault();
452                 this.app.deleteDialog(this.model, "list");
453             },
454
455             tabClick: function(tabId, regionName) {
456                     region = this.app[regionName];
457                     if (this.currentTabRegion != undefined) {
458                         this.currentTabRegion.$el.hide();
459                     }
460                     if (this.currentTabId != undefined) {
461                         $(this.currentTabId).removeClass('active');
462                     }
463                     this.currentTabRegion = region;
464                     this.currentTabRegion.$el.show();
465
466                     this.currentTabId = tabId;
467                     $(tabId).addClass('active');
468             },
469
470             showTabs: function(tabs) {
471                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
472                 $("#tabs").html(template(tabs));
473                 var that = this;
474
475                 _.each(tabs, function(tab) {
476                     var regionName = tab["region"];
477                     var tabId = '#xos-nav-'+regionName;
478                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
479                 });
480
481                 $("#tabs").show();
482             },
483
484             showLinkedItems: function() {
485                     tabs=[];
486
487                     tabs.push({name: "details", region: "detail"});
488
489                     makeFilter = function(relatedField, relatedId) {
490                         return function(model) { return model.attributes[relatedField] == relatedId; }
491                     };
492
493                     var index=0;
494                     for (relatedName in this.model.collection.relatedCollections) {
495                         var relatedField = this.model.collection.relatedCollections[relatedName];
496                         var relatedId = this.model.id;
497                         regionName = "linkedObjs" + (index+1);
498
499                         relatedListViewClassName = relatedName + "ListView";
500                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
501                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
502                                                                                           filter: makeFilter(relatedField, relatedId), //function(model) { return model.attributes[relatedField]==relatedId; },
503                                                                                           parentModel: this.model});
504                         this.app[regionName].show(new relatedListViewClass());
505                         if (this.app.hideTabsByDefault) {
506                             this.app[regionName].$el.hide();
507                         }
508                         tabs.push({name: relatedName, region: regionName});
509                         index = index + 1;
510                     }
511
512                     while (index<4) {
513                         this.app["linkedObjs" + (index+1)].empty();
514                         index = index + 1;
515                     }
516
517                     this.showTabs(tabs);
518                     this.tabClick('#xos-nav-detail', 'detail');
519               },
520
521             onFormDataInvalid: function(errors) {
522                 var self=this;
523                 var markErrors = function(value, key) {
524                     console.log("name='" + key + "'");
525                     var $inputElement = self.$el.find("[name='" + key + "']");
526                     var $inputContainer = $inputElement.parent();
527                     //$inputContainer.find(".help-inline").remove();
528                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
529                     $inputContainer.append($errorEl).addClass("error");
530                 }
531                 _.each(errors, markErrors);
532             },
533
534              templateHelpers: function() { return { modelName: this.model.modelName,
535                                                     collectionName: this.model.collectionName,
536                                                     addFields: this.model.addFields,
537                                                     listFields: this.model.listFields,
538                                                     detailFields: this.model.detailFields,
539                                                     foreignFields: this.model.foreignFields,
540                                                     detailLinkFields: this.model.detailLinkFields,
541                                                     inputType: this.model.inputType,
542                                                     model: this.model,
543                                          }},
544
545 });
546
547 /* XOSItemView
548       This is for items that will be displayed as table rows.
549       extend with:
550          app - MarionetteApplication
551          template - template (See XOSHelper.html)
552 */
553
554 XOSItemView = Marionette.ItemView.extend({
555              tagName: 'tr',
556              className: 'test-tablerow',
557
558              templateHelpers: function() { return { modelName: this.model.modelName,
559                                                     collectionName: this.model.collectionName,
560                                                     listFields: this.model.listFields,
561                                                     addFields: this.model.addFields,
562                                                     detailFields: this.model.detailFields,
563                                                     foreignFields: this.model.foreignFields,
564                                                     detailLinkFields: this.model.detailLinkFields,
565                                                     inputType: this.model.inputType,
566                                                     model: this.model,
567                                          }},
568 });
569
570 /* XOSListView:
571       extend with:
572          app - MarionetteApplication
573          childView - class of ItemView, probably an XOSItemView
574          template - template (see xosHelper.html)
575          collection - collection that holds these objects
576          title - title to display in template
577 */
578
579 XOSListView = FilteredCompositeView.extend({
580              childViewContainer: 'tbody',
581              parentModel: null,
582
583              events: {"click button.btn-xos-add": "addClicked",
584                       "click button.btn-xos-refresh": "refreshClicked",
585                      },
586
587              _fetchStateChange: function() {
588                  if (this.collection.fetching) {
589                     $("#xos-list-title-spinner").show();
590                  } else {
591                     $("#xos-list-title-spinner").hide();
592                  }
593              },
594
595              addClicked: function(e) {
596                 e.preventDefault();
597                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
598              },
599
600              refreshClicked: function(e) {
601                  e.preventDefault();
602                  this.collection.refresh(refreshRelated=true);
603              },
604
605              initialize: function() {
606                  this.listenTo(this.collection, 'change', this._renderChildren)
607                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
608                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
609                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
610
611                  // Because many of the templates use idToName(), we need to
612                  // listen to the collections that hold the names for the ids
613                  // that we want to display.
614                  for (i in this.collection.foreignCollections) {
615                      foreignName = this.collection.foreignCollections[i];
616                      if (xos[foreignName] == undefined) {
617                          console.log("Failed to find xos class " + foreignName);
618                      }
619                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
620                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
621                  }
622              },
623
624              getAddChildHash: function() {
625                 if (this.parentModel) {
626                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
627                     parentFieldName = parentFieldName || "unknown";
628
629                     /*parentFieldName = "unknown";
630
631                     for (fieldName in this.collection.foreignFields) {
632                         cname = this.collection.foreignFields[fieldName];
633                         if (cname = this.collection.collectionName) {
634                             parentFieldName = fieldName;
635                         }
636                     }*/
637                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
638                 } else {
639                     return null;
640                 }
641              },
642
643              templateHelpers: function() {
644                 return { title: this.title,
645                          addChildHash: this.getAddChildHash(),
646                          foreignFields: this.collection.foreignFields,
647                          listFields: this.collection.listFields,
648                          detailLinkFields: this.collection.detailLinkFields, };
649              },
650 });
651
652 idToName = function(id, collectionName, fieldName) {
653     return xos.idToName(id, collectionName, fieldName);
654 };
655
656 /* Constructs lists of <option> html blocks for items in a collection.
657
658    selectedId = the id of an object that should be selected, if any
659    collectionName = name of collection
660    fieldName = name of field within models of collection that will be displayed
661 */
662
663 idToOptions = function(selectedId, collectionName, fieldName) {
664     result=""
665     for (index in xos[collectionName].models) {
666         linkedObject = xos[collectionName].models[index];
667         linkedId = linkedObject["id"];
668         linkedName = linkedObject.attributes[fieldName];
669         if (linkedId == selectedId) {
670             selected = " selected";
671         } else {
672             selected = "";
673         }
674         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
675     }
676     return result;
677 };
678
679 /* Constructs an html <select> and the <option>s to go with it.
680
681    variable = variable name to return to form
682    selectedId = the id of an object that should be selected, if any
683    collectionName = name of collection
684    fieldName = name of field within models of collection that will be displayed
685 */
686
687 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
688     if (readOnly) {
689         readOnly = " readonly";
690     } else {
691         readOnly = "";
692     }
693     result = '<select name="' + variable + '"' + readOnly + '>' +
694              idToOptions(selectedId, collectionName, fieldName) +
695              '</select>';
696     console.log(result);
697     return result;
698 }
699