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;
145 model.readOnlyFields.push(parent_fieldName);
147 detailViewClass = app[addChildName];
148 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
149 detailView.dialog = $("xos-addchild-dialog");
150 app["addChildDetail"].show(detailView);
151 $("#xos-addchild-dialog").dialog({
156 "Save" : function() {
157 var addDialog = this;
158 detailView.synchronous = true;
159 detailView.afterSave = function() { console.log("afterSave"); $(addDialog).dialog("close"); }
162 //$(this).dialog("close");
164 "Cancel" : function() {
165 $(this).dialog("close");
169 $("#xos-addchild-dialog").dialog("open");
173 createDeleteHandler: function(collection_name) {
175 return function(model_id) {
176 console.log("deleteCalled");
177 collection = xos[collection_name];
178 model = collection.get(model_id);
179 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
180 app.Router.showPreviousURL();
181 app.deleteDialog(model);
185 createDetailHandler: function(detailName, collection_name, regionName, title) {
187 showModelId = function(model_id) {
188 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
190 collection = xos[collection_name];
191 model = collection.get(model_id);
192 if (model == undefined) {
193 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
195 detailViewClass = app[detailName];
196 detailView = new detailViewClass({model: model});
197 app[regionName].show(detailView);
198 detailView.showLinkedItems();
199 $("#xos-detail-button-box").show();
200 $("#xos-listview-button-box").hide();
206 /* error handling callbacks */
208 hideError: function() {
209 if (this.logWindowId) {
211 $(this.errorBoxId).hide();
212 $(this.successBoxId).hide();
216 showSuccess: function(result) {
217 result["statusclass"] = "success";
218 if (this.logTableId) {
219 this.appendLogWindow(result);
221 $(this.successBoxId).show();
222 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
224 $(this.successCloseButtonId).unbind().bind('click', function() {
225 $(that.successBoxId).hide();
230 showError: function(result) {
231 result["statusclass"] = "failure";
232 if (this.logTableId) {
233 this.appendLogWindow(result);
234 this.popupErrorDialog(result.responseText);
236 // this is really old stuff
237 $(this.errorBoxId).show();
238 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
240 $(this.errorCloseButtonId).unbind().bind('click', function() {
241 $(that.errorBoxId).hide();
246 showInformational: function(result) {
247 result["statusclass"] = "inprog";
248 if (this.logTableId) {
249 return this.appendLogWindow(result);
255 appendLogWindow: function(result) {
256 // compute a new logMessageId for this log message
257 logMessageId = "logMessage" + this.logMessageCount;
258 this.logMessageCount = this.logMessageCount + 1;
259 result["logMessageId"] = logMessageId;
261 logMessageTemplate=$("#xos-log-template").html();
262 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
263 newRow = _.template(logMessageTemplate, result);
264 assert(newRow != undefined, "newRow is undefined");
266 if (result["infoMsgId"] != undefined) {
267 // We were passed the logMessageId of an informational message,
268 // and the caller wants us to replace that message with our own.
269 // i.e. replace an informational message with a success or an error.
270 $("#"+result["infoMsgId"]).replaceWith(newRow);
272 // Create a brand new log message rather than replacing one.
273 logTableBody = $(this.logTableId + " tbody");
274 logTableBody.prepend(newRow);
277 if (this.statusMsgId) {
278 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
281 limitTableRows(this.logTableId, 5);
286 saveError: function(model, result, xhr, infoMsgId) {
287 console.log("saveError");
288 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
289 result["infoMsgId"] = infoMsgId;
290 this.showError(result);
293 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
294 console.log("saveSuccess");
295 if (model.addToCollection) {
296 console.log("addToCollection");
297 console.log(model.addToCollection);
298 model.addToCollection.add(model);
299 model.addToCollection.sort();
300 model.addToCollection = undefined;
302 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
303 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
304 result["infoMsgId"] = infoMsgId;
305 this.showSuccess(result);
308 destroyError: function(model, result, xhr, infoMsgId) {
309 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
310 result["infoMsgId"] = infoMsgId;
311 this.showError(result);
314 destroySuccess: function(model, result, xhr, infoMsgId) {
315 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
316 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
317 result["infoMsgId"] = infoMsgId;
318 this.showSuccess(result);
321 /* end error handling callbacks */
323 destroyModel: function(model) {
324 //console.log("destroyModel"); console.log(model);
326 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
328 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
329 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
332 deleteDialog: function(model, afterDelete) {
334 assert(model!=undefined, "deleteDialog's model is undefined");
335 //console.log("deleteDialog"); console.log(model);
336 this.confirmDialog(null, null, function() {
337 //console.log("deleteConfirm"); console.log(model);
338 modelName = model.modelName;
339 that.destroyModel(model);
340 if (afterDelete=="list") {
341 that.navigate("list", modelName);
349 app - MarionetteApplication
350 template - template (See XOSHelper.html)
353 XOSDetailView = Marionette.ItemView.extend({
356 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
357 "click button.btn-xos-save-leave": "submitLeaveClicked",
358 "click button.btn-xos-save-another": "submitAddAnotherClicked",
359 "click button.btn-xos-delete": "deleteClicked",
360 "change input": "inputChanged"},
362 /* inputChanged is watching the onChange events of the input controls. We
363 do this to track when this view is 'dirty', so we can throw up a warning
364 if the user tries to change his slices without saving first.
367 initialize: function() {
368 this.on("saveSuccess", this.onAfterSave);
369 this.synchronous = false;
372 afterSave: function(e) {
375 onAfterSave: function(e) {
379 inputChanged: function(e) {
383 submitContinueClicked: function(e) {
384 console.log("saveContinue");
386 this.afterSave = function() {};
390 submitLeaveClicked: function(e) {
391 console.log("saveLeave");
394 this.afterSave = function() {
395 that.app.navigate("list", that.model.modelName);
400 submitAddAnotherClicked: function(e) {
401 console.log("saveAnother");
404 this.afterSave = function() {
405 that.app.navigate("add", that.model.modelName);
411 this.app.hideError();
412 var data = Backbone.Syphon.serialize(this);
414 var isNew = !this.model.id;
416 this.$el.find(".help-inline").remove();
418 /* although model.validate() is called automatically by
419 model.save, we call it ourselves, so we can throw up our
420 validation error before creating the infoMsg in the log
422 errors = this.model.xosValidate(data);
424 this.onFormDataInvalid(errors);
429 this.model.attributes.humanReadableName = "new " + model.modelName;
430 this.model.addToCollection = this.collection;
432 this.model.addToCollection = undefined;
435 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
437 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
438 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
439 if (that.synchronous) {
440 that.trigger("saveSuccess");
445 if (!this.synchronous) {
450 deleteClicked: function(e) {
452 this.app.deleteDialog(this.model, "list");
455 tabClick: function(tabId, regionName) {
456 region = this.app[regionName];
457 if (this.currentTabRegion != undefined) {
458 this.currentTabRegion.$el.hide();
460 if (this.currentTabId != undefined) {
461 $(this.currentTabId).removeClass('active');
463 this.currentTabRegion = region;
464 this.currentTabRegion.$el.show();
466 this.currentTabId = tabId;
467 $(tabId).addClass('active');
470 showTabs: function(tabs) {
471 template = templateFromId("#xos-tabs-template", {tabs: tabs});
472 $("#tabs").html(template(tabs));
475 _.each(tabs, function(tab) {
476 var regionName = tab["region"];
477 var tabId = '#xos-nav-'+regionName;
478 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
484 showLinkedItems: function() {
487 tabs.push({name: "details", region: "detail"});
490 for (relatedName in this.model.collection.relatedCollections) {
491 var relatedField = this.model.collection.relatedCollections[relatedName];
492 var relatedId = this.model.id;
493 regionName = "linkedObjs" + (index+1);
495 relatedListViewClassName = relatedName + "ListView";
496 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
497 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
498 filter: function(model) { return model.attributes[relatedField]==relatedId;},
499 parentModel: this.model});
500 this.app[regionName].show(new relatedListViewClass());
501 if (this.app.hideTabsByDefault) {
502 this.app[regionName].$el.hide();
504 tabs.push({name: relatedName, region: regionName});
509 this.app["linkedObjs" + (index+1)].empty();
514 this.tabClick('#xos-nav-detail', 'detail');
517 onFormDataInvalid: function(errors) {
519 var markErrors = function(value, key) {
520 console.log("name='" + key + "'");
521 var $inputElement = self.$el.find("[name='" + key + "']");
522 var $inputContainer = $inputElement.parent();
523 //$inputContainer.find(".help-inline").remove();
524 var $errorEl = $("<span>", {class: "help-inline error", text: value});
525 $inputContainer.append($errorEl).addClass("error");
527 _.each(errors, markErrors);
530 templateHelpers: function() { return { modelName: this.model.modelName,
531 collectionName: this.model.collectionName,
532 addFields: this.model.addFields,
533 detailFields: this.model.detailFields,
534 foreignFields: this.model.foreignFields,
535 inputType: this.model.inputType,
542 This is for items that will be displayed as table rows.
544 app - MarionetteApplication
545 template - template (See XOSHelper.html)
546 detailClass - class of detail view, probably an XOSDetailView
549 XOSItemView = Marionette.ItemView.extend({
551 className: 'test-tablerow',
553 templateHelpers: function() { return { modelName: this.model.modelName,
554 collectionName: this.model.collectionName,
555 addFields: this.model.addFields,
556 detailFields: this.model.detailFields,
557 foreignFields: this.model.foreignFields,
558 inputType: this.model.inputType,
565 app - MarionetteApplication
566 childView - class of ItemView, probably an XOSItemView
567 template - template (see xosHelper.html)
568 collection - collection that holds these objects
569 title - title to display in template
572 XOSListView = FilteredCompositeView.extend({
573 childViewContainer: 'tbody',
576 events: {"click button.btn-xos-add": "addClicked",
577 "click button.btn-xos-refresh": "refreshClicked",
580 _fetchStateChange: function() {
581 if (this.collection.fetching) {
582 $("#xos-list-title-spinner").show();
584 $("#xos-list-title-spinner").hide();
588 addClicked: function(e) {
590 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
593 refreshClicked: function(e) {
595 this.collection.refresh(refreshRelated=true);
598 initialize: function() {
599 this.listenTo(this.collection, 'change', this._renderChildren)
600 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
601 this.listenTo(this.collection, 'add', function() { console.log("add"); })
602 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
604 // Because many of the templates use idToName(), we need to
605 // listen to the collections that hold the names for the ids
606 // that we want to display.
607 for (i in this.collection.foreignCollections) {
608 foreignName = this.collection.foreignCollections[i];
609 if (xos[foreignName] == undefined) {
610 console.log("Failed to find xos class " + foreignName);
612 this.listenTo(xos[foreignName], 'change', this._renderChildren);
613 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
617 getAddChildHash: function() {
618 if (this.parentModel) {
619 // Find the field name in the model that should point to
620 // the parent object. For example, when adding a sliver, the
621 // fieldName that should point to 'users' is 'creator'.
622 parentFieldName = "unknown";
623 for (fieldName in this.collection.foreignFields) {
624 cname = this.collection.foreignFields[fieldName];
625 if (cname = this.collection.collectionName) {
626 parentFieldName = fieldName;
629 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
635 templateHelpers: function() {
636 return { title: this.title,
637 addChildHash: this.getAddChildHash() };
641 idToName = function(id, collectionName, fieldName) {
642 return xos.idToName(id, collectionName, fieldName);
645 /* Constructs lists of <option> html blocks for items in a collection.
647 selectedId = the id of an object that should be selected, if any
648 collectionName = name of collection
649 fieldName = name of field within models of collection that will be displayed
652 idToOptions = function(selectedId, collectionName, fieldName) {
654 for (index in xos[collectionName].models) {
655 linkedObject = xos[collectionName].models[index];
656 linkedId = linkedObject["id"];
657 linkedName = linkedObject.attributes[fieldName];
658 if (linkedId == selectedId) {
659 selected = " selected";
663 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
668 /* Constructs an html <select> and the <option>s to go with it.
670 variable = variable name to return to form
671 selectedId = the id of an object that should be selected, if any
672 collectionName = name of collection
673 fieldName = name of field within models of collection that will be displayed
676 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
678 readOnly = " readonly";
682 result = '<select name="' + variable + '"' + readOnly + '>' +
683 idToOptions(selectedId, collectionName, fieldName) +