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