1 HTMLView = Marionette.ItemView.extend({
3 this.$el.append(this.options.html);
7 XOSRouter = Marionette.AppRouter.extend({
8 initialize: function() {
\r
12 onRoute: function(x,y,z) {
\r
13 this.routeStack.push(Backbone.history.fragment);
\r
16 prevPage: function() {
\r
17 return this.routeStack.slice(-2)[0];
20 showPreviousURL: function() {
21 prevPage = this.prevPage();
23 this.navigate("#"+prevPage, {trigger: false, replace: true} );
30 XOSApplication = Marionette.Application.extend({
31 detailBoxId: "#detailBox",
32 errorBoxId: "#errorBox",
33 errorCloseButtonId: "#close-error-box",
34 successBoxId: "#successBox",
35 successCloseButtonId: "#close-success-box",
36 errorTemplate: "#xos-error-template",
37 successTemplate: "#xos-success-template",
40 confirmDialog: function(view, event, callback) {
41 $("#xos-confirm-dialog").dialog({
45 "Confirm" : function() {
46 $(this).dialog("close");
54 "Cancel" : function() {
55 $(this).dialog("close");
59 $("#xos-confirm-dialog").dialog("open");
62 popupErrorDialog: function(responseText) {
64 parsed_error=$.parseJSON(responseText);
68 parsed_error=undefined;
69 width=640; // django stacktraces like wide width
72 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
74 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
77 $("#xos-error-dialog").dialog({
81 Ok: function() { $(this).dialog("close"); }
86 hideLinkedItems: function(result) {
89 this["linkedObjs" + (index+1)].empty();
94 createListHandler: function(listViewName, collection_name, regionName, title) {
97 app[regionName].show(new app[listViewName]);
98 app.hideLinkedItems();
99 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
101 $("#xos-listview-button-box").show();
103 $("#xos-detail-button-box").hide();
107 createAddHandler: function(detailName, collection_name, regionName, title) {
110 model = new xos[collection_name].model();
111 detailViewClass = app[detailName];
112 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
113 app[regionName].show(detailView);
114 $("#xos-detail-button-box").show();
115 $("#xos-listview-button-box").hide();
119 createAddChildHandler: function(addChildName, collection_name) {
121 return function(parent_modelName, parent_fieldName, parent_id) {
122 app.Router.showPreviousURL();
124 console.log(modelName);
125 console.log(parent_fieldName);
126 console.log(parent_id);
127 model = new xos[collection_name].model();
128 model.attributes[parent_fieldName] = parent_id;
129 detailViewClass = app[addChildName];
130 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
131 detailView.dialog = $("xos-addchild-dialog");
132 app["addChildDetail"].show(detailView);
133 $("#xos-addchild-dialog").dialog({
138 "Save" : function() {
139 var addDialog = this;
140 detailView.synchronous = true;
141 detailView.afterSave = function() { $(addDialog).dialog("close"); }
144 //$(this).dialog("close");
146 "Cancel" : function() {
147 $(this).dialog("close");
151 $("#xos-addchild-dialog").dialog("open");
155 createDeleteHandler: function(collection_name) {
157 return function(model_id) {
158 console.log("deleteCalled");
159 collection = xos[collection_name];
160 model = collection.get(model_id);
161 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
162 app.deleteDialog(model,"back");
166 createDetailHandler: function(detailName, collection_name, regionName, title) {
168 showModelId = function(model_id) {
169 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
171 collection = xos[collection_name];
172 model = collection.get(model_id);
173 if (model == undefined) {
174 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
176 detailViewClass = app[detailName];
177 detailView = new detailViewClass({model: model});
178 app[regionName].show(detailView);
179 detailView.showLinkedItems();
180 $("#xos-detail-button-box").show();
181 $("#xos-listview-button-box").hide();
187 /* error handling callbacks */
189 hideError: function() {
190 if (this.logWindowId) {
192 $(this.errorBoxId).hide();
193 $(this.successBoxId).hide();
197 showSuccess: function(result) {
198 result["statusclass"] = "success";
199 if (this.logTableId) {
200 this.appendLogWindow(result);
202 $(this.successBoxId).show();
203 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
205 $(this.successCloseButtonId).unbind().bind('click', function() {
206 $(that.successBoxId).hide();
211 showError: function(result) {
212 result["statusclass"] = "failure";
213 if (this.logTableId) {
214 this.appendLogWindow(result);
215 this.popupErrorDialog(result.responseText);
217 // this is really old stuff
218 $(this.errorBoxId).show();
219 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
221 $(this.errorCloseButtonId).unbind().bind('click', function() {
222 $(that.errorBoxId).hide();
227 showInformational: function(result) {
228 result["statusclass"] = "inprog";
229 if (this.logTableId) {
230 return this.appendLogWindow(result);
236 appendLogWindow: function(result) {
237 // compute a new logMessageId for this log message
238 logMessageId = "logMessage" + this.logMessageCount;
239 this.logMessageCount = this.logMessageCount + 1;
240 result["logMessageId"] = logMessageId;
242 logMessageTemplate=$("#xos-log-template").html();
243 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
244 newRow = _.template(logMessageTemplate, result);
245 assert(newRow != undefined, "newRow is undefined");
247 if (result["infoMsgId"] != undefined) {
248 // We were passed the logMessageId of an informational message,
249 // and the caller wants us to replace that message with our own.
250 // i.e. replace an informational message with a success or an error.
251 $("#"+result["infoMsgId"]).replaceWith(newRow);
253 // Create a brand new log message rather than replacing one.
254 logTableBody = $(this.logTableId + " tbody");
255 logTableBody.prepend(newRow);
258 if (this.statusMsgId) {
259 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
262 limitTableRows(this.logTableId, 5);
267 saveError: function(model, result, xhr, infoMsgId) {
268 console.log("saveError");
269 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
270 result["infoMsgId"] = infoMsgId;
271 this.showError(result);
274 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
275 console.log("saveSuccess");
276 if (model.addToCollection) {
277 console.log("addToCollection");
278 model.addToCollection.add(model);
279 model.addToCollection.sort();
280 model.addToCollection = undefined;
282 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
283 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
284 result["infoMsgId"] = infoMsgId;
285 this.showSuccess(result);
288 destroyError: function(model, result, xhr, infoMsgId) {
289 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
290 result["infoMsgId"] = infoMsgId;
291 this.showError(result);
294 destroySuccess: function(model, result, xhr, infoMsgId) {
295 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
296 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
297 result["infoMsgId"] = infoMsgId;
298 this.showSuccess(result);
301 /* end error handling callbacks */
303 destroyModel: function(model) {
304 //console.log("destroyModel"); console.log(model);
306 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
308 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
309 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
312 deleteDialog: function(model, afterDelete) {
315 console.log(Backbone.history.fragment);
316 assert(model!=undefined, "deleteDialog's model is undefined");
317 //console.log("deleteDialog"); console.log(model);
318 this.confirmDialog(null, null, function() {
319 //console.log("deleteConfirm"); console.log(model);
320 modelName = model.modelName;
321 that.destroyModel(model);
322 if (afterDelete=="list") {
323 that.navigate("list", modelName);
324 } else if (afterDelete=="back") {
325 that.Router.showPreviousURL();
334 app - MarionetteApplication
335 template - template (See XOSHelper.html)
338 XOSDetailView = Marionette.ItemView.extend({
341 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
342 "click button.btn-xos-save-leave": "submitLeaveClicked",
343 "click button.btn-xos-save-another": "submitAddAnotherClicked",
344 "click button.btn-xos-delete": "deleteClicked",
345 "change input": "inputChanged"},
347 /* inputChanged is watching the onChange events of the input controls. We
348 do this to track when this view is 'dirty', so we can throw up a warning
349 if the user tries to change his slices without saving first.
352 initialize: function() {
353 this.on("saveSuccess", this.afterSave);
354 this.synchronous = false;
357 afterSave: function(e) {
360 inputChanged: function(e) {
364 submitContinueClicked: function(e) {
365 console.log("saveContinue");
367 this.afterSave = function() {};
371 submitLeaveClicked: function(e) {
372 console.log("saveLeave");
375 this.afterSave = function() {
376 that.app.navigate("list", that.model.modelName);
381 submitAddAnotherClicked: function(e) {
382 console.log("saveAnother");
385 this.afterSave = function() {
386 that.app.navigate("add", that.model.modelName);
392 this.app.hideError();
393 var data = Backbone.Syphon.serialize(this);
395 var isNew = !this.model.id;
397 this.$el.find(".help-inline").remove();
399 /* although model.validate() is called automatically by
400 model.save, we call it ourselves, so we can throw up our
401 validation error before creating the infoMsg in the log
403 errors = this.model.xosValidate(data);
405 this.onFormDataInvalid(errors);
410 this.model.attributes.humanReadableName = "new " + model.modelName;
411 this.model.addToCollection = this.collection;
413 this.model.addToCollection = undefined;
416 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
418 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
419 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
420 if (that.synchronous) {
421 that.trigger("saveSuccess");
426 if (!this.synchronous) {
431 deleteClicked: function(e) {
433 this.app.deleteDialog(this.model, "list");
436 tabClick: function(tabId, regionName) {
437 region = this.app[regionName];
438 if (this.currentTabRegion != undefined) {
439 this.currentTabRegion.$el.hide();
441 if (this.currentTabId != undefined) {
442 $(this.currentTabId).removeClass('active');
444 this.currentTabRegion = region;
445 this.currentTabRegion.$el.show();
447 this.currentTabId = tabId;
448 $(tabId).addClass('active');
451 showTabs: function(tabs) {
452 template = templateFromId("#xos-tabs-template", {tabs: tabs});
453 $("#tabs").html(template(tabs));
456 _.each(tabs, function(tab) {
457 var regionName = tab["region"];
458 var tabId = '#xos-nav-'+regionName;
459 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
465 showLinkedItems: function() {
468 tabs.push({name: "details", region: "detail"});
471 for (relatedName in this.model.collection.relatedCollections) {
472 relatedField = this.model.collection.relatedCollections[relatedName];
473 regionName = "linkedObjs" + (index+1);
475 relatedListViewClassName = relatedName + "ListView";
476 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
477 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id),
478 parentModel: this.model});
479 this.app[regionName].show(new relatedListViewClass());
480 if (this.app.hideTabsByDefault) {
481 this.app[regionName].$el.hide();
483 tabs.push({name: relatedName, region: regionName});
488 this.app["linkedObjs" + (index+1)].empty();
493 this.tabClick('#xos-nav-detail', 'detail');
496 onFormDataInvalid: function(errors) {
498 var markErrors = function(value, key) {
499 console.log("name='" + key + "'");
500 var $inputElement = self.$el.find("[name='" + key + "']");
501 var $inputContainer = $inputElement.parent();
502 //$inputContainer.find(".help-inline").remove();
503 var $errorEl = $("<span>", {class: "help-inline error", text: value});
504 $inputContainer.append($errorEl).addClass("error");
506 _.each(errors, markErrors);
509 templateHelpers: function() { return { modelName: this.model.modelName,
510 collectionName: this.model.collectionName,
511 addFields: this.model.addFields,
512 detailFields: this.model.detailFields,
513 foreignFields: this.model.foreignFields,
514 inputType: this.model.inputType,
521 This is for items that will be displayed as table rows.
523 app - MarionetteApplication
524 template - template (See XOSHelper.html)
525 detailClass - class of detail view, probably an XOSDetailView
528 XOSItemView = Marionette.ItemView.extend({
530 className: 'test-tablerow',
532 templateHelpers: function() { return { modelName: this.model.modelName,
533 collectionName: this.model.collectionName,
534 addFields: this.model.addFields,
535 detailFields: this.model.detailFields,
536 foreignFields: this.model.foreignFields,
537 inputType: this.model.inputType,
544 app - MarionetteApplication
545 childView - class of ItemView, probably an XOSItemView
546 template - template (see xosHelper.html)
547 collection - collection that holds these objects
548 title - title to display in template
551 XOSListView = Marionette.CompositeView.extend({
552 childViewContainer: 'tbody',
555 events: {"click button.btn-xos-add": "addClicked",
556 "click button.btn-xos-refresh": "refreshClicked",
559 _fetchStateChange: function() {
560 if (this.collection.fetching) {
561 $("#xos-list-title-spinner").show();
563 $("#xos-list-title-spinner").hide();
567 addClicked: function(e) {
569 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
572 refreshClicked: function(e) {
574 this.collection.refresh(refreshRelated=true);
577 initialize: function() {
578 this.listenTo(this.collection, 'change', this._renderChildren)
579 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
581 // Because many of the templates use idToName(), we need to
582 // listen to the collections that hold the names for the ids
583 // that we want to display.
584 for (i in this.collection.foreignCollections) {
585 foreignName = this.collection.foreignCollections[i];
586 if (xos[foreignName] == undefined) {
587 console.log("Failed to find xos class " + foreignName);
589 this.listenTo(xos[foreignName], 'change', this._renderChildren);
590 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
594 getAddChildHash: function() {
595 if (this.parentModel) {
596 // Find the field name in the model that should point to
597 // the parent object. For example, when adding a sliver, the
598 // fieldName that should point to 'users' is 'creator'.
599 parentFieldName = "unknown";
600 for (fieldName in this.collection.foreignFields) {
601 cname = this.collection.foreignFields[fieldName];
602 if (cname = this.collection.collectionName) {
603 parentFieldName = fieldName;
606 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
612 templateHelpers: function() {
613 return { title: this.title,
614 addChildHash: this.getAddChildHash() };
618 /* Give an id, the name of a collection, and the name of a field for models
619 within that collection, lookup the id and return the value of the field.
622 idToName = function(id, collectionName, fieldName) {
623 linkedObject = xos[collectionName].get(id);
624 if (linkedObject == undefined) {
627 return linkedObject.attributes[fieldName];
631 /* Constructs lists of <option> html blocks for items in a collection.
633 selectedId = the id of an object that should be selected, if any
634 collectionName = name of collection
635 fieldName = name of field within models of collection that will be displayed
638 idToOptions = function(selectedId, collectionName, fieldName) {
640 for (index in xos[collectionName].models) {
641 linkedObject = xos[collectionName].models[index];
642 linkedId = linkedObject["id"];
643 linkedName = linkedObject.attributes[fieldName];
644 if (linkedId == selectedId) {
645 selected = " selected";
649 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
654 /* Constructs an html <select> and the <option>s to go with it.
656 variable = variable name to return to form
657 selectedId = the id of an object that should be selected, if any
658 collectionName = name of collection
659 fieldName = name of field within models of collection that will be displayed
662 idToSelect = function(variable, selectedId, collectionName, fieldName) {
663 result = '<select name="' + variable + '">' +
664 idToOptions(selectedId, collectionName, fieldName) +