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 filterFunc = this.options.filter || this.filter;
12 if (filterFunc && !filterFunc(child)) {
15 ChildView = this.getChildView(child);
16 this.addChild(child, ChildView, index);
22 SliceSelectorOption = Marionette.ItemView.extend({
23 template: "#xos-sliceselector-option",
25 attributes: function() {
26 if (this.options.selectedID == this.model.get("id")) {
27 return { value: this.model.get("id"), selected: 1 };
29 return { value: this.model.get("id") };
34 SliceSelectorView = FilteredCompositeView.extend({
35 template: "#xos-sliceselector-select",
36 childViewContainer: "select",
37 childView: SliceSelectorOption,
40 events: {"change select": "onSliceChanged"},
42 childViewOptions: function() {
43 return { selectedID: this.options.selectedID || this.selectedID || null };
46 onSliceChanged: function() {
47 this.sliceChanged(this.$el.find("select").val());
50 sliceChanged: function(id) {
51 console.log("sliceChanged " + id);
54 templateHelpers: function() { return {caption: this.options.caption || this.caption }; },
57 XOSRouter = Marionette.AppRouter.extend({
58 initialize: function() {
\r
62 onRoute: function(x,y,z) {
\r
63 this.routeStack.push(Backbone.history.fragment);
\r
64 this.routeStack = this.routeStack.slice(-32); // limit the size of routeStack to something reasonable
\r
67 prevPage: function() {
\r
68 return this.routeStack.slice(-1)[0];
71 showPreviousURL: function() {
72 prevPage = this.prevPage();
73 //console.log("showPreviousURL");
74 //console.log(this.routeStack);
76 this.navigate("#"+prevPage, {trigger: false, replace: true} );
80 navigate: function(href, options) {
82 Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true});
84 Marionette.AppRouter.prototype.navigate.call(this, href, options);
88 // XXX - We import backbone multiple times (BAD!) since the import happens
\r
89 // inside of the view's html. The second time it's imported (developer
\r
90 // view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for
\r
92 Backbone_Syphon = Backbone.Syphon
\r
93 Backbone_Syphon.InputReaders.register('select', function(el) {
\r
94 // Modify syphon so that if a select has "syphonall" in the class, then
95 // the value of every option will be returned, regardless of whether of
96 // not it is selected.
97 if (el.hasClass("syphonall")) {
99 _.each(el.find("option"), function(option) {
100 result.push($(option).val());
107 XOSApplication = Marionette.Application.extend({
108 detailBoxId: "#detailBox",
109 errorBoxId: "#errorBox",
110 errorCloseButtonId: "#close-error-box",
111 successBoxId: "#successBox",
112 successCloseButtonId: "#close-success-box",
113 errorTemplate: "#xos-error-template",
114 successTemplate: "#xos-success-template",
117 confirmDialog: function(view, event, callback) {
118 $("#xos-confirm-dialog").dialog({
122 "Confirm" : function() {
123 $(this).dialog("close");
131 "Cancel" : function() {
132 $(this).dialog("close");
136 $("#xos-confirm-dialog").dialog("open");
139 popupErrorDialog: function(responseText) {
141 parsed_error=$.parseJSON(responseText);
145 parsed_error=undefined;
146 width=640; // django stacktraces like wide width
148 console.log(responseText);
149 console.log(parsed_error);
151 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
153 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: strip_scripts(responseText)}))
156 $("#xos-error-dialog").dialog({
160 Ok: function() { $(this).dialog("close"); }
165 hideLinkedItems: function(result) {
168 this["linkedObjs" + (index+1)].empty();
173 hideTabs: function() { $("#tabs").hide(); },
174 showTabs: function() { $("#tabs").show(); },
176 createListHandler: function(listViewName, collection_name, regionName, title) {
179 listView = new app[listViewName];
180 app[regionName].show(listView);
181 app.hideLinkedItems();
182 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
186 listButtons = new XOSListButtonView({linkedView: listView});
187 app["rightButtonPanel"].show(listButtons);
191 createAddHandler: function(detailName, collection_name, regionName, title) {
194 console.log("addHandler");
196 app.hideLinkedItems();
199 model = new xos[collection_name].model();
200 detailViewClass = app[detailName];
201 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
202 app[regionName].show(detailView);
204 detailButtons = new XOSDetailButtonView({linkedView: detailView});
205 app["rightButtonPanel"].show(detailButtons);
209 createAddChildHandler: function(addChildName, collection_name) {
211 return function(parent_modelName, parent_fieldName, parent_id) {
212 app.Router.showPreviousURL();
213 model = new xos[collection_name].model();
214 model.attributes[parent_fieldName] = parent_id;
215 model.readOnlyFields.push(parent_fieldName);
216 detailViewClass = app[addChildName];
217 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
218 detailView.dialog = $("xos-addchild-dialog");
219 app["addChildDetail"].show(detailView);
220 $("#xos-addchild-dialog").dialog({
225 "Save" : function() {
226 var addDialog = this;
227 detailView.synchronous = true;
228 detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); }
231 //$(this).dialog("close");
233 "Cancel" : function() {
234 $(this).dialog("close");
238 $("#xos-addchild-dialog").dialog("open");
242 createDeleteHandler: function(collection_name) {
244 return function(model_id) {
245 console.log("deleteCalled");
246 collection = xos[collection_name];
247 model = collection.get(model_id);
248 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
249 app.Router.showPreviousURL();
250 app.deleteDialog(model);
254 createDetailHandler: function(detailName, collection_name, regionName, title) {
256 showModelId = function(model_id) {
257 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
259 collection = xos[collection_name];
260 model = collection.get(model_id);
261 if (model == undefined) {
262 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
264 detailViewClass = app[detailName];
265 detailView = new detailViewClass({model: model});
266 app[regionName].show(detailView);
267 detailView.showLinkedItems();
269 detailButtons = new XOSDetailButtonView({linkedView: detailView});
270 app["rightButtonPanel"].show(detailButtons);
276 /* error handling callbacks */
278 hideError: function() {
279 if (this.logWindowId) {
281 $(this.errorBoxId).hide();
282 $(this.successBoxId).hide();
286 showSuccess: function(result) {
287 result["statusclass"] = "success";
288 if (this.logTableId) {
289 this.appendLogWindow(result);
291 $(this.successBoxId).show();
292 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
294 $(this.successCloseButtonId).unbind().bind('click', function() {
295 $(that.successBoxId).hide();
300 showError: function(result) {
301 result["statusclass"] = "failure";
302 if (this.logTableId) {
303 this.appendLogWindow(result);
304 this.popupErrorDialog(result.responseText);
306 // this is really old stuff
307 $(this.errorBoxId).show();
308 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
310 $(this.errorCloseButtonId).unbind().bind('click', function() {
311 $(that.errorBoxId).hide();
316 showInformational: function(result) {
317 result["statusclass"] = "inprog";
318 if (this.logTableId) {
319 return this.appendLogWindow(result);
325 appendLogWindow: function(result) {
326 // compute a new logMessageId for this log message
327 logMessageId = "logMessage" + this.logMessageCount;
328 this.logMessageCount = this.logMessageCount + 1;
329 result["logMessageId"] = logMessageId;
331 logMessageTemplate=$("#xos-log-template").html();
332 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
333 newRow = _.template(logMessageTemplate, result);
334 assert(newRow != undefined, "newRow is undefined");
336 if (result["infoMsgId"] != undefined) {
337 // We were passed the logMessageId of an informational message,
338 // and the caller wants us to replace that message with our own.
339 // i.e. replace an informational message with a success or an error.
340 $("#"+result["infoMsgId"]).replaceWith(newRow);
342 // Create a brand new log message rather than replacing one.
343 logTableBody = $(this.logTableId + " tbody");
344 logTableBody.prepend(newRow);
347 if (this.statusMsgId) {
348 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
351 limitTableRows(this.logTableId, 5);
356 saveError: function(model, result, xhr, infoMsgId) {
357 console.log("saveError");
358 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
359 result["infoMsgId"] = infoMsgId;
360 this.showError(result);
363 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
364 console.log("saveSuccess");
365 if (model.addToCollection) {
366 console.log("addToCollection");
367 console.log(model.addToCollection);
368 model.addToCollection.add(model);
369 model.addToCollection.sort();
370 model.addToCollection = undefined;
372 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
373 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
374 result["infoMsgId"] = infoMsgId;
375 this.showSuccess(result);
378 destroyError: function(model, result, xhr, infoMsgId) {
379 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
380 result["infoMsgId"] = infoMsgId;
381 this.showError(result);
384 destroySuccess: function(model, result, xhr, infoMsgId) {
385 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
386 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
387 result["infoMsgId"] = infoMsgId;
388 this.showSuccess(result);
391 /* end error handling callbacks */
393 destroyModel: function(model) {
394 //console.log("destroyModel"); console.log(model);
396 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
398 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
399 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
402 deleteDialog: function(model, afterDelete) {
404 assert(model!=undefined, "deleteDialog's model is undefined");
405 //console.log("deleteDialog"); console.log(model);
406 this.confirmDialog(null, null, function() {
407 //console.log("deleteConfirm"); console.log(model);
408 modelName = model.modelName;
409 that.destroyModel(model);
410 if (afterDelete=="list") {
411 that.navigate("list", modelName);
412 } else if (afterDelete) {
419 XOSButtonView = Marionette.ItemView.extend({
420 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
421 "click button.btn-xos-save-leave": "submitLeaveClicked",
422 "click button.btn-xos-save-another": "submitAddAnotherClicked",
423 "click button.btn-xos-delete": "deleteClicked",
424 "click button.btn-xos-add": "addClicked",
425 "click button.btn-xos-refresh": "refreshClicked",
428 submitLeaveClicked: function(e) {
429 this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
432 submitContinueClicked: function(e) {
433 this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
436 submitAddAnotherClicked: function(e) {
437 this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
440 submitDeleteClicked: function(e) {
441 this.options.linkedView.deleteClicked.call(this.options.linkedView, e);
444 addClicked: function(e) {
445 this.options.linkedView.addClicked.call(this.options.linkedView, e);
448 refreshClicked: function(e) {
449 this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
453 XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
454 XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });
458 app - MarionetteApplication
459 template - template (See XOSHelper.html)
462 XOSDetailView = Marionette.ItemView.extend({
465 viewInitializers: [],
467 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
468 "click button.btn-xos-save-leave": "submitLeaveClicked",
469 "click button.btn-xos-save-another": "submitAddAnotherClicked",
470 "click button.btn-xos-delete": "deleteClicked",
471 "change input": "inputChanged"},
473 /* inputChanged is watching the onChange events of the input controls. We
474 do this to track when this view is 'dirty', so we can throw up a warning
475 if the user tries to change his slices without saving first.
478 initialize: function() {
479 this.on("saveSuccess", this.onSaveSuccess);
480 this.synchronous = false;
484 _.each(this.viewInitializers, function(initializer) {
489 saveSuccess: function(e) {
490 // always called after a save succeeds
493 afterSave: function(e) {
494 // if this.synchronous, then called after the save succeeds
495 // if !this.synchronous, then called after save is initiated
498 onSaveSuccess: function(e) {
500 if (this.synchronous) {
505 inputChanged: function(e) {
509 submitContinueClicked: function(e) {
510 console.log("saveContinue");
512 this.afterSave = function() { };
516 submitLeaveClicked: function(e) {
517 console.log("saveLeave");
519 if (this.options.noSubmitButton || this.noSubmitButton) {
523 this.afterSave = function() {
524 that.app.navigate("list", that.model.modelName);
529 submitAddAnotherClicked: function(e) {
530 console.log("saveAnother");
534 this.afterSave = function() {
535 console.log("addAnother afterSave");
536 that.app.navigate("add", that.model.modelName);
542 this.app.hideError();
543 var data = Backbone_Syphon.serialize(this);
545 var isNew = !this.model.id;
549 this.$el.find(".help-inline").remove();
551 /* although model.validate() is called automatically by
552 model.save, we call it ourselves, so we can throw up our
553 validation error before creating the infoMsg in the log
555 errors = this.model.xosValidate(data);
557 this.onFormDataInvalid(errors);
562 this.model.attributes.humanReadableName = "new " + this.model.modelName;
563 this.model.addToCollection = this.collection;
565 this.model.addToCollection = undefined;
568 var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
570 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
571 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
572 that.trigger("saveSuccess");
576 if (!this.synchronous) {
581 deleteClicked: function(e) {
583 this.app.deleteDialog(this.model, "list");
586 tabClick: function(tabId, regionName) {
587 region = this.app[regionName];
588 if (this.currentTabRegion != undefined) {
589 this.currentTabRegion.$el.hide();
591 if (this.currentTabId != undefined) {
592 $(this.currentTabId).removeClass('active');
594 this.currentTabRegion = region;
595 this.currentTabRegion.$el.show();
597 this.currentTabId = tabId;
598 $(tabId).addClass('active');
601 showTabs: function(tabs) {
602 template = templateFromId("#xos-tabs-template", {tabs: tabs});
603 $("#tabs").html(template(tabs));
606 _.each(tabs, function(tab) {
607 var regionName = tab["region"];
608 var tabId = '#xos-nav-'+regionName;
609 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
615 showLinkedItems: function() {
618 tabs.push({name: "details", region: "detail"});
620 makeFilter = function(relatedField, relatedId) {
621 return function(model) { return model.attributes[relatedField] == relatedId; }
625 for (relatedName in this.model.collection.relatedCollections) {
626 var relatedField = this.model.collection.relatedCollections[relatedName];
627 var relatedId = this.model.id;
628 regionName = "linkedObjs" + (index+1);
630 relatedListViewClassName = relatedName + "ListView";
631 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
632 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
633 filter: makeFilter(relatedField, relatedId),
634 parentModel: this.model});
635 this.app[regionName].show(new relatedListViewClass());
636 if (this.app.hideTabsByDefault) {
637 this.app[regionName].$el.hide();
639 tabs.push({name: relatedName, region: regionName});
644 this.app["linkedObjs" + (index+1)].empty();
649 this.tabClick('#xos-nav-detail', 'detail');
652 onFormDataInvalid: function(errors) {
654 var markErrors = function(value, key) {
655 var $inputElement = self.$el.find("[name='" + key + "']");
656 var $inputContainer = $inputElement.parent();
657 //$inputContainer.find(".help-inline").remove();
658 var $errorEl = $("<span>", {class: "help-inline error", text: value});
659 $inputContainer.append($errorEl).addClass("error");
661 _.each(errors, markErrors);
664 templateHelpers: function() { return { modelName: this.model.modelName,
665 collectionName: this.model.collectionName,
666 addFields: this.model.addFields,
667 listFields: this.model.listFields,
668 detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
669 fieldDisplayNames: this.options.fieldDisplayNames || this.fieldDisplayNames || this.model.fieldDisplayNames || {},
670 foreignFields: this.model.foreignFields,
671 detailLinkFields: this.model.detailLinkFields,
672 inputType: this.model.inputType,
675 choices: this.options.choices || this.choices || this.model.choices || {},
676 helpText: this.options.helpText || this.helpText || this.model.helpText || {},
680 XOSDetailView_sliver = XOSDetailView.extend( {
681 events: $.extend(XOSDetailView.events,
682 {"change #field_deploymentNetwork": "onDeploymentNetworkChange"}
686 // Note that this causes the selects to be updated a second time. The
687 // first time was when the template was originally invoked, and the
688 // selects will all have the full unfiltered set of candidates. Then
689 // onShow will fire, and we'll update them with the filtered values.
690 this.onDeploymentNetworkChange();
693 onDeploymentNetworkChange: function(e) {
694 var deploymentID = this.$el.find("#field_deploymentNetwork").val();
696 console.log("onDeploymentNetworkChange");
697 console.log(deploymentID);
699 filterFunc = function(model) { return (model.attributes.deployment==deploymentID); }
700 newSelect = idToSelect("node",
701 this.model.attributes.node,
702 this.model.foreignFields["node"],
706 this.$el.find("#field_node").html(newSelect);
708 filterFunc = function(model) { for (index in model.attributes.deployments) {
709 item=model.attributes.deployments[index];
710 if (item.toString()==deploymentID.toString()) return true;
714 newSelect = idToSelect("flavor",
715 this.model.attributes.flavor,
716 this.model.foreignFields["flavor"],
720 this.$el.find("#field_flavor").html(newSelect);
722 filterFunc = function(model) { for (index in xos.imageDeployments.models) {
723 imageDeployment = xos.imageDeployments.models[index];
724 if ((imageDeployment.attributes.deployment == deploymentID) && (imageDeployment.attributes.image == model.id)) {
730 newSelect = idToSelect("image",
731 this.model.attributes.image,
732 this.model.foreignFields["image"],
736 this.$el.find("#field_image").html(newSelect);
741 This is for items that will be displayed as table rows.
743 app - MarionetteApplication
744 template - template (See XOSHelper.html)
747 XOSItemView = Marionette.ItemView.extend({
749 className: 'test-tablerow',
751 templateHelpers: function() { return { modelName: this.model.modelName,
752 collectionName: this.model.collectionName,
753 listFields: this.model.listFields,
754 addFields: this.model.addFields,
755 detailFields: this.model.detailFields,
756 foreignFields: this.model.foreignFields,
757 detailLinkFields: this.model.detailLinkFields,
758 inputType: this.model.inputType,
765 app - MarionetteApplication
766 childView - class of ItemView, probably an XOSItemView
767 template - template (see xosHelper.html)
768 collection - collection that holds these objects
769 title - title to display in template
772 XOSListView = FilteredCompositeView.extend({
773 childViewContainer: 'tbody',
776 events: {"click button.btn-xos-add": "addClicked",
777 "click button.btn-xos-refresh": "refreshClicked",
780 _fetchStateChange: function() {
781 if (this.collection.fetching) {
782 $("#xos-list-title-spinner").show();
784 $("#xos-list-title-spinner").hide();
788 addClicked: function(e) {
790 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
793 refreshClicked: function(e) {
795 this.collection.refresh(refreshRelated=true);
798 initialize: function() {
799 this.listenTo(this.collection, 'change', this._renderChildren)
800 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
801 this.listenTo(this.collection, 'add', function() { console.log("add"); })
802 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
804 // Because many of the templates use idToName(), we need to
805 // listen to the collections that hold the names for the ids
806 // that we want to display.
807 for (i in this.collection.foreignCollections) {
808 foreignName = this.collection.foreignCollections[i];
809 if (xos[foreignName] == undefined) {
810 console.log("Failed to find xos class " + foreignName);
812 this.listenTo(xos[foreignName], 'change', this._renderChildren);
813 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
817 getAddChildHash: function() {
818 if (this.parentModel) {
819 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
820 parentFieldName = parentFieldName || "unknown";
822 /*parentFieldName = "unknown";
824 for (fieldName in this.collection.foreignFields) {
825 cname = this.collection.foreignFields[fieldName];
826 if (cname = this.collection.collectionName) {
827 parentFieldName = fieldName;
830 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
836 templateHelpers: function() {
837 return { title: this.title,
838 addChildHash: this.getAddChildHash(),
839 foreignFields: this.collection.foreignFields,
840 listFields: this.collection.listFields,
841 detailLinkFields: this.collection.detailLinkFields, };
845 XOSDataTableView = Marionette.View.extend( {
846 el: '<div style="overflow: hidden">' +
847 '<h3 class="xos-list-title title_placeholder"></h3>' +
848 '<div class="header_placeholder"></div>' +
850 '<div class="footer_placeholder"></div>' +
855 events: {"click button.btn-xos-add": "addClicked",
856 "click button.btn-xos-refresh": "refreshClicked",
859 _fetchStateChange: function() {
860 if (this.collection.fetching) {
861 $("#xos-list-title-spinner").show();
863 $("#xos-list-title-spinner").hide();
867 addClicked: function(e) {
869 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
872 refreshClicked: function(e) {
874 this.collection.refresh(refreshRelated=true);
878 initialize: function() {
879 $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
880 $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
882 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
887 var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};
889 view.columnsByIndex = [];
890 view.columnsByFieldName = {};
891 _.each(this.collection.listFields, function(fieldName) {
892 inputType = view.options.inputType || view.inputType || {};
894 mSearchText = undefined;
895 sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
897 if (fieldName=="backend_status") {
898 mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
901 } else if (fieldName in view.collection.foreignFields) {
902 var foreignCollection = view.collection.foreignFields[fieldName];
903 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
904 } else if (inputType[fieldName] == "spinner") {
905 mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
907 if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
908 var collectionName = view.collection.collectionName;
909 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
911 thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
912 view.columnsByIndex.push( thisColumn );
913 view.columnsByFieldName[fieldName] = thisColumn;
916 if (!view.noDeleteColumn) {
917 deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
918 view.columnsByIndex.push(deleteColumn);
919 view.columnsByFieldName["delete"] = deleteColumn;
922 oTable = $(this.el).find("table").dataTable( {
926 "bFilter": ! (view.options.disableFilter || view.disableFilter),
927 "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
928 "aoColumns": view.columnsByIndex,
930 fnServerData: function(sSource, aoData, fnCallback, settings) {
931 var compareColumns = function(sortCols, sortDirs, a, b) {
934 result = (a==b) ? 0 : ((a<b) ? -1 : 1);
935 if (sortDirs[0] == "desc") {
941 var searchMatch = function(row, sSearch) {
942 for (fieldName in row) {
943 if (fieldName in view.columnsByFieldName) {
945 value = row[fieldName].toString();
949 if (value.indexOf(sSearch) >= 0) {
957 //console.log(aoData);
959 // function used to populate the DataTable with the current
\r
960 // content of the collection
\r
961 var populateTable = function()
\r
963 //console.log("populatetable!");
\r
965 // clear out old row views
\r
970 iDisplayLength = 1000;
\r
973 _.each(aoData, function(param) {
\r
974 if (param.name == "sSortDir_0") {
\r
975 sortDirs = [param.value];
\r
976 } else if (param.name == "iSortCol_0") {
\r
977 sortCols = [view.columnsByIndex[param.value].mData];
\r
978 } else if (param.name == "iDisplayStart") {
\r
979 iDisplayStart = param.value;
\r
980 } else if (param.name == "iDisplayLength") {
\r
981 iDisplayLength = param.value;
\r
982 } else if (param.name == "sSearch") {
\r
983 sSearch = param.value;
\r
987 aaData = view.collection.toJSON();
\r
989 // apply backbone filtering on the models
\r
991 aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );
\r
994 var totalSize = aaData.length;
\r
996 // turn the ForeignKey fields into human readable things
\r
997 for (rowIndex in aaData) {
\r
998 row = aaData[rowIndex];
\r
999 for (fieldName in row) {
\r
1000 if (fieldName in view.columnsByFieldName) {
\r
1001 mSearchText = view.columnsByFieldName[fieldName].mSearchText;
\r
1002 if (mSearchText) {
\r
1003 row[fieldName] = mSearchText(row[fieldName]);
\r
1009 // apply datatables search
\r
1011 aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });
\r
1014 var filteredSize = aaData.length;
\r
1016 // apply datatables sort
\r
1017 aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });
\r
1019 // slice it for pagination
\r
1020 if (iDisplayLength >= 0) {
\r
1021 aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);
\r
1024 return fnCallback({iTotalRecords: totalSize,
\r
1025 iTotalDisplayRecords: filteredSize,
\r
1029 aoData.shift(); // ignore sEcho
1032 view.listenTo(view.collection, 'change', populateTable);
1033 view.listenTo(view.collection, 'add', populateTable);
1034 view.listenTo(view.collection, 'remove', populateTable);
1041 getAddChildHash: function() {
1042 if (this.parentModel) {
1043 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
1044 parentFieldName = parentFieldName || "unknown";
1046 /*parentFieldName = "unknown";
1048 for (fieldName in this.collection.foreignFields) {
1049 cname = this.collection.foreignFields[fieldName];
1050 if (cname = this.collection.collectionName) {
1051 parentFieldName = fieldName;
1054 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
1062 idToName = function(id, collectionName, fieldName) {
1063 return xos.idToName(id, collectionName, fieldName);
1066 makeIdToName = function(collectionName, fieldName) {
1067 return function(id) { return idToName(id, collectionName, fieldName); }
1070 /* Constructs lists of <option> html blocks for items in a collection.
1072 selectedId = the id of an object that should be selected, if any
1073 collectionName = name of collection
1074 fieldName = name of field within models of collection that will be displayed
1077 idToOptions = function(selectedId, collectionName, fieldName, filterFunc) {
1079 for (index in xos[collectionName].models) {
1080 linkedObject = xos[collectionName].models[index];
1081 linkedId = linkedObject["id"];
1082 linkedName = linkedObject.attributes[fieldName];
1083 if (linkedId == selectedId) {
1084 selected = " selected";
1088 if ((filterFunc) && (!filterFunc(linkedObject))) {
1091 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
1096 /* Constructs an html <select> and the <option>s to go with it.
1098 variable = variable name to return to form
1099 selectedId = the id of an object that should be selected, if any
1100 collectionName = name of collection
1101 fieldName = name of field within models of collection that will be displayed
1104 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly, filterFunc) {
1106 readOnly = " readonly";
1110 result = '<select name="' + variable + '" id="field_' + variable + '"' + readOnly + '>' +
1111 idToOptions(selectedId, collectionName, fieldName, filterFunc) +
1116 choicesToOptions = function(selectedValue, choices) {
1118 for (index in choices) {
1119 choice = choices[index];
1120 displayName = choice[0];
1122 if (value == selectedValue) {
1123 selected = " selected";
1127 result = result + '<option value="' + value + '"' + selected + '>' + displayName + '</option>';
1132 choicesToSelect = function(variable, selectedValue, choices) {
1133 result = '<select name="' + variable + '" id="field_' + variable + '">' +
1134 choicesToOptions(selectedValue, choices) +