+XOSDetailView = Marionette.ItemView.extend({
+ tagName: "div",
+
+ viewInitializers: [],
+
+ events: {"click button.btn-xos-save-continue": "submitContinueClicked",
+ "click button.btn-xos-save-leave": "submitLeaveClicked",
+ "click button.btn-xos-save-another": "submitAddAnotherClicked",
+ "click button.btn-xos-delete": "deleteClicked",
+ "change input": "inputChanged"},
+
+ /* inputChanged is watching the onChange events of the input controls. We
+ do this to track when this view is 'dirty', so we can throw up a warning
+ if the user tries to change his slices without saving first.
+ */
+
+ initialize: function() {
+ this.on("saveSuccess", this.onSaveSuccess);
+ this.synchronous = false;
+ },
+
+ onShow: function() {
+ _.each(this.viewInitializers, function(initializer) {
+ initializer();
+ });
+ },
+
+ saveSuccess: function(e) {
+ // always called after a save succeeds
+ },
+
+ afterSave: function(e) {
+ // if this.synchronous, then called after the save succeeds
+ // if !this.synchronous, then called after save is initiated
+ },
+
+ onSaveSuccess: function(e) {
+ this.saveSuccess(e);
+ if (this.synchronous) {
+ this.afterSave(e);
+ }
+ },
+
+ inputChanged: function(e) {
+ this.dirty = true;
+ },
+
+ submitContinueClicked: function(e) {
+ console.log("saveContinue");
+ e.preventDefault();
+ this.afterSave = function() { };
+ this.save();
+ },
+
+ submitLeaveClicked: function(e) {
+ console.log("saveLeave");
+ e.preventDefault();
+ if (this.options.noSubmitButton || this.noSubmitButton) {
+ return;
+ }
+ var that=this;
+ this.afterSave = function() {
+ that.app.navigate("list", that.model.modelName);
+ }
+ this.save();
+ },
+
+ submitAddAnotherClicked: function(e) {
+ console.log("saveAnother");
+ console.log(this);
+ e.preventDefault();
+ var that=this;
+ this.afterSave = function() {
+ console.log("addAnother afterSave");
+ that.app.navigate("add", that.model.modelName);
+ }
+ this.save();
+ },
+
+ save: function() {
+ this.app.hideError();
+ var data = Backbone_Syphon.serialize(this);
+ var that = this;
+ var isNew = !this.model.id;
+
+ console.log(data);
+
+ this.$el.find(".help-inline").remove();
+
+ /* although model.validate() is called automatically by
+ model.save, we call it ourselves, so we can throw up our
+ validation error before creating the infoMsg in the log
+ */
+ errors = this.model.xosValidate(data);
+ if (errors) {
+ this.onFormDataInvalid(errors);
+ return;
+ }
+
+ if (isNew) {
+ this.model.attributes.humanReadableName = "new " + this.model.modelName;
+ this.model.addToCollection = this.collection;
+ } else {
+ this.model.addToCollection = undefined;
+ }
+
+ var infoMsgId = this.app.showInformational( {what: "save " + this.model.modelName + " " + this.model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
+
+ this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
+ success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
+ that.trigger("saveSuccess");
+ }});
+ this.dirty = false;
+
+ if (!this.synchronous) {
+ this.afterSave();
+ }
+ },
+
+ deleteClicked: function(e) {
+ e.preventDefault();
+ this.app.deleteDialog(this.model, "list");
+ },
+
+ tabClick: function(tabId, regionName) {
+ region = this.app[regionName];
+ if (this.currentTabRegion != undefined) {
+ this.currentTabRegion.$el.hide();
+ }
+ if (this.currentTabId != undefined) {
+ $(this.currentTabId).removeClass('active');
+ }
+ this.currentTabRegion = region;
+ this.currentTabRegion.$el.show();
+
+ this.currentTabId = tabId;
+ $(tabId).addClass('active');
+ },
+
+ showTabs: function(tabs) {
+ template = templateFromId("#xos-tabs-template", {tabs: tabs});
+ $("#tabs").html(template(tabs));
+ var that = this;
+
+ _.each(tabs, function(tab) {
+ var regionName = tab["region"];
+ var tabId = '#xos-nav-'+regionName;
+ $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
+ });
+
+ $("#tabs").show();
+ },
+
+ showLinkedItems: function() {
+ tabs=[];
+
+ tabs.push({name: "details", region: "detail"});
+
+ makeFilter = function(relatedField, relatedId) {
+ return function(model) { return model.attributes[relatedField] == relatedId; }
+ };
+
+ var index=0;
+ for (relatedName in this.model.collection.relatedCollections) {
+ var relatedField = this.model.collection.relatedCollections[relatedName];
+ var relatedId = this.model.id;
+ regionName = "linkedObjs" + (index+1);
+
+ relatedListViewClassName = relatedName + "ListView";
+ assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
+ relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
+ filter: makeFilter(relatedField, relatedId),
+ parentModel: this.model});
+ this.app[regionName].show(new relatedListViewClass());
+ if (this.app.hideTabsByDefault) {
+ this.app[regionName].$el.hide();
+ }
+ tabs.push({name: relatedName, region: regionName});
+ index = index + 1;
+ }
+
+ while (index<4) {
+ this.app["linkedObjs" + (index+1)].empty();
+ index = index + 1;
+ }
+
+ this.showTabs(tabs);
+ this.tabClick('#xos-nav-detail', 'detail');
+ },
+
+ onFormDataInvalid: function(errors) {
+ var self=this;
+ var markErrors = function(value, key) {
+ var $inputElement = self.$el.find("[name='" + key + "']");
+ var $inputContainer = $inputElement.parent();
+ //$inputContainer.find(".help-inline").remove();
+ var $errorEl = $("<span>", {class: "help-inline error", text: value});
+ $inputContainer.append($errorEl).addClass("error");
+ }
+ _.each(errors, markErrors);
+ },
+
+ templateHelpers: function() { return { modelName: this.model.modelName,
+ collectionName: this.model.collectionName,
+ addFields: this.model.addFields,
+ listFields: this.model.listFields,
+ detailFields: this.options.detailFields || this.detailFields || this.model.detailFields,
+ fieldDisplayNames: this.options.fieldDisplayNames || this.fieldDisplayNames || this.model.fieldDisplayNames || {},
+ foreignFields: this.model.foreignFields,
+ detailLinkFields: this.model.detailLinkFields,
+ inputType: this.model.inputType,
+ model: this.model,
+ detailView: this,
+ choices: this.options.choices || this.choices || this.model.choices || {},
+ helpText: this.options.helpText || this.helpText || this.model.helpText || {},
+ }},
+});
+
+XOSDetailView_sliver = XOSDetailView.extend( {
+ events: $.extend(XOSDetailView.events,
+ {"change #field_deployment": "onDeploymentChange"}
+ ),
+
+ onShow: function() {
+ // Note that this causes the selects to be updated a second time. The
+ // first time was when the template was originally invoked, and the
+ // selects will all have the full unfiltered set of candidates. Then
+ // onShow will fire, and we'll update them with the filtered values.
+ this.onDeploymentChange();
+ },
+
+ onDeploymentChange: function(e) {
+ var deploymentID = this.$el.find("#field_deployment").val();
+
+ //console.log("onDeploymentChange");
+
+ filterFunc = function(model) { for (index in xos.siteDeployments.models) {
+ site_deployment = xos.siteDeployments.models[index];
+ if (site_deployment.attributes.id == model.attributes.site_deployment) {
+ return (site_deployment.attributes.deployment == deploymentID);
+ }
+ }
+ return false;
+ // return (model.attributes.deployment==deploymentID); }
+ };
+ newSelect = idToSelect("node",
+ this.model.attributes.node,
+ this.model.foreignFields["node"],
+ "humanReadableName",
+ false,
+ filterFunc);
+ this.$el.find("#field_node").html(newSelect);
+
+ filterFunc = function(model) { for (index in model.attributes.deployments) {
+ if (model.attributes.deployments[index] == deploymentID) return true;
+ };
+ return false;
+ }
+ newSelect = idToSelect("flavor",
+ this.model.attributes.flavor,
+ this.model.foreignFields["flavor"],
+ "humanReadableName",
+ false,
+ filterFunc);
+ this.$el.find("#field_flavor").html(newSelect);
+
+ filterFunc = function(model) { for (index in model.attributes.deployments) {
+ if (model.attributes.deployments[index] == deploymentID) return true;
+ };
+ return false;
+ };
+ newSelect = idToSelect("image",
+ this.model.attributes.image,
+ this.model.foreignFields["image"],
+ "humanReadableName",
+ false,
+ filterFunc);
+ this.$el.find("#field_image").html(newSelect);
+ },
+});
+
+/* XOSItemView
+ This is for items that will be displayed as table rows.
+ extend with:
+ app - MarionetteApplication
+ template - template (See XOSHelper.html)
+*/
+
+XOSItemView = Marionette.ItemView.extend({
+ tagName: 'tr',
+ className: 'test-tablerow',
+
+ templateHelpers: function() { return { modelName: this.model.modelName,
+ collectionName: this.model.collectionName,
+ listFields: this.model.listFields,
+ addFields: this.model.addFields,
+ detailFields: this.model.detailFields,
+ foreignFields: this.model.foreignFields,
+ detailLinkFields: this.model.detailLinkFields,
+ inputType: this.model.inputType,
+ model: this.model,
+ }},
+});
+
+/* XOSListView:
+ extend with:
+ app - MarionetteApplication
+ childView - class of ItemView, probably an XOSItemView
+ template - template (see xosHelper.html)
+ collection - collection that holds these objects
+ title - title to display in template
+*/
+
+XOSListView = FilteredCompositeView.extend({
+ childViewContainer: 'tbody',
+ parentModel: null,
+
+ events: {"click button.btn-xos-add": "addClicked",
+ "click button.btn-xos-refresh": "refreshClicked",
+ },
+
+ _fetchStateChange: function() {
+ if (this.collection.fetching) {
+ $("#xos-list-title-spinner").show();
+ } else {
+ $("#xos-list-title-spinner").hide();
+ }
+ },
+
+ addClicked: function(e) {
+ e.preventDefault();
+ this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
+ },
+
+ refreshClicked: function(e) {
+ e.preventDefault();
+ this.collection.refresh(refreshRelated=true);
+ },
+
+ initialize: function() {
+ this.listenTo(this.collection, 'change', this._renderChildren)
+ this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
+ this.listenTo(this.collection, 'add', function() { console.log("add"); })
+ this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
+
+ // Because many of the templates use idToName(), we need to
+ // listen to the collections that hold the names for the ids
+ // that we want to display.
+ for (i in this.collection.foreignCollections) {
+ foreignName = this.collection.foreignCollections[i];
+ if (xos[foreignName] == undefined) {
+ console.log("Failed to find xos class " + foreignName);
+ }
+ this.listenTo(xos[foreignName], 'change', this._renderChildren);
+ this.listenTo(xos[foreignName], 'sort', this._renderChildren);
+ }
+ },
+
+ getAddChildHash: function() {
+ if (this.parentModel) {
+ parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
+ parentFieldName = parentFieldName || "unknown";
+
+ /*parentFieldName = "unknown";
+
+ for (fieldName in this.collection.foreignFields) {
+ cname = this.collection.foreignFields[fieldName];
+ if (cname = this.collection.collectionName) {
+ parentFieldName = fieldName;
+ }
+ }*/
+ return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
+ } else {
+ return null;
+ }
+ },
+
+ templateHelpers: function() {
+ return { title: this.title,
+ addChildHash: this.getAddChildHash(),
+ foreignFields: this.collection.foreignFields,
+ listFields: this.collection.listFields,
+ detailLinkFields: this.collection.detailLinkFields, };
+ },
+});
+
+XOSDataTableView = Marionette.View.extend( {
+ el: '<div style="overflow: hidden">' +
+ '<h3 class="xos-list-title title_placeholder"></h3>' +
+ '<div class="header_placeholder"></div>' +
+ '<table></table>' +
+ '<div class="footer_placeholder"></div>' +
+ '</div>',
+
+ filter: undefined,
+
+ events: {"click button.btn-xos-add": "addClicked",
+ "click button.btn-xos-refresh": "refreshClicked",
+ },
+
+ _fetchStateChange: function() {
+ if (this.collection.fetching) {
+ $("#xos-list-title-spinner").show();
+ } else {
+ $("#xos-list-title-spinner").hide();
+ }
+ },
+
+ addClicked: function(e) {
+ e.preventDefault();
+ this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
+ },
+
+ refreshClicked: function(e) {
+ e.preventDefault();
+ this.collection.refresh(refreshRelated=true);
+ },
+
+
+ initialize: function() {
+ $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
+ $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
+
+ this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
+ },
+
+ render: function() {
+ var view = this;
+ var fieldDisplayNames = view.options.fieldDisplayNames || view.fieldDisplayNames || {};
+
+ view.columnsByIndex = [];
+ view.columnsByFieldName = {};
+ _.each(this.collection.listFields, function(fieldName) {
+ inputType = view.options.inputType || view.inputType || {};
+ mRender = undefined;
+ mSearchText = undefined;
+ sTitle = fieldName in fieldDisplayNames ? fieldDisplayNames[fieldName] : fieldNameToHumanReadable(fieldName);
+ bSortable = true;
+ if (fieldName=="backend_status") {
+ mRender = function(x,y,z) { return xosBackendStatusIconTemplate(z); };
+ sTitle = "";
+ bSortable = false;
+ } else if (fieldName in view.collection.foreignFields) {
+ var foreignCollection = view.collection.foreignFields[fieldName];
+ mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
+ } else if (inputType[fieldName] == "spinner") {
+ mRender = function(x,y,z) { return xosDataTableSpinnerTemplate( {value: x, collectionName: view.collection.collectionName, fieldName: fieldName, id: z.id, app: view.app} ); };
+ }
+ if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
+ var collectionName = view.collection.collectionName;
+ mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
+ }
+ thisColumn = {sTitle: sTitle, bSortable: bSortable, mData: fieldName, mRender: mRender, mSearchText: mSearchText};
+ view.columnsByIndex.push( thisColumn );
+ view.columnsByFieldName[fieldName] = thisColumn;
+ });
+
+ if (!view.noDeleteColumn) {
+ deleteColumn = {sTitle: "", bSortable: false, mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
+ view.columnsByIndex.push(deleteColumn);
+ view.columnsByFieldName["delete"] = deleteColumn;
+ };
+
+ oTable = $(this.el).find("table").dataTable( {
+ "bJQueryUI": true,
+ "bStateSave": true,
+ "bServerSide": true,
+ "bFilter": ! (view.options.disableFilter || view.disableFilter),
+ "bPaginate": ! (view.options.disablePaginate || view.disablePaginate),
+ "aoColumns": view.columnsByIndex,
+
+ fnServerData: function(sSource, aoData, fnCallback, settings) {
+ var compareColumns = function(sortCols, sortDirs, a, b) {
+ a = a[sortCols[0]];
+ b = b[sortCols[0]];
+ result = (a==b) ? 0 : ((a<b) ? -1 : 1);
+ if (sortDirs[0] == "desc") {
+ result = -result;
+ }
+ return result;
+ };
+
+ var searchMatch = function(row, sSearch) {
+ for (fieldName in row) {
+ if (fieldName in view.columnsByFieldName) {
+ try {
+ value = row[fieldName].toString();
+ } catch(e) {
+ continue;
+ }
+ if (value.indexOf(sSearch) >= 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ //console.log(aoData);
+\r
+ // function used to populate the DataTable with the current\r
+ // content of the collection\r
+ var populateTable = function()\r
+ {\r
+ //console.log("populatetable!");\r
+\r
+ // clear out old row views\r
+ rows = [];\r
+\r
+ sSearch = null;\r
+ iDisplayStart = 0;\r
+ iDisplayLength = 1000;\r
+ sortDirs = [];\r
+ sortCols = [];\r
+ _.each(aoData, function(param) {\r
+ if (param.name == "sSortDir_0") {\r
+ sortDirs = [param.value];\r
+ } else if (param.name == "iSortCol_0") {\r
+ sortCols = [view.columnsByIndex[param.value].mData];\r
+ } else if (param.name == "iDisplayStart") {\r
+ iDisplayStart = param.value;\r
+ } else if (param.name == "iDisplayLength") {\r
+ iDisplayLength = param.value;\r
+ } else if (param.name == "sSearch") {\r
+ sSearch = param.value;\r
+ }\r
+ });\r
+\r
+ aaData = view.collection.toJSON();\r
+\r
+ // apply backbone filtering on the models\r
+ if (view.filter) {\r
+ aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );\r
+ }\r
+\r
+ var totalSize = aaData.length;\r
+\r
+ // turn the ForeignKey fields into human readable things\r
+ for (rowIndex in aaData) {\r
+ row = aaData[rowIndex];\r
+ for (fieldName in row) {\r
+ if (fieldName in view.columnsByFieldName) {\r
+ mSearchText = view.columnsByFieldName[fieldName].mSearchText;\r
+ if (mSearchText) {\r
+ row[fieldName] = mSearchText(row[fieldName]);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ // apply datatables search\r
+ if (sSearch) {\r
+ aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });\r
+ }\r
+\r
+ var filteredSize = aaData.length;\r
+\r
+ // apply datatables sort\r
+ aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });\r
+\r
+ // slice it for pagination\r
+ if (iDisplayLength >= 0) {\r
+ aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);\r
+ }\r
+\r
+ return fnCallback({iTotalRecords: totalSize,\r
+ iTotalDisplayRecords: filteredSize,\r
+ aaData: aaData});\r
+ };\r
+\r
+ aoData.shift(); // ignore sEcho
+ populateTable();
+
+ view.listenTo(view.collection, 'change', populateTable);
+ view.listenTo(view.collection, 'add', populateTable);
+ view.listenTo(view.collection, 'remove', populateTable);
+ },
+ } );
+
+ return this;
+ },
+
+ getAddChildHash: function() {
+ if (this.parentModel) {
+ parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
+ parentFieldName = parentFieldName || "unknown";
+
+ /*parentFieldName = "unknown";
+
+ for (fieldName in this.collection.foreignFields) {
+ cname = this.collection.foreignFields[fieldName];
+ if (cname = this.collection.collectionName) {
+ parentFieldName = fieldName;
+ }
+ }*/
+ return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
+ } else {
+ return null;
+ }
+ },
+
+});
+