1 function assert(outcome, description) {
3 console.log(description);
7 function templateFromId(id) {
8 return _.template($(id).html());
11 function firstCharUpper(s) {
12 return s.charAt(0).toUpperCase() + s.slice(1);
15 HTMLView = Marionette.ItemView.extend({
17 this.$el.append(this.options.html);
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",
31 confirmDialog: function(view, event) {
32 $("#xos-confirm-dialog").dialog({
36 "Confirm" : function() {
\r
37 $(this).dialog("close");
\r
38 view.trigger(event);
\r
40 "Cancel" : function() {
\r
41 $(this).dialog("close");
\r
45 $("#xos-confirm-dialog").dialog("open");
48 hideError: function() {
49 if (this.logWindowId) {
51 $(this.errorBoxId).hide();
52 $(this.successBoxId).hide();
56 showSuccess: function(result) {
57 result["statusclass"] = "success";
58 if (this.logTableId) {
59 this.appendLogWindow(result);
61 $(this.successBoxId).show();
62 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
64 $(this.successCloseButtonId).unbind().bind('click', function() {
65 $(that.successBoxId).hide();
70 popupErrorDialog: function(responseText) {
72 parsed_error=$.parseJSON(responseText);
76 parsed_error=undefined;
77 width=640; // django stacktraces like wide width
80 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(json));
82 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
85 $("#xos-error-dialog").dialog({
89 Ok: function() { $(this).dialog("close"); }
94 showError: function(result) {
95 result["statusclass"] = "failure";
96 if (this.logTableId) {
97 this.appendLogWindow(result);
98 this.popupErrorDialog(result.responseText);
100 // this is really old stuff
101 $(this.errorBoxId).show();
102 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
104 $(this.errorCloseButtonId).unbind().bind('click', function() {
105 $(that.errorBoxId).hide();
110 showInformational: function(result) {
111 result["statusclass"] = "inprog";
112 if (this.logTableId) {
113 return this.appendLogWindow(result);
119 appendLogWindow: function(result) {
120 // compute a new logMessageId for this log message
121 logMessageId = "logMessage" + this.logMessageCount;
122 this.logMessageCount = this.logMessageCount + 1;
123 result["logMessageId"] = logMessageId;
125 logMessageTemplate=$("#xos-log-template").html();
126 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
127 newRow = _.template(logMessageTemplate, result);
128 assert(newRow != undefined, "newRow is undefined");
130 if (result["infoMsgId"] != undefined) {
131 // We were passed the logMessageId of an informational message,
132 // and the caller wants us to replace that message with our own.
133 // i.e. replace an informational message with a success or an error.
134 $("#"+result["infoMsgId"]).replaceWith(newRow);
136 // Create a brand new log message rather than replacing one.
137 logTableBody = $(this.logTableId + " tbody");
138 logTableBody.prepend(newRow);
141 if (this.statusMsgId) {
142 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
145 limitTableRows(this.logTableId, 5);
150 hideLinkedItems: function(result) {
153 this["linkedObjs" + (index+1)].empty();
\r
158 listViewShower: function(listViewName, collection_name, regionName, title) {
\r
160 return function() {
\r
161 app[regionName].show(new app[listViewName]);
\r
162 app.hideLinkedItems();
\r
163 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
\r
164 $("#detail").show();
\r
165 $("#xos-listview-button-box").show();
\r
167 $("#xos-detail-button-box").hide();
\r
171 addShower: function(detailName, collection_name, regionName, title) {
\r
173 return function() {
\r
174 model = new xos[collection_name].model();
\r
175 detailViewClass = app[detailName];
\r
176 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
\r
177 app[regionName].show(detailView);
\r
178 $("#xos-detail-button-box").show();
\r
179 $("#xos-listview-button-box").hide();
\r
183 detailShower: function(detailName, collection_name, regionName, title) {
\r
185 showModelId = function(model_id) {
\r
186 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
\r
188 collection = xos[collection_name];
\r
189 model = collection.get(model_id);
\r
190 if (model == undefined) {
\r
191 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
\r
193 detailViewClass = app[detailName];
\r
194 detailView = new detailViewClass({model: model});
\r
195 app[regionName].show(detailView);
\r
196 detailView.showLinkedItems();
\r
197 $("#xos-detail-button-box").show();
\r
198 $("#xos-listview-button-box").hide();
\r
201 return showModelId;
\r
207 app - MarionetteApplication
208 template - template (See XOSHelper.html)
211 XOSDetailView = Marionette.ItemView.extend({
214 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
215 "click button.btn-xos-save-leave": "submitLeaveClicked",
216 "click button.btn-xos-save-another": "submitAddAnotherClicked",
217 "click button.btn-xos-delete": "deleteClicked",
218 "change input": "inputChanged"},
220 initialize: function() {
221 this.on('deleteConfirmed', this.deleteConfirmed);
224 /* inputChanged is watching the onChange events of the input controls. We
225 do this to track when this view is 'dirty', so we can throw up a warning
\r
226 if the user tries to change his slices without saving first.
\r
229 inputChanged: function(e) {
\r
233 saveError: function(model, result, xhr, infoMsgId) {
\r
234 console.log("saveError");
\r
235 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
236 result["infoMsgId"] = infoMsgId;
\r
237 this.app.showError(result);
\r
240 saveSuccess: function(model, result, xhr, infoMsgId, isNew) {
\r
241 console.log("saveSuccess");
\r
243 this.collection.add(model);
\r
244 this.collection.sort();
\r
246 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
247 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
248 result["infoMsgId"] = infoMsgId;
\r
249 this.app.showSuccess(result);
\r
252 destroyError: function(model, result, xhr, infoMsgId) {
253 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
254 result["infoMsgId"] = infoMsgId;
\r
255 this.app.showError(result);
\r
258 destroySuccess: function(model, result, xhr, infoMsgId) {
\r
259 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
260 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
261 result["infoMsgId"] = infoMsgId;
\r
262 this.app.showSuccess(result);
\r
265 submitContinueClicked: function(e) {
266 console.log("saveContinue");
271 submitLeaveClicked: function(e) {
272 console.log("saveLeave");
275 this.app.navigate("list", this.model.modelName);
278 submitAddAnotherClicked: function(e) {
279 console.log("saveAnother");
282 this.app.navigate("add", this.model.modelName);
286 this.app.hideError();
287 var data = Backbone.Syphon.serialize(this);
\r
289 var isNew = !this.model.id;
\r
291 this.$el.find(".help-inline").remove();
\r
293 /* although model.validate() is called automatically by
\r
294 model.save, we call it ourselves, so we can throw up our
\r
295 validation error before creating the infoMsg in the log
\r
297 errors = this.model.xosValidate(data);
\r
299 this.onFormDataInvalid(errors);
\r
304 this.model.attributes.humanReadableName = "new " + model.modelName;
\r
307 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
\r
309 this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},
\r
310 success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId, isNew);}});
\r
312 this.collection.add(this.model);
\r
313 this.collection.sort();
\r
315 this.dirty = false;
\r
318 destroyModel: function() {
319 this.app.hideError();
320 var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
322 this.model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
323 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
326 deleteClicked: function(e) {
328 \r this.app.confirmDialog(this, "deleteConfirmed");
331 \r deleteConfirmed: function() {
332 \r modelName = this.model.modelName;
333 \r this.destroyModel();
334 \r this.app.navigate("list", modelName);
337 tabClick: function(tabId, regionName) {
338 region = this.app[regionName];
\r
339 if (this.currentTabRegion != undefined) {
\r
340 this.currentTabRegion.$el.hide();
\r
342 if (this.currentTabId != undefined) {
\r
343 $(this.currentTabId).removeClass('active');
\r
345 this.currentTabRegion = region;
\r
346 this.currentTabRegion.$el.show();
\r
348 this.currentTabId = tabId;
\r
349 $(tabId).addClass('active');
\r
352 showTabs: function(tabs) {
353 template = templateFromId("#xos-tabs-template", {tabs: tabs});
354 $("#tabs").html(template(tabs));
357 _.each(tabs, function(tab) {
358 var regionName = tab["region"];
359 var tabId = '#xos-nav-'+regionName;
360 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
366 showLinkedItems: function() {
369 tabs.push({name: "details", region: "detail"});
372 for (relatedName in this.model.collection.relatedCollections) {
\r
373 relatedField = this.model.collection.relatedCollections[relatedName];
\r
374 regionName = "linkedObjs" + (index+1);
\r
376 relatedListViewClassName = relatedName + "ListView";
\r
377 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
\r
378 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});
\r
379 this.app[regionName].show(new relatedListViewClass());
\r
380 if (this.app.hideTabsByDefault) {
\r
381 this.app[regionName].$el.hide();
\r
383 tabs.push({name: relatedName, region: regionName});
\r
388 this.app["linkedObjs" + (index+1)].empty();
\r
392 this.showTabs(tabs);
\r
393 this.tabClick('#xos-nav-detail', 'detail');
\r
396 onFormDataInvalid: function(errors) {
\r
398 var markErrors = function(value, key) {
\r
399 console.log("name='" + key + "'");
\r
400 var $inputElement = self.$el.find("[name='" + key + "']");
\r
401 var $inputContainer = $inputElement.parent();
\r
402 //$inputContainer.find(".help-inline").remove();
\r
403 var $errorEl = $("<span>", {class: "help-inline error", text: value});
\r
404 $inputContainer.append($errorEl).addClass("error");
\r
406 _.each(errors, markErrors);
\r
412 This is for items that will be displayed as table rows.
414 app - MarionetteApplication
415 template - template (See XOSHelper.html)
416 detailClass - class of detail view, probably an XOSDetailView
419 XOSItemView = Marionette.ItemView.extend({
421 className: 'test-tablerow',
423 templateHelpers: function() { return { modelName: this.model.modelName,
424 collectionName: this.model.collectionName,
430 app - MarionetteApplication
431 childView - class of ItemView, probably an XOSItemView
432 template - template (see xosHelper.html)
433 collection - collection that holds these objects
434 title - title to display in template
437 XOSListView = Marionette.CompositeView.extend({
438 childViewContainer: 'tbody',
\r
440 events: {"click button.btn-xos-add": "addClicked",
\r
441 "click button.btn-xos-refresh": "refreshClicked",
\r
444 _fetchStateChange: function() {
\r
445 if (this.collection.fetching) {
\r
446 $("#xos-list-title-spinner").show();
\r
448 $("#xos-list-title-spinner").hide();
\r
452 addClicked: function(e) {
454 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
457 \r refreshClicked: function(e) {
458 \r e.preventDefault();
459 \r this.collection.refresh(refreshRelated=true);
462 \r initialize: function() {
463 \r this.listenTo(this.collection, 'change', this._renderChildren)
464 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
466 // Because many of the templates use idToName(), we need to
467 // listen to the collections that hold the names for the ids
468 // that we want to display.
469 for (i in this.collection.foreignCollections) {
470 foreignName = this.collection.foreignCollections[i];
471 if (xos[foreignName] == undefined) {
472 console.log("Failed to find xos class " + foreignName);
474 this.listenTo(xos[foreignName], 'change', this._renderChildren);
475 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
479 templateHelpers: function() {
480 return { title: this.title };
484 /* Give an id, the name of a collection, and the name of a field for models
485 within that collection, lookup the id and return the value of the field.
488 idToName = function(id, collectionName, fieldName) {
489 linkedObject = xos[collectionName].get(id);
490 if (linkedObject == undefined) {
493 return linkedObject.attributes[fieldName];
497 /* Constructs lists of <option> html blocks for items in a collection.
499 selectedId = the id of an object that should be selected, if any
500 collectionName = name of collection
501 fieldName = name of field within models of collection that will be displayed
504 idToOptions = function(selectedId, collectionName, fieldName) {
506 for (index in xos[collectionName].models) {
507 linkedObject = xos[collectionName].models[index];
508 linkedId = linkedObject["id"];
509 linkedName = linkedObject.attributes[fieldName];
510 if (linkedId == selectedId) {
511 selected = " selected";
515 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
520 /* Constructs an html <select> and the <option>s to go with it.
522 variable = variable name to return to form
523 selectedId = the id of an object that should be selected, if any
524 collectionName = name of collection
525 fieldName = name of field within models of collection that will be displayed
528 idToSelect = function(variable, selectedId, collectionName, fieldName) {
529 result = '<select name="' + variable + '">' +
530 idToOptions(selectedId, collectionName, fieldName) +