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() {
\r
37 $(this).dialog("close");
\r
39 view.trigger(event);
\r
45 "Cancel" : function() {
\r
46 $(this).dialog("close");
\r
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();
\r
85 listViewShower: function(listViewName, collection_name, regionName, title) {
\r
88 app[regionName].show(new app[listViewName]);
\r
89 app.hideLinkedItems();
\r
90 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
\r
91 $("#detail").show();
\r
92 $("#xos-listview-button-box").show();
\r
94 $("#xos-detail-button-box").hide();
\r
98 addShower: function(detailName, collection_name, regionName, title) {
\r
100 return function() {
\r
101 model = new xos[collection_name].model();
\r
102 detailViewClass = app[detailName];
\r
103 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
\r
104 app[regionName].show(detailView);
\r
105 $("#xos-detail-button-box").show();
\r
106 $("#xos-listview-button-box").hide();
\r
110 detailShower: function(detailName, collection_name, regionName, title) {
\r
112 showModelId = function(model_id) {
\r
113 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
\r
115 collection = xos[collection_name];
\r
116 model = collection.get(model_id);
\r
117 if (model == undefined) {
\r
118 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
\r
120 detailViewClass = app[detailName];
\r
121 detailView = new detailViewClass({model: model});
\r
122 app[regionName].show(detailView);
\r
123 detailView.showLinkedItems();
\r
124 $("#xos-detail-button-box").show();
\r
125 $("#xos-listview-button-box").hide();
\r
128 return showModelId;
\r
131 /* error handling callbacks */
\r
133 hideError: function() {
\r
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) {
\r
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) {
\r
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) {
\r
212 console.log("saveError");
\r
213 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
214 result["infoMsgId"] = infoMsgId;
\r
215 this.showError(result);
\r
218 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
\r
219 console.log("saveSuccess");
\r
220 if (addToCollection) {
\r
221 addToCollection.add(model);
\r
222 addToCollection.sort();
\r
224 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
225 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
226 result["infoMsgId"] = infoMsgId;
\r
227 this.showSuccess(result);
\r
230 destroyError: function(model, result, xhr, infoMsgId) {
231 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
232 result["infoMsgId"] = infoMsgId;
\r
233 this.showError(result);
\r
236 destroySuccess: function(model, result, xhr, infoMsgId) {
\r
237 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
238 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
239 result["infoMsgId"] = infoMsgId;
\r
240 this.showSuccess(result);
\r
243 /* end error handling callbacks */
\r
245 destroyModel: function(model) {
\r
247 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
249 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
250 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
253 deleteDialog: function(model, navToListAfterDelete) {
255 this.confirmDialog(this, callback=function() {
256 modelName = model.modelName;
257 that.destroyModel(model);
258 if (navToListAfterDelete) {
259 that.navigate("list", modelName);
268 app - MarionetteApplication
269 template - template (See XOSHelper.html)
272 XOSDetailView = Marionette.ItemView.extend({
275 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
276 "click button.btn-xos-save-leave": "submitLeaveClicked",
277 "click button.btn-xos-save-another": "submitAddAnotherClicked",
278 "click button.btn-xos-delete": "deleteClicked",
279 "change input": "inputChanged"},
281 initialize: function() {
282 this.on('deleteConfirmed', this.deleteConfirmed);
285 /* inputChanged is watching the onChange events of the input controls. We
286 do this to track when this view is 'dirty', so we can throw up a warning
\r
287 if the user tries to change his slices without saving first.
\r
290 inputChanged: function(e) {
\r
294 submitContinueClicked: function(e) {
\r
295 console.log("saveContinue");
300 submitLeaveClicked: function(e) {
301 console.log("saveLeave");
304 this.app.navigate("list", this.model.modelName);
307 submitAddAnotherClicked: function(e) {
308 console.log("saveAnother");
311 this.app.navigate("add", this.model.modelName);
315 this.app.hideError();
316 var data = Backbone.Syphon.serialize(this);
\r
318 var isNew = !this.model.id;
\r
320 this.$el.find(".help-inline").remove();
\r
322 /* although model.validate() is called automatically by
\r
323 model.save, we call it ourselves, so we can throw up our
\r
324 validation error before creating the infoMsg in the log
\r
326 errors = this.model.xosValidate(data);
\r
328 this.onFormDataInvalid(errors);
\r
333 this.model.attributes.humanReadableName = "new " + model.modelName;
\r
334 this.model.addToCollection = this.collection;
\r
336 this.model.addToCollection = undefined;
\r
339 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
\r
341 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
\r
342 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);}});
\r
343 this.dirty = false;
\r
346 destroyModel: function() {
347 this.app.hideError();
348 var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
350 this.model.destroy({error: function(model, result, xhr) { that.app.destroyError(model,result,xhr,infoMsgId);},
351 success: function(model, result, xhr) { that.app.destroySuccess(model,result,xhr,infoMsgId);}});
354 deleteClicked: function(e) {
356 \r this.app.confirmDialog(this, "deleteConfirmed");
359 \r deleteConfirmed: function() {
360 \r modelName = this.model.modelName;
361 \r this.destroyModel();
362 \r this.app.navigate("list", modelName);
365 tabClick: function(tabId, regionName) {
366 region = this.app[regionName];
\r
367 if (this.currentTabRegion != undefined) {
\r
368 this.currentTabRegion.$el.hide();
\r
370 if (this.currentTabId != undefined) {
\r
371 $(this.currentTabId).removeClass('active');
\r
373 this.currentTabRegion = region;
\r
374 this.currentTabRegion.$el.show();
\r
376 this.currentTabId = tabId;
\r
377 $(tabId).addClass('active');
\r
380 showTabs: function(tabs) {
381 template = templateFromId("#xos-tabs-template", {tabs: tabs});
382 $("#tabs").html(template(tabs));
385 _.each(tabs, function(tab) {
386 var regionName = tab["region"];
387 var tabId = '#xos-nav-'+regionName;
388 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
394 showLinkedItems: function() {
397 tabs.push({name: "details", region: "detail"});
400 for (relatedName in this.model.collection.relatedCollections) {
\r
401 relatedField = this.model.collection.relatedCollections[relatedName];
\r
402 regionName = "linkedObjs" + (index+1);
\r
404 relatedListViewClassName = relatedName + "ListView";
\r
405 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
\r
406 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});
\r
407 this.app[regionName].show(new relatedListViewClass());
\r
408 if (this.app.hideTabsByDefault) {
\r
409 this.app[regionName].$el.hide();
\r
411 tabs.push({name: relatedName, region: regionName});
\r
416 this.app["linkedObjs" + (index+1)].empty();
\r
420 this.showTabs(tabs);
\r
421 this.tabClick('#xos-nav-detail', 'detail');
\r
424 onFormDataInvalid: function(errors) {
\r
426 var markErrors = function(value, key) {
\r
427 console.log("name='" + key + "'");
\r
428 var $inputElement = self.$el.find("[name='" + key + "']");
\r
429 var $inputContainer = $inputElement.parent();
\r
430 //$inputContainer.find(".help-inline").remove();
\r
431 var $errorEl = $("<span>", {class: "help-inline error", text: value});
\r
432 $inputContainer.append($errorEl).addClass("error");
\r
434 _.each(errors, markErrors);
\r
440 This is for items that will be displayed as table rows.
442 app - MarionetteApplication
443 template - template (See XOSHelper.html)
444 detailClass - class of detail view, probably an XOSDetailView
447 XOSItemView = Marionette.ItemView.extend({
449 className: 'test-tablerow',
451 templateHelpers: function() { return { modelName: this.model.modelName,
452 collectionName: this.model.collectionName,
458 app - MarionetteApplication
459 childView - class of ItemView, probably an XOSItemView
460 template - template (see xosHelper.html)
461 collection - collection that holds these objects
462 title - title to display in template
465 XOSListView = Marionette.CompositeView.extend({
466 childViewContainer: 'tbody',
\r
468 events: {"click button.btn-xos-add": "addClicked",
\r
469 "click button.btn-xos-refresh": "refreshClicked",
\r
472 _fetchStateChange: function() {
\r
473 if (this.collection.fetching) {
\r
474 $("#xos-list-title-spinner").show();
\r
476 $("#xos-list-title-spinner").hide();
\r
480 addClicked: function(e) {
482 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
485 \r refreshClicked: function(e) {
486 \r e.preventDefault();
487 \r this.collection.refresh(refreshRelated=true);
490 \r initialize: function() {
491 \r this.listenTo(this.collection, 'change', this._renderChildren)
492 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
494 // Because many of the templates use idToName(), we need to
495 // listen to the collections that hold the names for the ids
496 // that we want to display.
497 for (i in this.collection.foreignCollections) {
498 foreignName = this.collection.foreignCollections[i];
499 if (xos[foreignName] == undefined) {
500 console.log("Failed to find xos class " + foreignName);
502 this.listenTo(xos[foreignName], 'change', this._renderChildren);
503 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
507 templateHelpers: function() {
508 return { title: this.title };
512 /* Give an id, the name of a collection, and the name of a field for models
513 within that collection, lookup the id and return the value of the field.
516 idToName = function(id, collectionName, fieldName) {
517 linkedObject = xos[collectionName].get(id);
518 if (linkedObject == undefined) {
521 return linkedObject.attributes[fieldName];
525 /* Constructs lists of <option> html blocks for items in a collection.
527 selectedId = the id of an object that should be selected, if any
528 collectionName = name of collection
529 fieldName = name of field within models of collection that will be displayed
532 idToOptions = function(selectedId, collectionName, fieldName) {
534 for (index in xos[collectionName].models) {
535 linkedObject = xos[collectionName].models[index];
536 linkedId = linkedObject["id"];
537 linkedName = linkedObject.attributes[fieldName];
538 if (linkedId == selectedId) {
539 selected = " selected";
543 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
548 /* Constructs an html <select> and the <option>s to go with it.
550 variable = variable name to return to form
551 selectedId = the id of an object that should be selected, if any
552 collectionName = name of collection
553 fieldName = name of field within models of collection that will be displayed
556 idToSelect = function(variable, selectedId, collectionName, fieldName) {
557 result = '<select name="' + variable + '">' +
558 idToOptions(selectedId, collectionName, fieldName) +