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