Added support for schema level restriction. Fixes #5583

Allow user to edit the connection properties when the database server
is already connected.
This commit is contained in:
Nikhil Mohite 2020-06-30 19:15:23 +05:30 committed by Akshay Joshi
parent 4c05287677
commit c873218c32
15 changed files with 492 additions and 214 deletions

View File

@ -97,6 +97,19 @@ Follow these steps to add additional parameter value definitions; to discard a
parameter, click the trash icon to the left of the row and confirm deletion in parameter, click the trash icon to the left of the row and confirm deletion in
the *Delete Row* popup. the *Delete Row* popup.
Click the *Advanced* tab to continue.
.. image:: images/database_advanced.png
:alt: Database dialog advanced tab
:align: center
Use the *Advanced* tab to set advanced parameters for the database.
* Use *Schema restriction* field to provide a SQL restriction that will be used
against the pg_namespace table to limit the schemas that you see.
For example, you might enter: *public* so that only *public* are shown in
the pgAdmin browser.Separate entries with a comma or tab as you type.
Click the *SQL* tab to continue. Click the *SQL* tab to continue.
Your entries in the *Database* dialog generate a SQL command (see an example Your entries in the *Database* dialog generate a SQL command (see an example

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features New features
************ ************
| `Issue #5583 <https://redmine.postgresql.org/issues/5583>`_ - Added support for schema level restriction.
Housekeeping Housekeeping
************ ************

View File

@ -0,0 +1,32 @@
"""empty message
Revision ID: 84700139beb0
Revises: d39482714a2e
Create Date: 2020-06-24 15:53:56.489518
"""
from pgadmin.model import db
# revision identifiers, used by Alembic.
revision = '84700139beb0'
down_revision = 'd39482714a2e'
branch_labels = None
depends_on = None
def upgrade():
db.engine.execute("""
CREATE TABLE "database" (
"id" INTEGER NOT NULL,
"schema_res" TEXT,
"server" INTEGER NOT NULL,
PRIMARY KEY("id","server"),
FOREIGN KEY("server") REFERENCES "server"("id")
);
""")
def downgrade():
pass

View File

@ -536,7 +536,7 @@ class ServerNode(PGChildNodeView):
if connected: if connected:
for arg in ( for arg in (
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode', 'hostaddr', 'db', 'sslmode',
'role', 'service' 'role', 'service'
): ):
if arg in data: if arg in data:
@ -1016,6 +1016,7 @@ class ServerNode(PGChildNodeView):
# Connect the Server # Connect the Server
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
manager.update(server)
conn = manager.connection() conn = manager.connection()
# Get enc key # Get enc key

View File

@ -32,7 +32,7 @@ from pgadmin.utils.driver import get_driver
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.model import Server from pgadmin.model import db, Server, Database
class DatabaseModule(CollectionNodeModule): class DatabaseModule(CollectionNodeModule):
@ -423,6 +423,11 @@ class DatabaseView(PGChildNodeView):
) )
status, res1 = self.conn.execute_dict(SQL) status, res1 = self.conn.execute_dict(SQL)
database = Database.query.filter_by(id=did, server=sid).first()
if database:
result['schema_res'] = database.schema_res.split(
',') if database.schema_res else []
if not status: if not status:
return internal_server_error(errormsg=res1) return internal_server_error(errormsg=res1)
@ -611,6 +616,11 @@ class DatabaseView(PGChildNodeView):
return internal_server_error(errormsg=res) return internal_server_error(errormsg=res)
response = res['rows'][0] response = res['rows'][0]
# Add database entry into database table with schema_restrictions.
database = Database(id=response['did'], server=sid,
schema_res=','.join(data['schema_res']))
db.session.add(database)
db.session.commit()
return jsonify( return jsonify(
node=self.blueprint.generate_browser_node( node=self.blueprint.generate_browser_node(
@ -627,53 +637,27 @@ class DatabaseView(PGChildNodeView):
) )
) )
@check_precondition(action='update') @staticmethod
def update(self, gid, sid, did): def _update_db_schema_res(data, did, sid):
"""Update the database.""" database = Database.query.filter_by(id=did, server=sid).first()
if 'schema_res' in data:
if database:
data['schema_res'] = ','.join(data['schema_res'])
setattr(database, 'schema_res', data['schema_res'])
else:
database_obj = Database(id=did, server=sid,
schema_res=','.join(
data['schema_res']))
db.session.add(database_obj)
data = request.form if request.form else json.loads( def _check_rename_db_or_change_table_space(self, data, conn, all_ids):
request.data, encoding='utf-8'
)
# Generic connection for offline updates
conn = self.manager.connection(conn_id='db_offline_update')
status, errmsg = conn.connect()
if not status:
current_app.logger.error(
"Could not create database connection for offline updates\n"
"Err: {0}".format(errmsg)
)
return internal_server_error(errmsg)
if did is not None:
# Fetch the name of database for comparison
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
did=did, conn=self.conn, last_system_oid=0
)
)
if not status:
return internal_server_error(errormsg=rset)
if len(rset['rows']) == 0:
return gone(
_('Could not find the database on the server.')
)
data['old_name'] = (rset['rows'][0])['name']
if 'name' not in data:
data['name'] = data['old_name']
# Release any existing connection from connection manager
# to perform offline operation
self.manager.release(did=did)
for action in ["rename_database", "tablespace"]: for action in ["rename_database", "tablespace"]:
SQL = self.get_offline_sql(gid, sid, data, did, action) sql = self.get_offline_sql(all_ids['gid'], all_ids['sid'], data,
SQL = SQL.strip('\n').strip(' ') all_ids['did'], action)
if SQL and SQL != "": sql = sql.strip('\n').strip(' ')
status, msg = conn.execute_scalar(SQL) if sql and sql != "":
status, msg = conn.execute_scalar(sql)
if not status: if not status:
# In case of error from server while rename it, # In case of error from server while rename it,
# reconnect to the database with old name again. # reconnect to the database with old name again.
@ -684,13 +668,38 @@ class DatabaseView(PGChildNodeView):
if not status: if not status:
current_app.logger.error( current_app.logger.error(
'Could not reconnected to database(#{0}).\n' 'Could not reconnected to database(#{0}).\n'
'Error: {1}'.format(did, errmsg) 'Error: {1}'.format(all_ids['did'], errmsg)
) )
return internal_server_error(errormsg=msg) return True, msg
QueryHistory.update_history_dbname( QueryHistory.update_history_dbname(
current_user.id, sid, data['old_name'], data['name']) current_user.id, all_ids['sid'], data['old_name'],
# Make connection for database again data['name'])
return False, ''
def _fetch_db_details(self, data, did):
if did is not None:
# Fetch the name of database for comparison
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
did=did, conn=self.conn, last_system_oid=0
)
)
if not status:
return True, rset
if len(rset['rows']) == 0:
return gone(
_('Could not find the database on the server.')
)
data['old_name'] = (rset['rows'][0])['name']
if 'name' not in data:
data['name'] = data['old_name']
return False, ''
def _reconnect_connect_db(self, data, did):
if self._db['datallowconn']: if self._db['datallowconn']:
self.conn = self.manager.connection( self.conn = self.manager.connection(
database=data['name'], auto_reconnect=True database=data['name'], auto_reconnect=True
@ -702,12 +711,70 @@ class DatabaseView(PGChildNodeView):
'Could not connected to database(#{0}).\n' 'Could not connected to database(#{0}).\n'
'Error: {1}'.format(did, errmsg) 'Error: {1}'.format(did, errmsg)
) )
return internal_server_error(errmsg) return True, errmsg
return False, ''
SQL = self.get_online_sql(gid, sid, data, did) def _commit_db_changes(self, res, can_drop):
SQL = SQL.strip('\n').strip(' ') if self.manager.db == res['name']:
if SQL and SQL != "": can_drop = False
status, msg = self.conn.execute_scalar(SQL)
try:
db.session.commit()
except Exception as e:
current_app.logger.exception(e)
return True, e.message, False
return False, '', can_drop
def _get_data_from_request(self):
return request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
@check_precondition(action='update')
def update(self, gid, sid, did):
"""Update the database."""
data = self._get_data_from_request()
# Update schema restriction in db object.
DatabaseView._update_db_schema_res(data, did, sid)
# Generic connection for offline updates
conn = self.manager.connection(conn_id='db_offline_update')
status, errmsg = conn.connect()
if not status:
current_app.logger.error(
"Could not create database connection for offline updates\n"
"Err: {0}".format(errmsg)
)
return internal_server_error(errmsg)
fetching_error, err_msg = self._fetch_db_details(data, did)
if fetching_error:
return internal_server_error(errormsg=err_msg)
# Release any existing connection from connection manager
# to perform offline operation
self.manager.release(did=did)
all_ids = {
'gid': gid,
'sid': sid,
'did': did
}
is_error, errmsg = self._check_rename_db_or_change_table_space(data,
conn,
all_ids)
if is_error:
return internal_server_error(errmsg)
# Make connection for database again
connection_error, errmsg = self._reconnect_connect_db(data, did)
if connection_error:
return internal_server_error(errmsg)
sql = self.get_online_sql(gid, sid, data, did)
sql = sql.strip('\n').strip(' ')
if sql and sql != "":
status, msg = self.conn.execute_scalar(sql)
if not status: if not status:
return internal_server_error(errormsg=msg) return internal_server_error(errormsg=msg)
@ -733,9 +800,15 @@ class DatabaseView(PGChildNodeView):
res = rset['rows'][0] res = rset['rows'][0]
can_drop = can_dis_conn = True can_drop = True
if self.manager.db == res['name']: error, errmsg, is_can_drop = self._commit_db_changes(res, can_drop)
can_drop = can_dis_conn = False if error:
return make_json_response(
success=0,
errormsg=errmsg
)
can_drop = can_dis_conn = is_can_drop
return jsonify( return jsonify(
node=self.blueprint.generate_browser_node( node=self.blueprint.generate_browser_node(

View File

@ -24,6 +24,7 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error, \
make_response as ajax_response, gone, bad_request make_response as ajax_response, gone, bad_request
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.model import Database
""" """
This module is responsible for generating two nodes This module is responsible for generating two nodes
@ -384,10 +385,21 @@ class SchemaView(PGChildNodeView):
Returns: Returns:
JSON of available schema nodes JSON of available schema nodes
""" """
database = Database.query.filter_by(id=did, server=sid).first()
param = None
if database:
schema_restrictions = database.schema_res
if schema_restrictions:
schema_res = ",".join(
["'%s'"] * len(schema_restrictions.split(',')))
param = schema_res % (tuple(schema_restrictions.split(',')))
SQL = render_template( SQL = render_template(
"/".join([self.template_path, 'sql/properties.sql']), "/".join([self.template_path, 'sql/properties.sql']),
_=gettext, _=gettext,
show_sysobj=self.blueprint.show_system_objects show_sysobj=self.blueprint.show_system_objects,
schema_restrictions=param
) )
status, res = self.conn.execute_dict(SQL) status, res = self.conn.execute_dict(SQL)
@ -413,11 +425,22 @@ class SchemaView(PGChildNodeView):
JSON of available schema child nodes JSON of available schema child nodes
""" """
res = [] res = []
database = Database.query.filter_by(id=did, server=sid).first()
param = None
if database:
schema_restrictions = database.schema_res
if schema_restrictions:
schema_res = ",".join(
["'%s'"] * len(schema_restrictions.split(',')))
param = schema_res % (tuple(schema_restrictions.split(',')))
SQL = render_template( SQL = render_template(
"/".join([self.template_path, 'sql/nodes.sql']), "/".join([self.template_path, 'sql/nodes.sql']),
show_sysobj=self.blueprint.show_system_objects, show_sysobj=self.blueprint.show_system_objects,
_=gettext, _=gettext,
scid=scid scid=scid,
schema_restrictions=param
) )
status, rset = self.conn.execute_2darray(SQL) status, rset = self.conn.execute_2darray(SQL)
@ -428,10 +451,9 @@ class SchemaView(PGChildNodeView):
if scid is not None: if scid is not None:
if len(rset['rows']) == 0: if len(rset['rows']) == 0:
return gone(gettext(""" return gone(gettext(
Could not find the schema in the database. """Could not find the schema in the database.
It may have been removed by another user. It may have been removed by another user."""))
"""))
row = rset['rows'][0] row = rset['rows'][0]
return make_json_response( return make_json_response(
data=self.blueprint.generate_browser_node( data=self.blueprint.generate_browser_node(
@ -896,10 +918,9 @@ It may have been removed by another user.
return internal_server_error(errormsg=res) return internal_server_error(errormsg=res)
if len(res['rows']) == 0: if len(res['rows']) == 0:
return gone(gettext(""" return gone(gettext(
Could not find the schema in the database. """Could not find the schema in the database.
It may have been removed by another user. It may have been removed by another user."""))
"""))
data = res['rows'][0] data = res['rows'][0]
backend_support_keywords = kwargs.copy() backend_support_keywords = kwargs.copy()

View File

@ -17,4 +17,10 @@ WHERE
NOT ( NOT (
{{ CATALOGS.LIST('nsp') }} {{ CATALOGS.LIST('nsp') }}
) )
{% if schema_restrictions %}
AND
nsp.nspname in ({{schema_restrictions}})
{% endif %}
ORDER BY nspname; ORDER BY nspname;

View File

@ -50,4 +50,8 @@ WHERE
NOT ( NOT (
{{ CATALOGS.LIST('nsp') }} {{ CATALOGS.LIST('nsp') }}
) )
{% if schema_restrictions %}
AND
nsp.nspname in ({{schema_restrictions}})
{% endif %}
ORDER BY 1, nspname; ORDER BY 1, nspname;

View File

@ -46,6 +46,7 @@ define('pgadmin.node.database', [
node_image: function() { node_image: function() {
return 'pg-icon-database'; return 'pg-icon-database';
}, },
width: '700px',
Init: function() { Init: function() {
/* Avoid mulitple registration of menus */ /* Avoid mulitple registration of menus */
if (this.initialized) if (this.initialized)
@ -297,6 +298,7 @@ define('pgadmin.node.database', [
defseqacl: [], defseqacl: [],
is_template: false, is_template: false,
deftypeacl: [], deftypeacl: [],
schema_res:'',
}, },
// Default values! // Default values!
@ -310,150 +312,189 @@ define('pgadmin.node.database', [
pgBrowser.Node.Model.prototype.initialize.apply(this, arguments); pgBrowser.Node.Model.prototype.initialize.apply(this, arguments);
}, },
schema: [{ schema: [
id: 'name', label: gettext('Database'), cell: 'string', {
editable: false, type: 'text', id: 'name', label: gettext('Database'), cell: 'string',
},{ editable: false, type: 'text',
id: 'did', label: gettext('OID'), cell: 'string', mode: ['properties'], },{
editable: false, type: 'text', id: 'did', label: gettext('OID'), cell: 'string', mode: ['properties'],
},{ editable: false, type: 'text',
id: 'datowner', label: gettext('Owner'), },{
editable: false, type: 'text', node: 'role', id: 'datowner', label: gettext('Owner'),
control: Backform.NodeListByNameControl, select2: { allowClear: false }, editable: false, type: 'text', node: 'role',
},{ control: Backform.NodeListByNameControl, select2: { allowClear: false },
id: 'acl', label: gettext('Privileges'), type: 'text', },{
group: gettext('Security'), mode: ['properties'], id: 'acl', label: gettext('Privileges'), type: 'text',
},{ group: gettext('Security'), mode: ['properties'],
id: 'tblacl', label: gettext('Default TABLE privileges'), type: 'text', },{
group: gettext('Security'), mode: ['properties'], id: 'tblacl', label: gettext('Default TABLE privileges'), type: 'text',
},{ group: gettext('Security'), mode: ['properties'],
id: 'seqacl', label: gettext('Default SEQUENCE privileges'), type: 'text', },{
group: gettext('Security'), mode: ['properties'], id: 'seqacl', label: gettext('Default SEQUENCE privileges'), type: 'text',
},{ group: gettext('Security'), mode: ['properties'],
id: 'funcacl', label: gettext('Default FUNCTION privileges'), type: 'text', },{
group: gettext('Security'), mode: ['properties'], id: 'funcacl', label: gettext('Default FUNCTION privileges'), type: 'text',
},{ group: gettext('Security'), mode: ['properties'],
id: 'typeacl', label: gettext('Default TYPE privileges'), type: 'text', },{
group: gettext('Security'), mode: ['properties'], min_version: 90200, id: 'typeacl', label: gettext('Default TYPE privileges'), type: 'text',
},{ group: gettext('Security'), mode: ['properties'], min_version: 90200,
id: 'is_sys_obj', label: gettext('System database?'), },{
cell:'boolean', type: 'switch', mode: ['properties'], id: 'is_sys_obj', label: gettext('System database?'),
},{ cell:'boolean', type: 'switch', mode: ['properties'],
id: 'comments', label: gettext('Comment'), },{
editable: false, type: 'multiline', id: 'comments', label: gettext('Comment'),
},{ editable: false, type: 'multiline',
id: 'encoding', label: gettext('Encoding'), },{
editable: false, type: 'text', group: gettext('Definition'), id: 'encoding', label: gettext('Encoding'),
readonly: function(m) { return !m.isNew(); }, url: 'get_encodings', editable: false, type: 'text', group: gettext('Definition'),
control: 'node-ajax-options', cache_level: 'server', readonly: function(m) { return !m.isNew(); }, url: 'get_encodings',
},{ control: 'node-ajax-options', cache_level: 'server',
id: 'template', label: gettext('Template'), },{
editable: false, type: 'text', group: gettext('Definition'), id: 'template', label: gettext('Template'),
readonly: function(m) { return !m.isNew(); }, editable: false, type: 'text', group: gettext('Definition'),
control: 'node-list-by-name', url: 'get_databases', cache_level: 'server', readonly: function(m) { return !m.isNew(); },
select2: { allowClear: false }, mode: ['create'], control: 'node-list-by-name', url: 'get_databases', cache_level: 'server',
transform: function(data, cell) { select2: { allowClear: false }, mode: ['create'],
var res = [], transform: function(data, cell) {
control = cell || this, var res = [],
label = control.model.get('name'); control = cell || this,
label = control.model.get('name');
if (!control.model.isNew()) { if (!control.model.isNew()) {
res.push({label: label, value: label}); res.push({label: label, value: label});
}
else {
if (data && _.isArray(data)) {
_.each(data, function(d) {
res.push({label: d, value: d,
image: 'pg-icon-database'});
});
} }
} else {
return res; if (data && _.isArray(data)) {
}, _.each(data, function(d) {
},{ res.push({label: d, value: d,
id: 'spcname', label: gettext('Tablespace'), image: 'pg-icon-database'});
editable: false, type: 'text', group: gettext('Definition'), });
control: 'node-list-by-name', node: 'tablespace', }
select2: { allowClear: false }, }
filter: function(m) { return res;
return (m.label != 'pg_global'); },
},
},{
id: 'datcollate', label: gettext('Collation'),
editable: false, type: 'text', group: gettext('Definition'),
readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
control: 'node-ajax-options', cache_level: 'server',
},{
id: 'datctype', label: gettext('Character type'),
editable: false, type: 'text', group: gettext('Definition'),
readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
control: 'node-ajax-options', cache_level: 'server',
},{
id: 'datconnlimit', label: gettext('Connection limit'),
editable: false, type: 'int', group: gettext('Definition'), min: -1,
},{
id: 'is_template', label: gettext('Template?'),
editable: false, type: 'switch', group: gettext('Definition'),
readonly: true, mode: ['properties', 'edit'],
},{
id: 'datallowconn', label: gettext('Allow connections?'),
editable: false, type: 'switch', group: gettext('Definition'),
mode: ['properties'],
},{
id: 'datacl', label: gettext('Privileges'), type: 'collection',
model: pgBrowser.Node.PrivilegeRoleModel.extend({
privileges: ['C', 'T', 'c'],
}), uniqueCol : ['grantee', 'grantor'], editable: false,
group: gettext('Security'), mode: ['edit', 'create'],
canAdd: true, canDelete: true, control: 'unique-col-collection',
},{
id: 'variables', label: '', type: 'collection',
model: pgBrowser.Node.VariableModel.extend({keys:['name', 'role']}), editable: false,
group: gettext('Parameters'), mode: ['edit', 'create'],
canAdd: true, canEdit: false, canDelete: true, hasRole: true,
control: Backform.VariableCollectionControl, node: 'role',
},{
id: 'seclabels', label: gettext('Security labels'),
model: pgBrowser.SecLabelModel,
editable: false, type: 'collection', canEdit: false,
group: gettext('Security'), canDelete: true,
mode: ['edit', 'create'], canAdd: true,
control: 'unique-col-collection', uniqueCol : ['provider'],
min_version: 90200,
},{
type: 'nested', control: 'tab', group: gettext('Default Privileges'),
mode: ['edit'],
schema:[{
id: 'deftblacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
{privileges: ['a', 'r', 'w', 'd', 'D', 'x', 't']}), label: '',
editable: false, type: 'collection', group: gettext('Tables'),
mode: ['edit', 'create'], control: 'unique-col-collection',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
},{ },{
id: 'defseqacl', model: pgBrowser.Node.PrivilegeRoleModel.extend( id: 'spcname', label: gettext('Tablespace'),
{privileges: ['r', 'w', 'U']}), label: '', editable: false, type: 'text', group: gettext('Definition'),
editable: false, type: 'collection', group: gettext('Sequences'), control: 'node-list-by-name', node: 'tablespace',
mode: ['edit', 'create'], control: 'unique-col-collection', select2: { allowClear: false },
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'], filter: function(m) {
return (m.label != 'pg_global');
},
},{ },{
id: 'deffuncacl', model: pgBrowser.Node.PrivilegeRoleModel.extend( id: 'datcollate', label: gettext('Collation'),
{privileges: ['X']}), label: '', editable: false, type: 'text', group: gettext('Definition'),
editable: false, type: 'collection', group: gettext('Functions'), readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
mode: ['edit', 'create'], control: 'unique-col-collection', control: 'node-ajax-options', cache_level: 'server',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
},{ },{
id: 'deftypeacl', model: pgBrowser.Node.PrivilegeRoleModel.extend( id: 'datctype', label: gettext('Character type'),
{privileges: ['U']}), label: '', editable: false, type: 'text', group: gettext('Definition'),
editable: false, type: 'collection', group: 'deftypesacl_group', readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
mode: ['edit', 'create'], control: 'unique-col-collection', control: 'node-ajax-options', cache_level: 'server',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'], },{
id: 'datconnlimit', label: gettext('Connection limit'),
editable: false, type: 'int', group: gettext('Definition'), min: -1,
},{
id: 'is_template', label: gettext('Template?'),
editable: false, type: 'switch', group: gettext('Definition'),
readonly: true, mode: ['properties', 'edit'],
},{
id: 'datallowconn', label: gettext('Allow connections?'),
editable: false, type: 'switch', group: gettext('Definition'),
mode: ['properties'],
},{
id: 'datacl', label: gettext('Privileges'), type: 'collection',
model: pgBrowser.Node.PrivilegeRoleModel.extend({
privileges: ['C', 'T', 'c'],
}), uniqueCol : ['grantee', 'grantor'], editable: false,
group: gettext('Security'), mode: ['edit', 'create'],
canAdd: true, canDelete: true, control: 'unique-col-collection',
},{
id: 'variables', label: '', type: 'collection',
model: pgBrowser.Node.VariableModel.extend({keys:['name', 'role']}), editable: false,
group: gettext('Parameters'), mode: ['edit', 'create'],
canAdd: true, canEdit: false, canDelete: true, hasRole: true,
control: Backform.VariableCollectionControl, node: 'role',
},{
id: 'seclabels', label: gettext('Security labels'),
model: pgBrowser.SecLabelModel,
editable: false, type: 'collection', canEdit: false,
group: gettext('Security'), canDelete: true,
mode: ['edit', 'create'], canAdd: true,
control: 'unique-col-collection', uniqueCol : ['provider'],
min_version: 90200, min_version: 90200,
},{ },{
id: 'deftypesacl_group', type: 'group', label: gettext('Types'), type: 'nested', control: 'tab', group: gettext('Default Privileges'),
mode: ['edit', 'create'], min_version: 90200, mode: ['edit'],
schema:[{
id: 'deftblacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
{privileges: ['a', 'r', 'w', 'd', 'D', 'x', 't']}), label: '',
editable: false, type: 'collection', group: gettext('Tables'),
mode: ['edit', 'create'], control: 'unique-col-collection',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
},{
id: 'defseqacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
{privileges: ['r', 'w', 'U']}), label: '',
editable: false, type: 'collection', group: gettext('Sequences'),
mode: ['edit', 'create'], control: 'unique-col-collection',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
},{
id: 'deffuncacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
{privileges: ['X']}), label: '',
editable: false, type: 'collection', group: gettext('Functions'),
mode: ['edit', 'create'], control: 'unique-col-collection',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
},{
id: 'deftypeacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
{privileges: ['U']}), label: '',
editable: false, type: 'collection', group: 'deftypesacl_group',
mode: ['edit', 'create'], control: 'unique-col-collection',
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
min_version: 90200,
},{
id: 'deftypesacl_group', type: 'group', label: gettext('Types'),
mode: ['edit', 'create'], min_version: 90200,
},
],
},{
type: 'collection', group: gettext('Advanced'),
},
{
id: 'schema_res', label: gettext('Schema restriction'),
type: 'select2', group: gettext('Advanced'),
mode: ['properties', 'edit', 'create'],
select2: {
multiple: true, allowClear: false, tags: true,
tokenSeparators: [','], first_empty: false,
selectOnClose: true, emptyOptions: true,
},
control: Backform.Select2Control.extend({
onChange: function() {
Backform.Select2Control.prototype.onChange.apply(this, arguments);
if (!this.model || !(
this.model.changed &&
this.model.get('oid') !== undefined
)) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.schema_res != this.model.changed.schema_res)
{
this.model.inform_text = gettext(
'Please refresh the Schemas node to make changes to the schema restriction take effect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},
{
id: 'note', label: gettext('Note: Changes to the schema restriction will require the Schemas node in the browser to be refreshed before they will be shown.'),
group: gettext('Advanced'), type: 'help',
mode: ['edit', 'create'],
}, },
],
},
], ],
validate: function() { validate: function() {
var name = this.get('name'); var name = this.get('name');

View File

@ -41,7 +41,8 @@ class DatabasesUpdateTestCase(BaseTestGenerator):
try: try:
data = { data = {
"comments": "This is db update comment", "comments": "This is db update comment",
"id": self.db_id "id": self.db_id,
"schema_res": ["public"]
} }
response = self.tester.put( response = self.tester.put(
self.url + str(utils.SERVER_GROUP) + '/' + str( self.url + str(utils.SERVER_GROUP) + '/' + str(

View File

@ -73,7 +73,8 @@ def get_db_data(db_owner):
"name": "db_add_%s" % str(uuid.uuid4())[1: 8], "name": "db_add_%s" % str(uuid.uuid4())[1: 8],
"privileges": [], "privileges": [],
"securities": [], "securities": [],
"variables": [] "variables": [],
"schema_res": ["public", "sample"]
} }
return data return data

View File

@ -39,7 +39,6 @@ define('pgadmin.node.server', [
}], }],
validate: function() { validate: function() {
this.errorModel.clear(); this.errorModel.clear();
if (_.isUndefined(this.get('label')) || if (_.isUndefined(this.get('label')) ||
_.isNull(this.get('label')) || _.isNull(this.get('label')) ||
String(this.get('label')).replace(/^\s+|\s+$/g, '') == '') { String(this.get('label')).replace(/^\s+|\s+$/g, '') == '') {
@ -66,7 +65,6 @@ define('pgadmin.node.server', [
return d && d.connected; return d && d.connected;
}, },
Init: function() { Init: function() {
/* Avoid multiple registration of same menus */ /* Avoid multiple registration of same menus */
if (this.initialized) if (this.initialized)
return; return;
@ -785,16 +783,70 @@ define('pgadmin.node.server', [
mode: ['properties', 'edit', 'create'], mode: ['properties', 'edit', 'create'],
},{ },{
id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', mode: ['properties', 'edit', 'create'],
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.host != this.model.changed.host && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{ },{
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', min: 1, max: 65535, mode: ['properties', 'edit', 'create'], min: 1, max: 65535,
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.port != this.model.changed.port && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{ },{
id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'), id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
},{ },{
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', mode: ['properties', 'edit', 'create'],
control: Backform.InputControl.extend({
onChange: function() {
Backform.InputControl.prototype.onChange.apply(this, arguments);
if (!this.model || !this.model.changed) {
this.model.inform_text = undefined;
return;
}
if(this.model.origSessAttrs.username != this.model.changed.username && !this.model.isNew() && this.model.get('connected'))
{
this.model.inform_text = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
} else {
this.model.inform_text = undefined;
}
},
}),
},{ },{
id: 'password', label: gettext('Password'), type: 'password', maxlength: '2000', id: 'password', label: gettext('Password'), type: 'password', maxlength: '2000',
group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'], group: gettext('Connection'), control: 'input', mode: ['create'], deps: ['connect_now'],

View File

@ -1339,6 +1339,21 @@ define('pgadmin.browser.node', [
} }
}.bind(panel), }.bind(panel),
informBeforeAttributeChange = function(ok_callback) {
var j = this.$container.find('.obj_properties').first();
view = j && j.data('obj-view');
if (view && view.model && !_.isUndefined(view.model.inform_text) && !_.isNull(view.model.inform_text)) {
Alertify.alert(
gettext('Warning'),
gettext(view.model.inform_text)
);
}
ok_callback();
return true;
}.bind(panel),
onSave = function(view, saveBtn) { onSave = function(view, saveBtn) {
var m = view.model, var m = view.model,
d = m.toJSON(true), d = m.toJSON(true),
@ -1535,9 +1550,11 @@ define('pgadmin.browser.node', [
warnBeforeAttributeChange.call( warnBeforeAttributeChange.call(
panel, panel,
function() { function() {
setTimeout(function() { informBeforeAttributeChange.call(panel, function(){
onSave.call(this, view, btn); setTimeout(function() {
}, 0); onSave.call(this, view, btn);
}, 0);
});
} }
); );
}); });

View File

@ -290,3 +290,18 @@ class QueryHistoryModel(db.Model):
dbname = db.Column(db.String(), nullable=False, primary_key=True) dbname = db.Column(db.String(), nullable=False, primary_key=True)
query_info = db.Column(db.String(), nullable=False) query_info = db.Column(db.String(), nullable=False)
last_updated_flag = db.Column(db.String(), nullable=False) last_updated_flag = db.Column(db.String(), nullable=False)
class Database(db.Model):
"""
Define a Database.
"""
__tablename__ = 'database'
id = db.Column(db.Integer, primary_key=True)
schema_res = db.Column(db.String(256), nullable=True)
server = db.Column(
db.Integer,
db.ForeignKey('server.id'),
nullable=False,
primary_key=True
)