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 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
235 result["infoMsgId"] = infoMsgId;
\r
236 this.app.showError(result);
\r
239 saveSuccess: function(model, result, xhr, infoMsgId, isNew) {
\r
240 console.log("saveSuccess");
\r
242 this.collection.add(model);
\r
243 this.collection.sort();
\r
245 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
246 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
\r
247 result["infoMsgId"] = infoMsgId;
\r
248 this.app.showSuccess(result);
\r
251 destroyError: function(model, result, xhr, infoMsgId) {
252 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
253 result["infoMsgId"] = infoMsgId;
\r
254 this.app.showError(result);
\r
257 destroySuccess: function(model, result, xhr, infoMsgId) {
\r
258 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
\r
259 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
\r
260 result["infoMsgId"] = infoMsgId;
\r
261 this.app.showSuccess(result);
\r
264 submitContinueClicked: function(e) {
265 console.log("saveContinue");
270 submitLeaveClicked: function(e) {
271 console.log("saveLeave");
274 this.app.navigate("list", this.model.modelName);
277 submitAddAnotherClicked: function(e) {
278 console.log("saveAnother");
281 this.app.navigate("add", this.model.modelName);
285 this.app.hideError();
286 var data = Backbone.Syphon.serialize(this);
\r
288 var isNew = !this.model.id;
\r
290 this.$el.find(".help-inline").remove();
\r
292 /* although model.validate() is called automatically by
\r
293 model.save, we call it ourselves, so we can throw up our
\r
294 validation error before creating the infoMsg in the log
\r
296 errors = this.model.xosValidate(data);
\r
298 this.onFormDataInvalid(errors);
\r
303 this.model.attributes.humanReadableName = "new " + model.modelName;
\r
306 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
\r
308 this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},
\r
309 invalid: function(model, result, xhr) { console.log("invalid!"); 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 events: {"click": "changeItem"},
425 changeItem: function(e) {
\r
426 this.app.hideError();
\r
427 e.preventDefault();
\r
428 e.stopPropagation();
\r
430 this.app.navigateToModel(this.app, this.detailClass, this.detailNavLink, this.model);
\r
436 app - MarionetteApplication
437 childView - class of ItemView, probably an XOSItemView
438 template - template (see xosHelper.html)
439 collection - collection that holds these objects
440 title - title to display in template
443 XOSListView = Marionette.CompositeView.extend({
444 childViewContainer: 'tbody',
\r
446 events: {"click button.btn-xos-add": "addClicked",
\r
447 "click button.btn-xos-refresh": "refreshClicked",
\r
450 _fetchStateChange: function() {
\r
451 if (this.collection.fetching) {
\r
452 $("#xos-list-title-spinner").show();
\r
454 $("#xos-list-title-spinner").hide();
\r
458 addClicked: function(e) {
460 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
463 \r refreshClicked: function(e) {
464 \r e.preventDefault();
465 \r this.collection.refresh(refreshRelated=true);
468 \r initialize: function() {
469 \r this.listenTo(this.collection, 'change', this._renderChildren)
470 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
472 // Because many of the templates use idToName(), we need to
473 // listen to the collections that hold the names for the ids
474 // that we want to display.
475 for (i in this.collection.foreignCollections) {
476 foreignName = this.collection.foreignCollections[i];
477 if (xos[foreignName] == undefined) {
478 console.log("Failed to find xos class " + foreignName);
480 this.listenTo(xos[foreignName], 'change', this._renderChildren);
481 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
485 templateHelpers: function() {
486 return { title: this.title };
490 /* Give an id, the name of a collection, and the name of a field for models
491 within that collection, lookup the id and return the value of the field.
494 idToName = function(id, collectionName, fieldName) {
495 linkedObject = xos[collectionName].get(id);
496 if (linkedObject == undefined) {
499 return linkedObject.attributes[fieldName];
503 /* Constructs lists of <option> html blocks for items in a collection.
505 selectedId = the id of an object that should be selected, if any
506 collectionName = name of collection
507 fieldName = name of field within models of collection that will be displayed
510 idToOptions = function(selectedId, collectionName, fieldName) {
512 for (index in xos[collectionName].models) {
513 linkedObject = xos[collectionName].models[index];
514 linkedId = linkedObject["id"];
515 linkedName = linkedObject.attributes[fieldName];
516 if (linkedId == selectedId) {
517 selected = " selected";
521 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
526 /* Constructs an html <select> and the <option>s to go with it.
528 variable = variable name to return to form
529 selectedId = the id of an object that should be selected, if any
530 collectionName = name of collection
531 fieldName = name of field within models of collection that will be displayed
534 idToSelect = function(variable, selectedId, collectionName, fieldName) {
535 result = '<select name="' + variable + '">' +
536 idToOptions(selectedId, collectionName, fieldName) +