Introduction of the privilege support for database object (Patch:

Murtuza Zabuawala), and Unique column control for backform (Harshal
Dhumal).

Also, includes some fixes for the model, & collection handling.
This commit is contained in:
Ashesh Vashi
2015-12-17 18:30:39 +05:30
parent ca0b1f20df
commit 0273c29283
3 changed files with 554 additions and 27 deletions

View File

@@ -1231,7 +1231,7 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) {
delete self.origSessAttrs[o];
if ('startNewSession' in obj && _.isFunction(obj.startNewSession)) {
if (obj && 'startNewSession' in obj && _.isFunction(obj.startNewSession)) {
obj.startNewSession();
}
});

View File

@@ -38,7 +38,7 @@
pgAdmin.editableCell = function() {
if (this.attributes && this.attributes.disabled) {
if(_.isFunction(this.attributes.disabled)) {
return !(this.attributes.disabled.apply(this, [arguments]));
return !(this.attributes.disabled.apply(this, arguments));
}
if (_.isBoolean(this.attributes.disabled)) {
return !this.attributes.disabled;
@@ -66,6 +66,7 @@
'options': ['readonly-option', 'select', Backgrid.Extension.PGSelectCell],
'multiline': ['textarea', 'textarea', 'string'],
'collection': ['sub-node-collection', 'sub-node-collection', 'string'],
'uniqueColCollection': ['unique-col-collection', 'unique-col-collection', 'string'],
'switch' : 'switch'
};
@@ -154,6 +155,32 @@
});
};
Backform.Control.prototype.clearInvalid = function() {
this.$el.removeClass(Backform.errorClassName);
this.$el.find(".pgadmin-control-error-message").remove();
return this;
};
Backform.Control.prototype.updateInvalid = function() {
var self = this;
var errorModel = this.model.errorModel;
if (!(errorModel instanceof Backbone.Model)) return this;
this.clearInvalid();
this.$el.find(':input').not('button').each(function(ix, el) {
var attrArr = $(el).attr('name').split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
error = self.keyPathAccessor(errorModel.toJSON(), $(el).attr('name'));
if (_.isEmpty(error)) return;
self.$el.addClass(Backform.errorClassName).append(
$("<div></div>").addClass('pgadmin-control-error-message col-xs-12 help-block').text(error)
);
});
};
var ReadonlyOptionControl = Backform.ReadonlyOptionControl = Backform.SelectControl.extend({
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
@@ -379,13 +406,78 @@
var groups = Backform.generateViewSchema(node_info, m, type),
schema = [],
columns = [],
tblCols = [],
addAll = _.isUndefined(cols) || _.isNull(cols);
func,
idx = 0;
// Create another array if cols is of type object & store its keys in that array,
// If cols is object then chances that we have custom width class attached with in.
if(_.isObject(cols)) {
tblCols = Object.keys(cols);
if (_.isNull(cols) || _.isUndefined(cols)) {
func = function(f) {
f.cell_priority = idx;
idx = idx + 1;
// We can also provide custom header cell class in schema itself,
// But we will give priority to extraClass attached in cols
// If headerCell property is already set by cols then skip extraClass property from schema
if (!(f.headerCell) && f.cellHeaderClasses) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
}
};
} else if (_.isArray(cols)) {
func = function(f) {
f.cell_priority = _.indexOf(cols, f.name);
// We can also provide custom header cell class in schema itself,
// But we will give priority to extraClass attached in cols
// If headerCell property is already set by cols then skip extraClass property from schema
if ((!f.headerCell) && f.cellHeaderClasses) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
}
};
} else if(_.isObject(cols)) {
var tblCols = Object.keys(cols);
func = function(f) {
var val = (f.name in cols) && cols[f.name];
if (_.isNull(val) || _.isUndefined(val)) {
f.cell_priority = -1;
return;
}
if (_.isObject(val)) {
if ('index' in val) {
f.cell_priority = val['index'];
idx = (idx > val['index']) ? idx + 1 : val['index'];
} else {
var i = _.indexOf(tblCols, f.name);
f.cell_priority = idx = ((i > idx) ? i : idx);
idx = idx + 1;
}
// We can also provide custom header cell class in schema itself,
// But we will give priority to extraClass attached in cols
// If headerCell property is already set by cols then skip extraClass property from schema
if (!f.headerCell) {
if (f.cellHeaderClasses) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
}
if ('class' in val && _.isString(val['class'])) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
f.cellHeaderClasses = (f.cellHeaderClasses || '') + ' ' + val['class'];
}
}
}
if (_.isString(val)) {
var i = _.indexOf(tblCols, f.name);
f.cell_priority = idx = ((i > idx) ? i : idx);
idx = idx + 1;
if (!f.headerCell) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
}
f.cellHeaderClasses = (f.cellHeaderClasses || '') + ' ' + val;
}
};
}
// Prepare columns for backgrid
@@ -394,25 +486,9 @@
if (!f.control && !f.cell) {
return;
}
// Check custom property in cols & if it is present then attach it to current cell
if (tblCols.length > 0 && _.isString(cols[f.name])) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
f.cellHeaderClasses = cols[f.name];
f.cell_priority = _.indexOf(tblCols, f.name);
} else if(tblCols.length > 0) {
f.cell_priority = _.indexOf(tblCols, f.name);
} else {
f.cell_priority = _.indexOf(cols, f.name);
}
// We can also provide custom header cell class in schema itself,
// But we will give priority to extraClass attached in cols
// If headerCell property is already set by cols then skip extraClass property from schema
if (!(f.headerCell) && f.cellHeaderClasses) {
f.headerCell = Backgrid.Extension.CustomHeaderCell;
}
if (addAll || f.cell_priority != -1) {
func(f);
if (f.cell_priority != -1) {
columns.push(f);
}
});
@@ -426,6 +502,191 @@
};
};
var UniqueColCollectionControl = Backform.UniqueColCollectionControl = Backform.Control.extend({
initialize: function() {
Backform.Control.prototype.initialize.apply(this, arguments);
var uniqueCol = this.field.get('uniqueCol') || [];
var columns = this.field.get('columns')
// Check if unique columns provided are also in model attributes.
if (uniqueCol.length > _.intersection(columns, uniqueCol).length){
errorMsg = "Developer: Unique column/s [ "+_.difference(uniqueCol, columns)+" ] not found in collection model [ " + columns +" ]."
alert (errorMsg);
return null;
}
var collection = this.model.get(this.field.get('name')),
self = this;
if (!collection) {
collection = new (pgAdmin.Browser.Node.Collection)(null, {
model: self.field.get('model'),
silent: true,
handler: self.model.handler || self.model
});
self.model.set(self.field.get('name'), collection, {silent: true});
}
self.listenTo(collection, "add", self.collectionChanged);
self.listenTo(collection, "change", self.collectionChanged);
},
collectionChanged: function(newModel, coll, op) {
var uniqueCol = this.field.get('uniqueCol') || [],
uniqueChangedAttr = [],
changedAttr = newModel.changedAttributes();
// Check if changed model attributes are also in unique columns. And then only check for uniqueness.
if (changedAttr) {
_.each(uniqueCol, function(col) {
if ( _.has(changedAttr,col))
{
uniqueChangedAttr.push(col);
}
});
if(uniqueChangedAttr.length == 0) {
return;
}
} else {
return;
}
var collection = this.model.get(this.field.get('name'));
this.stopListening(collection, "change", this.collectionChanged);
// Check if changed attribute's value of new/updated model also exist for another model in collection.
// If duplicate value exists then set the attribute's value of new/updated model to it's previous values.
collection.each(function(model) {
if (newModel != model) {
var duplicateAttrValues = []
_.each(uniqueCol, function(attr) {
attrValue = newModel.get(attr);
if (!_.isUndefined(attrValue) && attrValue == model.get(attr)) {
duplicateAttrValues.push(attrValue)
}
});
if (duplicateAttrValues.length == uniqueCol.length){
newModel.set(uniqueChangedAttr[0], newModel.previous(uniqueChangedAttr[0]), {silent: true});
// TODO- Need to add notification in status bar for unique column.
}
}
});
this.listenTo(collection, "change", this.collectionChanged);
},
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults),
attributes = this.model.toJSON(),
attrArr = field.name.split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
rawValue = this.keyPathAccessor(attributes[name], path),
data = _.extend(field, {
rawValue: rawValue,
value: this.formatter.fromRaw(rawValue, this.model),
attributes: attributes,
formatter: this.formatter
}),
evalF = function(f, m) {
return (_.isFunction(f) ? !!f(m) : !!f);
};
// Evaluate the disabled, visible, required, canAdd, & canDelete option
_.extend(data, {
disabled: evalF(data.disabled, this.model),
visible: evalF(data.visible, this.model),
required: evalF(data.required, this.model),
canAdd: evalF(data.canAdd, this.model),
canDelete: evalF(data.canDelete, this.model)
});
// Show Backgrid Control
grid = (data.subnode == undefined) ? "" : this.showGridControl(data);
this.$el.html(grid).addClass(field.name);
this.updateInvalid();
return this;
},
showGridControl: function(data) {
var gridHeader = ["<div class='subnode-header'>",
" <label class='control-label col-sm-4'>" + data.label + "</label>" ,
" <button class='btn-sm btn-default add'>Add</buttton>",
"</div>"].join("\n"),
gridBody = $("<div class='pgadmin-control-group backgrid form-group col-xs-12 object subnode'></div>").append(gridHeader);
var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype,
gridSchema = Backform.generateGridColumnsFromModel(
data.node_info, subnode, this.field.get('mode'), data.columns
),
self = this;
// Set visibility of Add button
if (data.disabled || data.canAdd == false) {
$(gridBody).find("button.add").remove();
}
// Insert Delete Cell into Grid
if (data.disabled == false && data.canDelete) {
gridSchema.columns.unshift({
name: "pg-backform-delete", label: "",
cell: Backgrid.Extension.DeleteCell,
editable: false, cell_priority: -1
});
}
var collection = this.model.get(data.name);
// Initialize a new Grid instance
var grid = new Backgrid.Grid({
columns: gridSchema.columns,
collection: collection,
className: "backgrid table-bordered"
});
// Render subNode grid
subNodeGrid = grid.render().$el;
// Combine Edit and Delete Cell
if (data.canDelete && data.canEdit) {
$(subNodeGrid).find("th.pg-backform-delete").remove();
}
$dialog = gridBody.append(subNodeGrid);
// Add button callback
if (!(data.disabled || data.canAdd == false)) {
$dialog.find('button.add').first().click(function(e) {
e.preventDefault();
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 && collection){
var isEmpty = false;
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});
collection.add(m);
var idx = collection.indexOf(m);
newRow = grid.body.rows[idx].$el;
newRow.addClass("new");
return false;
});
}
return $dialog;
}
});
var SubNodeCollectionControl = Backform.SubNodeCollectionControl = Backform.Control.extend({
render: function() {
var field = _.defaults(this.field.toJSON(), this.defaults),
@@ -461,6 +722,24 @@
return this;
},
updateInvalid: function() {
var self = this;
var errorModel = this.model.errorModel;
if (!(errorModel instanceof Backbone.Model)) return this;
this.clearInvalid();
var attrArr = self.field.get('name').split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
error = self.keyPathAccessor(errorModel.toJSON(), path);
if (_.isEmpty(error)) return;
self.$el.addClass(Backform.errorClassName).append(
$("<div></div>").addClass('pgadmin-control-error-message col-xs-12 help-block').text(error)
);
},
showGridControl: function(data) {
var gridHeader = ["<div class='subnode-header'>",
" <label class='control-label col-sm-4'>" + data.label + "</label>" ,
@@ -499,11 +778,11 @@
});
}
var collections = this.model.get(data.name);
var collection = this.model.get(data.name);
// Initialize a new Grid instance
var grid = new Backgrid.Grid({
columns: gridSchema.columns,
collection: collections,
collection: collection,
className: "backgrid table-bordered"
});
@@ -522,7 +801,7 @@
$dialog.find('button.add').click(function(e) {
e.preventDefault();
grid.insertRow({});
newRow = $(grid.body.rows[collections.length - 1].$el);
newRow = $(grid.body.rows[collection.length - 1].$el);
newRow.attr("class", "new").click(function(e) {
$(this).attr("class", "");
});

View File

@@ -119,6 +119,254 @@
}
});
/**
Custom cell formatter for privileges.
*/
var PrivilegeCellFormatter = Backgrid.Extension.PrivilegeCellFormatter = function () {};
_.extend(PrivilegeCellFormatter.prototype, {
fromRaw: function (rawData, model) {
return rawData;
},
/* Convert string privileges to object privileges for manipulation.
E.g C*Tc ===> {"C":{"privilege":true,
"withGrantPrivilege":true},
"T":{"privilege":true,
"withGrantPrivilege":false},
"c":{"privilege":true,
"withGrantPrivilege":false}
}
*/
fromRawToObject: function (rawData, model) {
var objData = {};
var currentChar = "";
for (var i = 0, len = rawData.length; i < len; i++) {
if (rawData[i] == "*" && currentChar != ""){
if ( _.has(objData,currentChar)){
objData[currentChar]["withGrantPrivilege"] = true;
}
}else{
currentChar = rawData[i]
objData[currentChar] = {"privilege":true,
"withGrantPrivilege":false};
}
}
return objData;
},
toRaw: function (formattedData, model) {
return formattedData;
}
});
/**
Custom cell editor for editing privileges.
*/
var PrivilegeCellEditor = Backgrid.Extension.PrivilegeCellEditor = Backgrid.CellEditor.extend({
tagName: "div",
template: _.template(['<tr>',
'<td class="renderable"><label><input type="checkbox" name="<%- value %>" <%= privilege ? \'checked\' : "" %>><%- name %></label></td>',
'<td class="renderable"><label><input type="checkbox" name="<%- value %>_grant" <%= withGrantPrivilege ? \'checked\' : "" %>>WITH GRANT OPTION</label></td>',
'</tr>'].join(" "), null, {variable: null}),
initialize: function() {
Backgrid.CellEditor.prototype.initialize.apply(this, arguments);
this.elId = _.uniqueId('pgPriv_');
},
setPrivilegeOptions: function (privilegeOptions){
this.privilegeOptions = privilegeOptions;
},
render: function () {
this.$el.empty();
this.$el.attr('tabindex', '1');
this.$el.attr('id', this.elId);
this.$el.attr('privilegeseditor', '1');
var privilegeOptions = _.result(this, "privilegeOptions");
var model = this.model;
var selectedValues = this.formatter.fromRawToObject(model.get(this.column.get("name")), model),
tbl = $("<table></table>").appendTo(this.$el);
if (!_.isArray(privilegeOptions)) throw new TypeError("privilegeOptions must be an array");
self = this;
// For each privilege generate html template.
_.each(privilegeOptions, function (privilegeOption){
var templateData = {name: privilegeOption['name'],
value: privilegeOption['value'],
privilege : false,
withGrantPrivilege : false
};
if ( _.has(selectedValues,privilegeOption['value'])){
_.extend(templateData,{ privilege:selectedValues[privilegeOption['value']]["privilege"],
withGrantPrivilege:selectedValues[privilegeOption['value']]["withGrantPrivilege"]
});
}
var editorHtml = self.template(templateData);
tbl.append(editorHtml);
var $prvilegeGrantCheckbox = self.$el.find("[name='" + privilegeOption['value'] + "_grant']");
// Add event listeners on each privilege checkbox. And set initial state.
// Update model if user changes value.
$prvilegeGrantCheckbox.click(function(e) {
var addRemoveflag = $(this).is(':checked');
privilege = this.name;
self.updateModel(privilege, addRemoveflag);
});
var $prvilegeCheckbox = self.$el.find("[name='" + privilegeOption['value'] + "']");
if (!$prvilegeCheckbox.is(':checked')) {
$prvilegeGrantCheckbox.attr("disabled", true);
$prvilegeGrantCheckbox.attr("checked", false);
}
$prvilegeCheckbox.click(function(e) {
var addRemoveflag = $(this).is(':checked');
privilege = this.name;
if (addRemoveflag) {
$prvilegeGrantCheckbox.removeAttr("disabled");
} else {
$prvilegeGrantCheckbox.attr("disabled", true);
$prvilegeGrantCheckbox.attr("checked", false);
}
self.updateModel(privilege, addRemoveflag);
});
});
self.$el.find('input[type=checkbox]').blur(self.focusLost.bind(this)).first().focus();
self.delegateEvents();
return this;
},
updateModel: function(privilege, addRemoveflag){
// Update model with new privilege string. e.g. 'C*Tc'.
var self = this,
model = self.model,
column = self.column,
newVal = "",
withGrant = false,
privilegeConst = privilege[0];
if (privilege.length > 1){
withGrant = true;
}
oldValObj = self.formatter.fromRawToObject(model.get(self.column.get("name")), model);
if (addRemoveflag){
if (!withGrant){
oldValObj[privilegeConst] = {"privilege": true,
"withGrantPrivilege":false};
}else{
oldValObj[privilegeConst] = {"privilege": true,
"withGrantPrivilege":true}
}
}else{
if (!withGrant){
oldValObj[privilegeConst] = {"privilege": false,
"withGrantPrivilege":false};
}else{
oldValObj[privilegeConst] = {"privilege": true,
"withGrantPrivilege":false};
}
}
for (var i = 0, len = model.privileges.length; i < len; i++) {
if ( _.has(oldValObj, model.privileges[i])){
if(oldValObj[model.privileges[i]]["privilege"]){
newVal = newVal + model.privileges[i]
}
if(oldValObj[model.privileges[i]]["withGrantPrivilege"]){
newVal = newVal + "*"
}
}
}
model.set(column.get("name"), newVal);
},
focusLost :function(e) {
setTimeout(
function() {
var lostFocus = true;
if (document.activeElement) {
lostFocus = !(
$(document.activeElement).closest(
'div[privilegeseditor=1]'
).first().attr('id') == this.$el.attr('id')
);
}
if (lostFocus) {
this.model.trigger("backgrid:edited", this.model, this.column, new Backgrid.Command(e));
}
}.bind(this), 200);
}
});
var PrivilegeCell = Backgrid.Extension.PrivilegeCell = Backgrid.Cell.extend({
className: "edit-cell",
// All available privileges.
privilegeLabels: { "C": "CREATE",
"T": "TEMP",
"c": "CONNECT",
"a": "INSERT",
"r": "SELECT",
"w": "UPDATE",
"d": "DELETE",
"D": "TRUNCATE",
"x": "REFERENCES",
"t": "TRIGGER",
"U": "USAGE",
"X": "EXECUTE"
},
formatter: PrivilegeCellFormatter,
editor: PrivilegeCellEditor,
initialize: function(options) {
Backgrid.Cell.prototype.initialize.apply(this, arguments);
var privilegeOptions = [];
var privileges = this.model.privileges || [];
self = this;
// Generate array of privileges to be shown in editor.
_.each(privileges, function(privilege){
privilegeOptions.push({name:self.privilegeLabels[privilege],
value:privilege})
})
this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) {
if (column.get("name") == this.column.get("name"))
// Set available privilege options in editor.
editor.setPrivilegeOptions(privilegeOptions);
});
},
render: function(){
this.$el.empty();
var model = this.model;
this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model));
this.delegateEvents();
if (this.grabFocus)
this.$el.focus();
return this;
},
exitEditMode: function() {
Backgrid.Cell.prototype.exitEditMode.apply(this, arguments);
this.render();
}
});
var ObjectCell = Backgrid.Extension.ObjectCell = Backgrid.Cell.extend({
editorOptionDefaults: {
schema: []