Merge branch 'master' of git://git.planet-lab.org/plstackapi
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xosHelper.js
1 function assert(outcome, description) {
2     if (!outcome) {
3         console.log(description);
4     }
5 }
6
7 function templateFromId(id) {
8     return _.template($(id).html());
9 }
10
11 function firstCharUpper(s) {
12     return s.charAt(0).toUpperCase() + s.slice(1);
13 }
14
15 HTMLView = Marionette.ItemView.extend({
16   render: function() {
17       this.$el.append(this.options.html);
18   },
19 });
20
21 XOSApplication = Marionette.Application.extend({
22     detailBoxId: "#detailBox",
23     errorBoxId: "#errorBox",
24     errorCloseButtonId: "#close-error-box",
25     successBoxId: "#successBox",
26     successCloseButtonId: "#close-success-box",
27     errorTemplate: "#xos-error-template",
28     successTemplate: "#xos-success-template",
29     logMessageCount: 0,
30
31     hideError: function() {
32         if (this.logWindowId) {
33         } else {
34             $(this.errorBoxId).hide();
35             $(this.successBoxId).hide();
36         }
37     },
38
39     showSuccess: function(result) {
40          result["success"] = "success";
41          if (this.logTableId) {
42              this.appendLogWindow(result);
43          } else {
44              $(this.successBoxId).show();
45              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
46              var that=this;
47              $(this.successCloseButtonId).unbind().bind('click', function() {
48                  $(that.successBoxId).hide();
49              });
50          }
51     },
52
53     showError: function(result) {
54          result["success"] = "failure";
55          if (this.logTableId) {
56              this.appendLogWindow(result);
57          } else {
58              $(this.errorBoxId).show();
59              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
60              var that=this;
61              $(this.errorCloseButtonId).unbind().bind('click', function() {
62                  $(that.errorBoxId).hide();
63              });
64          }
65     },
66
67     showInformational: function(result) {
68          result["success"] = "information";
69          if (this.logTableId) {
70              return this.appendLogWindow(result);
71          } else {
72              return undefined;
73          }
74     },
75
76     appendLogWindow: function(result) {
77         // compute a new logMessageId for this log message
78         logMessageId = "logMessage" + this.logMessageCount;
79         this.logMessageCount = this.logMessageCount + 1;
80         result["logMessageId"] = logMessageId;
81
82         logMessageTemplate=$("#xos-log-template").html();
83         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
84         newRow = _.template(logMessageTemplate, result);
85         assert(newRow != undefined, "newRow is undefined");
86
87         if (result["infoMsgId"] != undefined) {
88             // We were passed the logMessageId of an informational message,
89             // and the caller wants us to replace that message with our own.
90             // i.e. replace an informational message with a success or an error.
91             $("#"+result["infoMsgId"]).replaceWith(newRow);
92         } else {
93             // Create a brand new log message rather than replacing one.
94             logTableBody = $(this.logTableId + " tbody");
95             logTableBody.prepend(newRow);
96         }
97
98         if (this.statusMsgId) {
99             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
100         }
101
102         return logMessageId;
103     },
104
105     hideLinkedItems: function(result) {
106         var index=0;
107         while (index<4) {\r
108             this["linkedObjs" + (index+1)].empty();\r
109             index = index + 1;\r
110         }\r
111     },\r
112 \r
113     listViewShower: function(listViewName, collection_name, regionName, title) {\r
114         var app=this;\r
115         return function() {\r
116             app[regionName].show(new app[listViewName]);\r
117             app.hideLinkedItems();\r
118             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));\r
119             $("#detail").show();\r
120             $("#xos-listview-button-box").show();\r
121             $("#tabs").hide();\r
122             $("#xos-detail-button-box").hide();\r
123         }\r
124     },\r
125 \r
126     addShower: function(detailName, collection_name, regionName, title) {\r
127         var app=this;\r
128         return function() {\r
129             model = new xos[collection_name].model();\r
130             detailViewClass = app[detailName];\r
131             detailView = new detailViewClass({model: model});\r
132             app[regionName].show(detailView);\r
133             $("#xos-detail-button-box").show();\r
134             $("#xos-listview-button-box").hide();\r
135         }\r
136     },\r
137 \r
138     detailShower: function(detailName, collection_name, regionName, title) {\r
139         var app=this;\r
140         showModelId = function(model_id) {\r
141             showModel = function(model) {\r
142                 detailViewClass = app[detailName];\r
143                 detailView = new detailViewClass({model: model});\r
144                 app[regionName].show(detailView);\r
145                 detailView.showLinkedItems();\r
146                 $("#xos-detail-button-box").show();\r
147                 $("#xos-listview-button-box").hide();\r
148             }\r
149 \r
150             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));\r
151 \r
152             collection = xos[collection_name];\r
153             model = collection.get(model_id);\r
154             if (model == undefined) {\r
155                 if (!collection.isLoaded) {\r
156                     // If the model cannot be found, then maybe it's because\r
157                     // we haven't finished loading the collection yet. So wait for\r
158                     // the sort event to complete, then try again.\r
159                     collection.once("sort", function() {\r
160                         collection = xos[collection_name];\r
161                         model = collection.get(model_id);\r
162                         if (model == undefined) {\r
163                             // We tried. It's not here. Complain to the user.\r
164                             app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));\r
165                         } else {\r
166                             showModel(model);\r
167                         }\r
168                     });\r
169                 } else {\r
170                     // The collection was loaded, the user must just be asking for something we don't have.\r
171                     app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));\r
172                 }\r
173             } else {\r
174                 showModel(model);\r
175             }\r
176         }\r
177         return showModelId;\r
178     },\r
179 });
180
181 /* XOSDetailView
182       extend with:
183          app - MarionetteApplication
184          template - template (See XOSHelper.html)
185 */
186
187 XOSDetailView = Marionette.ItemView.extend({
188             tagName: "div",
189
190             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
191                      "click button.btn-xos-save-leave": "submitLeaveClicked",
192                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
193                      "change input": "inputChanged"},
194
195             /* inputChanged is watching the onChange events of the input controls. We
196                do this to track when this view is 'dirty', so we can throw up a warning\r
197                if the user tries to change his slices without saving first.\r
198             */\r
199 \r
200             inputChanged: function(e) {\r
201                 this.dirty = true;\r
202             },\r
203 \r
204             saveError: function(model, result, xhr, infoMsgId) {\r
205                 result["what"] = "save " + model.__proto__.modelName;\r
206                 result["infoMsgId"] = infoMsgId;\r
207                 this.app.showError(result);\r
208             },\r
209 \r
210             saveSuccess: function(model, result, xhr, infoMsgId) {\r
211                 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};\r
212                 result["what"] = "save " + model.__proto__.modelName;\r
213                 result["infoMsgId"] = infoMsgId;\r
214                 this.app.showSuccess(result);\r
215             },
216
217             submitContinueClicked: function(e) {
218                 console.log("saveContinue");
219                 e.preventDefault();
220                 this.save();
221             },
222
223             submitLeaveClicked: function(e) {
224                 console.log("saveLeave");
225                 e.preventDefault();
226                 this.save();
227             },
228
229             submitAddAnotherClicked: function(e) {
230                 console.log("saveAnother");
231                 e.preventDefault();
232                 this.save();
233             },
234
235             save: function() {
236                 this.app.hideError();
237                 var infoMsgId = this.app.showInformational( {what: "save " + this.model.__proto__.modelName, status: "", statusText: "in progress..."} );\r
238                 var data = Backbone.Syphon.serialize(this);\r
239                 var that = this;\r
240                 this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},\r
241                                        success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId);}});\r
242                 this.dirty = false;\r
243             },
244
245             tabClick: function(tabId, regionName) {
246                     region = this.app[regionName];\r
247                     if (this.currentTabRegion != undefined) {\r
248                         this.currentTabRegion.$el.hide();\r
249                     }\r
250                     if (this.currentTabId != undefined) {\r
251                         $(this.currentTabId).removeClass('active');\r
252                     }\r
253                     this.currentTabRegion = region;\r
254                     this.currentTabRegion.$el.show();\r
255 \r
256                     this.currentTabId = tabId;\r
257                     $(tabId).addClass('active');\r
258             },
259
260             showTabs: function(tabs) {
261                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
262                 $("#tabs").html(template(tabs));
263                 var that = this;
264
265                 _.each(tabs, function(tab) {
266                     var regionName = tab["region"];
267                     var tabId = '#xos-nav-'+regionName;
268                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
269                 });
270
271                 $("#tabs").show();
272             },
273
274             showLinkedItems: function() {
275                     tabs=[];
276
277                     tabs.push({name: "details", region: "detail"});
278
279                     var index=0;
280                     for (relatedName in this.model.collection.relatedCollections) {\r
281                         relatedField = this.model.collection.relatedCollections[relatedName];\r
282                         regionName = "linkedObjs" + (index+1);\r
283 \r
284                         relatedListViewClassName = relatedName + "ListView";\r
285                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");\r
286                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});\r
287                         this.app[regionName].show(new relatedListViewClass());\r
288                         if (this.app.hideTabsByDefault) {\r
289                             this.app[regionName].$el.hide();\r
290                         }\r
291                         tabs.push({name: relatedName, region: regionName});\r
292                         index = index + 1;\r
293                     }\r
294 \r
295                     while (index<4) {\r
296                         this.app["linkedObjs" + (index+1)].empty();\r
297                         index = index + 1;\r
298                     }\r
299 \r
300                     this.showTabs(tabs);\r
301                     this.tabClick('#xos-nav-detail', 'detail');\r
302               },\r
303 \r
304 });\r
305
306 /* XOSItemView
307       This is for items that will be displayed as table rows.
308       extend with:
309          app - MarionetteApplication
310          template - template (See XOSHelper.html)
311          detailClass - class of detail view, probably an XOSDetailView
312 */
313
314 XOSItemView = Marionette.ItemView.extend({
315              tagName: 'tr',
316              className: 'test-tablerow',
317
318              events: {"click": "changeItem"},
319
320              changeItem: function(e) {\r
321                     this.app.hideError();\r
322                     e.preventDefault();\r
323                     e.stopPropagation();\r
324 \r
325                     this.app.navigateToModel(this.app, this.detailClass, this.detailNavLink, this.model);\r
326              },\r
327 });
328
329 /* XOSListView:
330       extend with:
331          app - MarionetteApplication
332          childView - class of ItemView, probably an XOSItemView
333          template - template (see xosHelper.html)
334          collection - collection that holds these objects
335          title - title to display in template
336 */
337
338 XOSListView = Marionette.CompositeView.extend({
339              childViewContainer: 'tbody',\r
340 \r
341              events: {"click button.btn-xos-add": "addClicked",\r
342                      },
343
344              addClicked: function(e) {
345                 console.log("add");
346                 e.preventDefault();
347                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
348              },
349 \r
350              initialize: function() {\r
351                  this.listenTo(this.collection, 'change', this._renderChildren)
352
353                  // Because many of the templates use idToName(), we need to
354                  // listen to the collections that hold the names for the ids
355                  // that we want to display.
356                  for (i in this.collection.foreignCollections) {
357                      foreignName = this.collection.foreignCollections[i];
358                      if (xos[foreignName] == undefined) {
359                          console.log("Failed to find xos class " + foreignName);
360                      }
361                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
362                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
363                  }
364              },
365
366              templateHelpers: function() {
367                 return { title: this.title };
368              },\r
369 });
370
371 /* Give an id, the name of a collection, and the name of a field for models
372    within that collection, lookup the id and return the value of the field.
373 */
374
375 idToName = function(id, collectionName, fieldName) {
376     linkedObject = xos[collectionName].get(id);
377     if (linkedObject == undefined) {
378         return "#" + id;
379     } else {
380         return linkedObject.attributes[fieldName];
381     }
382 };
383
384 /* Constructs lists of <option> html blocks for items in a collection.
385
386    selectedId = the id of an object that should be selected, if any
387    collectionName = name of collection
388    fieldName = name of field within models of collection that will be displayed
389 */
390
391 idToOptions = function(selectedId, collectionName, fieldName) {
392     result=""
393     for (index in xos[collectionName].models) {
394         linkedObject = xos[collectionName].models[index];
395         linkedId = linkedObject["id"];
396         linkedName = linkedObject.attributes[fieldName];
397         if (linkedId == selectedId) {
398             selected = " selected";
399         } else {
400             selected = "";
401         }
402         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
403     }
404     return result;
405 };
406
407 /* Constructs an html <select> and the <option>s to go with it.
408
409    variable = variable name to return to form
410    selectedId = the id of an object that should be selected, if any
411    collectionName = name of collection
412    fieldName = name of field within models of collection that will be displayed
413 */
414
415 idToSelect = function(variable, selectedId, collectionName, fieldName) {
416     result = '<select name="' + variable + '">' +
417              idToOptions(selectedId, collectionName, fieldName) +
418              '</select>';
419     return result;
420 }
421