1 HTMLView = Marionette.ItemView.extend({
3 this.$el.append(this.options.html);
7 FilteredCompositeView = Marionette.CompositeView.extend( {
8 showCollection: function() {
10 this.collection.each(function(child, index) {
11 if (this.filter && !this.filter(child)) {
14 ChildView = this.getChildView(child);
15 this.addChild(child, ChildView, index);
21 XOSRouter = Marionette.AppRouter.extend({
22 initialize: function() {
\r
26 onRoute: function(x,y,z) {
\r
27 this.routeStack.push(Backbone.history.fragment);
\r
30 prevPage: function() {
\r
31 return this.routeStack.slice(-1)[0];
34 showPreviousURL: function() {
35 prevPage = this.prevPage();
36 console.log("showPreviousURL");
37 console.log(this.routeStack);
39 this.navigate("#"+prevPage, {trigger: false, replace: true} );
46 XOSApplication = Marionette.Application.extend({
47 detailBoxId: "#detailBox",
48 errorBoxId: "#errorBox",
49 errorCloseButtonId: "#close-error-box",
50 successBoxId: "#successBox",
51 successCloseButtonId: "#close-success-box",
52 errorTemplate: "#xos-error-template",
53 successTemplate: "#xos-success-template",
56 confirmDialog: function(view, event, callback) {
57 $("#xos-confirm-dialog").dialog({
61 "Confirm" : function() {
62 $(this).dialog("close");
70 "Cancel" : function() {
71 $(this).dialog("close");
75 $("#xos-confirm-dialog").dialog("open");
78 popupErrorDialog: function(responseText) {
80 parsed_error=$.parseJSON(responseText);
84 parsed_error=undefined;
85 width=640; // django stacktraces like wide width
88 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
90 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
93 $("#xos-error-dialog").dialog({
97 Ok: function() { $(this).dialog("close"); }
102 hideLinkedItems: function(result) {
105 this["linkedObjs" + (index+1)].empty();
110 createListHandler: function(listViewName, collection_name, regionName, title) {
113 app[regionName].show(new app[listViewName]);
114 app.hideLinkedItems();
115 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
117 $("#xos-listview-button-box").show();
119 $("#xos-detail-button-box").hide();
123 createAddHandler: function(detailName, collection_name, regionName, title) {
126 model = new xos[collection_name].model();
127 detailViewClass = app[detailName];
128 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
129 app[regionName].show(detailView);
130 $("#xos-detail-button-box").show();
131 $("#xos-listview-button-box").hide();
135 createAddChildHandler: function(addChildName, collection_name) {
137 return function(parent_modelName, parent_fieldName, parent_id) {
138 app.Router.showPreviousURL();
140 console.log(parent_modelName);
141 console.log(parent_fieldName);
142 console.log(parent_id);
143 model = new xos[collection_name].model();
144 model.attributes[parent_fieldName] = parent_id;
146 detailViewClass = app[addChildName];
147 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
148 detailView.dialog = $("xos-addchild-dialog");
149 app["addChildDetail"].show(detailView);
150 $("#xos-addchild-dialog").dialog({
155 "Save" : function() {
156 var addDialog = this;
157 detailView.synchronous = true;
158 detailView.afterSave = function() { console.log("afterSave"); $(addDialog).dialog("close"); }
161 //$(this).dialog("close");
163 "Cancel" : function() {
164 $(this).dialog("close");
168 $("#xos-addchild-dialog").dialog("open");
172 createDeleteHandler: function(collection_name) {
174 return function(model_id) {
175 console.log("deleteCalled");
176 collection = xos[collection_name];
177 model = collection.get(model_id);
178 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
179 app.Router.showPreviousURL();
180 app.deleteDialog(model);
184 createDetailHandler: function(detailName, collection_name, regionName, title) {
186 showModelId = function(model_id) {
187 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
189 collection = xos[collection_name];
190 model = collection.get(model_id);
191 if (model == undefined) {
192 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
194 detailViewClass = app[detailName];
195 detailView = new detailViewClass({model: model});
196 app[regionName].show(detailView);
197 detailView.showLinkedItems();
198 $("#xos-detail-button-box").show();
199 $("#xos-listview-button-box").hide();
205 /* error handling callbacks */
207 hideError: function() {
208 if (this.logWindowId) {
210 $(this.errorBoxId).hide();
211 $(this.successBoxId).hide();
215 showSuccess: function(result) {
216 result["statusclass"] = "success";
217 if (this.logTableId) {
218 this.appendLogWindow(result);
220 $(this.successBoxId).show();
221 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
223 $(this.successCloseButtonId).unbind().bind('click', function() {
224 $(that.successBoxId).hide();
229 showError: function(result) {
230 result["statusclass"] = "failure";
231 if (this.logTableId) {
232 this.appendLogWindow(result);
233 this.popupErrorDialog(result.responseText);
235 // this is really old stuff
236 $(this.errorBoxId).show();
237 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
239 $(this.errorCloseButtonId).unbind().bind('click', function() {
240 $(that.errorBoxId).hide();
245 showInformational: function(result) {
246 result["statusclass"] = "inprog";
247 if (this.logTableId) {
248 return this.appendLogWindow(result);
254 appendLogWindow: function(result) {
255 // compute a new logMessageId for this log message
256 logMessageId = "logMessage" + this.logMessageCount;
257 this.logMessageCount = this.logMessageCount + 1;
258 result["logMessageId"] = logMessageId;
260 logMessageTemplate=$("#xos-log-template").html();
261 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
262 newRow = _.template(logMessageTemplate, result);
263 assert(newRow != undefined, "newRow is undefined");
265 if (result["infoMsgId"] != undefined) {
266 // We were passed the logMessageId of an informational message,
267 // and the caller wants us to replace that message with our own.
268 // i.e. replace an informational message with a success or an error.
269 $("#"+result["infoMsgId"]).replaceWith(newRow);
271 // Create a brand new log message rather than replacing one.
272 logTableBody = $(this.logTableId + " tbody");
273 logTableBody.prepend(newRow);
276 if (this.statusMsgId) {
277 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
280 limitTableRows(this.logTableId, 5);
285 saveError: function(model, result, xhr, infoMsgId) {
286 console.log("saveError");
287 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
288 result["infoMsgId"] = infoMsgId;
289 this.showError(result);
292 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
293 console.log("saveSuccess");
294 if (model.addToCollection) {
295 console.log("addToCollection");
296 console.log(model.addToCollection);
297 model.addToCollection.add(model);
298 model.addToCollection.sort();
299 model.addToCollection = undefined;
301 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
302 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
303 result["infoMsgId"] = infoMsgId;
304 this.showSuccess(result);
307 destroyError: function(model, result, xhr, infoMsgId) {
308 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
309 result["infoMsgId"] = infoMsgId;
310 this.showError(result);
313 destroySuccess: function(model, result, xhr, infoMsgId) {
314 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
315 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
316 result["infoMsgId"] = infoMsgId;
317 this.showSuccess(result);
320 /* end error handling callbacks */
322 destroyModel: function(model) {
323 //console.log("destroyModel"); console.log(model);
325 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
327 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
328 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
331 deleteDialog: function(model, afterDelete) {
333 assert(model!=undefined, "deleteDialog's model is undefined");
334 //console.log("deleteDialog"); console.log(model);
335 this.confirmDialog(null, null, function() {
336 //console.log("deleteConfirm"); console.log(model);
337 modelName = model.modelName;
338 that.destroyModel(model);
339 if (afterDelete=="list") {
340 that.navigate("list", modelName);
348 app - MarionetteApplication
349 template - template (See XOSHelper.html)
352 XOSDetailView = Marionette.ItemView.extend({
355 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
356 "click button.btn-xos-save-leave": "submitLeaveClicked",
357 "click button.btn-xos-save-another": "submitAddAnotherClicked",
358 "click button.btn-xos-delete": "deleteClicked",
359 "change input": "inputChanged"},
361 /* inputChanged is watching the onChange events of the input controls. We
362 do this to track when this view is 'dirty', so we can throw up a warning
363 if the user tries to change his slices without saving first.
366 initialize: function() {
367 this.on("saveSuccess", this.onAfterSave);
368 this.synchronous = false;
371 afterSave: function(e) {
374 onAfterSave: function(e) {
378 inputChanged: function(e) {
382 submitContinueClicked: function(e) {
383 console.log("saveContinue");
385 this.afterSave = function() {};
389 submitLeaveClicked: function(e) {
390 console.log("saveLeave");
393 this.afterSave = function() {
394 that.app.navigate("list", that.model.modelName);
399 submitAddAnotherClicked: function(e) {
400 console.log("saveAnother");
403 this.afterSave = function() {
404 that.app.navigate("add", that.model.modelName);
410 this.app.hideError();
411 var data = Backbone.Syphon.serialize(this);
413 var isNew = !this.model.id;
415 this.$el.find(".help-inline").remove();
417 /* although model.validate() is called automatically by
418 model.save, we call it ourselves, so we can throw up our
419 validation error before creating the infoMsg in the log
421 errors = this.model.xosValidate(data);
423 this.onFormDataInvalid(errors);
428 this.model.attributes.humanReadableName = "new " + model.modelName;
429 this.model.addToCollection = this.collection;
431 this.model.addToCollection = undefined;
434 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
436 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
437 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
438 if (that.synchronous) {
439 that.trigger("saveSuccess");
444 if (!this.synchronous) {
449 deleteClicked: function(e) {
451 this.app.deleteDialog(this.model, "list");
454 tabClick: function(tabId, regionName) {
455 region = this.app[regionName];
456 if (this.currentTabRegion != undefined) {
457 this.currentTabRegion.$el.hide();
459 if (this.currentTabId != undefined) {
460 $(this.currentTabId).removeClass('active');
462 this.currentTabRegion = region;
463 this.currentTabRegion.$el.show();
465 this.currentTabId = tabId;
466 $(tabId).addClass('active');
469 showTabs: function(tabs) {
470 template = templateFromId("#xos-tabs-template", {tabs: tabs});
471 $("#tabs").html(template(tabs));
474 _.each(tabs, function(tab) {
475 var regionName = tab["region"];
476 var tabId = '#xos-nav-'+regionName;
477 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
483 showLinkedItems: function() {
486 tabs.push({name: "details", region: "detail"});
489 for (relatedName in this.model.collection.relatedCollections) {
490 var relatedField = this.model.collection.relatedCollections[relatedName];
491 var relatedId = this.model.id;
492 regionName = "linkedObjs" + (index+1);
494 relatedListViewClassName = relatedName + "ListView";
495 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
496 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
497 filter: function(model) { return model.attributes[relatedField]==relatedId;},
498 parentModel: this.model});
499 this.app[regionName].show(new relatedListViewClass());
500 if (this.app.hideTabsByDefault) {
501 this.app[regionName].$el.hide();
503 tabs.push({name: relatedName, region: regionName});
508 this.app["linkedObjs" + (index+1)].empty();
513 this.tabClick('#xos-nav-detail', 'detail');
516 onFormDataInvalid: function(errors) {
518 var markErrors = function(value, key) {
519 console.log("name='" + key + "'");
520 var $inputElement = self.$el.find("[name='" + key + "']");
521 var $inputContainer = $inputElement.parent();
522 //$inputContainer.find(".help-inline").remove();
523 var $errorEl = $("<span>", {class: "help-inline error", text: value});
524 $inputContainer.append($errorEl).addClass("error");
526 _.each(errors, markErrors);
529 templateHelpers: function() { return { modelName: this.model.modelName,
530 collectionName: this.model.collectionName,
531 addFields: this.model.addFields,
532 detailFields: this.model.detailFields,
533 foreignFields: this.model.foreignFields,
534 inputType: this.model.inputType,
541 This is for items that will be displayed as table rows.
543 app - MarionetteApplication
544 template - template (See XOSHelper.html)
545 detailClass - class of detail view, probably an XOSDetailView
548 XOSItemView = Marionette.ItemView.extend({
550 className: 'test-tablerow',
552 templateHelpers: function() { return { modelName: this.model.modelName,
553 collectionName: this.model.collectionName,
554 addFields: this.model.addFields,
555 detailFields: this.model.detailFields,
556 foreignFields: this.model.foreignFields,
557 inputType: this.model.inputType,
564 app - MarionetteApplication
565 childView - class of ItemView, probably an XOSItemView
566 template - template (see xosHelper.html)
567 collection - collection that holds these objects
568 title - title to display in template
571 XOSListView = FilteredCompositeView.extend({
572 childViewContainer: 'tbody',
575 events: {"click button.btn-xos-add": "addClicked",
576 "click button.btn-xos-refresh": "refreshClicked",
579 _fetchStateChange: function() {
580 if (this.collection.fetching) {
581 $("#xos-list-title-spinner").show();
583 $("#xos-list-title-spinner").hide();
587 addClicked: function(e) {
589 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
592 refreshClicked: function(e) {
594 this.collection.refresh(refreshRelated=true);
597 initialize: function() {
598 this.listenTo(this.collection, 'change', this._renderChildren)
599 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
600 this.listenTo(this.collection, 'add', function() { console.log("add"); })
601 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
603 // Because many of the templates use idToName(), we need to
604 // listen to the collections that hold the names for the ids
605 // that we want to display.
606 for (i in this.collection.foreignCollections) {
607 foreignName = this.collection.foreignCollections[i];
608 if (xos[foreignName] == undefined) {
609 console.log("Failed to find xos class " + foreignName);
611 this.listenTo(xos[foreignName], 'change', this._renderChildren);
612 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
616 getAddChildHash: function() {
617 if (this.parentModel) {
618 // Find the field name in the model that should point to
619 // the parent object. For example, when adding a sliver, the
620 // fieldName that should point to 'users' is 'creator'.
621 parentFieldName = "unknown";
622 for (fieldName in this.collection.foreignFields) {
623 cname = this.collection.foreignFields[fieldName];
624 if (cname = this.collection.collectionName) {
625 parentFieldName = fieldName;
628 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
634 templateHelpers: function() {
635 return { title: this.title,
636 addChildHash: this.getAddChildHash() };
640 idToName = function(id, collectionName, fieldName) {
641 return xos.idToName(id, collectionName, fieldName);
644 /* Constructs lists of <option> html blocks for items in a collection.
646 selectedId = the id of an object that should be selected, if any
647 collectionName = name of collection
648 fieldName = name of field within models of collection that will be displayed
651 idToOptions = function(selectedId, collectionName, fieldName) {
653 for (index in xos[collectionName].models) {
654 linkedObject = xos[collectionName].models[index];
655 linkedId = linkedObject["id"];
656 linkedName = linkedObject.attributes[fieldName];
657 if (linkedId == selectedId) {
658 selected = " selected";
662 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
667 /* Constructs an html <select> and the <option>s to go with it.
669 variable = variable name to return to form
670 selectedId = the id of an object that should be selected, if any
671 collectionName = name of collection
672 fieldName = name of field within models of collection that will be displayed
675 idToSelect = function(variable, selectedId, collectionName, fieldName) {
676 result = '<select name="' + variable + '">' +
677 idToOptions(selectedId, collectionName, fieldName) +