readonly fields, wip
[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                     var index=0;
490                     for (relatedName in this.model.collection.relatedCollections) {
491                         var relatedField = this.model.collection.relatedCollections[relatedName];
492                         var relatedId = this.model.id;
493                         regionName = "linkedObjs" + (index+1);
494
495                         relatedListViewClassName = relatedName + "ListView";
496                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
497                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
498                                                                                           filter: function(model) { return model.attributes[relatedField]==relatedId;},
499                                                                                           parentModel: this.model});
500                         this.app[regionName].show(new relatedListViewClass());
501                         if (this.app.hideTabsByDefault) {
502                             this.app[regionName].$el.hide();
503                         }
504                         tabs.push({name: relatedName, region: regionName});
505                         index = index + 1;
506                     }
507
508                     while (index<4) {
509                         this.app["linkedObjs" + (index+1)].empty();
510                         index = index + 1;
511                     }
512
513                     this.showTabs(tabs);
514                     this.tabClick('#xos-nav-detail', 'detail');
515               },
516
517             onFormDataInvalid: function(errors) {
518                 var self=this;
519                 var markErrors = function(value, key) {
520                     console.log("name='" + key + "'");
521                     var $inputElement = self.$el.find("[name='" + key + "']");
522                     var $inputContainer = $inputElement.parent();
523                     //$inputContainer.find(".help-inline").remove();
524                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
525                     $inputContainer.append($errorEl).addClass("error");
526                 }
527                 _.each(errors, markErrors);
528             },
529
530              templateHelpers: function() { return { modelName: this.model.modelName,
531                                                     collectionName: this.model.collectionName,
532                                                     addFields: this.model.addFields,
533                                                     detailFields: this.model.detailFields,
534                                                     foreignFields: this.model.foreignFields,
535                                                     inputType: this.model.inputType,
536                                                     model: this.model,
537                                          }},
538
539 });
540
541 /* XOSItemView
542       This is for items that will be displayed as table rows.
543       extend with:
544          app - MarionetteApplication
545          template - template (See XOSHelper.html)
546          detailClass - class of detail view, probably an XOSDetailView
547 */
548
549 XOSItemView = Marionette.ItemView.extend({
550              tagName: 'tr',
551              className: 'test-tablerow',
552
553              templateHelpers: function() { return { modelName: this.model.modelName,
554                                                     collectionName: this.model.collectionName,
555                                                     addFields: this.model.addFields,
556                                                     detailFields: this.model.detailFields,
557                                                     foreignFields: this.model.foreignFields,
558                                                     inputType: this.model.inputType,
559                                                     model: this.model,
560                                          }},
561 });
562
563 /* XOSListView:
564       extend with:
565          app - MarionetteApplication
566          childView - class of ItemView, probably an XOSItemView
567          template - template (see xosHelper.html)
568          collection - collection that holds these objects
569          title - title to display in template
570 */
571
572 XOSListView = FilteredCompositeView.extend({
573              childViewContainer: 'tbody',
574              parentModel: null,
575
576              events: {"click button.btn-xos-add": "addClicked",
577                       "click button.btn-xos-refresh": "refreshClicked",
578                      },
579
580              _fetchStateChange: function() {
581                  if (this.collection.fetching) {
582                     $("#xos-list-title-spinner").show();
583                  } else {
584                     $("#xos-list-title-spinner").hide();
585                  }
586              },
587
588              addClicked: function(e) {
589                 e.preventDefault();
590                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
591              },
592
593              refreshClicked: function(e) {
594                  e.preventDefault();
595                  this.collection.refresh(refreshRelated=true);
596              },
597
598              initialize: function() {
599                  this.listenTo(this.collection, 'change', this._renderChildren)
600                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
601                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
602                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
603
604                  // Because many of the templates use idToName(), we need to
605                  // listen to the collections that hold the names for the ids
606                  // that we want to display.
607                  for (i in this.collection.foreignCollections) {
608                      foreignName = this.collection.foreignCollections[i];
609                      if (xos[foreignName] == undefined) {
610                          console.log("Failed to find xos class " + foreignName);
611                      }
612                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
613                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
614                  }
615              },
616
617              getAddChildHash: function() {
618                 if (this.parentModel) {
619                     // Find the field name in the model that should point to
620                     // the parent object. For example, when adding a sliver, the
621                     // fieldName that should point to 'users' is 'creator'.
622                     parentFieldName = "unknown";
623                     for (fieldName in this.collection.foreignFields) {
624                         cname = this.collection.foreignFields[fieldName];
625                         if (cname = this.collection.collectionName) {
626                             parentFieldName = fieldName;
627                         }
628                     }
629                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
630                 } else {
631                     return null;
632                 }
633              },
634
635              templateHelpers: function() {
636                 return { title: this.title,
637                          addChildHash: this.getAddChildHash() };
638              },
639 });
640
641 idToName = function(id, collectionName, fieldName) {
642     return xos.idToName(id, collectionName, fieldName);
643 };
644
645 /* Constructs lists of <option> html blocks for items in a collection.
646
647    selectedId = the id of an object that should be selected, if any
648    collectionName = name of collection
649    fieldName = name of field within models of collection that will be displayed
650 */
651
652 idToOptions = function(selectedId, collectionName, fieldName) {
653     result=""
654     for (index in xos[collectionName].models) {
655         linkedObject = xos[collectionName].models[index];
656         linkedId = linkedObject["id"];
657         linkedName = linkedObject.attributes[fieldName];
658         if (linkedId == selectedId) {
659             selected = " selected";
660         } else {
661             selected = "";
662         }
663         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
664     }
665     return result;
666 };
667
668 /* Constructs an html <select> and the <option>s to go with it.
669
670    variable = variable name to return to form
671    selectedId = the id of an object that should be selected, if any
672    collectionName = name of collection
673    fieldName = name of field within models of collection that will be displayed
674 */
675
676 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
677     if (readOnly) {
678         readOnly = " readonly";
679     } else {
680         readOnly = "";
681     }
682     result = '<select name="' + variable + '"' + readOnly + '>' +
683              idToOptions(selectedId, collectionName, fieldName) +
684              '</select>';
685     console.log(result);
686     return result;
687 }
688