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, callback) {
32 $("#xos-confirm-dialog").dialog({
36 "Confirm" : function() {
37 $(this).dialog("close");
45 "Cancel" : function() {
46 $(this).dialog("close");
50 $("#xos-confirm-dialog").dialog("open");
53 popupErrorDialog: function(responseText) {
55 parsed_error=$.parseJSON(responseText);
59 parsed_error=undefined;
60 width=640; // django stacktraces like wide width
63 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
65 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
68 $("#xos-error-dialog").dialog({
72 Ok: function() { $(this).dialog("close"); }
77 hideLinkedItems: function(result) {
80 this["linkedObjs" + (index+1)].empty();
85 listViewShower: function(listViewName, collection_name, regionName, title) {
88 app[regionName].show(new app[listViewName]);
89 app.hideLinkedItems();
90 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
92 $("#xos-listview-button-box").show();
94 $("#xos-detail-button-box").hide();
98 addShower: function(detailName, collection_name, regionName, title) {
101 model = new xos[collection_name].model();
102 detailViewClass = app[detailName];
103 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
104 app[regionName].show(detailView);
105 $("#xos-detail-button-box").show();
106 $("#xos-listview-button-box").hide();
110 detailShower: function(detailName, collection_name, regionName, title) {
112 showModelId = function(model_id) {
113 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
115 collection = xos[collection_name];
116 model = collection.get(model_id);
117 if (model == undefined) {
118 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
120 detailViewClass = app[detailName];
121 detailView = new detailViewClass({model: model});
122 app[regionName].show(detailView);
123 detailView.showLinkedItems();
124 $("#xos-detail-button-box").show();
125 $("#xos-listview-button-box").hide();
131 /* error handling callbacks */
133 hideError: function() {
134 if (this.logWindowId) {
136 $(this.errorBoxId).hide();
137 $(this.successBoxId).hide();
141 showSuccess: function(result) {
142 result["statusclass"] = "success";
143 if (this.logTableId) {
144 this.appendLogWindow(result);
146 $(this.successBoxId).show();
147 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
149 $(this.successCloseButtonId).unbind().bind('click', function() {
150 $(that.successBoxId).hide();
155 showError: function(result) {
156 result["statusclass"] = "failure";
157 if (this.logTableId) {
158 this.appendLogWindow(result);
159 this.popupErrorDialog(result.responseText);
161 // this is really old stuff
162 $(this.errorBoxId).show();
163 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
165 $(this.errorCloseButtonId).unbind().bind('click', function() {
166 $(that.errorBoxId).hide();
171 showInformational: function(result) {
172 result["statusclass"] = "inprog";
173 if (this.logTableId) {
174 return this.appendLogWindow(result);
180 appendLogWindow: function(result) {
181 // compute a new logMessageId for this log message
182 logMessageId = "logMessage" + this.logMessageCount;
183 this.logMessageCount = this.logMessageCount + 1;
184 result["logMessageId"] = logMessageId;
186 logMessageTemplate=$("#xos-log-template").html();
187 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
188 newRow = _.template(logMessageTemplate, result);
189 assert(newRow != undefined, "newRow is undefined");
191 if (result["infoMsgId"] != undefined) {
192 // We were passed the logMessageId of an informational message,
193 // and the caller wants us to replace that message with our own.
194 // i.e. replace an informational message with a success or an error.
195 $("#"+result["infoMsgId"]).replaceWith(newRow);
197 // Create a brand new log message rather than replacing one.
198 logTableBody = $(this.logTableId + " tbody");
199 logTableBody.prepend(newRow);
202 if (this.statusMsgId) {
203 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
206 limitTableRows(this.logTableId, 5);
211 saveError: function(model, result, xhr, infoMsgId) {
212 console.log("saveError");
213 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
214 result["infoMsgId"] = infoMsgId;
215 this.showError(result);
218 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
219 console.log("saveSuccess");
220 if (addToCollection) {
221 addToCollection.add(model);
222 addToCollection.sort();
224 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
225 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
226 result["infoMsgId"] = infoMsgId;
227 this.showSuccess(result);
230 destroyError: function(model, result, xhr, infoMsgId) {
231 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
232 result["infoMsgId"] = infoMsgId;
233 this.showError(result);
236 destroySuccess: function(model, result, xhr, infoMsgId) {
237 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
238 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
239 result["infoMsgId"] = infoMsgId;
240 this.showSuccess(result);
243 /* end error handling callbacks */
245 destroyModel: function(model) {
246 //console.log("destroyModel"); console.log(model);
248 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
250 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
251 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
254 deleteDialog: function(model, navToListAfterDelete) {
256 //console.log("deleteDialog"); console.log(model);
257 this.confirmDialog(null, null, function() {
258 //console.log("deleteConfirm"); console.log(model);
259 modelName = model.modelName;
260 that.destroyModel(model);
261 if (navToListAfterDelete) {
262 that.navigate("list", modelName);
271 app - MarionetteApplication
272 template - template (See XOSHelper.html)
275 XOSDetailView = Marionette.ItemView.extend({
278 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
279 "click button.btn-xos-save-leave": "submitLeaveClicked",
280 "click button.btn-xos-save-another": "submitAddAnotherClicked",
281 "click button.btn-xos-delete": "deleteClicked",
282 "change input": "inputChanged"},
284 /*initialize: function() {
285 this.on('deleteConfirmed', this.deleteConfirmed);
288 /* inputChanged is watching the onChange events of the input controls. We
289 do this to track when this view is 'dirty', so we can throw up a warning
290 if the user tries to change his slices without saving first.
293 inputChanged: function(e) {
297 submitContinueClicked: function(e) {
298 console.log("saveContinue");
303 submitLeaveClicked: function(e) {
304 console.log("saveLeave");
307 this.app.navigate("list", this.model.modelName);
310 submitAddAnotherClicked: function(e) {
311 console.log("saveAnother");
314 this.app.navigate("add", this.model.modelName);
318 this.app.hideError();
319 var data = Backbone.Syphon.serialize(this);
321 var isNew = !this.model.id;
323 this.$el.find(".help-inline").remove();
325 /* although model.validate() is called automatically by
326 model.save, we call it ourselves, so we can throw up our
327 validation error before creating the infoMsg in the log
329 errors = this.model.xosValidate(data);
331 this.onFormDataInvalid(errors);
336 this.model.attributes.humanReadableName = "new " + model.modelName;
337 this.model.addToCollection = this.collection;
339 this.model.addToCollection = undefined;
342 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
344 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
345 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);}});
349 /*destroyModel: function() {
350 this.app.hideError();
351 var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
353 this.model.destroy({error: function(model, result, xhr) { that.app.destroyError(model,result,xhr,infoMsgId);},
354 success: function(model, result, xhr) { that.app.destroySuccess(model,result,xhr,infoMsgId);}});
357 deleteClicked: function(e) {
359 this.app.confirmDialog(this, "deleteConfirmed");
362 deleteConfirmed: function() {
363 modelName = this.model.modelName;
365 this.app.navigate("list", modelName);
368 deleteClicked: function(e) {
370 this.app.deleteDialog(this.model, true);
373 tabClick: function(tabId, regionName) {
374 region = this.app[regionName];
375 if (this.currentTabRegion != undefined) {
376 this.currentTabRegion.$el.hide();
378 if (this.currentTabId != undefined) {
379 $(this.currentTabId).removeClass('active');
381 this.currentTabRegion = region;
382 this.currentTabRegion.$el.show();
384 this.currentTabId = tabId;
385 $(tabId).addClass('active');
388 showTabs: function(tabs) {
389 template = templateFromId("#xos-tabs-template", {tabs: tabs});
390 $("#tabs").html(template(tabs));
393 _.each(tabs, function(tab) {
394 var regionName = tab["region"];
395 var tabId = '#xos-nav-'+regionName;
396 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
402 showLinkedItems: function() {
405 tabs.push({name: "details", region: "detail"});
408 for (relatedName in this.model.collection.relatedCollections) {
409 relatedField = this.model.collection.relatedCollections[relatedName];
410 regionName = "linkedObjs" + (index+1);
412 relatedListViewClassName = relatedName + "ListView";
413 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
414 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});
415 this.app[regionName].show(new relatedListViewClass());
416 if (this.app.hideTabsByDefault) {
417 this.app[regionName].$el.hide();
419 tabs.push({name: relatedName, region: regionName});
424 this.app["linkedObjs" + (index+1)].empty();
429 this.tabClick('#xos-nav-detail', 'detail');
432 onFormDataInvalid: function(errors) {
434 var markErrors = function(value, key) {
435 console.log("name='" + key + "'");
436 var $inputElement = self.$el.find("[name='" + key + "']");
437 var $inputContainer = $inputElement.parent();
438 //$inputContainer.find(".help-inline").remove();
439 var $errorEl = $("<span>", {class: "help-inline error", text: value});
440 $inputContainer.append($errorEl).addClass("error");
442 _.each(errors, markErrors);
448 This is for items that will be displayed as table rows.
450 app - MarionetteApplication
451 template - template (See XOSHelper.html)
452 detailClass - class of detail view, probably an XOSDetailView
455 XOSItemView = Marionette.ItemView.extend({
457 className: 'test-tablerow',
459 templateHelpers: function() { return { modelName: this.model.modelName,
460 collectionName: this.model.collectionName,
466 app - MarionetteApplication
467 childView - class of ItemView, probably an XOSItemView
468 template - template (see xosHelper.html)
469 collection - collection that holds these objects
470 title - title to display in template
473 XOSListView = Marionette.CompositeView.extend({
474 childViewContainer: 'tbody',
476 events: {"click button.btn-xos-add": "addClicked",
477 "click button.btn-xos-refresh": "refreshClicked",
480 _fetchStateChange: function() {
481 if (this.collection.fetching) {
482 $("#xos-list-title-spinner").show();
484 $("#xos-list-title-spinner").hide();
488 addClicked: function(e) {
490 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
493 refreshClicked: function(e) {
495 this.collection.refresh(refreshRelated=true);
498 initialize: function() {
499 this.listenTo(this.collection, 'change', this._renderChildren)
500 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
502 // Because many of the templates use idToName(), we need to
503 // listen to the collections that hold the names for the ids
504 // that we want to display.
505 for (i in this.collection.foreignCollections) {
506 foreignName = this.collection.foreignCollections[i];
507 if (xos[foreignName] == undefined) {
508 console.log("Failed to find xos class " + foreignName);
510 this.listenTo(xos[foreignName], 'change', this._renderChildren);
511 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
515 templateHelpers: function() {
516 return { title: this.title };
520 /* Give an id, the name of a collection, and the name of a field for models
521 within that collection, lookup the id and return the value of the field.
524 idToName = function(id, collectionName, fieldName) {
525 linkedObject = xos[collectionName].get(id);
526 if (linkedObject == undefined) {
529 return linkedObject.attributes[fieldName];
533 /* Constructs lists of <option> html blocks for items in a collection.
535 selectedId = the id of an object that should be selected, if any
536 collectionName = name of collection
537 fieldName = name of field within models of collection that will be displayed
540 idToOptions = function(selectedId, collectionName, fieldName) {
542 for (index in xos[collectionName].models) {
543 linkedObject = xos[collectionName].models[index];
544 linkedId = linkedObject["id"];
545 linkedName = linkedObject.attributes[fieldName];
546 if (linkedId == selectedId) {
547 selected = " selected";
551 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
556 /* Constructs an html <select> and the <option>s to go with it.
558 variable = variable name to return to form
559 selectedId = the id of an object that should be selected, if any
560 collectionName = name of collection
561 fieldName = name of field within models of collection that will be displayed
564 idToSelect = function(variable, selectedId, collectionName, fieldName) {
565 result = '<select name="' + variable + '">' +
566 idToOptions(selectedId, collectionName, fieldName) +