Allow user to change the database connection from an open query tool tab. Fixes #3794

This commit is contained in:
Nikhil Mohite
2020-10-01 13:29:00 +05:30
committed by Akshay Joshi
parent 228d4bb321
commit be7bb81a19
30 changed files with 2394 additions and 89 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -300,3 +300,24 @@ transaction status by clicking on the status icon in the Query Tool:
.. image:: images/query_tool_connection_status.png .. image:: images/query_tool_connection_status.png
:alt: Query tool connection and transaction statuses :alt: Query tool connection and transaction statuses
:align: center :align: center
Change connection
*****************
User can connect to another server or database from existing open session of query tool.
* Click on the connection link next to connection status.
* Now click on the *<New Connection>* option from the dropdown.
.. image:: images/new_connection_options.png
:alt: Query tool connection options
:align: center
* Now select server, database, user, and role to connect and click OK.
.. image:: images/new_connection_dialog.png
:alt: Query tool connection dialog
:align: center
* A newly created connection will now get listed in the options.
* To connect, select the newly created connection from the dropdown list.

View File

@@ -10,6 +10,7 @@ New features
************ ************
| `Issue #1402 <https://redmine.postgresql.org/issues/1402>`_ - Added Macro support. | `Issue #1402 <https://redmine.postgresql.org/issues/1402>`_ - Added Macro support.
| `Issue #3794 <https://redmine.postgresql.org/issues/3794>`_ - Allow user to change the database connection from an open query tool tab.
| `Issue #5200 <https://redmine.postgresql.org/issues/5200>`_ - Added support to ignore the owner while comparing objects in the Schema Diff tool | `Issue #5200 <https://redmine.postgresql.org/issues/5200>`_ - Added support to ignore the owner while comparing objects in the Schema Diff tool
Housekeeping Housekeeping

View File

@@ -1247,7 +1247,7 @@ class ServerNode(PGChildNodeView):
} }
) )
def connect(self, gid, sid): def connect(self, gid, sid, user_name=None):
""" """
Connect the Server and return the connection object. Connect the Server and return the connection object.
Verification Process before Connection: Verification Process before Connection:
@@ -1368,7 +1368,8 @@ class ServerNode(PGChildNodeView):
# not provided, or password has not been saved earlier. # not provided, or password has not been saved earlier.
if prompt_password or prompt_tunnel_password: if prompt_password or prompt_tunnel_password:
return self.get_response_for_password(server, 428, prompt_password, return self.get_response_for_password(server, 428, prompt_password,
prompt_tunnel_password) prompt_tunnel_password,
user=user_name)
status = True status = True
try: try:
@@ -1802,7 +1803,8 @@ class ServerNode(PGChildNodeView):
return internal_server_error(errormsg=str(e)) return internal_server_error(errormsg=str(e))
def get_response_for_password(self, server, status, prompt_password=False, def get_response_for_password(self, server, status, prompt_password=False,
prompt_tunnel_password=False, errmsg=None): prompt_tunnel_password=False, errmsg=None,
user=None):
if server.use_ssh_tunnel: if server.use_ssh_tunnel:
return make_json_response( return make_json_response(
@@ -1829,7 +1831,7 @@ class ServerNode(PGChildNodeView):
result=render_template( result=render_template(
'servers/password.html', 'servers/password.html',
server_label=server.name, server_label=server.name,
username=server.username, username=user if user else server.username,
errmsg=errmsg, errmsg=errmsg,
service=server.service, service=server.service,
_=gettext, _=gettext,

View File

@@ -152,3 +152,38 @@ def delete_role(connection, role_names):
exception = "Error while deleting role: %s: line:%s %s" % ( exception = "Error while deleting role: %s: line:%s %s" % (
file_name, sys.exc_traceback.tb_lineno, exception) file_name, sys.exc_traceback.tb_lineno, exception)
print(exception, file=sys.stderr) print(exception, file=sys.stderr)
def create_role_with_password(server, role_name, role_password):
"""
This function create the role.
:param server:
:param role_name:
:param role_password:
:return:
"""
try:
connection = utils.get_db_connection(server['db'],
server['username'],
server['db_password'],
server['host'],
server['port'],
server['sslmode'])
pg_cursor = connection.cursor()
pg_cursor.execute(
"CREATE ROLE %s LOGIN PASSWORD '%s'" % (role_name, role_password))
connection.commit()
# Get 'oid' from newly created tablespace
pg_cursor.execute(
"SELECT pr.oid from pg_catalog.pg_roles pr WHERE pr.rolname='%s'" %
role_name)
oid = pg_cursor.fetchone()
role_id = ''
if oid:
role_id = oid[0]
connection.close()
return role_id
except Exception as exception:
exception = "Error while deleting role: %s: line:%s %s" % (
file_name, sys.exc_traceback.tb_lineno, exception)
print(exception, file=sys.stderr)

View File

@@ -95,6 +95,15 @@ class ServerGroup(db.Model):
name = db.Column(db.String(128), nullable=False) name = db.Column(db.String(128), nullable=False)
__table_args__ = (db.UniqueConstraint('user_id', 'name'),) __table_args__ = (db.UniqueConstraint('user_id', 'name'),)
@property
def serialize(self):
"""Return object data in easily serializable format"""
return {
'id': self.id,
'user_id': self.user_id,
'name': self.name,
}
class Server(db.Model): class Server(db.Model):
"""Define a registered Postgres server""" """Define a registered Postgres server"""
@@ -176,6 +185,44 @@ class Server(db.Model):
tunnel_password = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True)
shared = db.Column(db.Boolean(), nullable=False) shared = db.Column(db.Boolean(), nullable=False)
@property
def serialize(self):
"""Return object data in easily serializable format"""
return {
"id": self.id,
"user_id": self.user_id,
"servergroup_id": self.servergroup_id,
"name": self.name,
"host": self.host,
"hostaddr": self.hostaddr,
"port": self.port,
"maintenance_db": self.maintenance_db,
"username": self.username,
"password": self.password,
"save_password": self.save_password,
"role": self.role,
"ssl_mode": self.ssl_mode,
"comment": self.comment,
"discovery_id": self.discovery_id,
"db_res": self.db_res,
"passfile": self.passfile,
"sslcert": self.sslcert,
"sslkey": self.sslkey,
"sslrootcert": self.sslrootcert,
"sslcrl": self.sslcrl,
"sslcompression": self.sslcompression,
"bgcolor": self.bgcolor,
"fgcolor": self.fgcolor,
"service": self.service,
"connect_timeout": self.connect_timeout,
"use_ssh_tunnel": self.use_ssh_tunnel,
"tunnel_host": self.tunnel_host,
"tunnel_port": self.tunnel_port,
"tunnel_authentication": self.tunnel_authentication,
"tunnel_identity_file": self.tunnel_identity_file,
"tunnel_password": self.tunnel_password
}
class ModulePreference(db.Model): class ModulePreference(db.Model):
"""Define a preferences table for any modules.""" """Define a preferences table for any modules."""

View File

@@ -0,0 +1,262 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import $ from 'jquery';
import Alertify from 'pgadmin.alertifyjs';
import pgAdmin from 'sources/pgadmin';
import Backform from 'pgadmin.backform';
import newConnectionDialogModel from 'sources/sqleditor/new_connection_dialog_model';
let NewConnectionDialog = {
'dialog': function(handler, reconnect) {
let url = url_for('sqleditor.get_new_connection_data', {
'sid': handler.url_params.sid,
'sgid': handler.url_params.sgid,
});
if(reconnect) {
url += '?connect=1';
}
let title = gettext('Connect to server');
$.ajax({
url: url,
headers: {
'Cache-Control' : 'no-cache',
},
}).done(function (res) {
let response = res.data.result;
response.database_list = [];
response.user_list = [];
if (Alertify.newConnectionDialog) {
delete Alertify.newConnectionDialog;
}
// Create Dialog
Alertify.dialog('newConnectionDialog', function factory() {
let $container = $('<div class=\'new-connection-dialog\'></div>');
return {
main: function(message) {
this.msg = message;
},
build: function() {
this.elements.content.appendChild($container.get(0));
Alertify.pgDialogBuild.apply(this);
},
setup: function(){
return {
buttons: [
{
text: '',
key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Help'),
'aria-label': gettext('Help'),
url: url_for('help.static', {
'filename': 'query_tool.html',
}),
},
},
{
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-times pg-alertify-button',
'data-btn-name': 'cancel',
}, {
text: gettext('OK'),
key: 13,
className: 'btn btn-primary fa fa-check pg-alertify-button',
'data-btn-name': 'ok',
},
],
// Set options for dialog
options: {
title: title,
//disable both padding and overflow control.
padding: !1,
overflow: !1,
model: 0,
resizable: true,
maximizable: false,
pinnable: false,
closableByDimmer: false,
modal: false,
autoReset: false,
closable: true,
},
};
},
prepare: function() {
let self = this;
$container.html('');
// Disable Ok button
this.__internal.buttons[2].element.disabled = true;
// Status bar
this.statusBar = $(
'<div class=\'pg-prop-status-bar pg-el-xs-12 d-none\'>' +
' <div class="error-in-footer"> ' +
' <div class="d-flex px-2 py-1"> ' +
' <div class="pr-2"> ' +
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
' </div> ' +
' <div class="alert-text" role="alert"></div> ' +
' </div> ' +
' </div> ' +
'</div>').appendTo($container);
// To show progress on filter Saving/Updating on AJAX
this.showNewConnectionProgress = $(
`<div id="show_filter_progress" class="pg-sp-container sql-editor-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row"><div class="col-12 pg-sp-icon sql-editor-busy-icon"></div></div>
<div class="row"><div class="col-12 pg-sp-text sql-editor-busy-text">` + gettext('Loading data...') + `</div></div>
</div>
</div>`
).appendTo($container);
$(
self.showNewConnectionProgress[0]
).removeClass('d-none');
self.newConnCollectionModel = newConnectionDialogModel(response, handler.url_params.sgid, handler.url_params.sid);
let fields = Backform.generateViewSchema(null, self.newConnCollectionModel, 'create', null, null, true);
let view = this.view = new Backform.Dialog({
el: '<div></div>',
model: self.newConnCollectionModel,
schema: fields,
});
$(this.elements.body.childNodes[0]).addClass(
'alertify_tools_dialog_properties obj_properties'
);
$container.append(view.render().$el);
// Enable/disable save button and show/hide statusbar based on session
view.listenTo(view.model, 'pgadmin-session:start', function() {
view.listenTo(view.model, 'pgadmin-session:invalid', function(msg) {
self.statusBar.removeClass('d-none');
$(self.statusBar.find('.alert-text')).html(msg);
// Disable Okay button
self.__internal.buttons[2].element.disabled = true;
});
view.listenTo(view.model, 'pgadmin-session:valid', function() {
self.statusBar.addClass('d-none');
$(self.statusBar.find('.alert-text')).html('');
// Enable Okay button
self.__internal.buttons[2].element.disabled = false;
});
});
view.listenTo(view.model, 'pgadmin-session:stop', function() {
view.stopListening(view.model, 'pgadmin-session:invalid');
view.stopListening(view.model, 'pgadmin-session:valid');
});
// Starts monitoring changes to model
view.model.startNewSession();
// Hide Progress ...
$(
self.showNewConnectionProgress[0]
).addClass('d-none');
},
callback: function(e) {
let self = this;
if (e.button.element.name == 'dialog_help') {
e.cancel = true;
pgAdmin.Browser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
null, null);
return;
} else if (e.button['data-btn-name'] === 'ok') {
e.cancel = true; // Do not close dialog
let newConnCollectionModel = this.newConnCollectionModel.toJSON();
let selected_database_name = null;
response.database_list.forEach(function(data){
if(newConnCollectionModel['database'] == data['value']) {
selected_database_name = data['label'];
return false;
}
});
let tab_title = '';
if(newConnCollectionModel['role']) {
tab_title = selected_database_name + '/' + newConnCollectionModel['role'] + '@' + response.server_name;
} else {
tab_title = selected_database_name + '/' + newConnCollectionModel['user'] + '@' + response.server_name;
newConnCollectionModel['role'] = null;
}
let is_create_connection = true;
handler.gridView.connection_list.forEach(function(connection_data){
if(parseInt(connection_data['server']) == newConnCollectionModel['server']
&& parseInt(connection_data['database']) == newConnCollectionModel['database']
&& connection_data['user'] == newConnCollectionModel['user'] && connection_data['role'] == newConnCollectionModel['role']) {
is_create_connection = false;
// break for loop by return false.
return false;
}
if(tab_title == connection_data['title']) {
is_create_connection = false;
return false;
}
});
if(!is_create_connection) {
let errmsg = 'Connection with this configuration already present.';
Alertify.info(errmsg);
}else {
let connection_details = {
'server_group': handler.gridView.handler.url_params.sgid,
'server': newConnCollectionModel['server'],
'database': newConnCollectionModel['database'],
'title': tab_title,
'user': newConnCollectionModel['user'],
'role': newConnCollectionModel['role'],
'password': response.password,
};
handler.gridView.on_change_connection(connection_details, self);
}
} else {
self.close();
}
},
};
});
setTimeout(function(){
Alertify.newConnectionDialog('Connect to server.').resizeTo(pgAdmin.Browser.stdW.md,pgAdmin.Browser.stdH.md);
}, 500);
}).fail(function(error) {
Alertify.alert().setting({
'title': gettext('Connection lost'),
'label':gettext('Ok'),
'message': gettext('Connection to the server has been lost.'),
'onok': function(){
alert(error);
//Close the window after connection is lost
window.close();
},
}).show();
});
},
};
module.exports = NewConnectionDialog;

View File

@@ -0,0 +1,339 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import _ from 'underscore';
import $ from 'jquery';
import pgAdmin from 'sources/pgadmin';
import Backform from 'pgadmin.backform';
import url_for from 'sources/url_for';
import alertify from 'pgadmin.alertifyjs';
export default function newConnectionDialogModel(response, sgid, sid) {
let server_name = '';
let database_name = '';
let NewConnectionSelect2Control = Backform.Select2Control.extend({
fetchData: function(){
let self = this;
let url = self.field.get('url');
url = url_for(url, {
'sid': self.model.attributes.server,
'sgid': sgid,
});
$.ajax({
async: false,
url: url,
headers: {
'Cache-Control' : 'no-cache',
},
}).done(function (res) {
var transform = self.field.get('transform');
if(res.data.status){
let data = res.data.result.data;
if (transform && _.isFunction(transform)) {
self.field.set('options', transform.bind(self, data));
} else {
self.field.set('options', data);
}
} else {
if (transform && _.isFunction(transform)) {
self.field.set('options', transform.bind(self, []));
} else {
self.field.set('options', []);
}
//alertify.error(res.data.msg);
}
}).fail(function(e){
let msg = '';
if(e.status == 404) {
msg = 'Unable to find url.';
} else {
msg = e.responseJSON.errormsg;
}
alertify.error(msg);
});
},
render: function() {
this.fetchData();
return Backform.Select2Control.prototype.render.apply(this, arguments);
},
onChange: function() {
Backform.Select2Control.prototype.onChange.apply(this, arguments);
},
});
let newConnectionModel = pgAdmin.Browser.DataModel.extend({
idAttribute: 'name',
defaults: {
server: parseInt(sid),
database: null,
user: null,
password: null,
server_name: server_name,
database_name: database_name,
},
schema: [{
id: 'server',
name: 'server',
label: gettext('Server'),
type: 'text',
editable: true,
disabled: false,
select2: {
allowClear: false,
},
control: Backform.Select2Control.extend({
connect: function(self) {
let local_self = self;
if(!alertify.connectServer){
alertify.dialog('connectServer', function factory() {
return {
main: function(
title, message, server_id, submit_password=true
) {
this.set('title', title);
this.message = message;
this.server_id = server_id;
this.submit_password = submit_password;
},
setup:function() {
return {
buttons:[{
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
key: 27,
},{
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
}],
focus: {element: '#password', select: true},
options: {
modal: 0, resizable: false, maximizable: false, pinnable: false,
},
};
},
build:function() {
},
prepare:function() {
this.setContent(this.message);
},
callback: function(closeEvent) {
if (closeEvent.button.text == gettext('OK')) {
if(this.submit_password) {
var _url = url_for('sqleditor.connect_server', {'sid': this.server_id});
$.ajax({
type: 'POST',
timeout: 30000,
url: _url,
data: $('#frmPassword').serialize(),
})
.done(function() {
local_self.model.attributes.database = null;
local_self.model.attributes.user = null;
local_self.model.attributes.role = null;
Backform.Select2Control.prototype.onChange.apply(local_self, arguments);
response.server_list.forEach(function(obj){
if(obj.id==self.model.changed.server) {
response.server_name = obj.name;
}
});
})
.fail(function(xhr) {
alertify.connectServer('Connect to server', xhr.responseJSON.result, local_self.getValueFromDOM());
});
} else {
response.password = $('#password').val();
}
} else {
local_self.model.attributes.database = null;
local_self.model.attributes.user = null;
local_self.model.attributes.role = null;
Backform.Select2Control.prototype.onChange.apply(local_self, arguments);
}
closeEvent.close = true;
},
};
});
}
},
render: function() {
let self = this;
self.connect(self);
return Backform.Select2Control.prototype.render.apply(self, arguments);
},
onChange: function() {
this.model.attributes.database = null;
this.model.attributes.user = null;
let self = this;
self.connect(self);
let url = url_for('sqleditor.connect_server', {
'sid': self.getValueFromDOM(),
'usr': self.model.attributes.user,
});
$.ajax({
async: false,
url: url,
type: 'POST',
headers: {
'Cache-Control' : 'no-cache',
},
}).done(function () {
Backform.Select2Control.prototype.onChange.apply(self, arguments);
response.server_list.forEach(function(obj){
if(obj.id==self.model.changed.server) {
response.server_name = obj.name;
}
});
}).fail(function(xhr){
alertify.connectServer('Connect to server', xhr.responseJSON.result, self.getValueFromDOM());
});
},
}),
options: function() {
return _.map(response.server_list, (obj) => {
if (obj.id == parseInt(sid))
response.server_name = obj.name;
return {
value: obj.id,
label: obj.name,
};
});
},
},
{
id: 'database',
name: 'database',
label: gettext('Database'),
type: 'text',
editable: true,
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('server')) && !_.isNull(m.get('server'))
&& m.get('server') !== '') {
setTimeout(function() {
if(self_local.options.length) {
m.set('database', self_local.options[0].value);
}
}, 10);
return false;
}
return true;
},
deps: ['server'],
url: 'sqleditor.get_new_connection_database',
select2: {
allowClear: false,
width: '100%',
first_empty: true,
select_first: false,
},
extraClasses:['new-connection-dialog-style'],
control: NewConnectionSelect2Control,
transform: function(data) {
response.database_list = data;
return data;
},
},
{
id: 'user',
name: 'user',
label: gettext('User'),
type: 'text',
editable: true,
deps: ['server'],
select2: {
allowClear: false,
width: '100%',
},
control: NewConnectionSelect2Control,
url: 'sqleditor.get_new_connection_user',
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('server')) && !_.isNull(m.get('server'))
&& m.get('server') !== '') {
setTimeout(function() {
if(self_local.options.length) {
m.set('user', self_local.options[0].value);
}
}, 10);
return false;
}
return true;
},
},{
id: 'role',
name: 'role',
label: gettext('Role'),
type: 'text',
editable: true,
deps: ['server'],
select2: {
allowClear: false,
width: '100%',
first_empty: true,
},
control: NewConnectionSelect2Control,
url: 'sqleditor.get_new_connection_role',
disabled: false,
},
/*{
id: 'password',
name: 'password',
label: gettext('Password'tools/sqleditor/__init__.py),
type: 'password',
editable: true,
disabled: true,
deps: ['user'],
control: Backform.InputControl.extend({
render: function() {
let self = this;
self.model.attributes.password = null;
Backform.InputControl.prototype.render.apply(self, arguments);
return self;
},
onChange: function() {
let self = this;
Backform.InputControl.prototype.onChange.apply(self, arguments);
},
}),
},*/
],
validate: function() {
let msg = null;
this.errorModel.clear();
if(_.isUndefined(this.get('database')) || _.isNull(this.get('database'))){
msg = gettext('Please select database');
this.errorModel.set('database', msg);
return msg;
} else if(_.isUndefined(this.get('database')) || _.isUndefined(this.get('user'))|| _.isNull(this.get('user'))) {
msg = gettext('Please select user');
this.errorModel.set('user', msg);
return msg;
}
/*else if((this.attributes.password == '' || _.isUndefined(this.get('password')) || _.isNull(this.get('password')))) {
msg = gettext('Please enter password');
this.errorModel.set('password', msg);
return msg;
}*/
return null;
},
});
let model = new newConnectionModel();
return model;
}

View File

@@ -92,6 +92,7 @@
right: 0; right: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
z-index: 1;
} }
.pg-prop-status-bar { .pg-prop-status-bar {

View File

@@ -18,22 +18,23 @@ from flask import Response, url_for, session, request, make_response
from werkzeug.useragents import UserAgent from werkzeug.useragents import UserAgent
from flask import current_app as app, render_template from flask import current_app as app, render_template
from flask_babelex import gettext from flask_babelex import gettext
from flask_security import login_required from flask_security import login_required, current_user
from pgadmin.tools.sqleditor.command import ObjectRegistry, SQLFilter from pgadmin.tools.sqleditor.command import ObjectRegistry, SQLFilter
from pgadmin.tools.sqleditor import check_transaction_status
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response, bad_request, \ from pgadmin.utils.ajax import make_json_response, bad_request, \
internal_server_error internal_server_error, unauthorized
from config import PG_DEFAULT_DRIVER from config import PG_DEFAULT_DRIVER
from pgadmin.model import Server from pgadmin.model import Server, User
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.settings import get_setting from pgadmin.settings import get_setting
from pgadmin.browser.utils import underscore_unescape from pgadmin.browser.utils import underscore_unescape
from pgadmin.utils.exception import ObjectGone from pgadmin.utils.exception import ObjectGone
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.tools.sqleditor.utils.macros import get_user_macros from pgadmin.tools.sqleditor.utils.macros import get_user_macros
from pgadmin.utils.constants import MIMETYPE_APP_JS, UNAUTH_REQ
MODULE_NAME = 'datagrid' MODULE_NAME = 'datagrid'
@@ -74,7 +75,8 @@ class DataGridModule(PgAdminModule):
'datagrid.filter_validate', 'datagrid.filter_validate',
'datagrid.filter', 'datagrid.filter',
'datagrid.panel', 'datagrid.panel',
'datagrid.close' 'datagrid.close',
'datagrid.update_query_tool_connection'
] ]
def on_logout(self, user): def on_logout(self, user):
@@ -324,10 +326,48 @@ def initialize_query_tool(trans_id, sgid, sid, did=None):
req_args['recreate'] == '1'): req_args['recreate'] == '1'):
connect = False connect = False
is_error, errmsg, conn_id, version = _init_query_tool(trans_id, connect,
sgid, sid, did)
if is_error:
return errmsg
return make_json_response(
data={
'connId': str(conn_id),
'serverVersion': version,
}
)
def _connect(conn, **kwargs):
"""
Connect the database.
:param conn: Connection instance.
:param kwargs: user, role and password data from user.
:return:
"""
user = None
role = None
password = None
is_ask_password = False
if 'user' in kwargs and 'role' in kwargs:
user = kwargs['user']
role = kwargs['role'] if kwargs['role'] else None
password = kwargs['password'] if kwargs['password'] else None
is_ask_password = True
if user:
status, msg = conn.connect(user=user, role=role,
password=password)
else:
status, msg = conn.connect()
return status, msg, is_ask_password, user, role, password
def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs):
# Create asynchronous connection using random connection id. # Create asynchronous connection using random connection id.
conn_id = str(random.randint(1, 9999999)) conn_id = str(random.randint(1, 9999999))
# Use Maintenance database OID
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
if did is None: if did is None:
@@ -338,24 +378,41 @@ def initialize_query_tool(trans_id, sgid, sid, did=None):
) )
except Exception as e: except Exception as e:
app.logger.error(e) app.logger.error(e)
return internal_server_error(errormsg=str(e)) return True, internal_server_error(errormsg=str(e)), '', ''
try: try:
conn = manager.connection(did=did, conn_id=conn_id, conn = manager.connection(did=did, conn_id=conn_id,
auto_reconnect=False, auto_reconnect=False,
use_binary_placeholder=True, use_binary_placeholder=True,
array_to_string=True) array_to_string=True)
if connect: if connect:
status, msg = conn.connect() status, msg, is_ask_password, user, role, password = _connect(
conn, **kwargs)
if not status: if not status:
app.logger.error(msg) app.logger.error(msg)
return internal_server_error(errormsg=str(msg)) if is_ask_password:
server = Server.query.filter_by(id=sid).first()
return True, make_json_response(
success=0,
status=428,
result=render_template(
'servers/password.html',
server_label=server.name,
username=user,
errmsg=msg,
_=gettext,
)
), '', ''
else:
return True, internal_server_error(
errormsg=str(msg)), '', ''
except (ConnectionLost, SSHTunnelConnectionLost) as e: except (ConnectionLost, SSHTunnelConnectionLost) as e:
app.logger.error(e) app.logger.error(e)
raise raise
except Exception as e: except Exception as e:
app.logger.error(e) app.logger.error(e)
return internal_server_error(errormsg=str(e)) return True, internal_server_error(errormsg=str(e)), '', ''
if 'gridData' not in session: if 'gridData' not in session:
sql_grid_data = dict() sql_grid_data = dict()
@@ -377,10 +434,77 @@ def initialize_query_tool(trans_id, sgid, sid, did=None):
# Store the grid dictionary into the session variable # Store the grid dictionary into the session variable
session['gridData'] = sql_grid_data session['gridData'] = sql_grid_data
return False, '', conn_id, manager.version
@blueprint.route(
'/initialize/query_tool/update_connection/<int:trans_id>/'
'<int:sgid>/<int:sid>/<int:did>',
methods=["POST"], endpoint='update_query_tool_connection'
)
def update_query_tool_connection(trans_id, sgid, sid, did):
# Remove transaction Id.
with query_tool_close_session_lock:
data = json.loads(request.data, encoding='utf-8')
if 'gridData' not in session:
return make_json_response(data={'status': True})
grid_data = session['gridData']
# Return from the function if transaction id not found
if str(trans_id) not in grid_data:
return make_json_response(data={'status': True})
connect = True
req_args = request.args
if ('recreate' in req_args and
req_args['recreate'] == '1'):
connect = False
new_trans_id = str(random.randint(1, 9999999))
kwargs = {
'user': data['user'],
'role': data['role'],
'password': data['password'] if 'password' in data else None
}
is_error, errmsg, conn_id, version = _init_query_tool(
new_trans_id, connect, sgid, sid, did, **kwargs)
if is_error:
return errmsg
else:
try:
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = \
check_transaction_status(trans_id)
status, error_msg, new_conn, new_trans_obj, new_session_obj = \
check_transaction_status(new_trans_id)
new_session_obj['primary_keys'] = session_obj[
'primary_keys'] if 'primary_keys' in session_obj else None
new_session_obj['columns_info'] = session_obj[
'columns_info'] if 'columns_info' in session_obj else None
new_session_obj['client_primary_key'] = session_obj[
'client_primary_key'] if 'client_primary_key'\
in session_obj else None
close_query_tool_session(trans_id)
# Remove the information of unique transaction id from the
# session variable.
grid_data.pop(str(trans_id), None)
session['gridData'] = grid_data
except Exception as e:
app.logger.error(e)
return make_json_response( return make_json_response(
data={ data={
'connId': str(conn_id), 'connId': str(conn_id),
'serverVersion': manager.version, 'serverVersion': version,
'tran_id': new_trans_id
} }
) )

View File

@@ -417,8 +417,17 @@
title="" role="img"> title="" role="img">
</i> </i>
</div> </div>
<div class="editor-title" <div class="connection-info btn-group mr-1" role="group" aria-label="">
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% endif %}; color: {% if fgcolor %}{{ fgcolor }}{% endif %};">&nbsp;</div> <div class="editor-title" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% endif %}; color: {% if fgcolor %}{{ fgcolor }}{% endif %};">&nbsp;
</div>
<span class="conn-info-dd dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<ul class="dropdown-menu" id="connections-list">
</ul>
</div>
</div> </div>
<div id="editor-panel" tabindex="0"> <div id="editor-panel" tabindex="0">
<div id="fetching_data" class="pg-sp-container sql-editor-busy-fetching"> <div id="fetching_data" class="pg-sp-container sql-editor-busy-fetching">
@@ -481,6 +490,7 @@ require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sou
var script_type_url = ''; var script_type_url = '';
{% endif %} {% endif %}
// Start the query tool. // Start the query tool.
sqlEditorController.start( sqlEditorController.start(
{{ uniqueId }}, {{ uniqueId }},
{{ url_params|safe}}, {{ url_params|safe}},

View File

@@ -0,0 +1,134 @@
{
"data_grid_init_query_tool": [
{
"name": "Datagrid init query tool",
"url": "/datagrid/initialize/query_tool/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_query_tool_close": [
{
"name": "Datagrid query tool close",
"url": "/datagrid/close/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_validate_filter": [
{
"name": "Datagrid validate filter",
"url": "/datagrid/filter/validate/",
"is_positive_test": true,
"mocking_required": false,
"test_data": "id = 1",
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid validate filter",
"url": "/datagrid/filter/validate/",
"is_positive_test": false,
"mocking_required": true,
"test_data": "id = 1",
"mock_data": {
"function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar",
"return_value": "(False, 'Mocked Internal Server Error while validate filter')"
},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_update_connection": [
{
"name": "Datagrid update connection positive",
"url": "/datagrid/initialize/query_tool/update_connection/",
"is_positive_test": true,
"mocking_required": false,
"is_create_role": false,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid update connection with new user",
"url": "/datagrid/initialize/query_tool/update_connection/",
"is_positive_test": true,
"mocking_required": false,
"is_create_role": true,
"test_data": {},
"mock_data": {},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_panel": [
{
"name": "Datagrid Panel",
"url": "/datagrid/panel/",
"is_positive_test": true,
"mocking_required": false,
"test_data": {},
"mock_data": {
},
"expected_data": {
"status_code": 200
}
}
],
"data_grid_initialize": [
{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": true,
"mocking_required": false,
"test_data": "id=1",
"mock_data": {
},
"expected_data": {
"status_code": 200
}
},{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": true,
"mocking_required": false,
"test_data": null,
"mock_data": {},
"expected_data": {
"status_code": 200
}
},
{
"name": "Datagrid Initialize",
"url": "/datagrid/initialize/datagrid/",
"is_positive_test": false,
"mocking_required": true,
"test_data": "id=1",
"mock_data": {
"function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict",
"return_value": "(False, 'Mocked Internal Server Error while initialize datagrid.')"
},
"expected_data": {
"status_code": 500
}
}
]
}

View File

@@ -0,0 +1,73 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.exception import ExecuteError
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
class DatagridInitQueryToolTestCase(BaseTestGenerator):
"""
This will init query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_init_query_tool',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
self.trans_id = str(random.randint(1, 9999999))
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
def init_query_tool(self):
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' + str(
self.sid) + '/' + str(self.did),
content_type='html/json'
)
return response
def runTest(self):
""" This function will init query tool connection."""
if self.is_positive_test:
response = self.init_query_tool()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,90 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.exception import ExecuteError
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
class DatagridPanelTestCase(BaseTestGenerator):
"""
This will data grid panel.
"""
scenarios = utils.generate_scenarios(
'data_grid_panel',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
def panel(self):
query_param = \
'?is_query_tool={0}&sgid={1}&sid={2}&server_type={3}' \
'&did={4}&title={5}'.format(True, self.sgid, self.sid,
self.server_information['type'],
self.did, 'Query panel')
response = self.tester.post(
self.url + str(self.trans_id) + query_param,
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.panel()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.panel()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,78 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridQueryToolCloseTestCase(BaseTestGenerator):
"""
This will close query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_query_tool_close',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
def close_connection(self):
response = self.tester.delete(
self.url + str(self.trans_id),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.close_connection()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,121 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.roles.tests import \
utils as roles_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridUpdateConnectionTestCase(BaseTestGenerator):
"""
This will update query-tool connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_update_connection',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
self.trans_id = str(random.randint(1, 9999999))
self.roles = None
if self.is_create_role:
data = roles_utils.get_role_data(self.server['db_password'])
self.role_name = data['rolname']
self.role_password = data['rolpassword']
roles_utils.create_role_with_password(
self.server, self.role_name, self.role_password)
if not self.is_positive_test or self.is_create_role:
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize querty tool.")
self.test_data = {
"database": self.did,
"server": self.sid,
}
if self.server_information['type'] == 'ppas':
self.test_data['password'] = 'enterprisedb'
self.test_data['user'] = 'enterprisedb'
else:
self.test_data['password'] = 'postgres'
self.test_data['user'] = 'postgres'
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
def update_connection(self, user_data=None):
if user_data:
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) +
'/' + str(self.sid) + '/' + str(self.did),
data=json.dumps(user_data),
content_type='html/json'
)
else:
response = self.tester.post(
self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' +
str(self.sid) + '/' + str(self.did),
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
user_data = dict()
if self.is_create_role:
user_data['user'] = self.role_name
user_data['password'] = self.role_password
user_data['role'] = None
response = self.update_connection(user_data=user_data)
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
response = self.update_connection()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,92 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridValidateFilterTestCase(BaseTestGenerator):
"""
This will validate filter connection.
"""
scenarios = utils.generate_scenarios(
'data_grid_validate_filter',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.schema_id = parent_node_dict['schema'][-1]["schema_id"]
self.schema_name = parent_node_dict['schema'][-1]["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise ExecuteError("Could not find the schema to add a table.")
self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
def validate_filter(self):
response = self.tester.post(
self.url + str(self.sid) + '/' + str(self.did) + '/' +
str(self.table_id),
data=json.dumps(self.test_data),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.validate_filter()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.validate_filter()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,109 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
import uuid
import random
from unittest.mock import patch
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
from regression.test_setup import config_data
from pgadmin.browser.server_groups.servers.databases.schemas.tests import \
utils as schema_utils
from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \
import utils as tables_utils
from . import utils as data_grid_utils
from pgadmin.utils.exception import ExecuteError
class DatagridInitializeTestCase(BaseTestGenerator):
"""
This will Initialize datagrid
"""
scenarios = utils.generate_scenarios(
'data_grid_initialize',
data_grid_utils.test_cases
)
def setUp(self):
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.did = self.database_info["db_id"]
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
db_con = database_utils.connect_database(self, utils.SERVER_GROUP,
self.sid, self.did)
if not db_con['data']["connected"]:
raise ExecuteError("Could not connect to database to add a table.")
self.schema_id = parent_node_dict['schema'][-1]["schema_id"]
self.schema_name = parent_node_dict['schema'][-1]["schema_name"]
schema_response = schema_utils.verify_schemas(self.server,
self.db_name,
self.schema_name)
if not schema_response:
raise ExecuteError("Could not find the schema to add a table.")
self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8])
self.table_id = tables_utils.create_table(self.server, self.db_name,
self.schema_name,
self.table_name)
self.trans_id = str(random.randint(1, 9999999))
qt_init = data_grid_utils._init_query_tool(self, self.trans_id,
self.sgid, self.sid,
self.did)
if not qt_init['success']:
raise ExecuteError("Could not initialize query tool.")
def initialize_datagrid(self):
if self.test_data:
response = self.tester.post(
self.url + str(self.trans_id) + '/4/table/' +
str(self.sgid) + '/' + str(self.sid) + '/' +
str(self.did) + '/' + str(self.table_id),
data=json.dumps(self.test_data),
content_type='html/json'
)
else:
response = self.tester.post(
self.url + str(self.trans_id) + '/4/table/' +
str(self.sgid) + '/' + str(self.sid) + '/' +
str(self.did) + '/' + str(self.table_id),
content_type='html/json'
)
return response
def runTest(self):
""" This function will update query tool connection."""
if self.is_positive_test:
response = self.initialize_datagrid()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
with patch(self.mock_data["function_name"],
return_value=eval(self.mock_data["return_value"])):
response = self.initialize_datagrid()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)
def tearDown(self):
"""This function disconnect database."""
database_utils.disconnect_database(self, self.sid,
self.did)

View File

@@ -0,0 +1,33 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import os
import json
file_name = os.path.basename(__file__)
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
with open(CURRENT_PATH + "/datagrid_test_data.json") as data_file:
test_cases = json.load(data_file)
def _init_query_tool(self, trans_id, server_group, server_id, db_id):
QUERY_TOOL_INIT_URL = '/datagrid/initialize/query_tool'
qt_init = self.tester.post(
'{0}/{1}/{2}/{3}/{4}'.format(
QUERY_TOOL_INIT_URL,
trans_id,
server_group,
server_id,
db_id
),
follow_redirects=True
)
assert qt_init.status_code == 200
qt_init = json.loads(qt_init.data.decode('utf-8'))
return qt_init

View File

@@ -10,17 +10,15 @@
"""A blueprint module implementing the sqleditor frame.""" """A blueprint module implementing the sqleditor frame."""
import os import os
import pickle import pickle
import sys
import re import re
import simplejson as json
from flask import Response, url_for, render_template, session, request, \
current_app
from flask_babelex import gettext
from flask_security import login_required, current_user
from urllib.parse import unquote from urllib.parse import unquote
import simplejson as json
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT
from flask import Response, url_for, render_template, session, current_app
from flask import request, jsonify
from flask_babelex import gettext
from flask_security import login_required, current_user
from pgadmin.misc.file_manager import Filemanager from pgadmin.misc.file_manager import Filemanager
from pgadmin.tools.sqleditor.command import QueryToolCommand from pgadmin.tools.sqleditor.command import QueryToolCommand
from pgadmin.tools.sqleditor.utils.constant_definition import ASYNC_OK, \ from pgadmin.tools.sqleditor.utils.constant_definition import ASYNC_OK, \
@@ -32,11 +30,11 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils import get_storage_directory from pgadmin.utils import get_storage_directory
from pgadmin.utils.ajax import make_json_response, bad_request, \ from pgadmin.utils.ajax import make_json_response, bad_request, \
success_return, internal_server_error, make_response as ajax_response success_return, internal_server_error
from pgadmin.utils.driver import get_driver from pgadmin.utils.driver import get_driver
from pgadmin.utils.menu import MenuItem from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost, \
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
CryptKeyMissing CryptKeyMissing
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \ from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
register_query_tool_preferences register_query_tool_preferences
@@ -44,13 +42,16 @@ from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
read_file_generator read_file_generator
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\
ERROR_MSG_TRANS_ID_NOT_FOUND
from pgadmin.tools.sqleditor.utils.macros import get_macros,\ from pgadmin.tools.sqleditor.utils.macros import get_macros,\
get_user_macros, set_macros get_user_macros, set_macros
from pgadmin.utils.constants import MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, ERROR_FETCHING_DATA
from pgadmin.model import Server
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
MODULE_NAME = 'sqleditor' MODULE_NAME = 'sqleditor'
TRANSACTION_STATUS_CHECK_FAILED = gettext("Transaction status check failed.") TRANSACTION_STATUS_CHECK_FAILED = gettext("Transaction status check failed.")
_NODES_SQL = 'nodes.sql'
class SqlEditorModule(PgAdminModule): class SqlEditorModule(PgAdminModule):
@@ -114,7 +115,13 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.clear_query_history', 'sqleditor.clear_query_history',
'sqleditor.get_macro', 'sqleditor.get_macro',
'sqleditor.get_macros', 'sqleditor.get_macros',
'sqleditor.set_macros' 'sqleditor.set_macros',
'sqleditor.get_new_connection_data',
'sqleditor.get_new_connection_database',
'sqleditor.get_new_connection_user',
'sqleditor.get_new_connection_role',
'sqleditor.connect_server',
'sqleditor.connect_server_with_user',
] ]
def register_preferences(self): def register_preferences(self):
@@ -230,7 +237,7 @@ def start_view_data(trans_id):
) )
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
# set fetched row count to 0 as we are executing query again. # set fetched row count to 0 as we are executing query again.
trans_obj.update_fetched_row_cnt(0) trans_obj.update_fetched_row_cnt(0)
@@ -376,7 +383,7 @@ def poll(trans_id):
if isinstance(trans_obj, QueryToolCommand): if isinstance(trans_obj, QueryToolCommand):
trans_status = conn.transaction_status() trans_status = conn.transaction_status()
if trans_status == TX_STATUS_INERROR and \ if trans_status == TX_STATUS_INERROR and \
trans_obj.auto_rollback: trans_obj.auto_rollback:
conn.execute_void("ROLLBACK;") conn.execute_void("ROLLBACK;")
st, result = conn.async_fetchmany_2darray(ON_DEMAND_RECORD_COUNT) st, result = conn.async_fetchmany_2darray(ON_DEMAND_RECORD_COUNT)
@@ -686,13 +693,12 @@ def save(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
# If there is no primary key found then return from the function. # If there is no primary key found then return from the function.
if ('primary_keys' not in session_obj or if ('primary_keys' not in session_obj or
len(session_obj['primary_keys']) <= 0 or len(session_obj['primary_keys']) <= 0 or
len(changed_data) <= 0) and \ len(changed_data) <= 0) and 'has_oids' not in session_obj:
'has_oids' not in session_obj:
return make_json_response( return make_json_response(
data={ data={
'status': False, 'status': False,
@@ -759,7 +765,7 @@ def append_filter_inclusive(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
filter_sql = '' filter_sql = ''
@@ -813,7 +819,7 @@ def append_filter_exclusive(trans_id):
info='DATAGRID_TRANSACTION_REQUIRED', info='DATAGRID_TRANSACTION_REQUIRED',
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
filter_sql = '' filter_sql = ''
@@ -866,7 +872,7 @@ def remove_filter(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
@@ -910,7 +916,7 @@ def set_limit(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
@@ -1052,7 +1058,7 @@ def get_object_name(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = trans_obj.object_name res = trans_obj.object_name
else: else:
status = False status = False
@@ -1088,7 +1094,7 @@ def set_auto_commit(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
@@ -1133,7 +1139,7 @@ def set_auto_rollback(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
res = None res = None
@@ -1185,7 +1191,7 @@ def auto_complete(trans_id):
status=404) status=404)
if status and conn is not None and \ if status and conn is not None and \
trans_obj is not None and session_obj is not None: trans_obj is not None and session_obj is not None:
# Create object of SQLAutoComplete class and pass connection object # Create object of SQLAutoComplete class and pass connection object
auto_complete_obj = SQLAutoComplete( auto_complete_obj = SQLAutoComplete(
@@ -1472,6 +1478,282 @@ def get_filter_data(trans_id):
return FilterDialog.get(status, error_msg, conn, trans_obj, session_ob) return FilterDialog.get(status, error_msg, conn, trans_obj, session_ob)
@blueprint.route(
'/new_connection_dialog/<int:sgid>/<int:sid>',
methods=["GET"], endpoint='get_new_connection_data'
)
@login_required
def get_new_connection_data(sgid, sid=None):
"""
This method is used to get required data for get new connection.
:extract_sql_from_network_parameters,
"""
try:
# if sid and not did:
servers = Server.query.all()
server_list = [
{'name': server.serialize['name'], "id": server.serialize['id']}
for server in servers]
msg = "Success"
return make_json_response(
data={
'status': True,
'msg': msg,
'result': {
'server_list': server_list
}
}
)
except Exception:
return make_json_response(
data={
'status': False,
'msg': ERROR_FETCHING_DATA,
'result': {
'server_list': []
}
}
)
@blueprint.route(
'/new_connection_database/<int:sgid>/<int:sid>',
methods=["GET"], endpoint='get_new_connection_database'
)
@login_required
def get_new_connection_database(sgid, sid=None):
"""
This method is used to get required data for get new connection.
:extract_sql_from_network_parameters,
"""
try:
database_list = []
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
if conn.connected():
is_connected = True
else:
is_connected = False
if is_connected:
if sid:
template_path = 'databases/sql/#{0}#'.format(manager.version)
last_system_oid = 0
server_node_res = manager
db_disp_res = None
params = None
if server_node_res and server_node_res.db_res:
db_disp_res = ", ".join(
['%s'] * len(server_node_res.db_res.split(','))
)
params = tuple(server_node_res.db_res.split(','))
sql = render_template(
"/".join([template_path, _NODES_SQL]),
last_system_oid=last_system_oid,
db_restrictions=db_disp_res
)
status, databases = conn.execute_dict(sql, params)
database_list = [
{'label': database['name'], 'value': database['did']} for
database in databases['rows']]
else:
status = False
msg = "Success"
return make_json_response(
data={
'status': status,
'msg': msg,
'result': {
'data': database_list,
}
}
)
else:
return make_json_response(
data={
'status': False,
'msg': SERVER_CONNECTION_CLOSED,
'result': {
'database_list': [],
}
}
)
except Exception:
return make_json_response(
data={
'status': False,
'msg': ERROR_FETCHING_DATA,
'result': {
'database_list': [],
}
}
)
@blueprint.route(
'/new_connection_user/<int:sgid>/<int:sid>',
methods=["GET"], endpoint='get_new_connection_user'
)
@login_required
def get_new_connection_user(sgid, sid=None):
"""
This method is used to get required data for get new connection.
:extract_sql_from_network_parameters,
"""
try:
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
user_list = []
if conn.connected():
is_connected = True
else:
is_connected = False
if is_connected:
if sid:
sql_path = 'roles/sql/#{0}#'.format(manager.version)
status, users = conn.execute_2darray(
render_template(sql_path + _NODES_SQL)
)
user_list = [
{'value': user['rolname'], 'label': user['rolname']} for
user in users['rows'] if user['rolcanlogin']]
else:
status = False
msg = "Success"
return make_json_response(
data={
'status': status,
'msg': msg,
'result': {
'data': user_list,
}
}
)
else:
return make_json_response(
data={
'status': False,
'msg': SERVER_CONNECTION_CLOSED,
'result': {
'user_list': [],
}
}
)
except Exception:
return make_json_response(
data={
'status': False,
'msg': 'Unable to fetch data.',
'result': {
'user_list': [],
}
}
)
@blueprint.route(
'/new_connection_role/<int:sgid>/<int:sid>',
methods=["GET"], endpoint='get_new_connection_role'
)
@login_required
def get_new_connection_role(sgid, sid=None):
"""
This method is used to get required data for get new connection.
:extract_sql_from_network_parameters,
"""
try:
from pgadmin.utils.driver import get_driver
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
role_list = []
if conn.connected():
is_connected = True
else:
is_connected = False
if is_connected:
if sid:
sql_path = 'roles/sql/#{0}#'.format(manager.version)
status, roles = conn.execute_2darray(
render_template(sql_path + _NODES_SQL)
)
role_list = [
{'value': role['rolname'], 'label': role['rolname']} for
role in roles['rows']]
else:
status = False
msg = "Success"
return make_json_response(
data={
'status': status,
'msg': msg,
'result': {
'data': role_list,
}
}
)
else:
return make_json_response(
data={
'status': False,
'msg': SERVER_CONNECTION_CLOSED,
'result': {
'user_list': [],
}
}
)
except Exception:
return make_json_response(
data={
'status': False,
'msg': 'Unable to fetch data.',
'result': {
'user_list': [],
}
}
)
@blueprint.route(
'/connect_server/<int:sid>/<usr>',
methods=["POST"],
endpoint="connect_server_with_user"
)
@blueprint.route(
'/connect_server/<int:sid>',
methods=["POST"],
endpoint="connect_server"
)
@login_required
def connect_server(sid, usr=None):
# Check if server is already connected then no need to reconnect again.
server = Server.query.filter_by(id=sid).first()
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(sid)
conn = manager.connection()
user = None
if usr and manager.user != usr:
user = usr
else:
user = manager.user
if conn.connected():
return make_json_response(
success=1,
info=gettext("Server connected."),
data={}
)
view = SchemaDiffRegistry.get_node_view('server')
return view.connect(server.servergroup_id, sid, user_name=user)
@blueprint.route( @blueprint.route(
'/filter_dialog/<int:trans_id>', '/filter_dialog/<int:trans_id>',
methods=["PUT"], endpoint='set_filter_data' methods=["PUT"], endpoint='set_filter_data'

View File

@@ -315,10 +315,6 @@ input.editor-checkbox:focus {
padding: 10px 0px; padding: 10px 0px;
} }
.editor-title {
width:100%;
}
.connection-status-hide { .connection-status-hide {
display: none !important; display: none !important;
} }
@@ -396,7 +392,6 @@ input.editor-checkbox:focus {
overflow-y: hidden; overflow-y: hidden;
} }
/* Macros */ /* Macros */
.macro-tab { .macro-tab {
@@ -424,3 +419,7 @@ input.editor-checkbox:focus {
.macro_dialog .pg-prop-status-bar { .macro_dialog .pg-prop-status-bar {
z-index: 1; z-index: 1;
} }
.new-connection-dialog-style {
width: 100% !important;
}

View File

@@ -14,6 +14,7 @@ define('tools.querytool', [
'jqueryui.position', 'underscore', 'pgadmin.alertifyjs', 'jqueryui.position', 'underscore', 'pgadmin.alertifyjs',
'sources/pgadmin', 'backbone', 'bundled_codemirror', 'sources/utils', 'sources/pgadmin', 'backbone', 'bundled_codemirror', 'sources/utils',
'pgadmin.misc.explain', 'pgadmin.misc.explain',
'pgadmin.user_management.current_user',
'sources/selection/grid_selector', 'sources/selection/grid_selector',
'sources/selection/active_cell_capture', 'sources/selection/active_cell_capture',
'sources/selection/clipboard', 'sources/selection/clipboard',
@@ -26,6 +27,7 @@ define('tools.querytool', [
'sources/sqleditor/execute_query', 'sources/sqleditor/execute_query',
'sources/sqleditor/query_tool_http_error_handler', 'sources/sqleditor/query_tool_http_error_handler',
'sources/sqleditor/filter_dialog', 'sources/sqleditor/filter_dialog',
'sources/sqleditor/new_connection_dialog',
'sources/sqleditor/geometry_viewer', 'sources/sqleditor/geometry_viewer',
'sources/sqleditor/history/history_collection.js', 'sources/sqleditor/history/history_collection.js',
'sources/sqleditor/history/query_history', 'sources/sqleditor/history/query_history',
@@ -53,8 +55,8 @@ define('tools.querytool', [
'pgadmin.tools.user_management', 'pgadmin.tools.user_management',
], function( ], function(
gettext, url_for, $, jqueryui, jqueryui_position, _, alertify, pgAdmin, Backbone, codemirror, pgadminUtils, gettext, url_for, $, jqueryui, jqueryui_position, _, alertify, pgAdmin, Backbone, codemirror, pgadminUtils,
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, pgExplain, current_user, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, newConnectionHandler,
GeometryViewer, historyColl, queryHist, querySources, GeometryViewer, historyColl, queryHist, querySources,
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
@@ -98,6 +100,9 @@ define('tools.querytool', [
this.layout = opts.layout; this.layout = opts.layout;
this.set_server_version(opts.server_ver); this.set_server_version(opts.server_ver);
this.trigger('pgadmin-sqleditor:view:initialised'); this.trigger('pgadmin-sqleditor:view:initialised');
this.connection_list = [
{'server_group': null,'server': null, 'database': null, 'user': null, 'role': null, 'title': '&lt;New Connection&gt;'},
];
}, },
// Bind all the events // Bind all the events
@@ -163,6 +168,35 @@ define('tools.querytool', [
'click .btn-macro': 'on_execute_macro', 'click .btn-macro': 'on_execute_macro',
}, },
render_connection: function(data_list) {
if(this.handler.is_query_tool) {
var dropdownElement = document.getElementById('connections-list');
dropdownElement.innerHTML = '';
data_list.forEach((option, index) => {
$('#connections-list').append('<li class="connection-list-item" data-index='+ index +'><a class="dropdown-item" href="#" tabindex="0">'+ option.title +'</a></li>');
});
var self = this;
$('.connection-list-item').click(function() {
self.get_connection_data(this);
});
} else {
$('.conn-info-dd').hide();
$('.editor-title').css({pointerEvents: 'none'});
}
},
get_connection_data: function(event){
var index = $(event).attr('data-index');
var connection_details = this.connection_list[index];
if(connection_details.server_group) {
this.on_change_connection(connection_details);
} else {
this.on_new_connection();
}
},
reflectPreferences: function() { reflectPreferences: function() {
let self = this, let self = this,
browser = pgWindow.default.pgAdmin.Browser, browser = pgWindow.default.pgAdmin.Browser,
@@ -213,6 +247,7 @@ define('tools.querytool', [
set_editor_title: function(title) { set_editor_title: function(title) {
this.$el.find('.editor-title').text(title); this.$el.find('.editor-title').text(title);
this.render_connection(this.connection_list);
}, },
// This function is used to render the template. // This function is used to render the template.
@@ -696,6 +731,8 @@ define('tools.querytool', [
pgBrowser.register_to_activity_listener(document, ()=>{ pgBrowser.register_to_activity_listener(document, ()=>{
alertify.alert(gettext('Timeout'), gettext('Your session has timed out due to inactivity. Please close the window and login again.')); alertify.alert(gettext('Timeout'), gettext('Your session has timed out due to inactivity. Please close the window and login again.'));
}); });
self.render_connection(self.connection_list);
}, },
/* Regarding SlickGrid usage in render_grid function. /* Regarding SlickGrid usage in render_grid function.
@@ -1607,6 +1644,17 @@ define('tools.querytool', [
); );
}, },
on_new_connection: function() {
var self = this;
// Trigger the show_filter signal to the SqlEditorController class
self.handler.trigger(
'pgadmin-sqleditor:button:show_new_connection',
self,
self.handler
);
},
// Callback function for include filter button click. // Callback function for include filter button click.
on_include_filter: function(ev) { on_include_filter: function(ev) {
var self = this; var self = this;
@@ -2070,6 +2118,83 @@ define('tools.querytool', [
queryToolActions.executeMacro(this.handler, macroId); queryToolActions.executeMacro(this.handler, macroId);
}, },
on_change_connection: function(connection_details, ref) {
let title = this.$el.find('.editor-title').html();
if(connection_details['title'] != title) {
var self = this;
$.ajax({
async: false,
url: url_for('datagrid.update_query_tool_connection', {
'trans_id': self.transId,
'sgid': connection_details['server_group'],
'sid': connection_details['server'],
'did': connection_details['database'],
}),
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(connection_details),
})
.done(function(res) {
if(res.success) {
self.transId = res.data.tran_id;
self.handler.transId = res.data.tran_id;
self.handler.url_params = {
'did': connection_details['database'],
'is_query_tool': self.handler.url_params.is_query_tool,
'server_type': self.handler.url_params.server_type,
'sgid': connection_details['server_group'],
'sid': connection_details['server'],
'title': connection_details['title'],
};
self.set_editor_title(self.handler.url_params.title);
self.handler.setTitle(self.handler.url_params.title);
alertify.success('connected successfully');
if(ref){
let connection_data = {
'server_group': self.handler.url_params.sgid,
'server': connection_details['server'],
'database': connection_details['database'],
'user': connection_details['user'],
'title': connection_details['title'],
'role': connection_details['role'],
'password': connection_details['password'],
'is_allow_new_connection': true,
};
self.connection_list.unshift(connection_data);
self.render_connection(self.connection_list);
ref.close();
}
}
return true;
})
.fail(function(xhr) {
if(xhr.status == 428) {
alertify.connectServer('Connect to server', xhr.responseJSON.result, connection_details['server'], false);
} else {
alertify.error(xhr.responseJSON['errormsg']);
}
/*let url = url_for('sqleditor.connect_server_with_user', {
'sid': newConnCollectionModel['server'],
'usr': newConnCollectionModel['user']
});
$.ajax({
async: false,
url: url,
headers: {
'Cache-Control' : 'no-cache',
},
}).done(function () {
Backform.Select2Control.prototype.onChange.apply(self, arguments);
response.server_list.forEach(function(obj){
if(obj.id==self.model.changed.server) {
response.server_name = obj.name;
}
});
}).fail(function(xhr){});*/
});
}
},
}); });
@@ -2393,6 +2518,17 @@ define('tools.querytool', [
$('#btn-conn-status i').removeClass('obtaining-conn'); $('#btn-conn-status i').removeClass('obtaining-conn');
self.gridView.set_editor_title(_.unescape(url_params.title)); self.gridView.set_editor_title(_.unescape(url_params.title));
let connection_data = {
'server_group': self.gridView.handler.url_params.sgid,
'server': self.gridView.handler.url_params.sid,
'database': self.gridView.handler.url_params.did,
'user': null,
'role': null,
'title': _.unescape(url_params.title),
'is_allow_new_connection': false,
};
self.gridView.connection_list.unshift(connection_data);
self.gridView.render_connection(self.gridView.connection_list);
}; };
pgBrowser.Events.on('pgadmin:query_tool:connected:' + transId, afterConn); pgBrowser.Events.on('pgadmin:query_tool:connected:' + transId, afterConn);
@@ -2487,6 +2623,7 @@ define('tools.querytool', [
self.on('pgadmin-sqleditor:button:save_file', self._save_file, self); self.on('pgadmin-sqleditor:button:save_file', self._save_file, self);
self.on('pgadmin-sqleditor:button:deleterow', self._delete, self); self.on('pgadmin-sqleditor:button:deleterow', self._delete, self);
self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self);
self.on('pgadmin-sqleditor:button:show_new_connection', self._show_new_connection, self);
self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self);
self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self);
self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self);
@@ -3696,7 +3833,6 @@ define('tools.querytool', [
} }
}; };
}, },
// This function will show the filter in the text area. // This function will show the filter in the text area.
_show_filter: function() { _show_filter: function() {
let self = this, let self = this,
@@ -3711,7 +3847,19 @@ define('tools.querytool', [
} }
FilterHandler.dialog(self, reconnect); FilterHandler.dialog(self, reconnect);
}, },
// This function will show the new connection.
_show_new_connection: function() {
let self = this,
reconnect = false;
/* When server is disconnected and connected, connection is lost,
* To reconnect pass true
*/
if (arguments.length > 0 && arguments[arguments.length - 1] == 'connect') {
reconnect = true;
}
newConnectionHandler.dialog(self, reconnect);
},
// This function will include the filter by selection. // This function will include the filter by selection.
_include_filter: function() { _include_filter: function() {
var self = this, var self = this,

View File

@@ -30,6 +30,19 @@
color: $sql-title-fg; color: $sql-title-fg;
} }
.connection-info {
background: $sql-title-bg;
color: $sql-title-fg;
width:100%;
display: inherit;
}
.conn-info-dd {
padding-top: 0.3em;
padding-left: 0.2em;
cursor: pointer;
}
#editor-panel { #editor-panel {
z-index: 0; z-index: 0;

View File

@@ -0,0 +1,100 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.test_setup import config_data
from regression.python_test_utils import test_utils as utils
class TestNewConnectionDatabase(BaseTestGenerator):
""" This class will test new connection database. """
API_URL = "/sqleditor/new_connection_database/"
scenarios = [
('New connection dialog',
dict(
url=API_URL,
is_positive_test=True,
mocking_required=False,
is_server_conn_required=False,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
('New connection dialog connect server',
dict(
url=API_URL,
is_positive_test=True,
mocking_required=False,
is_server_conn_required=True,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
('New connection dialog negative',
dict(
url=API_URL,
is_positive_test=False,
mocking_required=False,
is_server_conn_required=True,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
]
def setUp(self):
self.content_type = 'html/json'
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
def get_database(self):
response = self.tester.get(
self.url + str(self.sgid) + '/' + str(self.sid),
content_type=self.content_type
)
return response
def runTest(self):
if self.is_positive_test:
if self.is_server_conn_required:
self.server['password'] = self.server['db_password']
self.tester.post(
'/browser/server/connect/{0}/{1}'.format(
utils.SERVER_GROUP,
self.sid),
data=json.dumps(self.server),
content_type=self.content_type
)
response = self.get_database()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
if self.is_server_conn_required:
self.server['password'] = self.server['db_password']
self.tester.post(
'/browser/server/connect/{0}/{1}'.format(
utils.SERVER_GROUP,
self.sid),
data=json.dumps(self.server),
content_type=self.content_type
)
self.sid = 0
response = self.get_database()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)

View File

@@ -0,0 +1,50 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.test_setup import config_data
from regression.python_test_utils import test_utils as utils
class TestNewConnectionDialog(BaseTestGenerator):
""" This class will test new connection dialog. """
scenarios = [
('New connection dialog',
dict(
url="/sqleditor/new_connection_dialog/",
is_positive_test=True,
mocking_required=False,
is_connect_server=False,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
]
def setUp(self):
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
def new_connection(self):
response = self.tester.get(
self.url + str(self.sgid) + '/' + str(self.sgid),
content_type='html/json'
)
return response
def runTest(self):
if self.is_positive_test:
response = self.new_connection()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)

View File

@@ -0,0 +1,100 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.test_setup import config_data
from regression.python_test_utils import test_utils as utils
class TestNewConnectionUser(BaseTestGenerator):
""" This class will test new connection user. """
API_URL = '/sqleditor/new_connection_user/'
scenarios = [
('New connection dialog',
dict(
url=API_URL,
is_positive_test=True,
mocking_required=False,
is_server_conn_required=False,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
('New connection dialog connect server',
dict(
url=API_URL,
is_positive_test=True,
mocking_required=False,
is_server_conn_required=True,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
('New connection dialog negative',
dict(
url=API_URL,
is_positive_test=False,
mocking_required=False,
is_server_conn_required=True,
test_data={},
mock_data={},
expected_data={
"status_code": 200
}
)),
]
def setUp(self):
self.content_type = 'html/json'
self.sid = parent_node_dict["server"][-1]["server_id"]
self.sgid = config_data['server_group']
def get_use(self):
response = self.tester.get(
self.url + str(self.sgid) + '/' + str(self.sid),
content_type=self.content_type
)
return response
def runTest(self):
if self.is_positive_test:
if self.is_server_conn_required:
self.server['password'] = self.server['db_password']
self.tester.post(
'/browser/server/connect/{0}/{1}'.format(
utils.SERVER_GROUP,
self.sid),
data=json.dumps(self.server),
content_type=self.content_type
)
response = self.get_use()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
else:
if self.is_server_conn_required:
self.server['password'] = self.server['db_password']
self.tester.post(
'/browser/server/connect/{0}/{1}'.format(
utils.SERVER_GROUP,
self.sid),
data=json.dumps(self.server),
content_type='html/json'
)
self.sid = 0
response = self.get_use()
actual_response_code = response.status_code
expected_response_code = self.expected_data['status_code']
self.assertEqual(actual_response_code, expected_response_code)

View File

@@ -44,3 +44,5 @@ ERROR_MSG_TRANS_ID_NOT_FOUND = gettext(
# Role module constant # Role module constant
ERROR_FETCHING_ROLE_INFORMATION = gettext( ERROR_FETCHING_ROLE_INFORMATION = gettext(
'Error fetching role information from the database server.') 'Error fetching role information from the database server.')
ERROR_FETCHING_DATA = gettext('Unable to fetch data.')

View File

@@ -21,7 +21,7 @@ import psycopg2
from flask import g, current_app from flask import g, current_app
from flask_babelex import gettext from flask_babelex import gettext
from flask_security import current_user from flask_security import current_user
from pgadmin.utils.crypto import decrypt from pgadmin.utils.crypto import decrypt, encrypt
from psycopg2.extensions import encodings from psycopg2.extensions import encodings
import config import config
@@ -204,6 +204,45 @@ class Connection(BaseConnection):
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
def _check_user_password(self, kwargs):
"""
Check user and password.
"""
password = None
encpass = None
is_update_password = True
if 'user' in kwargs and kwargs['password']:
password = kwargs['password']
kwargs.pop('password')
is_update_password = False
else:
encpass = kwargs['password'] if 'password' in kwargs else None
return password, encpass, is_update_password
def _decode_password(self, encpass, manager, password, crypt_key):
if encpass:
# Fetch Logged in User Details.
user = User.query.filter_by(id=current_user.id).first()
if user is None:
return True, self.UNAUTHORIZED_REQUEST, password
try:
password = decrypt(encpass, crypt_key)
# password is in bytes, for python3 we need it in string
if isinstance(password, bytes):
password = password.decode()
except Exception as e:
manager.stop_ssh_tunnel()
current_app.logger.exception(e)
return True, \
_(
"Failed to decrypt the saved password.\nError: {0}"
).format(str(e))
return False, '', password
def connect(self, **kwargs): def connect(self, **kwargs):
if self.conn: if self.conn:
if self.conn.closed: if self.conn.closed:
@@ -212,11 +251,13 @@ class Connection(BaseConnection):
return True, None return True, None
pg_conn = None pg_conn = None
password = None
passfile = None passfile = None
manager = self.manager manager = self.manager
crypt_key_present, crypt_key = get_crypt_key()
password, encpass, is_update_password = self._check_user_password(
kwargs)
encpass = kwargs['password'] if 'password' in kwargs else None
passfile = kwargs['passfile'] if 'passfile' in kwargs else None passfile = kwargs['passfile'] if 'passfile' in kwargs else None
tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \ tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \
kwargs else '' kwargs else ''
@@ -231,38 +272,23 @@ class Connection(BaseConnection):
if manager.use_ssh_tunnel == 1: if manager.use_ssh_tunnel == 1:
manager.check_ssh_tunnel_alive() manager.check_ssh_tunnel_alive()
if encpass is None: if is_update_password:
encpass = self.password or getattr(manager, 'password', None) if encpass is None:
encpass = self.password or getattr(manager, 'password', None)
self.password = encpass self.password = encpass
# Reset the existing connection password # Reset the existing connection password
if self.reconnecting is not False: if self.reconnecting is not False:
self.password = None self.password = None
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present: if not crypt_key_present:
raise CryptKeyMissing() raise CryptKeyMissing()
if encpass: is_error, errmsg, password = self._decode_password(encpass, manager,
# Fetch Logged in User Details. password, crypt_key)
user = User.query.filter_by(id=current_user.id).first() if is_error:
return False, errmsg
if user is None:
return False, self.UNAUTHORIZED_REQUEST
try:
password = decrypt(encpass, crypt_key)
# password is in bytes, for python3 we need it in string
if isinstance(password, bytes):
password = password.decode()
except Exception as e:
manager.stop_ssh_tunnel()
current_app.logger.exception(e)
return False, \
_(
"Failed to decrypt the saved password.\nError: {0}"
).format(str(e))
# If no password credential is found then connect request might # If no password credential is found then connect request might
# come from Query tool, ViewData grid, debugger etc tools. # come from Query tool, ViewData grid, debugger etc tools.
@@ -273,7 +299,10 @@ class Connection(BaseConnection):
try: try:
database = self.db database = self.db
user = manager.user if 'user' in kwargs and kwargs['user']:
user = kwargs['user']
else:
user = manager.user
conn_id = self.conn_id conn_id = self.conn_id
import os import os
@@ -342,10 +371,10 @@ class Connection(BaseConnection):
self.wasConnected = False self.wasConnected = False
raise e raise e
if status: if status and is_update_password:
manager._update_password(encpass) manager._update_password(encpass)
else: else:
if not self.reconnecting: if not self.reconnecting and is_update_password:
self.wasConnected = False self.wasConnected = False
return status, msg return status, msg
@@ -363,7 +392,7 @@ class Connection(BaseConnection):
else: else:
self.conn.autocommit = True self.conn.autocommit = True
def _set_role(self, manager, cur, conn_id): def _set_role(self, manager, cur, conn_id, **kwargs):
""" """
Set role Set role
:param manager: :param manager:
@@ -371,8 +400,18 @@ class Connection(BaseConnection):
:param conn_id: :param conn_id:
:return: :return:
""" """
if manager.role: is_set_role = False
status = self._execute(cur, "SET ROLE TO %s", [manager.role]) role = None
if 'role' in kwargs and kwargs['role']:
is_set_role = True
role = kwargs['role']
elif manager.role:
is_set_role = True
role = manager.role
if is_set_role:
status = self._execute(cur, "SET ROLE TO %s", [role])
if status is not None: if status is not None:
self.conn.close() self.conn.close()
@@ -386,7 +425,7 @@ class Connection(BaseConnection):
msg=status msg=status
) )
) )
return False, \ return True, \
_( _(
"Failed to setup the role with error message:\n{0}" "Failed to setup the role with error message:\n{0}"
).format(status) ).format(status)
@@ -449,7 +488,7 @@ class Connection(BaseConnection):
return False, status return False, status
is_error, errmsg = self._set_role(manager, cur, conn_id) is_error, errmsg = self._set_role(manager, cur, conn_id, **kwargs)
if is_error: if is_error:
return False, errmsg return False, errmsg
@@ -495,7 +534,7 @@ WHERE db.datname = current_database()""")
if len(manager.db_info) == 1: if len(manager.db_info) == 1:
manager.did = res['did'] manager.did = res['did']
self._set_user_info(cur, manager) self._set_user_info(cur, manager, **kwargs)
self._set_server_type_and_password(kwargs, manager) self._set_server_type_and_password(kwargs, manager)
@@ -503,7 +542,7 @@ WHERE db.datname = current_database()""")
return True, None return True, None
def _set_user_info(self, cur, manager): def _set_user_info(self, cur, manager, **kwargs):
""" """
Set user info. Set user info.
:param cur: :param cur:
@@ -521,7 +560,7 @@ WHERE db.datname = current_database()""")
WHERE WHERE
rolname = current_user""") rolname = current_user""")
if status is None: if status is None and 'user' not in kwargs:
manager.user_info = dict() manager.user_info = dict()
if cur.rowcount > 0: if cur.rowcount > 0:
manager.user_info = cur.fetchmany(1)[0] manager.user_info = cur.fetchmany(1)[0]