X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=planetstack%2Fcore%2Fxoslib%2Fstatic%2Fjs%2Fxoslib%2FxosHelper.js;h=76254f6d16605456145fc802addc9c608325b69d;hb=753a13edfd39603fbdc59747c4d7c2b59a2d06b9;hp=8fa27a671e98d10236ac425e7412b7ac8852d3e7;hpb=3f130078930766267756b68e971e0a295d351740;p=plstackapi.git diff --git a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js index 8fa27a6..76254f6 100644 --- a/planetstack/core/xoslib/static/js/xoslib/xosHelper.js +++ b/planetstack/core/xoslib/static/js/xoslib/xosHelper.js @@ -1,23 +1,108 @@ -function assert(outcome, description) { - if (!outcome) { - console.log(description); - } -} - -function templateFromId(id) { - return _.template($(id).html()); -} - -function firstCharUpper(s) { - return s.charAt(0).toUpperCase() + s.slice(1); -} - HTMLView = Marionette.ItemView.extend({ render: function() { this.$el.append(this.options.html); }, }); +SliceSelectorOption = Marionette.ItemView.extend({ + template: "#xos-sliceselector-option", + tagName: "option", + attributes: function() { + if (this.options.selectedID == this.model.get("id")) { + return { value: this.model.get("id"), selected: 1 }; + } else { + return { value: this.model.get("id") }; + } + }, +}); + +SliceSelectorView = Marionette.CompositeView.extend({ + template: "#xos-sliceselector-select", + childViewContainer: "select", + childView: SliceSelectorOption, + caption: "Slice", + + events: {"change select": "onSliceChanged"}, + + childViewOptions: function() { + return { selectedID: this.options.selectedID || this.selectedID || null }; + }, + + onSliceChanged: function() { + this.sliceChanged(this.$el.find("select").val()); + }, + + sliceChanged: function(id) { + console.log("sliceChanged " + id); + }, + + templateHelpers: function() { return {caption: this.options.caption || this.caption }; }, +}); + +FilteredCompositeView = Marionette.CompositeView.extend( { + showCollection: function() { + var ChildView; + this.collection.each(function(child, index) { + if (this.filter && !this.filter(child)) { + return; + } + ChildView = this.getChildView(child); + this.addChild(child, ChildView, index); + }, this); + + }, +}); + +XOSRouter = Marionette.AppRouter.extend({ + initialize: function() { + this.routeStack=[]; + }, + + onRoute: function(x,y,z) { + this.routeStack.push(Backbone.history.fragment); + this.routeStack = this.routeStack.slice(-32); // limit the size of routeStack to something reasonable + }, + + prevPage: function() { + return this.routeStack.slice(-1)[0]; + }, + + showPreviousURL: function() { + prevPage = this.prevPage(); + //console.log("showPreviousURL"); + //console.log(this.routeStack); + if (prevPage) { + this.navigate("#"+prevPage, {trigger: false, replace: true} ); + } + }, + + navigate: function(href, options) { + if (options.force) { + Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true}); + } + Marionette.AppRouter.prototype.navigate.call(this, href, options); + }, + }); + +// XXX - We import backbone multiple times (BAD!) since the import happens +// inside of the view's html. The second time it's imported (developer +// view), it wipes out Backbone.Syphon. So, save it as Backbone_Syphon for +// now. +Backbone_Syphon = Backbone.Syphon +Backbone_Syphon.InputReaders.register('select', function(el) { + // Modify syphon so that if a select has "syphonall" in the class, then + // the value of every option will be returned, regardless of whether of + // not it is selected. + if (el.hasClass("syphonall")) { + result = []; + _.each(el.find("option"), function(option) { + result.push($(option).val()); + }); + return result; + } + return el.val(); +}); + XOSApplication = Marionette.Application.extend({ detailBoxId: "#detailBox", errorBoxId: "#errorBox", @@ -28,23 +113,167 @@ XOSApplication = Marionette.Application.extend({ successTemplate: "#xos-success-template", logMessageCount: 0, - confirmDialog: function(view, event) { + confirmDialog: function(view, event, callback) { $("#xos-confirm-dialog").dialog({ - autoOpen: false, - modal: true, - buttons : { - "Confirm" : function() { - $(this).dialog("close"); - view.trigger(event); - }, - "Cancel" : function() { - $(this).dialog("close"); - } - } + autoOpen: false, + modal: true, + buttons : { + "Confirm" : function() { + $(this).dialog("close"); + if (event) { + view.trigger(event); + } + if (callback) { + callback(); + } + }, + "Cancel" : function() { + $(this).dialog("close"); + } + } }); $("#xos-confirm-dialog").dialog("open"); }, + popupErrorDialog: function(responseText) { + try { + parsed_error=$.parseJSON(responseText); + width=300; + } + catch(err) { + parsed_error=undefined; + width=640; // django stacktraces like wide width + } + console.log(responseText); + console.log(parsed_error); + if (parsed_error) { + $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error)); + } else { + $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText})) + } + + $("#xos-error-dialog").dialog({ + modal: true, + width: width, + buttons: { + Ok: function() { $(this).dialog("close"); } + } + }); + }, + + hideLinkedItems: function(result) { + var index=0; + while (index<4) { + this["linkedObjs" + (index+1)].empty(); + index = index + 1; + } + }, + + hideTabs: function() { $("#tabs").hide(); }, + showTabs: function() { $("#tabs").show(); }, + + createListHandler: function(listViewName, collection_name, regionName, title) { + var app=this; + return function() { + listView = new app[listViewName]; + app[regionName].show(listView); + app.hideLinkedItems(); + $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title})); + $("#detail").show(); + app.hideTabs(); + + listButtons = new XOSListButtonView({linkedView: listView}); + app["rightButtonPanel"].show(listButtons); + } + }, + + createAddHandler: function(detailName, collection_name, regionName, title) { + var app=this; + return function() { + console.log("addHandler"); + + app.hideLinkedItems(); + app.hideTabs(); + + model = new xos[collection_name].model(); + detailViewClass = app[detailName]; + detailView = new detailViewClass({model: model, collection:xos[collection_name]}); + app[regionName].show(detailView); + + detailButtons = new XOSDetailButtonView({linkedView: detailView}); + app["rightButtonPanel"].show(detailButtons); + } + }, + + createAddChildHandler: function(addChildName, collection_name) { + var app=this; + return function(parent_modelName, parent_fieldName, parent_id) { + app.Router.showPreviousURL(); + model = new xos[collection_name].model(); + model.attributes[parent_fieldName] = parent_id; + model.readOnlyFields.push(parent_fieldName); + detailViewClass = app[addChildName]; + var detailView = new detailViewClass({model: model, collection:xos[collection_name]}); + detailView.dialog = $("xos-addchild-dialog"); + app["addChildDetail"].show(detailView); + $("#xos-addchild-dialog").dialog({ + autoOpen: false, + modal: true, + width: 640, + buttons : { + "Save" : function() { + var addDialog = this; + detailView.synchronous = true; + detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); } + detailView.save(); + + //$(this).dialog("close"); + }, + "Cancel" : function() { + $(this).dialog("close"); + } + } + }); + $("#xos-addchild-dialog").dialog("open"); + } + }, + + createDeleteHandler: function(collection_name) { + var app=this; + return function(model_id) { + console.log("deleteCalled"); + collection = xos[collection_name]; + model = collection.get(model_id); + assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name); + app.Router.showPreviousURL(); + app.deleteDialog(model); + } + }, + + createDetailHandler: function(detailName, collection_name, regionName, title) { + var app=this; + showModelId = function(model_id) { + $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title})); + + collection = xos[collection_name]; + model = collection.get(model_id); + if (model == undefined) { + app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name})); + } else { + detailViewClass = app[detailName]; + detailView = new detailViewClass({model: model}); + app[regionName].show(detailView); + detailView.showLinkedItems(); + + detailButtons = new XOSDetailButtonView({linkedView: detailView}); + app["rightButtonPanel"].show(detailButtons); + } + } + return showModelId; + }, + + /* error handling callbacks */ + hideError: function() { if (this.logWindowId) { } else { @@ -67,16 +296,6 @@ XOSApplication = Marionette.Application.extend({ } }, - popupErrorDialog: function(responseText) { - $("#xos-error-dialog").html(templateFromId("#xos-error-response")($.parseJSON(responseText))); - $("#xos-error-dialog").dialog({ - modal: true, - buttons: { - Ok: function() { $(this).dialog("close"); } - } - }); - }, - showError: function(result) { result["statusclass"] = "failure"; if (this.logTableId) { @@ -133,61 +352,106 @@ XOSApplication = Marionette.Application.extend({ return logMessageId; }, - hideLinkedItems: function(result) { - var index=0; - while (index<4) { - this["linkedObjs" + (index+1)].empty(); - index = index + 1; - } - }, - - listViewShower: function(listViewName, collection_name, regionName, title) { - var app=this; - return function() { - app[regionName].show(new app[listViewName]); - app.hideLinkedItems(); - $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title})); - $("#detail").show(); - $("#xos-listview-button-box").show(); - $("#tabs").hide(); - $("#xos-detail-button-box").hide(); - } - }, - - addShower: function(detailName, collection_name, regionName, title) { - var app=this; - return function() { - model = new xos[collection_name].model(); - detailViewClass = app[detailName]; - detailView = new detailViewClass({model: model, collection:xos[collection_name]}); - app[regionName].show(detailView); - $("#xos-detail-button-box").show(); - $("#xos-listview-button-box").hide(); - } - }, - - detailShower: function(detailName, collection_name, regionName, title) { - var app=this; - showModelId = function(model_id) { - $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title})); - - collection = xos[collection_name]; - model = collection.get(model_id); - if (model == undefined) { - app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name})); - } else { - detailViewClass = app[detailName]; - detailView = new detailViewClass({model: model}); - app[regionName].show(detailView); - detailView.showLinkedItems(); - $("#xos-detail-button-box").show(); - $("#xos-listview-button-box").hide(); - } - } - return showModelId; - }, + saveError: function(model, result, xhr, infoMsgId) { + console.log("saveError"); + result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName; + result["infoMsgId"] = infoMsgId; + this.showError(result); + }, + + saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) { + console.log("saveSuccess"); + if (model.addToCollection) { + console.log("addToCollection"); + console.log(model.addToCollection); + model.addToCollection.add(model); + model.addToCollection.sort(); + model.addToCollection = undefined; + } + result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText}; + result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName; + result["infoMsgId"] = infoMsgId; + this.showSuccess(result); + }, + + destroyError: function(model, result, xhr, infoMsgId) { + result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName; + result["infoMsgId"] = infoMsgId; + this.showError(result); + }, + + destroySuccess: function(model, result, xhr, infoMsgId) { + result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText}; + result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName; + result["infoMsgId"] = infoMsgId; + this.showSuccess(result); + }, + + /* end error handling callbacks */ + + destroyModel: function(model) { + //console.log("destroyModel"); console.log(model); + this.hideError(); + var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} ); + var that = this; + model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);}, + success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}}); + }, + + deleteDialog: function(model, afterDelete) { + var that=this; + assert(model!=undefined, "deleteDialog's model is undefined"); + //console.log("deleteDialog"); console.log(model); + this.confirmDialog(null, null, function() { + //console.log("deleteConfirm"); console.log(model); + modelName = model.modelName; + that.destroyModel(model); + if (afterDelete=="list") { + that.navigate("list", modelName); + } else if (afterDelete) { + afterDelete(); + } + }); + }, }); +XOSButtonView = Marionette.ItemView.extend({ + 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", + "click button.btn-xos-add": "addClicked", + "click button.btn-xos-refresh": "refreshClicked", + }, + + submitLeaveClicked: function(e) { + this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e); + }, + + submitContinueClicked: function(e) { + this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e); + }, + + submitAddAnotherClicked: function(e) { + this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e); + }, + + submitDeleteClicked: function(e) { + this.options.linkedView.deleteClicked.call(this.options.linkedView, e); + }, + + addClicked: function(e) { + this.options.linkedView.addClicked.call(this.options.linkedView, e); + }, + + refreshClicked: function(e) { + this.options.linkedView.refreshClicked.call(this.options.linkedView, e); + }, + }); + +XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" }); +XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" }); + /* XOSDetailView extend with: app - MarionetteApplication @@ -197,119 +461,140 @@ XOSApplication = Marionette.Application.extend({ 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('deleteConfirmed', this.deleteConfirmed); + this.on("saveSuccess", this.onSaveSuccess); + this.synchronous = false; }, - /* 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. - */ - - inputChanged: function(e) { - this.dirty = true; - }, - - saveError: function(model, result, xhr, infoMsgId) { - result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName; - result["infoMsgId"] = infoMsgId; - this.app.showError(result); - }, - - saveSuccess: function(model, result, xhr, infoMsgId) { - result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText}; - result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName; - result["infoMsgId"] = infoMsgId; - this.app.showSuccess(result); + onShow: function() { + _.each(this.viewInitializers, function(initializer) { + initializer(); + }); }, - destroyError: function(model, result, xhr, infoMsgId) { - result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName; - result["infoMsgId"] = infoMsgId; - this.app.showError(result); - }, - - destroySuccess: function(model, result, xhr, infoMsgId) { - result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText}; - result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName; - result["infoMsgId"] = infoMsgId; - this.app.showSuccess(result); + 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(); - this.app.navigate("list", this.model.modelName); }, 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(); - this.app.navigate("add", this.model.modelName); }, save: function() { this.app.hideError(); - var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} ); - var data = Backbone.Syphon.serialize(this); - var that = this; - var isNew = !this.model.id; - this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);}, - success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId);}}); - if (isNew) { - console.log(this.model); - this.collection.add(this.model); - this.collection.sort(); - } - this.dirty = false; + 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(); + } }, - destroyModel: function() { - this.app.hideError(); - var infoMsgId = this.app.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} ); - var that = this; - this.model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);}, - success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}}); + deleteClicked: function(e) { + e.preventDefault(); + this.app.deleteDialog(this.model, "list"); }, - deleteClicked: function(e) { - e.preventDefault(); - this.app.confirmDialog(this, "deleteConfirmed"); - }, - - deleteConfirmed: function() { - modelName = this.model.modelName; - this.destroyModel(); - this.app.navigate("list", modelName); - }, - 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'); + 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) { @@ -331,54 +616,147 @@ XOSDetailView = Marionette.ItemView.extend({ 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) { - relatedField = this.model.collection.relatedCollections[relatedName]; - regionName = "linkedObjs" + (index+1); - - relatedListViewClassName = relatedName + "ListView"; - assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found"); - relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)}); - 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'); - }, - -}); + 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 = $("", {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_deploymentNetwork": "onDeploymentNetworkChange"} + ), + + 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.onDeploymentNetworkChange(); + }, + + onDeploymentNetworkChange: function(e) { + var deploymentID = this.$el.find("#field_deploymentNetwork").val(); + + console.log("onDeploymentNetworkChange"); + console.log(deploymentID); + + filterFunc = function(model) { 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) { + item=model.attributes.deployments[index]; + if (item.toString()==deploymentID.toString()) 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 xos.imageDeployments.models) { + imageDeployment = xos.imageDeployments.models[index]; + if ((imageDeployment.attributes.deployment == deploymentID) && (imageDeployment.attributes.image == model.id)) { + 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) - detailClass - class of detail view, probably an XOSDetailView */ XOSItemView = Marionette.ItemView.extend({ tagName: 'tr', className: 'test-tablerow', - events: {"click": "changeItem"}, - - changeItem: function(e) { - this.app.hideError(); - e.preventDefault(); - e.stopPropagation(); - - this.app.navigateToModel(this.app, this.detailClass, this.detailNavLink, this.model); - }, + 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: @@ -390,33 +768,36 @@ XOSItemView = Marionette.ItemView.extend({ title - title to display in template */ -XOSListView = Marionette.CompositeView.extend({ - childViewContainer: 'tbody', - - 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(); - } - }, - +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) + + 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 @@ -432,22 +813,257 @@ XOSListView = Marionette.CompositeView.extend({ } }, + 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 }; - }, + return { title: this.title, + addChildHash: this.getAddChildHash(), + foreignFields: this.collection.foreignFields, + listFields: this.collection.listFields, + detailLinkFields: this.collection.detailLinkFields, }; + }, }); -/* Give an id, the name of a collection, and the name of a field for models - within that collection, lookup the id and return the value of the field. -*/ +XOSDataTableView = Marionette.View.extend( { + el: '
' + + '

' + + '
' + + '
' + + '' + + '
', + + 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 '' + x + ''; }; + } + 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= 0) { + return true; + } + } + } + return false; + }; + + //console.log(aoData); + + // function used to populate the DataTable with the current + // content of the collection + var populateTable = function() + { + //console.log("populatetable!"); + + // clear out old row views + rows = []; + + sSearch = null; + iDisplayStart = 0; + iDisplayLength = 1000; + sortDirs = []; + sortCols = []; + _.each(aoData, function(param) { + if (param.name == "sSortDir_0") { + sortDirs = [param.value]; + } else if (param.name == "iSortCol_0") { + sortCols = [view.columnsByIndex[param.value].mData]; + } else if (param.name == "iDisplayStart") { + iDisplayStart = param.value; + } else if (param.name == "iDisplayLength") { + iDisplayLength = param.value; + } else if (param.name == "sSearch") { + sSearch = param.value; + } + }); + + aaData = view.collection.toJSON(); + + // apply backbone filtering on the models + if (view.filter) { + aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } ); + } + + var totalSize = aaData.length; + + // turn the ForeignKey fields into human readable things + for (rowIndex in aaData) { + row = aaData[rowIndex]; + for (fieldName in row) { + if (fieldName in view.columnsByFieldName) { + mSearchText = view.columnsByFieldName[fieldName].mSearchText; + if (mSearchText) { + row[fieldName] = mSearchText(row[fieldName]); + } + } + } + } + + // apply datatables search + if (sSearch) { + aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); }); + } + + var filteredSize = aaData.length; + + // apply datatables sort + aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); }); + + // slice it for pagination + if (iDisplayLength >= 0) { + aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength); + } + + return fnCallback({iTotalRecords: totalSize, + iTotalDisplayRecords: filteredSize, + aaData: aaData}); + }; + + 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; + } + }, + +}); idToName = function(id, collectionName, fieldName) { - linkedObject = xos[collectionName].get(id); - if (linkedObject == undefined) { - return "#" + id; - } else { - return linkedObject.attributes[fieldName]; - } + return xos.idToName(id, collectionName, fieldName); +}; + +makeIdToName = function(collectionName, fieldName) { + return function(id) { return idToName(id, collectionName, fieldName); } }; /* Constructs lists of '; } return result; @@ -481,10 +1100,37 @@ idToOptions = function(selectedId, collectionName, fieldName) { fieldName = name of field within models of collection that will be displayed */ -idToSelect = function(variable, selectedId, collectionName, fieldName) { - result = '' + + idToOptions(selectedId, collectionName, fieldName, filterFunc) + ''; return result; } +choicesToOptions = function(selectedValue, choices) { + result=""; + for (index in choices) { + choice = choices[index]; + displayName = choice[0]; + value = choice[1]; + if (value == selectedValue) { + selected = " selected"; + } else { + selected = ""; + } + result = result + ''; + } + return result; +} + +choicesToSelect = function(variable, selectedValue, choices) { + result = ''; + return result; +}