Hi, PFA patch for issue RM1241
Changes: 1. Altered variable control to make its UI consistence with privileges and Security labels. 2. Changed datamodel.js to handle duplicate rows at datamodel level and not UI/Control level. (See variable control for example) -- *Harshal Dhumal* *Software Engineer* EnterpriseDB India: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Wed, Jun 15, 2016 at 7:57 PM, Ashesh Vashi <ashesh.va...@enterprisedb.com > wrote: > On Wed, Jun 15, 2016 at 7:24 PM, Dave Page <dp...@pgadmin.org> wrote: > >> >> >> On Wed, Jun 15, 2016 at 2:08 PM, Ashesh Vashi < >> ashesh.va...@enterprisedb.com> wrote: >> >>> On Wed, Jun 15, 2016 at 6:25 PM, Dave Page <dp...@pgadmin.org> wrote: >>> >>>> On Wed, Jun 15, 2016 at 1:49 PM, Ashesh Vashi >>>> <ashesh.va...@enterprisedb.com> wrote: >>>> > On Thu, Jun 2, 2016 at 1:59 PM, Dave Page <dp...@pgadmin.org> wrote: >>>> >> >>>> >> >>>> >> >>>> >> On Tue, May 31, 2016 at 8:53 PM, Harshal Dhumal >>>> >> <harshal.dhu...@enterprisedb.com> wrote: >>>> >>> >>>> >>> Hi Dave, >>>> >>> >>>> >>> Regarding Issue 1241: >>>> >>> >>>> >>> We have added header section for parameter tab deliberately so that >>>> we >>>> >>> can force user to select parameter name (and therefore parameter's >>>> data >>>> >>> type) before adding new row. This is required because behavior of >>>> second >>>> >>> cell (Value cell) is dependent on what parameter name user has >>>> selected in >>>> >>> first cell (Name cell). See attached screen-shot. >>>> >>> >>>> >>> For example: >>>> >>> 1. If user selects parameter 'array_nulls' then value for this >>>> should be >>>> >>> either true or false (and hence switch cell). >>>> >>> 2. If user selects parameter 'cpu_index_tuple_cost' then value for >>>> this >>>> >>> should be Integer (and hence Integer cell). >>>> >>> >>>> >>> Without the custom header (and forcing user to select parameter) we >>>> >>> cannot decide what type of cell we need in second column. >>>> >>> >>>> >>> Let me know your opinion on this. >>>> >> >>>> >> >>>> >> We need to figure out a way to fix it. Our difficulties encountered >>>> >> writing code should not dictate usability compromises. >>>> >> >>>> >> In this case, something that needs some thought and maybe some >>>> tricky code >>>> >> has caused us to create an inconsistent UI workflow to side-step the >>>> >> problem, which is not appropriate as it leads to a poor look and >>>> feel and >>>> >> potentially confusion for the user. >>>> > >>>> > Agree - we should handle these cases gracefully. >>>> > We need to over come the limitation by brain storming, which we >>>> already >>>> > started offline. :-) >>>> > >>>> > To be honest - it is a time consuming work, and there is no quick fix >>>> for >>>> > this. >>>> > We can handle it as one case for each change instead of targeting all >>>> UI >>>> > changes as one whole problem. >>>> > And, we can utilize the same time to fix a lot more cases in beta 2. >>>> >>>> As far as I'm aware, this is the only case where there's a real problem. >>>> >>>> > I can ask Harshal to find out all possible places, where the similar >>>> changes >>>> > are required, and create a separate case for each (though - not >>>> without your >>>> > agreement). >>>> >>>> I don't think we need to. This one sub-node grid (parameters) is the >>>> only one that I've seen where we deviate from the intended design - >>>> and I think I've seen them all now! >>>> >>> Hmm.. >>> >>> Unfortunately - some set of columns needs to be unique in most of the >>> cases (where these controls are used), and the checks for the unique >>> dataset is done at the control side, which was wrong at our end. >>> And, we will need to change the model validation code to check the >>> uniqueness of data set at data level (through Backbone.Model) now, which >>> will require a lot more changes than it looks. >>> >>> For example - in table node, we have too many UniqueCollControl, which >>> requires these changes. >>> >> >> Perhaps - but I fail to see how this justifies the different UI design >> for this one use. Are we talking about the same thing? >> > Yes - we do. > It is not change in the design of the UI control, but - we will need to > replace simplified subnode control (which is already present in the > system), and make the validation check in each of the data model one at a > time. > > We need to keep the UI at other place, until we fix the data validation > part at each of those places. > We will remove the UniqueColControl once we complete all these changes. > > That's why - I said it was mistake to do the validation in Control rather > than the data (Backbone.Model). > > > -- > Thanks & Regards, > Ashesh Vashi > > >> >> -- >> Dave Page >> Blog: http://pgsnake.blogspot.com >> Twitter: @pgsnake >> >> EnterpriseDB UK: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> > >
diff --git a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/js/databases.js b/web/pgadmin/browser/server_groups/servers/databases/templates/databases/js/databases.js index 61671f4..f2a49c9 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/templates/databases/js/databases.js +++ b/web/pgadmin/browser/server_groups/servers/databases/templates/databases/js/databases.js @@ -284,7 +284,7 @@ function($, _, S, pgAdmin, pgBrowser, Alertify) { canAdd: true, canDelete: true, control: 'unique-col-collection', },{ id: 'variables', label: '{{ _('Parameters') }}', type: 'collection', - model: pgBrowser.Node.VariableModel, editable: false, + model: pgBrowser.Node.VariableModel.extend({keys:['name', 'role']}), editable: false, group: '{{ _('Parameters') }}', mode: ['edit', 'create'], canAdd: true, canEdit: false, canDelete: true, hasRole: true, control: Backform.VariableCollectionControl, node: 'role' diff --git a/web/pgadmin/browser/server_groups/servers/roles/templates/role/js/role.js b/web/pgadmin/browser/server_groups/servers/roles/templates/role/js/role.js index 64fd32b..444e8ca 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/templates/role/js/role.js +++ b/web/pgadmin/browser/server_groups/servers/roles/templates/role/js/role.js @@ -476,7 +476,8 @@ function($, _, S, pgAdmin, pgBrowser, alertify, Backform) { },{ id: 'variables', label: '{{ _('Parameters') }}', type: 'collection', group: '{{ _('Parameters') }}', hasDatabase: true, url: 'variables', - model: pgBrowser.Node.VariableModel, control: 'variable-collection', + model: pgBrowser.Node.VariableModel.extend({keys:['name', 'database']}), + control: 'variable-collection', mode: [ 'edit', 'create'], canAdd: true, canDelete: true, url: "variables", disabled: 'readonly' },{ diff --git a/web/pgadmin/browser/server_groups/servers/static/js/variable.js b/web/pgadmin/browser/server_groups/servers/static/js/variable.js index 2d13728..82b0ac5 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/variable.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/variable.js @@ -20,6 +20,7 @@ Alertify = require('alertify') || root.Alertify; pgAdmin = require('pgadmin') || root.pgAdmin, pgNode = require('pgadmin.browser.node') || root.pgAdmin.Browser.Node; + factory(root, _, $, Backbone, Backform, Alertify, pgAdmin, pgNode); // Finally, as a browser global. @@ -38,8 +39,17 @@ var cellFunction = function(model) { var self = this, name = model.get("name"), - availVariables = self.get('availVariables'), - variable = availVariables[name]; + availVariables = {}; + + self.collection.each(function(col) { + if (col.get("name") == "name") { + availVariables = col.get('availVariables'); + } + }); + + + var variable = name ? availVariables[name]: undefined, + value = model.get("value"); switch(variable && variable.vartype) { case "bool": @@ -47,13 +57,13 @@ * bool cell and variable can not be stateless (i.e undefined). * It should be either true or false. */ - if (_.isUndefined(model.get("value"))) { - model.set("value", false); - } + + model.set("value", !!model.get("value"), {silent: true}); return Backgrid.Extension.SwitchCell; break; case "enum": + model.set({'value': undefined}, {silent:true}); var options = [], enumVals = variable.enumvals; @@ -64,40 +74,138 @@ return Backgrid.Extension.Select2Cell.extend({optionValues: options}); break; case "integer": + if (!_.isNaN(parseInt(value))) { + model.set({'value': parseInt(value)}, {silent:true}); + } else { + model.set({'value': undefined}, {silent:true}); + } return Backgrid.IntegerCell; break; case "real": + if (!_.isNaN(parseFloat(value))) { + model.set({'value': parseFloat(value)}, {silent:true}); + } else { + model.set({'value': undefined}, {silent:true}); + } return Backgrid.NumberCell.extend({decimals: 0}); break; case "string": return Backgrid.StringCell; break; default: + model.set({'value': undefined}, {silent:true}); return Backgrid.Cell; break; } + model.set({'value': undefined}, {silent:true}); + return Backgrid.Cell; } + /* + * This row will define behaviour or value column cell depending upon + * variable name. + */ + var VariableRow = Backgrid.Row.extend({ + modelDuplicateColor: "lightYellow", + + modelUniqueColor: "#fff", + + initialize: function () { + Backgrid.Row.prototype.initialize.apply(this, arguments); + var self = this; + self.model.on("change:name", function() { + setTimeout(function() { + self.columns.each(function(col) { + if (col.get('name') == 'value') { + + var idx = self.columns.indexOf(col), + cf = col.get("cellFunction"), + cell = new (cf.apply(col, [self.model]))({ + column: col, + model: self.model + }), + oldCell = self.cells[idx]; + oldCell.remove(); + self.cells[idx] = cell; + self.render(); + } + + }); + }, 10); + }); + self.listenTo(self.model, 'pgadmin-session:model:duplicate', self.modelDuplicate); + self.listenTo(self.model, 'pgadmin-session:model:unique', self.modelUnique); + }, + modelDuplicate: function() { + $(this.el).removeClass("new"); + this.el.style.backgroundColor = this.modelDuplicateColor; + }, + modelUnique: function() { + this.el.style.backgroundColor = this.modelUniqueColor; + } + + }) /** * VariableModel used to represent configuration parameters (variables tab) * for database objects. **/ var VariableModel = pgNode.VariableModel = pgNode.Model.extend({ + keys: ['name'], defaults: { name: undefined, value: undefined, - role: undefined, - database: undefined, + role: null, + database: null, }, - keys: ['name', 'role', 'database'], schema: [ - {id: 'name', label:'Name', type:'text', editable: false, cellHeaderClasses: 'width_percent_30'}, + { + id: 'name', label:'Name', type:'text', editable: true, cellHeaderClasses: 'width_percent_30', + editable: function(m) { + return (m instanceof Backbone.Collection) ? true : m.isNew(); + }, + cell: Backgrid.Extension.NodeAjaxOptionsCell.extend({ + initialize: function() { + Backgrid.Extension.NodeAjaxOptionsCell.prototype.initialize.apply(this, arguments); + + // Immediately process options as we need them before render. + + var opVals = _.clone(this.optionValues || + (_.isFunction(this.column.get('options')) ? + (this.column.get('options'))(this) : + this.column.get('options'))); + + this.column.set('options', opVals); + } + }), + url: 'vopts', + select2: { allowClear: false }, + transform: function(vars, cell) { + var self = this, + res = [], + availVariables = {}; + + _.each(vars, function(v) { + res.push({ + 'value': v.name, + 'image': undefined, + 'label': v.name + }); + availVariables[v.name] = v; + }); + + cell.column.set("availVariables", availVariables); + return res; + } + }, { id: 'value', label:'Value', type: 'text', editable: true, - cellFunction: cellFunction, cellHeaderClasses: 'width_percent_50' + cellFunction: cellFunction, cellHeaderClasses: 'width_percent_40' + }, + {id: 'database', label:'Database', type: 'text', editable: true, + node: 'database', cell: Backgrid.Extension.NodeListByNameCell }, - {id: 'database', label:'Database', type: 'text', editable: false}, - {id: 'role', label:'Role', type: 'text', editable: false} + {id: 'role', label:'Role', type: 'text', editable: true, + node: 'role', cell: Backgrid.Extension.NodeListByNameCell} ], toJSON: function() { var d = Backbone.Model.prototype.toJSON.apply(this); @@ -144,7 +252,7 @@ initialize: function(opts) { var self = this, - uniqueCols = ['name']; + keys = ['name']; /* * Read from field schema whether user wants to use database and role @@ -155,24 +263,22 @@ // Update unique coll field based on above flag status. if (self.hasDatabase) { - uniqueCols.push('database') + keys.push('database'); } else if (self.hasRole) { - uniqueCols.push('role') + keys.push('role'); } // Overriding the uniqueCol in the field if (opts && opts.field) { if (opts.field instanceof Backform.Field) { opts.field.set({ - uniqueCol: uniqueCols, - model: pgNode.VariableModel + model: pgNode.VariableModel.extend({keys:keys}) }, { silent: true }); } else { opts.field.extend({ - uniqueCol: uniqueCols, - model: pgNode.VariableModel + model: pgNode.VariableModel.extend({keys:keys}) }); } } @@ -181,94 +287,25 @@ self, arguments ); - self.availVariables = {}; var node = self.field.get('node').type, - headerSchema = [{ - id: 'name', label:'', type:'text', - url: self.field.get('variable_opts') || 'vopts', - control: Backform.NodeAjaxOptionsControl, - cache_level: 'server', - select2: { - allowClear: false, width: 'style' - }, - availVariables: self.availVariables, - node: node, first_empty: false, - version_compatible: self.field.get('version_compatible'), - transform: function(vars) { - var self = this, - opts = self.field.get('availVariables'); - - res = []; - - for (var prop in opts) { - if (opts.hasOwnProperty(prop)) { - delete opts[prop]; - } - } - - _.each(vars, function(v) { - opts[v.name] = _.extend({}, v); - res.push({ - 'label': v.name, - 'value': v.name - }); - }); - - return res; - } - }], - headerDefaults = {name: null}, gridCols = ['name', 'value']; if (self.hasDatabase) { - headerSchema.push({ - id: 'database', label:'', type: 'text', cache_level: 'server', - control: Backform.NodeListByNameControl, node: 'database', - version_compatible: self.field.get('version_compatible') - }); - headerDefaults['database'] = null; gridCols.push('database'); } if (self.hasRole) { - headerSchema.push({ - id: 'role', label:'', type: 'text', cache_level: 'server', - control: Backform.NodeListByNameControl, node: 'role', - version_compatible: self.field.get('version_compatible') - }); - headerDefaults['role'] = null; gridCols.push('role'); } - self.headerData = new (Backbone.Model.extend({ - defaults: headerDefaults, - schema: headerSchema - }))({}); - - var headerGroups = Backform.generateViewSchema( - self.field.get('node_info'), self.headerData, 'create', - node, self.field.get('node_data') - ), - fields = []; - - _.each(headerGroups, function(o) { - fields = fields.concat(o.fields); - }); - - self.headerFields = new Backform.Fields(fields); self.gridSchema = Backform.generateGridColumnsFromModel( - null, VariableModel, 'edit', gridCols + self.field.get('node_info'), VariableModel.extend({keys:keys}), 'edit', gridCols, self.field.get('schema_node') ); // Make sure - we do have the data for variables self.getVariables(); - - self.controls = []; - self.listenTo(self.headerData, "change", self.headerDataChanged); - self.listenTo(self.headerData, "select2", self.headerDataChanged); - self.listenTo(self.collection, "remove", self.onRemoveVariable); }, /* * Get the variable data for this node. @@ -321,101 +358,19 @@ } }, - generateHeader: function(data) { - var header = [ - '<div class="subnode-header-form">', - ' <div class="container-fluid">', - ' <div class="row">', - ' <div class="col-md-4">', - ' <label class="control-label"><%-variable_label%></label>', - ' </div>', - ' <div class="col-md-4" header="name"></div>', - ' <div class="col-md-4">', - ' <button class="btn-sm btn-default add" <%=canAdd ? "" : "disabled=\'disabled\'"%> ><%-add_label%></buttton>', - ' </div>', - ' </div>']; - - if(this.hasDatabase) { - header.push([ - ' <div class="row">', - ' <div class="col-md-4">', - ' <label class="control-label"><%-database_label%></label>', - ' </div>', - ' <div class="col-md-4" header="database"></div>', - ' </div>'].join("\n") - ); - } - - if (this.hasRole) { - header.push([ - ' <div class="row">', - ' <div class="col-md-4">', - ' <label class="control-label"><%-role_label%></label>', - ' </div>', - ' <div class="col-md-4" header="role"></div>', - ' </div>'].join("\n") - ); - } - - header.push([ - ' </div>', - '</div>'].join("\n")); - - // TODO:: Do the i18n - _.extend(data, { - variable_label: "Parameter name", - add_label: "ADD", - database_label: "Database", - role_label: "Role" - }); - - var self = this, - headerTmpl = _.template(header.join("\n")), - $header = $(headerTmpl(data)), - controls = this.controls; - - this.headerFields.each(function(field) { - var control = new (field.get("control"))({ - field: field, - model: self.headerData - }); - - $header.find('div[header="' + field.get('name') + '"]').append( - control.render().$el - ); - - controls.push(control); - }); - - // We should not show add but in properties mode - if (data.mode == 'properties') { - $header.find("button.add").remove(); - } - - self.$header = $header; - - return $header; - }, - - events: _.extend( - {}, Backform.UniqueColCollectionControl.prototype.events, - {'click button.add': 'addVariable'} - ), - showGridControl: function(data) { var self = this, titleTmpl = _.template([ "<div class='subnode-header'>", "<label class='control-label'><%-label%></label>", + "<button class='btn-sm btn-default add' <%=canAdd ? '' : 'disabled=\"disabled\"'%>>Add</buttton>", "</div>"].join("\n")), $gridBody = $("<div class='pgadmin-control-group backgrid form-group col-xs-12 object subnode'></div>").append( - titleTmpl({label: data.label}) + titleTmpl(data) ); - $gridBody.append(self.generateHeader(data)); - var gridSchema = _.clone(this.gridSchema); _.each(gridSchema.columns, function(col) { @@ -460,18 +415,61 @@ var grid = self.grid = new Backgrid.Grid({ columns: gridSchema.columns, collection: self.collection, + row: VariableRow, className: "backgrid table-bordered" }); self.$grid = grid.render().$el; $gridBody.append(self.$grid); - self.headerData.set( - 'name', - self.$header.find( - 'div[header="name"] select option:first' - ).val() - ); + // Add button callback + if (!(data.disabled || data.canAdd == false)) { + $gridBody.find('button.add').first().click(function(e) { + e.preventDefault(); + var canAddRow = _.isFunction(data.canAddRow) ? + data.canAddRow.apply(self, [self.model]) : true; + if (canAddRow) { + + var allowMultipleEmptyRows = !!self.field.get('allowMultipleEmptyRows'); + + // If allowMultipleEmptyRows is not set or is false then don't allow second new empty row. + // There should be only one empty row. + if (!allowMultipleEmptyRows && self.collection) { + var isEmpty = false; + self.collection.each(function(model) { + var modelValues = []; + _.each(model.attributes, function(val, key) { + modelValues.push(val); + }) + if(!_.some(modelValues, _.identity)) { + isEmpty = true; + } + }); + if(isEmpty) { + return false; + } + } + + $(grid.body.$el.find($("tr.new"))).removeClass("new") + var m = new (data.model) (null, { + silent: true, + handler: self.collection, + top: self.model.top || self.model, + collection: self.collection, + node_info: self.model.node_info + }); + self.collection.add(m); + + var idx = self.collection.indexOf(m), + newRow = grid.body.rows[idx].$el; + + newRow.addClass("new"); + $(newRow).pgMakeVisible('backform-tab'); + + return false; + } + }); + } // Render node grid return $gridBody; @@ -505,83 +503,7 @@ delete m; } - this.headerDataChanged(); - return false; - }, - - headerDataChanged: function() { - var self = this, val, - data = this.headerData.toJSON(), - inSelected = false, - checkVars = ['name']; - - if (!self.$header) { - return; - } - - if (self.hasDatabase) { - checkVars.push('database'); - } - - if (self.hasRole) { - checkVars.push('role'); - } - - if (self.control_data.canAdd) { - self.collection.each(function(m) { - if (!inSelected) { - var has = true; - _.each(checkVars, function(v) { - val = m.get(v); - has = has && (( - (_.isUndefined(val) || _.isNull(val)) && - (_.isUndefined(data[v]) || _.isNull(data[v])) - ) || - (val == data[v])); - }); - - inSelected = has; - } - }); - } - else { - inSelected = true; - } - - self.$header.find('button.add').prop('disabled', inSelected); - }, - - onRemoveVariable: function() { - var self = this; - - // Wait for collection to be updated before checking for the button to be - // enabled, or not. - setTimeout(function() { - self.headerDataChanged(); - }, 10); - }, - - remove: function() { - /* - * Stop listening the events registered by this control. - */ - this.stopListening(this.headerData, "change", this.headerDataChanged); - this.listenTo(this.headerData, "select2", this.headerDataChanged); - this.listenTo(this.collection, "remove", this.onRemoveVariable); - - // Remove header controls. - _.each(this.controls, function(control) { - control.remove(); - }); - - VariableCollectionControl.__super__.remove.apply(this, arguments); - - // Remove the header model - delete (this.headerData); - - // Clear the available Variables object - self.availVariables = {}; } }); diff --git a/web/pgadmin/browser/static/js/datamodel.js b/web/pgadmin/browser/static/js/datamodel.js index 87b5a64..8cb7034 100644 --- a/web/pgadmin/browser/static/js/datamodel.js +++ b/web/pgadmin/browser/static/js/datamodel.js @@ -7,6 +7,7 @@ function(_, pgAdmin, $, Backbone) { /* * Parsing the existing data */ + on_server: false, parse: function(res) { var self = this; if (res && _.isObject(res) && 'node' in res && res['node']) { @@ -33,9 +34,21 @@ function(_, pgAdmin, $, Backbone) { silent: true, attrName: s.id }); - self.set(s.id, obj, {silent: true, parse: true}); + + /* + * Nested collection models may or may not have idAttribute. + * So to decide whether model is new or not set 'on_server' + * flag on such models. + */ + + self.set(s.id, obj, {silent: true, parse: true, on_server : true}); } else { - obj.reset(val, {silent: true, parse: true}); + /* + * Nested collection models may or may not have idAttribute. + * So to decide whether model is new or not set 'on_server' + * flag on such models. + */ + obj.reset(val, {silent: true, parse: true, on_server : true}); } } else { @@ -89,6 +102,12 @@ function(_, pgAdmin, $, Backbone) { return res; }, + isNew: function() { + if (this.has(this.idAttribute)) { + return !this.has(this.idAttribute); + } + return !this.on_server; + }, primary_key: function() { if (this.keys && _.isArray(this.keys)) { var res = {}, self = this; @@ -103,6 +122,11 @@ function(_, pgAdmin, $, Backbone) { }, initialize: function(attributes, options) { var self = this; + self._previous_key_values = {}; + + if ('on_server' in options && options.on_server) { + self.on_server = true; + } Backbone.Model.prototype.initialize.apply(self, arguments); @@ -198,6 +222,13 @@ function(_, pgAdmin, $, Backbone) { self.startNewSession(); } + if ('keys' in self && _.isArray(self.keys)) { + _.each(self.keys, function(key) { + self.on("change:" + key, function(m) { + self._previous_key_values[key] = m.previous(key); + }) + }) + } return self; }, // Create a reset function, which allow us to remove the nested object. @@ -725,13 +756,19 @@ function(_, pgAdmin, $, Backbone) { invalidModels = self.sessAttrs['invalid']; if (self.trackChanges) { - // Find the object the invalid list, if found remove it from the list + // Now check uniqueness of current model with other models. + var isUnique = self.checkDuplicateWithModel(m); + + // If unique then find the object the invalid list, if found remove it from the list // and inform the parent that - I am a valid object now. - if (m.cid in invalidModels) { + + if (isUnique && m.cid in invalidModels) { delete invalidModels[m.cid]; } - this.triggerValidationEvent.apply(this); + if (isUnique) { + this.triggerValidationEvent.apply(this); + } } return true; @@ -837,7 +874,6 @@ function(_, pgAdmin, $, Backbone) { return (_.findIndex(this.sessAttrs[type], comparator)); }, onModelAdd: function(obj) { - if (!this.trackChanges) return true; @@ -871,25 +907,21 @@ function(_, pgAdmin, $, Backbone) { (self.sessAttrs['invalid'])[obj.cid] = msg; } } + } else { + if ('validate' in obj && typeof(obj.validate) === 'function') { + msg = obj.validate(); - // Let the parent/listener know about my status (valid/invalid). - this.triggerValidationEvent.apply(this); - - return true; - } - if ('validate' in obj && typeof(obj.validate) === 'function') { - msg = obj.validate(); - - if (msg) { - (self.sessAttrs['invalid'])[obj.cid] = msg; + if (msg) { + (self.sessAttrs['invalid'])[obj.cid] = msg; + } } - } - self.sessAttrs['added'].push(obj); + self.sessAttrs['added'].push(obj); - /* - * Session has been changed - */ - (self.handler || self).trigger('pgadmin-session:added', self, obj); + /* + * Session has been changed + */ + (self.handler || self).trigger('pgadmin-session:added', self, obj); + } // Let the parent/listener know about my status (valid/invalid). this.triggerValidationEvent.apply(this); @@ -897,7 +929,6 @@ function(_, pgAdmin, $, Backbone) { return true; }, onModelRemove: function(obj) { - if (!this.trackChanges) return true; @@ -917,6 +948,8 @@ function(_, pgAdmin, $, Backbone) { (self.handler || self).trigger('pgadmin-session:removed', self, copy); + self.checkDuplicateWithModel(copy); + // Let the parent/listener know about my status (valid/invalid). this.triggerValidationEvent.apply(this); @@ -936,6 +969,8 @@ function(_, pgAdmin, $, Backbone) { self.sessAttrs['deleted'].push(obj); + self.checkDuplicateWithModel(obj); + // Let the parent/listener know about my status (valid/invalid). this.triggerValidationEvent.apply(this); @@ -985,12 +1020,12 @@ function(_, pgAdmin, $, Backbone) { } }, onModelChange: function(obj) { + var self = this; if (!this.trackChanges || !(obj instanceof pgBrowser.Node.Model)) return true; - var self = this, - idx = self.objFindInSession(obj, 'added'); + var idx = self.objFindInSession(obj, 'added'); // It was newly added model, we don't need to add into the changed // list. @@ -1034,6 +1069,74 @@ function(_, pgAdmin, $, Backbone) { (self.handler || self).trigger('pgadmin-session:changed', self, obj); return true; + }, + + /* + * This function will check if given model is unique or duplicate in + * collection and set/clear duplicate errors on models. + */ + checkDuplicateWithModel: function(model) { + if (!('keys' in model) || _.isEmpty(model.keys)) { + return true; + } + + var self = this, + condition = {}, + previous_condition = {}; + + _.each(model.keys, function(key) { + condition[key] = model.get(key); + if(key in model._previous_key_values) { + previous_condition[key] = model._previous_key_values[key]; + } else { + previous_condition[key] = model.previous(key); + } + }); + + // Reset previously changed values. + model._previous_key_values = {}; + + var old_conflicting_models = self.where(previous_condition); + + if (old_conflicting_models.length == 1) { + var m = old_conflicting_models[0]; + self.clearInvalidSessionIfModelValid(m); + } + + new_conflicting_models = self.where(condition); + if (new_conflicting_models.length == 0) { + self.clearInvalidSessionIfModelValid(model); + } else if (new_conflicting_models.length == 1) { + self.clearInvalidSessionIfModelValid(model); + self.clearInvalidSessionIfModelValid(new_conflicting_models[0]); + } else { + var msg = "Duplicate rows."; + setTimeout(function() { + _.each(new_conflicting_models, function(m) { + self.trigger( + 'pgadmin-session:model:invalid', msg, m, self.handler + ); + m.trigger( + 'pgadmin-session:model:duplicate', m, msg + ); + }); + }, 10); + + return false; + } + return true; + }, + clearInvalidSessionIfModelValid: function(m) { + var errors = m.errorModel.attributes, + invalidModels = this.sessAttrs['invalid']; + + m.trigger('pgadmin-session:model:unique', m + ); + if (_.size(errors) == 0) { + delete invalidModels[m.cid]; + } else { + invalidModels[m.cid] = errors[Object.keys(errors)[0]]; + } } });
-- Sent via pgadmin-hackers mailing list (pgadmin-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers