public inbox for [email protected]
help / color / mirror / Atom feed[pgAdmin4][RM#3140] Add service parameter
14+ messages / 5 participants
[nested] [flat]
* [pgAdmin4][RM#3140] Add service parameter
@ 2018-03-09 11:47 Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Murtuza Zabuawala @ 2018-03-09 11:47 UTC (permalink / raw)
To: pgadmin-hackers
Hi,
PFA patch to add service parameter in server dialog.
- Docs updated
- Test case added for Service ID parameter
Please note,
I have extracted Connection class and Server manager class from our own
custom Psycopg2 driver module.
Patch also covers RM#3120
Please review.
--
Regards,
Murtuza Zabuawala
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
[application/octet-stream] RM_3140.diff (257.4K, 3-RM_3140.diff)
download | inline diff:
diff --git a/docs/en_US/images/server_advanced.png b/docs/en_US/images/server_advanced.png
index 10f1809a364579a39b2fe1911391568260b379b8..2291d95f4378a21554a8a668a6f52ea0a485f485 100644
GIT binary patch
literal 55652
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
Q0g%5h5(?rKqI&-SA3I!p=l}o!
literal 34067
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
diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst
index 24a726b..6ac72a6 100644
--- a/docs/en_US/server_dialog.rst
+++ b/docs/en_US/server_dialog.rst
@@ -62,6 +62,7 @@ Use the fields in the *Advanced* tab to configure a connection:
* Specify the IP address of the server host in the *Host address* field. Using this field to specify the host IP address may save time by avoiding a DNS lookup on connection, but it may be useful to specify both a host name and address when using Kerberos, GSSAPI, or SSPI authentication methods, as well as for verify-full SSL certificate verification.
* Use the *DB restriction* field to provide a SQL restriction that will be used against the pg_database table to limit the databases that you see. For example, you might enter: *live_db test_db* so that only live_db and test_db are shown in the pgAdmin browser. Separate entries with a comma or tab as you type.
* Use the *Password File* field to specify the location of a password file (.pgpass). A .pgpass file allows a user to login without providing a password when they connect. For more information, see `Section 33.15 of the Postgres documentation <http://www.postgresql.org/docs/current/static/libpq-pgpass.html>`_.
+* Use the *Service ID* field to specify the service name. For more information, see `Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/10/static/libpq-pgservice.html>`_.
*NOTE:* The password file option is only supported when pgAdmin is using libpq v10.0 or later to connect to the server.
diff --git a/web/migrations/versions/50aad68f99c2_.py b/web/migrations/versions/50aad68f99c2_.py
new file mode 100644
index 0000000..f8c97f2
--- /dev/null
+++ b/web/migrations/versions/50aad68f99c2_.py
@@ -0,0 +1,82 @@
+
+"""Added service field option in server table (RM#3140)
+
+Revision ID: 50aad68f99c2
+Revises: 02b9dccdcfcb
+Create Date: 2018-03-07 11:53:57.584280
+
+"""
+from pgadmin.model import db
+
+
+# revision identifiers, used by Alembic.
+revision = '50aad68f99c2'
+down_revision = '02b9dccdcfcb'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # To Save previous data
+ db.engine.execute("ALTER TABLE server RENAME TO server_old")
+
+ # With service file some fields won't be mandatory as user can provide
+ # them using service file. Removed NOT NULL constraint from few columns
+ db.engine.execute("""
+ CREATE TABLE server (
+ id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ servergroup_id INTEGER NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ host VARCHAR(128),
+ port INTEGER NOT NULL CHECK(port >= 1024 AND port <= 65534),
+ maintenance_db VARCHAR(64),
+ username VARCHAR(64) NOT NULL,
+ password VARCHAR(64),
+ role VARCHAR(64),
+ ssl_mode VARCHAR(16) NOT NULL CHECK(ssl_mode IN
+ ( 'allow' , 'prefer' , 'require' , 'disable' ,
+ 'verify-ca' , 'verify-full' )
+ ),
+ comment VARCHAR(1024),
+ discovery_id VARCHAR(128),
+ hostaddr TEXT(1024),
+ db_res TEXT,
+ passfile TEXT,
+ sslcert TEXT,
+ sslkey TEXT,
+ sslrootcert TEXT,
+ sslcrl TEXT,
+ sslcompression INTEGER DEFAULT 0,
+ bgcolor TEXT(10),
+ fgcolor TEXT(10),
+ PRIMARY KEY(id),
+ FOREIGN KEY(user_id) REFERENCES user(id),
+ FOREIGN KEY(servergroup_id) REFERENCES servergroup(id)
+ )
+ """)
+
+ # Copy old data again into table
+ db.engine.execute("""
+ INSERT INTO server (
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ ) SELECT
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ FROM server_old""")
+
+ # Remove old data
+ db.engine.execute("DROP TABLE server_old")
+
+ # Add column for Service ID
+ db.engine.execute(
+ 'ALTER TABLE server ADD COLUMN service TEXT'
+ )
+
+def downgrade():
+ pass
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index dfa9d62..fecb66d 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -478,7 +478,8 @@ class ServerNode(PGChildNodeView):
'sslcrl': 'sslcrl',
'sslcompression': 'sslcompression',
'bgcolor': 'bgcolor',
- 'fgcolor': 'fgcolor'
+ 'fgcolor': 'fgcolor',
+ 'service': 'service'
}
disp_lbl = {
@@ -515,7 +516,7 @@ class ServerNode(PGChildNodeView):
if connected:
for arg in (
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode',
- 'role'
+ 'role', 'service'
):
if arg in data:
return forbidden(
@@ -663,7 +664,8 @@ class ServerNode(PGChildNodeView):
'sslrootcert': server.sslrootcert if is_ssl else None,
'sslcrl': server.sslcrl if is_ssl else None,
'sslcompression': True if is_ssl and server.sslcompression
- else False
+ else False,
+ 'service': server.service if server.service else None
}
)
@@ -672,18 +674,22 @@ class ServerNode(PGChildNodeView):
"""Add a server node to the settings database"""
required_args = [
u'name',
- u'host',
u'port',
- u'db',
- u'username',
u'sslmode',
- u'role'
+ u'username'
]
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
+ # Some fields can be provided with service file so they are optional
+ if 'service' in data and not data['service']:
+ required_args.extend([
+ u'host',
+ u'db',
+ u'role'
+ ])
for arg in required_args:
if arg not in data:
return make_json_response(
@@ -711,29 +717,26 @@ class ServerNode(PGChildNodeView):
try:
server = Server(
user_id=current_user.id,
- servergroup_id=data[u'gid'] if u'gid' in data else gid,
- name=data[u'name'],
- host=data[u'host'],
- hostaddr=data[u'hostaddr'] if u'hostaddr' in data else None,
- port=data[u'port'],
- maintenance_db=data[u'db'],
- username=data[u'username'],
- ssl_mode=data[u'sslmode'],
- comment=data[u'comment'] if u'comment' in data else None,
- role=data[u'role'] if u'role' in data else None,
+ servergroup_id=data.get('gid', gid),
+ name=data.get('name'),
+ host=data.get('host', None),
+ hostaddr=data.get('hostaddr', None),
+ port=data.get('port'),
+ maintenance_db=data.get('db', None),
+ username=data.get('username'),
+ ssl_mode=data.get('sslmode'),
+ comment=data.get('comment', None),
+ role=data.get('role', None),
db_res=','.join(data[u'db_res'])
- if u'db_res' in data
- else None,
- sslcert=data['sslcert'] if is_ssl else None,
- sslkey=data['sslkey'] if is_ssl else None,
- sslrootcert=data['sslrootcert'] if is_ssl else None,
- sslcrl=data['sslcrl'] if is_ssl else None,
+ if u'db_res' in data else None,
+ sslcert=data.get('sslcert', None),
+ sslkey=data.get('sslkey', None),
+ sslrootcert=data.get('sslrootcert', None),
+ sslcrl=data.get('sslcrl', None),
sslcompression=1 if is_ssl and data['sslcompression'] else 0,
- bgcolor=data['bgcolor'] if u'bgcolor' in data
- else None,
- fgcolor=data['fgcolor'] if u'fgcolor' in data
- else None
-
+ bgcolor=data.get('bgcolor', None),
+ fgcolor=data.get('fgcolor', None),
+ service=data.get('service', None)
)
db.session.add(server)
db.session.commit()
@@ -930,7 +933,7 @@ class ServerNode(PGChildNodeView):
if 'password' not in data:
conn_passwd = getattr(conn, 'password', None)
if conn_passwd is None and server.password is None and \
- server.passfile is None:
+ server.passfile is None and server.service is None:
# Return the password template in case password is not
# provided, or password has not been saved earlier.
return make_json_response(
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 9932808..bedb38d 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -665,6 +665,7 @@ define('pgadmin.node.server', [
sslkey: undefined,
sslrootcert: undefined,
sslcrl: undefined,
+ service: undefined,
},
// Default values!
initialize: function(attrs, args) {
@@ -841,12 +842,18 @@ define('pgadmin.node.server', [
var passfile = m.get('passfile');
return !_.isUndefined(passfile) && !_.isNull(passfile);
},
+ },{
+ id: 'service', label: gettext('Service ID'), type: 'text',
+ mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
+ group: gettext('Advanced'),
}],
validate: function() {
var err = {},
errmsg,
self = this;
+ var service_id = this.get('service');
+
var check_for_empty = function(id, msg) {
var v = self.get(id);
if (
@@ -903,26 +910,41 @@ define('pgadmin.node.server', [
}
check_for_empty('name', gettext('Name must be specified.'));
- if (check_for_empty(
- 'host', gettext('Either Host name or Host address must be specified.')
- ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
- errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ // If no service id then only check
+ if (
+ _.isUndefined(service_id) || _.isNull(service_id) ||
+ String(service_id).replace(/^\s+|\s+$/g, '') == ''
+ ) {
+ if (check_for_empty(
+ 'host', gettext('Either Host name or Host address must be specified.')
+ ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
+ errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ } else {
+ errmsg = undefined;
+ delete err['host'];
+ delete err['hostaddr'];
+ }
+
+ check_for_empty(
+ 'db', gettext('Maintenance database must be specified.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
} else {
- errmsg = undefined;
- delete err['host'];
- delete err['hostaddr'];
+ _.each(['host', 'hostaddr', 'db'], (item) => {
+ self.errorModel.unset(item);
+ });
}
check_for_empty(
- 'db', gettext('Maintenance database must be specified.')
- );
- check_for_empty(
'username', gettext('Username must be specified.')
);
check_for_empty('port', gettext('Port must be specified.'));
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
+
this.errorModel.set(err);
if (_.size(err)) {
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
new file mode 100644
index 0000000..3b03d49
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
@@ -0,0 +1,47 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import json
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+
+
+class ServersWithServiceIDAddTestCase(BaseTestGenerator):
+ """ This class will add the servers under default server group. """
+
+ scenarios = [
+ # Fetch the default url for server object
+ (
+ 'Default Server Node url', dict(
+ url='/browser/server/obj/'
+ )
+ )
+ ]
+
+ def setUp(self):
+ pass
+
+ def runTest(self):
+ """ This function will add the server under default server group."""
+ url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+ # Add service name in the config
+ self.server['service'] = "TestDB"
+ response = self.tester.post(
+ url,
+ data=json.dumps(self.server),
+ content_type='html/json'
+ )
+ self.assertEquals(response.status_code, 200)
+ response_data = json.loads(response.data.decode('utf-8'))
+ self.server_id = response_data['node']['_id']
+
+ def tearDown(self):
+ """This function delete the server from SQLite """
+ utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index 674f945..11bc9f0 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -107,13 +107,13 @@ class Server(db.Model):
nullable=False
)
name = db.Column(db.String(128), nullable=False)
- host = db.Column(db.String(128), nullable=False)
+ host = db.Column(db.String(128), nullable=True)
hostaddr = db.Column(db.String(128), nullable=True)
port = db.Column(
db.Integer(),
db.CheckConstraint('port >= 1024 AND port <= 65534'),
nullable=False)
- maintenance_db = db.Column(db.String(64), nullable=False)
+ maintenance_db = db.Column(db.String(64), nullable=True)
username = db.Column(db.String(64), nullable=False)
password = db.Column(db.String(64), nullable=True)
role = db.Column(db.String(64), nullable=True)
@@ -144,6 +144,7 @@ class Server(db.Model):
)
bgcolor = db.Column(db.Text(10), nullable=True)
fgcolor = db.Column(db.Text(10), nullable=True)
+ service = db.Column(db.Text(), nullable=True)
class ModulePreference(db.Model):
diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py
index 941a694..95a49fb 100644
--- a/web/pgadmin/utils/driver/psycopg2/__init__.py
+++ b/web/pgadmin/utils/driver/psycopg2/__init__.py
@@ -8,1985 +8,23 @@
##########################################################################
"""
-Implementation of Connection, ServerManager and Driver classes using the
-psycopg2. It is a wrapper around the actual psycopg2 driver, and connection
+Implementation of Driver class
+It is a wrapper around the actual psycopg2 driver, and connection
object.
-"""
+"""
import datetime
-import os
-import random
-import select
-import sys
-
-import simplejson as json
-import psycopg2
-from flask import g, current_app, session
+from flask import session
from flask_babel import gettext
-from flask_security import current_user
-from pgadmin.utils.crypto import decrypt
-from psycopg2.extensions import adapt, encodings
+import psycopg2
+from psycopg2.extensions import adapt
import config
from pgadmin.model import Server, User
-from pgadmin.utils.exception import ConnectionLost
-from pgadmin.utils import get_complete_file_path
from .keywords import ScanKeyword
-from ..abstract import BaseDriver, BaseConnection
-from .cursor import DictCursor
-from .typecast import register_global_typecasters, \
- register_string_typecasters, register_binary_typecasters, \
- register_array_to_string_typecasters, ALL_JSON_TYPES
-from collections import deque
-
-
-if sys.version_info < (3,):
- # Python2 in-built csv module do not handle unicode
- # backports.csv module ported from PY3 csv module for unicode handling
- from backports import csv
- from StringIO import StringIO
- IS_PY2 = True
-else:
- from io import StringIO
- import csv
- IS_PY2 = False
-
-_ = gettext
-
-
-# Register global type caster which will be applicable to all connections.
-register_global_typecasters()
-
-
-class Connection(BaseConnection):
- """
- class Connection(object)
-
- A wrapper class, which wraps the psycopg2 connection object, and
- delegate the execution to the actual connection object, when required.
-
- Methods:
- -------
- * connect(**kwargs)
- - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
- driver
-
- * execute_scalar(query, params, formatted_exception_msg)
- - Execute the given query and returns single datum result
-
- * execute_async(query, params, formatted_exception_msg)
- - Execute the given query asynchronously and returns result.
-
- * execute_void(query, params, formatted_exception_msg)
- - Execute the given query with no result.
-
- * execute_2darray(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as a 2 dimensional
- array.
-
- * execute_dict(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as an array of dict
- (column name -> value) format.
-
- * connected()
- - Get the status of the connection.
- Returns True if connected, otherwise False.
-
- * reset()
- - Reconnect the database server (if possible)
-
- * transaction_status()
- - Transaction Status
-
- * ping()
- - Ping the server.
-
- * _release()
- - Release the connection object of psycopg2
-
- * _reconnect()
- - Attempt to reconnect to the database
-
- * _wait(conn)
- - This method is used to wait for asynchronous connection. This is a
- blocking call.
-
- * _wait_timeout(conn)
- - This method is used to wait for asynchronous connection with timeout.
- This is a non blocking call.
-
- * poll(formatted_exception_msg)
- - This method is used to poll the data of query running on asynchronous
- connection.
-
- * status_message()
- - Returns the status message returned by the last command executed on
- the server.
-
- * rows_affected()
- - Returns the no of rows affected by the last command executed on
- the server.
-
- * cancel_transaction(conn_id, did=None)
- - This method is used to cancel the transaction for the
- specified connection id and database id.
-
- * messages()
- - Returns the list of messages/notices sends from the PostgreSQL database
- server.
-
- * _formatted_exception_msg(exception_obj, formatted_msg)
- - This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- """
-
- def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
- use_binary_placeholder=False, array_to_string=False):
- assert (manager is not None)
- assert (conn_id is not None)
-
- self.conn_id = conn_id
- self.manager = manager
- self.db = db if db is not None else manager.db
- self.conn = None
- self.auto_reconnect = auto_reconnect
- self.async = async
- self.__async_cursor = None
- self.__async_query_id = None
- self.__backend_pid = None
- self.execution_aborted = False
- self.row_count = 0
- self.__notices = None
- self.password = None
- # This flag indicates the connection status (connected/disconnected).
- self.wasConnected = False
- # This flag indicates the connection reconnecting status.
- self.reconnecting = False
- self.use_binary_placeholder = use_binary_placeholder
- self.array_to_string = array_to_string
-
- super(Connection, self).__init__()
-
- def as_dict(self):
- """
- Returns the dictionary object representing this object.
- """
- # In case, it cannot be auto reconnectable, or already been released,
- # then we will return None.
- if not self.auto_reconnect and not self.conn:
- return None
-
- res = dict()
- res['conn_id'] = self.conn_id
- res['database'] = self.db
- res['async'] = self.async
- res['wasConnected'] = self.wasConnected
- res['auto_reconnect'] = self.auto_reconnect
- res['use_binary_placeholder'] = self.use_binary_placeholder
- res['array_to_string'] = self.array_to_string
-
- return res
-
- def __repr__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def __str__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def connect(self, **kwargs):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- else:
- return True, None
-
- pg_conn = None
- password = None
- passfile = None
- mgr = self.manager
-
- encpass = kwargs['password'] if 'password' in kwargs else None
- passfile = kwargs['passfile'] if 'passfile' in kwargs else None
-
- if encpass is None:
- encpass = self.password or getattr(mgr, 'password', None)
-
- # Reset the existing connection password
- if self.reconnecting is not False:
- self.password = None
-
- if encpass:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- try:
- password = decrypt(encpass, user.password)
- # Handling of non ascii password (Python2)
- if hasattr(str, 'decode'):
- password = password.decode('utf-8').encode('utf-8')
- # password is in bytes, for python3 we need it in string
- elif isinstance(password, bytes):
- password = password.decode()
-
- except Exception as e:
- 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
- # come from Query tool, ViewData grid, debugger etc tools.
- # we will check for pgpass file availability from connection manager
- # if it's present then we will use it
- if not password and not encpass and not passfile:
- passfile = mgr.passfile if mgr.passfile else None
-
- try:
- if hasattr(str, 'decode'):
- database = self.db.encode('utf-8')
- user = mgr.user.encode('utf-8')
- conn_id = self.conn_id.encode('utf-8')
- else:
- database = self.db
- user = mgr.user
- conn_id = self.conn_id
-
- import os
- os.environ['PGAPPNAME'] = '{0} - {1}'.format(
- config.APP_NAME, conn_id)
-
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=database,
- user=user,
- password=password,
- async=self.async,
- passfile=get_complete_file_path(passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- # If connection is asynchronous then we will have to wait
- # until the connection is ready to use.
- if self.async == 1:
- self._wait(pg_conn)
-
- except psycopg2.Error as e:
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- current_app.logger.info(
- u"Failed to connect to the database server(#{server_id}) for "
- u"connection ({conn_id}) with error message as below"
- u":{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
- )
- )
- return False, msg
-
- # Overwrite connection notice attr to support
- # more than 50 notices at a time
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.wasConnected = True
- try:
- status, msg = self._initialize(conn_id, **kwargs)
- except Exception as e:
- current_app.logger.exception(e)
- self.conn = None
- if not self.reconnecting:
- self.wasConnected = False
- raise e
-
- if status:
- mgr._update_password(encpass)
- else:
- if not self.reconnecting:
- self.wasConnected = False
-
- return status, msg
-
- def _initialize(self, conn_id, **kwargs):
- self.execution_aborted = False
- self.__backend_pid = self.conn.get_backend_pid()
-
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- status, cur = self.__cursor()
- formatted_exception_msg = self._formatted_exception_msg
- mgr = self.manager
-
- def _execute(cur, query, params=None):
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- return formatted_exception_msg(pe, False)
- return None
-
- # autocommit flag does not work with asynchronous connections.
- # By default asynchronous connection runs in autocommit mode.
- if self.async == 0:
- if 'autocommit' in kwargs and kwargs['autocommit'] is False:
- self.conn.autocommit = False
- else:
- self.conn.autocommit = True
-
- register_string_typecasters(self.conn)
-
- if self.array_to_string:
- register_array_to_string_typecasters(self.conn)
-
- # Register type casters for binary data only after registering array to
- # string type casters.
- if self.use_binary_placeholder:
- register_binary_typecasters(self.conn)
-
- status = _execute(cur, "SET DateStyle=ISO;"
- "SET client_min_messages=notice;"
- "SET bytea_output=escape;"
- "SET client_encoding='UNICODE';")
-
- if status is not None:
- self.conn.close()
- self.conn = None
-
- return False, status
-
- if mgr.role:
- status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
-
- if status is not None:
- self.conn.close()
- self.conn = None
- current_app.logger.error(
- "Connect to the database server (#{server_id}) for "
- "connection ({conn_id}), but - failed to setup the role "
- "with error message as below:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status
- )
- )
- return False, \
- _(
- "Failed to setup the role with error message:\n{0}"
- ).format(status)
-
- if mgr.ver is None:
- status = _execute(cur, "SELECT version()")
-
- if status is not None:
- self.conn.close()
- self.conn = None
- self.wasConnected = False
- current_app.logger.error(
- "Failed to fetch the version information on the "
- "established connection to the database server "
- "(#{server_id}) for '{conn_id}' with below error "
- "message:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status)
- )
- return False, status
-
- if cur.rowcount > 0:
- row = cur.fetchmany(1)[0]
- mgr.ver = row['version']
- mgr.sversion = self.conn.server_version
-
- status = _execute(cur, """
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.datname = current_database()""")
-
- if status is None:
- mgr.db_info = mgr.db_info or dict()
- if cur.rowcount > 0:
- res = cur.fetchmany(1)[0]
- mgr.db_info[res['did']] = res.copy()
-
- # We do not have database oid for the maintenance database.
- if len(mgr.db_info) == 1:
- mgr.did = res['did']
-
- status = _execute(cur, """
-SELECT
- oid as id, rolname as name, rolsuper as is_superuser,
- rolcreaterole as can_create_role, rolcreatedb as can_create_db
-FROM
- pg_catalog.pg_roles
-WHERE
- rolname = current_user""")
-
- if status is None:
- mgr.user_info = dict()
- if cur.rowcount > 0:
- mgr.user_info = cur.fetchmany(1)[0]
-
- if 'password' in kwargs:
- mgr.password = kwargs['password']
-
- server_types = None
- if 'server_types' in kwargs and isinstance(
- kwargs['server_types'], list):
- server_types = mgr.server_types = kwargs['server_types']
-
- if server_types is None:
- from pgadmin.browser.server_groups.servers.types import ServerType
- server_types = ServerType.types()
-
- for st in server_types:
- if st.instanceOf(mgr.ver):
- mgr.server_type = st.stype
- mgr.server_cls = st
- break
-
- mgr.update_session()
-
- return True, None
-
- def __cursor(self, server_cursor=False):
- if self.wasConnected is False:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- cur = getattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- if self.connected() and cur and not cur.closed:
- if not server_cursor or (server_cursor and cur.name):
- return True, cur
-
- if not self.connected():
- errmsg = ""
-
- current_app.logger.warning(
- "Connection to database server (#{server_id}) for the "
- "connection - '{conn_id}' has been lost.".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
-
- if self.auto_reconnect and not self.reconnecting:
- self.__attempt_execution_reconnect(None)
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- try:
- if server_cursor:
- # Providing name to cursor will create server side cursor.
- cursor_name = "CURSOR:{0}".format(self.conn_id)
- cur = self.conn.cursor(
- name=cursor_name, cursor_factory=DictCursor
- )
- else:
- cur = self.conn.cursor(cursor_factory=DictCursor)
- except psycopg2.Error as pe:
- current_app.logger.exception(pe)
- errmsg = gettext(
- "Failed to create cursor for psycopg2 connection with error "
- "message for the server#{1}:{2}:\n{0}"
- ).format(
- str(pe), self.manager.sid, self.db
- )
-
- current_app.logger.error(errmsg)
- if self.conn.closed:
- self.conn = None
- if self.auto_reconnect and not self.reconnecting:
- current_app.logger.info(
- gettext(
- "Attempting to reconnect to the database server "
- "(#{server_id}) for the connection - '{conn_id}'."
- ).format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- return self.__attempt_execution_reconnect(
- self.__cursor, server_cursor
- )
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:'
- else self.conn_id[5:]
- )
-
- setattr(
- g, "{0}#{1}".format(
- self.manager.sid, self.conn_id.encode('utf-8')
- ), cur
- )
-
- return True, cur
-
- def __internal_blocking_execute(self, cur, query, params):
- """
- This function executes the query using cursor's execute function,
- but in case of asynchronous connection we need to wait for the
- transaction to be completed. If self.async is 1 then it is a
- blocking call.
-
- Args:
- cur: Cursor object
- query: SQL query to run.
- params: Extra parameters
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- cur.execute(query, params)
- if self.async == 1:
- self._wait(cur.connection)
-
- def execute_on_server_as_csv(self,
- query, params=None,
- formatted_exception_msg=False,
- records=2000):
- """
- To fetch query result and generate CSV output
-
- Args:
- query: SQL
- params: Additional parameters
- formatted_exception_msg: For exception
- records: Number of initial records
- Returns:
- Generator response
- """
- status, cur = self.__cursor(server_cursor=True)
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- if IS_PY2 and type(query) == unicode:
- query = query.encode('utf-8')
-
- current_app.logger.log(
- 25,
- u"Execute (with server cursor) for server #{server_id} - "
- u"{conn_id} (Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8') if
- sys.version_info < (3,) else query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"failed to execute query ((with server cursor) "
- u"for the server #{server_id} - {conn_id} "
- u"(query-id: {query_id}):\nerror message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- def handle_json_data(json_columns, results):
- """
- [ This is only for Python2.x]
- This function will be useful to handle json data types.
- We will dump json data as proper json instead of unicode values
-
- Args:
- json_columns: Columns which contains json data
- results: Query result
-
- Returns:
- results
- """
- # Only if Python2 and there are columns with JSON type
- if IS_PY2 and len(json_columns) > 0:
- temp_results = []
- for row in results:
- res = dict()
- for k, v in row.items():
- if k in json_columns:
- res[k] = json.dumps(v)
- else:
- res[k] = v
- temp_results.append(res)
- results = temp_results
- return results
-
- def convert_keys_to_unicode(results, conn_encoding):
- """
- [ This is only for Python2.x]
- We need to convert all keys to unicode as psycopg2
- sends them as string
-
- Args:
- res: Query result set from psycopg2
- conn_encoding: Connection encoding
-
- Returns:
- Result set (With all the keys converted to unicode)
- """
- new_results = []
- for row in results:
- new_results.append(
- dict([(k.decode(conn_encoding), v)
- for k, v in row.items()])
- )
- return new_results
-
- def gen(quote='strings', quote_char="'", field_separator=','):
-
- results = cur.fetchmany(records)
- if not results:
- if not cur.closed:
- cur.close()
- yield gettext('The query executed did not return any data.')
- return
-
- header = []
- json_columns = []
- conn_encoding = cur.connection.encoding
-
- for c in cur.ordered_description():
- # This is to handle the case in which column name is non-ascii
- column_name = c.to_dict()['name']
- if IS_PY2:
- column_name = column_name.decode(conn_encoding)
- header.append(column_name)
- if c.to_dict()['type_code'] in ALL_JSON_TYPES:
- json_columns.append(column_name)
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- res_io = StringIO()
-
- if quote == 'strings':
- quote = csv.QUOTE_NONNUMERIC
- elif quote == 'all':
- quote = csv.QUOTE_ALL
- else:
- quote = csv.QUOTE_NONE
-
- if hasattr(str, 'decode'):
- # Decode the field_separator
- try:
- field_separator = field_separator.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- # Decode the quote_char
- try:
- quote_char = quote_char.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- csv_writer.writeheader()
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
-
- yield res_io.getvalue()
-
- while True:
- results = cur.fetchmany(records)
-
- if not results:
- if not cur.closed:
- cur.close()
- break
- res_io = StringIO()
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
- yield res_io.getvalue()
-
- return True, gen
-
- def execute_scalar(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_scalar) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- res = cur.fetchone()
- if len(res) > 0:
- return True, res[0]
-
- return True, None
-
- def execute_async(self, query, params=None, formatted_exception_msg=True):
- """
- This function executes the given query asynchronously and returns
- result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- self.__async_cursor = None
- status, cur = self.__cursor()
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- query_id=query_id
- )
- )
-
- try:
- self.__notices = []
- self.execution_aborted = False
- cur.execute(query, params)
- res = self._wait_timeout(cur.connection)
- except psycopg2.Error as pe:
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_async) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- errmsg=errmsg,
- query_id=query_id
- )
- )
-
- if self.is_disconnected(pe):
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- return False, errmsg
-
- self.__async_cursor = cur
- self.__async_query_id = query_id
-
- return True, res
-
- def execute_void(self, query, params=None, formatted_exception_msg=False):
- """
- This function executes the given query with no result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
-
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_void, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_void) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
-
- return True, None
-
- def __attempt_execution_reconnect(self, fn, *args, **kwargs):
- self.reconnecting = True
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
- try:
- status, res = self.connect()
- if status:
- if fn:
- status, res = fn(*args, **kwargs)
- self.reconnecting = False
- return status, res
- except Exception as e:
- current_app.logger.exception(e)
- self.reconnecting = False
-
- current_app.warning(
- "Failed to reconnect the database server "
- "(#{server_id})".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- self.reconnecting = False
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- def execute_2darray(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
-
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (2darray) for server #{server_id} - {conn_id} "
- u"(Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and \
- not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_2darray, query, params,
- formatted_exception_msg
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_2darray) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(row)
-
- return True, {'columns': columns, 'rows': rows}
-
- def execute_dict(self, query, params=None, formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_dict) for the server "
- u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query_id=query_id,
- errmsg=errmsg
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(dict(row))
-
- return True, {'columns': columns, 'rows': rows}
-
- def async_fetchmany_2darray(self, records=2000,
- formatted_exception_msg=False):
- """
- User should poll and check if status is ASYNC_OK before calling this
- function
- Args:
- records: no of records to fetch. use -1 to fetchall.
- formatted_exception_msg:
-
- Returns:
-
- """
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- if self.conn.isexecuting():
- return False, gettext(
- "Asynchronous query execution/operation underway."
- )
-
- if self.row_count > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML and
- # DDL operations, we need to rely on exception to figure
- # that out at the moment.
- try:
- if records == -1:
- res = cur.fetchall()
- else:
- res = cur.fetchmany(records)
- for row in res:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
- except psycopg2.ProgrammingError as e:
- result = None
- else:
- # User performed operation which dose not produce record/s as
- # result.
- # for eg. DDL operations.
- return True, None
-
- return True, result
-
- def connected(self):
- if self.conn:
- if not self.conn.closed:
- return True
- self.conn = None
- return False
-
- def reset(self):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- pg_conn = None
- mgr = self.manager
-
- password = getattr(mgr, 'password', None)
-
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=self.db,
- user=mgr.user,
- password=password,
- passfile=get_complete_file_path(mgr.passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- except psycopg2.Error as e:
- msg = e.pgerror if e.pgerror else e.message \
- if e.message else e.diag.message_detail \
- if e.diag.message_detail else str(e)
-
- current_app.logger.error(
- gettext(
- """
-Failed to reset the connection to the server due to following error:
-{0}"""
- ).Format(msg)
- )
- return False, msg
-
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.__backend_pid = pg_conn.get_backend_pid()
-
- return True, None
-
- def transaction_status(self):
- if self.conn:
- return self.conn.get_transaction_status()
- return None
-
- def ping(self):
- return self.execute_scalar('SELECT 1')
-
- def _release(self):
- if self.wasConnected:
- if self.conn:
- self.conn.close()
- self.conn = None
- self.password = None
- self.wasConnected = False
-
- def _wait(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method in a infinite loop till poll
- returns psycopg2.extensions.POLL_OK. This is a blocking
- call.
-
- Args:
- conn: connection object
- """
-
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- break
- elif state == psycopg2.extensions.POLL_WRITE:
- select.select([], [conn.fileno()], [])
- elif state == psycopg2.extensions.POLL_READ:
- select.select([conn.fileno()], [], [])
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait function" % state)
-
- def _wait_timeout(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method and return the status. If state is
- psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
- function will wait for the given timeout.This is not a blocking call.
-
- Args:
- conn: connection object
- """
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- return self.ASYNC_OK
- elif state == psycopg2.extensions.POLL_WRITE:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([], [conn.fileno()], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_WRITE_TIMEOUT
- elif state == psycopg2.extensions.POLL_READ:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([conn.fileno()], [], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_READ_TIMEOUT
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait_timeout function" % state
- )
-
- def poll(self, formatted_exception_msg=False, no_result=False):
- """
- This function is a wrapper around connection's poll function.
- It internally uses the _wait_timeout method to poll the
- result on the connection object. In case of success it
- returns the result of the query.
-
- Args:
- formatted_exception_msg: if True then function return the formatted
- exception message, otherwise error string.
- no_result: If True then only poll status will be returned.
- """
-
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Polling result for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- is_error = False
- try:
- status = self._wait_timeout(self.conn)
- except psycopg2.Error as pe:
- if self.conn.closed:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- is_error = True
-
- if self.conn.notices and self.__notices is not None:
- self.__notices.extend(self.conn.notices)
- self.conn.notices.clear()
-
- # We also need to fetch notices before we return from function in case
- # of any Exception, To avoid code duplication we will return after
- # fetching the notices in case of any Exception
- if is_error:
- return False, errmsg
-
- result = None
- self.row_count = 0
- self.column_info = None
-
- if status == self.ASYNC_OK:
-
- # if user has cancelled the transaction then changed the status
- if self.execution_aborted:
- status = self.ASYNC_EXECUTION_ABORTED
- self.execution_aborted = False
- return status, result
-
- # Fetch the column information
- if cur.description is not None:
- self.column_info = [
- desc.to_dict() for desc in cur.ordered_description()
- ]
-
- pos = 0
- for col in self.column_info:
- col['pos'] = pos
- pos += 1
-
- self.row_count = cur.rowcount
- if not no_result:
- if cur.rowcount > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML
- # and DDL operations, we need to rely on exception to
- # figure that out at the moment.
- try:
- for row in cur:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
-
- except psycopg2.ProgrammingError:
- result = None
-
- return status, result
-
- def status_message(self):
- """
- This function will return the status message returned by the last
- command executed on the server.
- """
- cur = self.__async_cursor
- if not cur:
- return gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Status message for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- return cur.statusmessage
-
- def rows_affected(self):
- """
- This function will return the no of rows affected by the last command
- executed on the server.
- """
-
- return self.row_count
-
- def get_column_info(self):
- """
- This function will returns list of columns for last async sql command
- executed on the server.
- """
-
- return self.column_info
-
- def cancel_transaction(self, conn_id, did=None):
- """
- This function is used to cancel the running transaction
- of the given connection id and database id using
- PostgreSQL's pg_cancel_backend.
-
- Args:
- conn_id: Connection id
- did: Database id (optional)
- """
- cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
- query = """SELECT pg_cancel_backend({0});""".format(
- cancel_conn.__backend_pid)
-
- status = True
- msg = ''
-
- # if backend pid is same then create a new connection
- # to cancel the query and release it.
- if cancel_conn.__backend_pid == self.__backend_pid:
- password = getattr(self.manager, 'password', None)
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=self.manager.host,
- hostaddr=self.manager.hostaddr,
- port=self.manager.port,
- database=self.db,
- user=self.manager.user,
- password=password,
- passfile=get_complete_file_path(self.manager.passfile),
- sslmode=self.manager.ssl_mode,
- sslcert=get_complete_file_path(self.manager.sslcert),
- sslkey=get_complete_file_path(self.manager.sslkey),
- sslrootcert=get_complete_file_path(
- self.manager.sslrootcert
- ),
- sslcrl=get_complete_file_path(self.manager.sslcrl),
- sslcompression=True if self.manager.sslcompression
- else False
- )
-
- # Get the cursor and run the query
- cur = pg_conn.cursor()
- cur.execute(query)
-
- # Close the connection
- pg_conn.close()
- pg_conn = None
-
- except psycopg2.Error as e:
- status = False
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- return status, msg
- else:
- if self.connected():
- status, msg = self.execute_void(query)
-
- if status:
- cancel_conn.execution_aborted = True
- else:
- status = False
- msg = gettext("Not connected to the database server.")
-
- return status, msg
-
- def messages(self):
- """
- Returns the list of the messages/notices send from the database server.
- """
- resp = []
- while self.__notices:
- resp.append(self.__notices.pop(0))
- return resp
-
- def decode_to_utf8(self, value):
- """
- This method will decode values to utf-8
- Args:
- value: String to be decode
-
- Returns:
- Decoded string
- """
- is_error = False
- if hasattr(str, 'decode'):
- try:
- value = value.decode('utf-8')
- except UnicodeDecodeError:
- # Let's try with python's preferred encoding
- # On Windows lc_messages mostly has environment dependent
- # encoding like 'French_France.1252'
- try:
- import locale
- pref_encoding = locale.getpreferredencoding()
- value = value.decode(pref_encoding)\
- .encode('utf-8')\
- .decode('utf-8')
- except Exception:
- is_error = True
- except Exception:
- is_error = True
-
- # If still not able to decode then
- if is_error:
- value = value.decode('ascii', 'ignore')
-
- return value
-
- def _formatted_exception_msg(self, exception_obj, formatted_msg):
- """
- This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- Args:
- exception_obj: exception object
- formatted_msg: if True then function return the formatted exception
- message
-
- """
- if exception_obj.pgerror:
- errmsg = exception_obj.pgerror
- elif exception_obj.diag.message_detail:
- errmsg = exception_obj.diag.message_detail
- else:
- errmsg = str(exception_obj)
- # errmsg might contains encoded value, lets decode it
- errmsg = self.decode_to_utf8(errmsg)
-
- # if formatted_msg is false then return from the function
- if not formatted_msg:
- return errmsg
-
- # Do not append if error starts with `ERROR:` as most pg related
- # error starts with `ERROR:`
- if not errmsg.startswith(u'ERROR:'):
- errmsg = u'ERROR: ' + errmsg + u'\n\n'
-
- if exception_obj.diag.severity is not None \
- and exception_obj.diag.message_primary is not None:
- ex_diag_message = u"{0}: {1}".format(
- exception_obj.diag.severity,
- self.decode_to_utf8(exception_obj.diag.message_primary)
- )
- # If both errors are different then only append it
- if errmsg and ex_diag_message and \
- ex_diag_message.strip().strip('\n').lower() not in \
- errmsg.strip().strip('\n').lower():
- errmsg += ex_diag_message
- elif exception_obj.diag.message_primary is not None:
- message_primary = self.decode_to_utf8(
- exception_obj.diag.message_primary
- )
- if message_primary.lower() not in errmsg.lower():
- errmsg += message_primary
-
- if exception_obj.diag.sqlstate is not None:
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('SQL state: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
-
- if exception_obj.diag.message_detail is not None:
- if 'Detail:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Detail: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.message_detail
- )
-
- if exception_obj.diag.message_hint is not None:
- if 'Hint:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Hint: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
-
- if exception_obj.diag.statement_position is not None:
- if 'Character:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Character: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.statement_position
- )
-
- if exception_obj.diag.context is not None:
- if 'Context:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Context: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.context)
-
- return errmsg
-
- #####
- # As per issue reported on pgsycopg2 github repository link is shared below
- # conn.closed is not reliable enough to identify the disconnection from the
- # database server for some unknown reasons.
- #
- # (https://github.com/psycopg/psycopg2/issues/263)
- #
- # In order to resolve the issue, sqlalchamey follows the below logic to
- # identify the disconnection. It relies on exception message to identify
- # the error.
- #
- # Reference (MIT license):
- # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
- #
- def is_disconnected(self, err):
- if not self.conn.closed:
- # checks based on strings. in the case that .closed
- # didn't cut it, fall back onto these.
- str_e = str(err).partition("\n")[0]
- for msg in [
- # these error messages from libpq: interfaces/libpq/fe-misc.c
- # and interfaces/libpq/fe-secure.c.
- 'terminating connection',
- 'closed the connection',
- 'connection not open',
- 'could not receive data from server',
- 'could not send data to server',
- # psycopg2 client errors, psycopg2/conenction.h,
- # psycopg2/cursor.h
- 'connection already closed',
- 'cursor already closed',
- # not sure where this path is originally from, it may
- # be obsolete. It really says "losed", not "closed".
- 'losed the connection unexpectedly',
- # these can occur in newer SSL
- 'connection has been closed unexpectedly',
- 'SSL SYSCALL error: Bad file descriptor',
- 'SSL SYSCALL error: EOF detected',
- ]:
- idx = str_e.find(msg)
- if idx >= 0 and '"' not in str_e[:idx]:
- return True
-
- return False
- return True
-
-
-class ServerManager(object):
- """
- class ServerManager
-
- This class contains the information about the given server.
- And, acts as connection manager for that particular session.
- """
-
- def __init__(self, server):
- self.connections = dict()
-
- self.update(server)
-
- def update(self, server):
- assert (server is not None)
- assert (isinstance(server, Server))
-
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.sid = server.id
- self.host = server.host
- self.hostaddr = server.hostaddr
- self.port = server.port
- self.db = server.maintenance_db
- self.did = None
- self.user = server.username
- self.password = server.password
- self.role = server.role
- self.ssl_mode = server.ssl_mode
- self.pinged = datetime.datetime.now()
- self.db_info = dict()
- self.server_types = None
- self.db_res = server.db_res
- self.passfile = server.passfile
- self.sslcert = server.sslcert
- self.sslkey = server.sslkey
- self.sslrootcert = server.sslrootcert
- self.sslcrl = server.sslcrl
- self.sslcompression = True if server.sslcompression else False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.update_session()
-
- self.connections = dict()
-
- def as_dict(self):
- """
- Returns a dictionary object representing the server manager.
- """
- if self.ver is None or len(self.connections) == 0:
- return None
-
- res = dict()
- res['sid'] = self.sid
- res['ver'] = self.ver
- res['sversion'] = self.sversion
- if hasattr(self, 'password') and self.password:
- # If running under PY2
- if hasattr(self.password, 'decode'):
- res['password'] = self.password.decode('utf-8')
- else:
- res['password'] = str(self.password)
- else:
- res['password'] = self.password
-
- connections = res['connections'] = dict()
-
- for conn_id in self.connections:
- conn = self.connections[conn_id].as_dict()
-
- if conn is not None:
- connections[conn_id] = conn
-
- return res
-
- def ServerVersion(self):
- return self.ver
-
- @property
- def version(self):
- return self.sversion
-
- def MajorVersion(self):
- if self.sversion is not None:
- return int(self.sversion / 10000)
- raise Exception("Information is not available.")
-
- def MinorVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) % 100)
- raise Exception("Information is not available.")
-
- def PatchVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) / 100)
- raise Exception("Information is not available.")
-
- def connection(
- self, database=None, conn_id=None, auto_reconnect=True, did=None,
- async=None, use_binary_placeholder=False, array_to_string=False
- ):
- if database is not None:
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if did is not None:
- if did in self.db_info:
- self.db_info[did]['datname'] = database
- else:
- if did is None:
- database = self.db
- elif did in self.db_info:
- database = self.db_info[did]['datname']
- else:
- maintenance_db_id = u'DB:{0}'.format(self.db)
- if maintenance_db_id in self.connections:
- conn = self.connections[maintenance_db_id]
- if conn.connected():
- status, res = conn.execute_dict(u"""
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.oid = {0}""".format(did))
-
- if status and len(res['rows']) > 0:
- for row in res['rows']:
- self.db_info[did] = row
- database = self.db_info[did]['datname']
-
- if did not in self.db_info:
- raise Exception(gettext(
- "Could not find the specified database."
- ))
-
- if database is None:
- raise ConnectionLost(self.sid, None, None)
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database))
-
- self.pinged = datetime.datetime.now()
-
- if my_id in self.connections:
- return self.connections[my_id]
- else:
- if async is None:
- async = 1 if conn_id is not None else 0
- else:
- async = 1 if async is True else 0
- self.connections[my_id] = Connection(
- self, my_id, database, auto_reconnect, async,
- use_binary_placeholder=use_binary_placeholder,
- array_to_string=array_to_string
- )
-
- return self.connections[my_id]
-
- def _restore(self, data):
- """
- Helps restoring to reconnect the auto-connect connections smoothly on
- reload/restart of the app server..
- """
- # restore server version from flask session if flask server was
- # restarted. As we need server version to resolve sql template paths.
-
- self.ver = data.get('ver', None)
- self.sversion = data.get('sversion', None)
-
- if self.ver and not self.server_type:
- from pgadmin.browser.server_groups.servers.types import ServerType
- for st in ServerType.types():
- if st.instanceOf(self.ver):
- self.server_type = st.stype
- self.server_cls = st
- break
-
- # Hmm.. we will not honour this request, when I already have
- # connections
- if len(self.connections) != 0:
- return
-
- # We need to know about the existing server variant supports during
- # first connection for identifications.
- from pgadmin.browser.server_groups.servers.types import ServerType
- self.pinged = datetime.datetime.now()
- try:
- if 'password' in data and data['password']:
- data['password'] = data['password'].encode('utf-8')
- except Exception as e:
- current_app.logger.exception(e)
-
- connections = data['connections']
- for conn_id in connections:
- conn_info = connections[conn_id]
- conn = self.connections[conn_info['conn_id']] = Connection(
- self, conn_info['conn_id'], conn_info['database'],
- conn_info['auto_reconnect'], conn_info['async'],
- use_binary_placeholder=conn_info['use_binary_placeholder'],
- array_to_string=conn_info['array_to_string']
- )
-
- # only try to reconnect if connection was connected previously and
- # auto_reconnect is true.
- if conn_info['wasConnected'] and conn_info['auto_reconnect']:
- try:
- conn.connect(
- password=data['password'],
- server_types=ServerType.types()
- )
- # This will also update wasConnected flag in connection so
- # no need to update the flag manually.
- except Exception as e:
- current_app.logger.exception(e)
- self.connections.pop(conn_info['conn_id'])
-
- def release(self, database=None, conn_id=None, did=None):
- if did is not None:
- if did in self.db_info and 'datname' in self.db_info[did]:
- database = self.db_info[did]['datname']
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if database is None:
- return False
- else:
- return False
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database)) if database is not None else None
-
- if my_id is not None:
- if my_id in self.connections:
- self.connections[my_id]._release()
- del self.connections[my_id]
- if did is not None:
- del self.db_info[did]
-
- if len(self.connections) == 0:
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
- else:
- return False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.connections = dict()
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
-
- def _update_password(self, passwd):
- self.password = passwd
- for conn_id in self.connections:
- conn = self.connections[conn_id]
- if conn.conn is not None or conn.wasConnected is True:
- conn.password = passwd
-
- def update_session(self):
- managers = session['__pgsql_server_managers'] \
- if '__pgsql_server_managers' in session else dict()
- updated_mgr = self.as_dict()
-
- if not updated_mgr:
- if self.sid in managers:
- managers.pop(self.sid)
- else:
- managers[self.sid] = updated_mgr
- session['__pgsql_server_managers'] = managers
- session.force_write = True
-
- def utility(self, operation):
- """
- utility(operation)
-
- Returns: name of the utility which used for the operation
- """
- if self.server_cls is not None:
- return self.server_cls.utility(operation, self.sversion)
-
- return None
-
- def export_password_env(self, env):
- if self.password:
- password = decrypt(
- self.password, current_user.password
- ).decode()
- os.environ[str(env)] = password
+from ..abstract import BaseDriver
+from .connection import Connection
+from .server_manager import ServerManager
class Driver(BaseDriver):
@@ -2164,8 +202,9 @@ class Driver(BaseDriver):
continue
if curr_time - sess_mgr['pinged'] >= session_idle_timeout:
- for mgr in [m for m in sess_mgr if isinstance(m,
- ServerManager)]:
+ for mgr in [
+ m for m in sess_mgr if isinstance(m, ServerManager)
+ ]:
mgr.release()
@staticmethod
diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py
new file mode 100644
index 0000000..d4c9573
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/connection.py
@@ -0,0 +1,1682 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of Connection.
+It is a wrapper around the actual psycopg2 driver, and connection
+object.
+"""
+
+import random
+import select
+import sys
+from collections import deque
+import simplejson as json
+import psycopg2
+from flask import g, current_app
+from flask_babel import gettext
+from flask_security import current_user
+from pgadmin.utils.crypto import decrypt
+from psycopg2.extensions import adapt, encodings
+
+import config
+from pgadmin.model import Server, User
+from pgadmin.utils.exception import ConnectionLost
+from pgadmin.utils import get_complete_file_path
+from ..abstract import BaseDriver, BaseConnection
+from .cursor import DictCursor
+from .typecast import register_global_typecasters, \
+ register_string_typecasters, register_binary_typecasters, \
+ register_array_to_string_typecasters, ALL_JSON_TYPES
+
+
+if sys.version_info < (3,):
+ # Python2 in-built csv module do not handle unicode
+ # backports.csv module ported from PY3 csv module for unicode handling
+ from backports import csv
+ from StringIO import StringIO
+ IS_PY2 = True
+else:
+ from io import StringIO
+ import csv
+ IS_PY2 = False
+
+_ = gettext
+
+
+# Register global type caster which will be applicable to all connections.
+register_global_typecasters()
+
+
+class Connection(BaseConnection):
+ """
+ class Connection(object)
+
+ A wrapper class, which wraps the psycopg2 connection object, and
+ delegate the execution to the actual connection object, when required.
+
+ Methods:
+ -------
+ * connect(**kwargs)
+ - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
+ driver
+
+ * execute_scalar(query, params, formatted_exception_msg)
+ - Execute the given query and returns single datum result
+
+ * execute_async(query, params, formatted_exception_msg)
+ - Execute the given query asynchronously and returns result.
+
+ * execute_void(query, params, formatted_exception_msg)
+ - Execute the given query with no result.
+
+ * execute_2darray(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as a 2 dimensional
+ array.
+
+ * execute_dict(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as an array of dict
+ (column name -> value) format.
+
+ * connected()
+ - Get the status of the connection.
+ Returns True if connected, otherwise False.
+
+ * reset()
+ - Reconnect the database server (if possible)
+
+ * transaction_status()
+ - Transaction Status
+
+ * ping()
+ - Ping the server.
+
+ * _release()
+ - Release the connection object of psycopg2
+
+ * _reconnect()
+ - Attempt to reconnect to the database
+
+ * _wait(conn)
+ - This method is used to wait for asynchronous connection. This is a
+ blocking call.
+
+ * _wait_timeout(conn)
+ - This method is used to wait for asynchronous connection with timeout.
+ This is a non blocking call.
+
+ * poll(formatted_exception_msg)
+ - This method is used to poll the data of query running on asynchronous
+ connection.
+
+ * status_message()
+ - Returns the status message returned by the last command executed on
+ the server.
+
+ * rows_affected()
+ - Returns the no of rows affected by the last command executed on
+ the server.
+
+ * cancel_transaction(conn_id, did=None)
+ - This method is used to cancel the transaction for the
+ specified connection id and database id.
+
+ * messages()
+ - Returns the list of messages/notices sends from the PostgreSQL database
+ server.
+
+ * _formatted_exception_msg(exception_obj, formatted_msg)
+ - This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ """
+
+ def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
+ use_binary_placeholder=False, array_to_string=False):
+ assert (manager is not None)
+ assert (conn_id is not None)
+
+ self.conn_id = conn_id
+ self.manager = manager
+ self.db = db if db is not None else manager.db
+ self.conn = None
+ self.auto_reconnect = auto_reconnect
+ self.async = async
+ self.__async_cursor = None
+ self.__async_query_id = None
+ self.__backend_pid = None
+ self.execution_aborted = False
+ self.row_count = 0
+ self.__notices = None
+ self.password = None
+ # This flag indicates the connection status (connected/disconnected).
+ self.wasConnected = False
+ # This flag indicates the connection reconnecting status.
+ self.reconnecting = False
+ self.use_binary_placeholder = use_binary_placeholder
+ self.array_to_string = array_to_string
+
+ super(Connection, self).__init__()
+
+ def as_dict(self):
+ """
+ Returns the dictionary object representing this object.
+ """
+ # In case, it cannot be auto reconnectable, or already been released,
+ # then we will return None.
+ if not self.auto_reconnect and not self.conn:
+ return None
+
+ res = dict()
+ res['conn_id'] = self.conn_id
+ res['database'] = self.db
+ res['async'] = self.async
+ res['wasConnected'] = self.wasConnected
+ res['auto_reconnect'] = self.auto_reconnect
+ res['use_binary_placeholder'] = self.use_binary_placeholder
+ res['array_to_string'] = self.array_to_string
+
+ return res
+
+ def __repr__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def __str__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def connect(self, **kwargs):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ else:
+ return True, None
+
+ pg_conn = None
+ password = None
+ passfile = None
+ mgr = self.manager
+
+ encpass = kwargs['password'] if 'password' in kwargs else None
+ passfile = kwargs['passfile'] if 'passfile' in kwargs else None
+
+ if encpass is None:
+ encpass = self.password or getattr(mgr, 'password', None)
+
+ # Reset the existing connection password
+ if self.reconnecting is not False:
+ self.password = None
+
+ if encpass:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ try:
+ password = decrypt(encpass, user.password)
+ # Handling of non ascii password (Python2)
+ if hasattr(str, 'decode'):
+ password = password.decode('utf-8').encode('utf-8')
+ # password is in bytes, for python3 we need it in string
+ elif isinstance(password, bytes):
+ password = password.decode()
+
+ except Exception as e:
+ 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
+ # come from Query tool, ViewData grid, debugger etc tools.
+ # we will check for pgpass file availability from connection manager
+ # if it's present then we will use it
+ if not password and not encpass and not passfile:
+ passfile = mgr.passfile if mgr.passfile else None
+
+ try:
+ if hasattr(str, 'decode'):
+ database = self.db.encode('utf-8')
+ user = mgr.user.encode('utf-8')
+ conn_id = self.conn_id.encode('utf-8')
+ else:
+ database = self.db
+ user = mgr.user
+ conn_id = self.conn_id
+
+ import os
+ os.environ['PGAPPNAME'] = '{0} - {1}'.format(
+ config.APP_NAME, conn_id)
+
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=database,
+ user=user,
+ password=password,
+ async=self.async,
+ passfile=get_complete_file_path(passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ # If connection is asynchronous then we will have to wait
+ # until the connection is ready to use.
+ if self.async == 1:
+ self._wait(pg_conn)
+
+ except psycopg2.Error as e:
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ current_app.logger.info(
+ u"Failed to connect to the database server(#{server_id}) for "
+ u"connection ({conn_id}) with error message as below"
+ u":{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
+ )
+ )
+ return False, msg
+
+ # Overwrite connection notice attr to support
+ # more than 50 notices at a time
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+
+ self.conn = pg_conn
+ self.wasConnected = True
+ try:
+ status, msg = self._initialize(conn_id, **kwargs)
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.conn = None
+ if not self.reconnecting:
+ self.wasConnected = False
+ raise e
+
+ if status:
+ mgr._update_password(encpass)
+ else:
+ if not self.reconnecting:
+ self.wasConnected = False
+
+ return status, msg
+
+ def _initialize(self, conn_id, **kwargs):
+ self.execution_aborted = False
+ self.__backend_pid = self.conn.get_backend_pid()
+
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ status, cur = self.__cursor()
+ formatted_exception_msg = self._formatted_exception_msg
+ mgr = self.manager
+
+ def _execute(cur, query, params=None):
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ return formatted_exception_msg(pe, False)
+ return None
+
+ # autocommit flag does not work with asynchronous connections.
+ # By default asynchronous connection runs in autocommit mode.
+ if self.async == 0:
+ if 'autocommit' in kwargs and kwargs['autocommit'] is False:
+ self.conn.autocommit = False
+ else:
+ self.conn.autocommit = True
+
+ register_string_typecasters(self.conn)
+
+ if self.array_to_string:
+ register_array_to_string_typecasters(self.conn)
+
+ # Register type casters for binary data only after registering array to
+ # string type casters.
+ if self.use_binary_placeholder:
+ register_binary_typecasters(self.conn)
+
+ status = _execute(cur, "SET DateStyle=ISO;"
+ "SET client_min_messages=notice;"
+ "SET bytea_output=escape;"
+ "SET client_encoding='UNICODE';")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+
+ return False, status
+
+ if mgr.role:
+ status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ current_app.logger.error(
+ "Connect to the database server (#{server_id}) for "
+ "connection ({conn_id}), but - failed to setup the role "
+ "with error message as below:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status
+ )
+ )
+ return False, \
+ _(
+ "Failed to setup the role with error message:\n{0}"
+ ).format(status)
+
+ if mgr.ver is None:
+ status = _execute(cur, "SELECT version()")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ self.wasConnected = False
+ current_app.logger.error(
+ "Failed to fetch the version information on the "
+ "established connection to the database server "
+ "(#{server_id}) for '{conn_id}' with below error "
+ "message:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status)
+ )
+ return False, status
+
+ if cur.rowcount > 0:
+ row = cur.fetchmany(1)[0]
+ mgr.ver = row['version']
+ mgr.sversion = self.conn.server_version
+
+ status = _execute(cur, """
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.datname = current_database()""")
+
+ if status is None:
+ mgr.db_info = mgr.db_info or dict()
+ if cur.rowcount > 0:
+ res = cur.fetchmany(1)[0]
+ mgr.db_info[res['did']] = res.copy()
+
+ # We do not have database oid for the maintenance database.
+ if len(mgr.db_info) == 1:
+ mgr.did = res['did']
+
+ status = _execute(cur, """
+SELECT
+ oid as id, rolname as name, rolsuper as is_superuser,
+ rolcreaterole as can_create_role, rolcreatedb as can_create_db
+FROM
+ pg_catalog.pg_roles
+WHERE
+ rolname = current_user""")
+
+ if status is None:
+ mgr.user_info = dict()
+ if cur.rowcount > 0:
+ mgr.user_info = cur.fetchmany(1)[0]
+
+ if 'password' in kwargs:
+ mgr.password = kwargs['password']
+
+ server_types = None
+ if 'server_types' in kwargs and isinstance(
+ kwargs['server_types'], list):
+ server_types = mgr.server_types = kwargs['server_types']
+
+ if server_types is None:
+ from pgadmin.browser.server_groups.servers.types import ServerType
+ server_types = ServerType.types()
+
+ for st in server_types:
+ if st.instanceOf(mgr.ver):
+ mgr.server_type = st.stype
+ mgr.server_cls = st
+ break
+
+ mgr.update_session()
+
+ return True, None
+
+ def __cursor(self, server_cursor=False):
+ if self.wasConnected is False:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ cur = getattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ if self.connected() and cur and not cur.closed:
+ if not server_cursor or (server_cursor and cur.name):
+ return True, cur
+
+ if not self.connected():
+ errmsg = ""
+
+ current_app.logger.warning(
+ "Connection to database server (#{server_id}) for the "
+ "connection - '{conn_id}' has been lost.".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+
+ if self.auto_reconnect and not self.reconnecting:
+ self.__attempt_execution_reconnect(None)
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ try:
+ if server_cursor:
+ # Providing name to cursor will create server side cursor.
+ cursor_name = "CURSOR:{0}".format(self.conn_id)
+ cur = self.conn.cursor(
+ name=cursor_name, cursor_factory=DictCursor
+ )
+ else:
+ cur = self.conn.cursor(cursor_factory=DictCursor)
+ except psycopg2.Error as pe:
+ current_app.logger.exception(pe)
+ errmsg = gettext(
+ "Failed to create cursor for psycopg2 connection with error "
+ "message for the server#{1}:{2}:\n{0}"
+ ).format(
+ str(pe), self.manager.sid, self.db
+ )
+
+ current_app.logger.error(errmsg)
+ if self.conn.closed:
+ self.conn = None
+ if self.auto_reconnect and not self.reconnecting:
+ current_app.logger.info(
+ gettext(
+ "Attempting to reconnect to the database server "
+ "(#{server_id}) for the connection - '{conn_id}'."
+ ).format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ return self.__attempt_execution_reconnect(
+ self.__cursor, server_cursor
+ )
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:'
+ else self.conn_id[5:]
+ )
+
+ setattr(
+ g, "{0}#{1}".format(
+ self.manager.sid, self.conn_id.encode('utf-8')
+ ), cur
+ )
+
+ return True, cur
+
+ def __internal_blocking_execute(self, cur, query, params):
+ """
+ This function executes the query using cursor's execute function,
+ but in case of asynchronous connection we need to wait for the
+ transaction to be completed. If self.async is 1 then it is a
+ blocking call.
+
+ Args:
+ cur: Cursor object
+ query: SQL query to run.
+ params: Extra parameters
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ cur.execute(query, params)
+ if self.async == 1:
+ self._wait(cur.connection)
+
+ def execute_on_server_as_csv(self,
+ query, params=None,
+ formatted_exception_msg=False,
+ records=2000):
+ """
+ To fetch query result and generate CSV output
+
+ Args:
+ query: SQL
+ params: Additional parameters
+ formatted_exception_msg: For exception
+ records: Number of initial records
+ Returns:
+ Generator response
+ """
+ status, cur = self.__cursor(server_cursor=True)
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ if IS_PY2 and type(query) == unicode:
+ query = query.encode('utf-8')
+
+ current_app.logger.log(
+ 25,
+ u"Execute (with server cursor) for server #{server_id} - "
+ u"{conn_id} (Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8') if
+ sys.version_info < (3,) else query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"failed to execute query ((with server cursor) "
+ u"for the server #{server_id} - {conn_id} "
+ u"(query-id: {query_id}):\nerror message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ def handle_json_data(json_columns, results):
+ """
+ [ This is only for Python2.x]
+ This function will be useful to handle json data types.
+ We will dump json data as proper json instead of unicode values
+
+ Args:
+ json_columns: Columns which contains json data
+ results: Query result
+
+ Returns:
+ results
+ """
+ # Only if Python2 and there are columns with JSON type
+ if IS_PY2 and len(json_columns) > 0:
+ temp_results = []
+ for row in results:
+ res = dict()
+ for k, v in row.items():
+ if k in json_columns:
+ res[k] = json.dumps(v)
+ else:
+ res[k] = v
+ temp_results.append(res)
+ results = temp_results
+ return results
+
+ def convert_keys_to_unicode(results, conn_encoding):
+ """
+ [ This is only for Python2.x]
+ We need to convert all keys to unicode as psycopg2
+ sends them as string
+
+ Args:
+ res: Query result set from psycopg2
+ conn_encoding: Connection encoding
+
+ Returns:
+ Result set (With all the keys converted to unicode)
+ """
+ new_results = []
+ for row in results:
+ new_results.append(
+ dict([(k.decode(conn_encoding), v)
+ for k, v in row.items()])
+ )
+ return new_results
+
+ def gen(quote='strings', quote_char="'", field_separator=','):
+
+ results = cur.fetchmany(records)
+ if not results:
+ if not cur.closed:
+ cur.close()
+ yield gettext('The query executed did not return any data.')
+ return
+
+ header = []
+ json_columns = []
+ conn_encoding = cur.connection.encoding
+
+ for c in cur.ordered_description():
+ # This is to handle the case in which column name is non-ascii
+ column_name = c.to_dict()['name']
+ if IS_PY2:
+ column_name = column_name.decode(conn_encoding)
+ header.append(column_name)
+ if c.to_dict()['type_code'] in ALL_JSON_TYPES:
+ json_columns.append(column_name)
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ res_io = StringIO()
+
+ if quote == 'strings':
+ quote = csv.QUOTE_NONNUMERIC
+ elif quote == 'all':
+ quote = csv.QUOTE_ALL
+ else:
+ quote = csv.QUOTE_NONE
+
+ if hasattr(str, 'decode'):
+ # Decode the field_separator
+ try:
+ field_separator = field_separator.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ # Decode the quote_char
+ try:
+ quote_char = quote_char.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ csv_writer.writeheader()
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+
+ yield res_io.getvalue()
+
+ while True:
+ results = cur.fetchmany(records)
+
+ if not results:
+ if not cur.closed:
+ cur.close()
+ break
+ res_io = StringIO()
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+ yield res_io.getvalue()
+
+ return True, gen
+
+ def execute_scalar(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_scalar) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ res = cur.fetchone()
+ if len(res) > 0:
+ return True, res[0]
+
+ return True, None
+
+ def execute_async(self, query, params=None, formatted_exception_msg=True):
+ """
+ This function executes the given query asynchronously and returns
+ result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ self.__async_cursor = None
+ status, cur = self.__cursor()
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__notices = []
+ self.execution_aborted = False
+ cur.execute(query, params)
+ res = self._wait_timeout(cur.connection)
+ except psycopg2.Error as pe:
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_async) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+
+ if self.is_disconnected(pe):
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ return False, errmsg
+
+ self.__async_cursor = cur
+ self.__async_query_id = query_id
+
+ return True, res
+
+ def execute_void(self, query, params=None, formatted_exception_msg=False):
+ """
+ This function executes the given query with no result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_void, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_void) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+
+ return True, None
+
+ def __attempt_execution_reconnect(self, fn, *args, **kwargs):
+ self.reconnecting = True
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+ try:
+ status, res = self.connect()
+ if status:
+ if fn:
+ status, res = fn(*args, **kwargs)
+ self.reconnecting = False
+ return status, res
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.reconnecting = False
+
+ current_app.warning(
+ "Failed to reconnect the database server "
+ "(#{server_id})".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ self.reconnecting = False
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ def execute_2darray(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (2darray) for server #{server_id} - {conn_id} "
+ u"(Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and \
+ not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_2darray, query, params,
+ formatted_exception_msg
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_2darray) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(row)
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def execute_dict(self, query, params=None, formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_dict) for the server "
+ u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query_id=query_id,
+ errmsg=errmsg
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(dict(row))
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def async_fetchmany_2darray(self, records=2000,
+ formatted_exception_msg=False):
+ """
+ User should poll and check if status is ASYNC_OK before calling this
+ function
+ Args:
+ records: no of records to fetch. use -1 to fetchall.
+ formatted_exception_msg:
+
+ Returns:
+
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ if self.conn.isexecuting():
+ return False, gettext(
+ "Asynchronous query execution/operation underway."
+ )
+
+ if self.row_count > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML and
+ # DDL operations, we need to rely on exception to figure
+ # that out at the moment.
+ try:
+ if records == -1:
+ res = cur.fetchall()
+ else:
+ res = cur.fetchmany(records)
+ for row in res:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+ except psycopg2.ProgrammingError as e:
+ result = None
+ else:
+ # User performed operation which dose not produce record/s as
+ # result.
+ # for eg. DDL operations.
+ return True, None
+
+ return True, result
+
+ def connected(self):
+ if self.conn:
+ if not self.conn.closed:
+ return True
+ self.conn = None
+ return False
+
+ def reset(self):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ pg_conn = None
+ mgr = self.manager
+
+ password = getattr(mgr, 'password', None)
+
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=self.db,
+ user=mgr.user,
+ password=password,
+ passfile=get_complete_file_path(mgr.passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ except psycopg2.Error as e:
+ msg = e.pgerror if e.pgerror else e.message \
+ if e.message else e.diag.message_detail \
+ if e.diag.message_detail else str(e)
+
+ current_app.logger.error(
+ gettext(
+ """
+Failed to reset the connection to the server due to following error:
+{0}"""
+ ).Format(msg)
+ )
+ return False, msg
+
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+ self.conn = pg_conn
+ self.__backend_pid = pg_conn.get_backend_pid()
+
+ return True, None
+
+ def transaction_status(self):
+ if self.conn:
+ return self.conn.get_transaction_status()
+ return None
+
+ def ping(self):
+ return self.execute_scalar('SELECT 1')
+
+ def _release(self):
+ if self.wasConnected:
+ if self.conn:
+ self.conn.close()
+ self.conn = None
+ self.password = None
+ self.wasConnected = False
+
+ def _wait(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method in a infinite loop till poll
+ returns psycopg2.extensions.POLL_OK. This is a blocking
+ call.
+
+ Args:
+ conn: connection object
+ """
+
+ while 1:
+ state = conn.poll()
+ if state == psycopg2.extensions.POLL_OK:
+ break
+ elif state == psycopg2.extensions.POLL_WRITE:
+ select.select([], [conn.fileno()], [])
+ elif state == psycopg2.extensions.POLL_READ:
+ select.select([conn.fileno()], [], [])
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait function" % state)
+
+ def _wait_timeout(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method and return the status. If state is
+ psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
+ function will wait for the given timeout.This is not a blocking call.
+
+ Args:
+ conn: connection object
+ time: wait time
+ """
+
+ while 1:
+ state = conn.poll()
+
+ if state == psycopg2.extensions.POLL_OK:
+ return self.ASYNC_OK
+ elif state == psycopg2.extensions.POLL_WRITE:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [], [conn.fileno()], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_WRITE_TIMEOUT
+ elif state == psycopg2.extensions.POLL_READ:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [conn.fileno()], [], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_READ_TIMEOUT
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait_timeout function" % state
+ )
+
+ def poll(self, formatted_exception_msg=False, no_result=False):
+ """
+ This function is a wrapper around connection's poll function.
+ It internally uses the _wait_timeout method to poll the
+ result on the connection object. In case of success it
+ returns the result of the query.
+
+ Args:
+ formatted_exception_msg: if True then function return the formatted
+ exception message, otherwise error string.
+ no_result: If True then only poll status will be returned.
+ """
+
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Polling result for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ is_error = False
+ try:
+ status = self._wait_timeout(self.conn)
+ except psycopg2.Error as pe:
+ if self.conn.closed:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ is_error = True
+
+ if self.conn.notices and self.__notices is not None:
+ self.__notices.extend(self.conn.notices)
+ self.conn.notices.clear()
+
+ # We also need to fetch notices before we return from function in case
+ # of any Exception, To avoid code duplication we will return after
+ # fetching the notices in case of any Exception
+ if is_error:
+ return False, errmsg
+
+ result = None
+ self.row_count = 0
+ self.column_info = None
+
+ if status == self.ASYNC_OK:
+
+ # if user has cancelled the transaction then changed the status
+ if self.execution_aborted:
+ status = self.ASYNC_EXECUTION_ABORTED
+ self.execution_aborted = False
+ return status, result
+
+ # Fetch the column information
+ if cur.description is not None:
+ self.column_info = [
+ desc.to_dict() for desc in cur.ordered_description()
+ ]
+
+ pos = 0
+ for col in self.column_info:
+ col['pos'] = pos
+ pos += 1
+
+ self.row_count = cur.rowcount
+ if not no_result:
+ if cur.rowcount > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML
+ # and DDL operations, we need to rely on exception to
+ # figure that out at the moment.
+ try:
+ for row in cur:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+
+ except psycopg2.ProgrammingError:
+ result = None
+
+ return status, result
+
+ def status_message(self):
+ """
+ This function will return the status message returned by the last
+ command executed on the server.
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Status message for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ return cur.statusmessage
+
+ def rows_affected(self):
+ """
+ This function will return the no of rows affected by the last command
+ executed on the server.
+ """
+
+ return self.row_count
+
+ def get_column_info(self):
+ """
+ This function will returns list of columns for last async sql command
+ executed on the server.
+ """
+
+ return self.column_info
+
+ def cancel_transaction(self, conn_id, did=None):
+ """
+ This function is used to cancel the running transaction
+ of the given connection id and database id using
+ PostgreSQL's pg_cancel_backend.
+
+ Args:
+ conn_id: Connection id
+ did: Database id (optional)
+ """
+ cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
+ query = """SELECT pg_cancel_backend({0});""".format(
+ cancel_conn.__backend_pid)
+
+ status = True
+ msg = ''
+
+ # if backend pid is same then create a new connection
+ # to cancel the query and release it.
+ if cancel_conn.__backend_pid == self.__backend_pid:
+ password = getattr(self.manager, 'password', None)
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=self.manager.host,
+ hostaddr=self.manager.hostaddr,
+ port=self.manager.port,
+ database=self.db,
+ user=self.manager.user,
+ password=password,
+ passfile=get_complete_file_path(self.manager.passfile),
+ sslmode=self.manager.ssl_mode,
+ sslcert=get_complete_file_path(self.manager.sslcert),
+ sslkey=get_complete_file_path(self.manager.sslkey),
+ sslrootcert=get_complete_file_path(
+ self.manager.sslrootcert
+ ),
+ sslcrl=get_complete_file_path(self.manager.sslcrl),
+ sslcompression=True if self.manager.sslcompression
+ else False,
+ service=self.manager.service
+ )
+
+ # Get the cursor and run the query
+ cur = pg_conn.cursor()
+ cur.execute(query)
+
+ # Close the connection
+ pg_conn.close()
+ pg_conn = None
+
+ except psycopg2.Error as e:
+ status = False
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ return status, msg
+ else:
+ if self.connected():
+ status, msg = self.execute_void(query)
+
+ if status:
+ cancel_conn.execution_aborted = True
+ else:
+ status = False
+ msg = gettext("Not connected to the database server.")
+
+ return status, msg
+
+ def messages(self):
+ """
+ Returns the list of the messages/notices send from the database server.
+ """
+ resp = []
+ while self.__notices:
+ resp.append(self.__notices.pop(0))
+ return resp
+
+ def decode_to_utf8(self, value):
+ """
+ This method will decode values to utf-8
+ Args:
+ value: String to be decode
+
+ Returns:
+ Decoded string
+ """
+ is_error = False
+ if hasattr(str, 'decode'):
+ try:
+ value = value.decode('utf-8')
+ except UnicodeDecodeError:
+ # Let's try with python's preferred encoding
+ # On Windows lc_messages mostly has environment dependent
+ # encoding like 'French_France.1252'
+ try:
+ import locale
+ pref_encoding = locale.getpreferredencoding()
+ value = value.decode(pref_encoding)\
+ .encode('utf-8')\
+ .decode('utf-8')
+ except Exception:
+ is_error = True
+ except Exception:
+ is_error = True
+
+ # If still not able to decode then
+ if is_error:
+ value = value.decode('ascii', 'ignore')
+
+ return value
+
+ def _formatted_exception_msg(self, exception_obj, formatted_msg):
+ """
+ This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ Args:
+ exception_obj: exception object
+ formatted_msg: if True then function return the formatted exception
+ message
+
+ """
+ if exception_obj.pgerror:
+ errmsg = exception_obj.pgerror
+ elif exception_obj.diag.message_detail:
+ errmsg = exception_obj.diag.message_detail
+ else:
+ errmsg = str(exception_obj)
+ # errmsg might contains encoded value, lets decode it
+ errmsg = self.decode_to_utf8(errmsg)
+
+ # if formatted_msg is false then return from the function
+ if not formatted_msg:
+ return errmsg
+
+ # Do not append if error starts with `ERROR:` as most pg related
+ # error starts with `ERROR:`
+ if not errmsg.startswith(u'ERROR:'):
+ errmsg = u'ERROR: ' + errmsg + u'\n\n'
+
+ if exception_obj.diag.severity is not None \
+ and exception_obj.diag.message_primary is not None:
+ ex_diag_message = u"{0}: {1}".format(
+ exception_obj.diag.severity,
+ self.decode_to_utf8(exception_obj.diag.message_primary)
+ )
+ # If both errors are different then only append it
+ if errmsg and ex_diag_message and \
+ ex_diag_message.strip().strip('\n').lower() not in \
+ errmsg.strip().strip('\n').lower():
+ errmsg += ex_diag_message
+ elif exception_obj.diag.message_primary is not None:
+ message_primary = self.decode_to_utf8(
+ exception_obj.diag.message_primary
+ )
+ if message_primary.lower() not in errmsg.lower():
+ errmsg += message_primary
+
+ if exception_obj.diag.sqlstate is not None:
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('SQL state: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
+
+ if exception_obj.diag.message_detail is not None:
+ if 'Detail:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Detail: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.message_detail
+ )
+
+ if exception_obj.diag.message_hint is not None:
+ if 'Hint:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Hint: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
+
+ if exception_obj.diag.statement_position is not None:
+ if 'Character:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Character: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.statement_position
+ )
+
+ if exception_obj.diag.context is not None:
+ if 'Context:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Context: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.context)
+
+ return errmsg
+
+ #####
+ # As per issue reported on pgsycopg2 github repository link is shared below
+ # conn.closed is not reliable enough to identify the disconnection from the
+ # database server for some unknown reasons.
+ #
+ # (https://github.com/psycopg/psycopg2/issues/263)
+ #
+ # In order to resolve the issue, sqlalchamey follows the below logic to
+ # identify the disconnection. It relies on exception message to identify
+ # the error.
+ #
+ # Reference (MIT license):
+ # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
+ #
+ def is_disconnected(self, err):
+ if not self.conn.closed:
+ # checks based on strings. in the case that .closed
+ # didn't cut it, fall back onto these.
+ str_e = str(err).partition("\n")[0]
+ for msg in [
+ # these error messages from libpq: interfaces/libpq/fe-misc.c
+ # and interfaces/libpq/fe-secure.c.
+ 'terminating connection',
+ 'closed the connection',
+ 'connection not open',
+ 'could not receive data from server',
+ 'could not send data to server',
+ # psycopg2 client errors, psycopg2/conenction.h,
+ # psycopg2/cursor.h
+ 'connection already closed',
+ 'cursor already closed',
+ # not sure where this path is originally from, it may
+ # be obsolete. It really says "losed", not "closed".
+ 'losed the connection unexpectedly',
+ # these can occur in newer SSL
+ 'connection has been closed unexpectedly',
+ 'SSL SYSCALL error: Bad file descriptor',
+ 'SSL SYSCALL error: EOF detected',
+ ]:
+ idx = str_e.find(msg)
+ if idx >= 0 and '"' not in str_e[:idx]:
+ return True
+
+ return False
+ return True
diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
new file mode 100644
index 0000000..2299e28
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -0,0 +1,333 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of ServerManager
+"""
+import os
+import datetime
+from flask import current_app, session
+from flask_security import current_user
+from flask_babel import gettext
+
+from pgadmin.utils.crypto import decrypt
+from .connection import Connection
+from pgadmin.model import Server
+
+
+class ServerManager(object):
+ """
+ class ServerManager
+
+ This class contains the information about the given server.
+ And, acts as connection manager for that particular session.
+ """
+
+ def __init__(self, server):
+ self.connections = dict()
+
+ self.update(server)
+
+ def update(self, server):
+ assert (server is not None)
+ assert (isinstance(server, Server))
+
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.sid = server.id
+ self.host = server.host
+ self.hostaddr = server.hostaddr
+ self.port = server.port
+ self.db = server.maintenance_db
+ self.did = None
+ self.user = server.username
+ self.password = server.password
+ self.role = server.role
+ self.ssl_mode = server.ssl_mode
+ self.pinged = datetime.datetime.now()
+ self.db_info = dict()
+ self.server_types = None
+ self.db_res = server.db_res
+ self.passfile = server.passfile
+ self.sslcert = server.sslcert
+ self.sslkey = server.sslkey
+ self.sslrootcert = server.sslrootcert
+ self.sslcrl = server.sslcrl
+ self.sslcompression = True if server.sslcompression else False
+ self.service = server.service
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.update_session()
+
+ self.connections = dict()
+
+ def as_dict(self):
+ """
+ Returns a dictionary object representing the server manager.
+ """
+ if self.ver is None or len(self.connections) == 0:
+ return None
+
+ res = dict()
+ res['sid'] = self.sid
+ res['ver'] = self.ver
+ res['sversion'] = self.sversion
+ if hasattr(self, 'password') and self.password:
+ # If running under PY2
+ if hasattr(self.password, 'decode'):
+ res['password'] = self.password.decode('utf-8')
+ else:
+ res['password'] = str(self.password)
+ else:
+ res['password'] = self.password
+
+ connections = res['connections'] = dict()
+
+ for conn_id in self.connections:
+ conn = self.connections[conn_id].as_dict()
+
+ if conn is not None:
+ connections[conn_id] = conn
+
+ return res
+
+ def ServerVersion(self):
+ return self.ver
+
+ @property
+ def version(self):
+ return self.sversion
+
+ def MajorVersion(self):
+ if self.sversion is not None:
+ return int(self.sversion / 10000)
+ raise Exception("Information is not available.")
+
+ def MinorVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) % 100)
+ raise Exception("Information is not available.")
+
+ def PatchVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) / 100)
+ raise Exception("Information is not available.")
+
+ def connection(
+ self, database=None, conn_id=None, auto_reconnect=True, did=None,
+ async=None, use_binary_placeholder=False, array_to_string=False
+ ):
+ if database is not None:
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if did is not None:
+ if did in self.db_info:
+ self.db_info[did]['datname'] = database
+ else:
+ if did is None:
+ database = self.db
+ elif did in self.db_info:
+ database = self.db_info[did]['datname']
+ else:
+ maintenance_db_id = u'DB:{0}'.format(self.db)
+ if maintenance_db_id in self.connections:
+ conn = self.connections[maintenance_db_id]
+ if conn.connected():
+ status, res = conn.execute_dict(u"""
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.oid = {0}""".format(did))
+
+ if status and len(res['rows']) > 0:
+ for row in res['rows']:
+ self.db_info[did] = row
+ database = self.db_info[did]['datname']
+
+ if did not in self.db_info:
+ raise Exception(gettext(
+ "Could not find the specified database."
+ ))
+
+ if database is None:
+ raise ConnectionLost(self.sid, None, None)
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database))
+
+ self.pinged = datetime.datetime.now()
+
+ if my_id in self.connections:
+ return self.connections[my_id]
+ else:
+ if async is None:
+ async = 1 if conn_id is not None else 0
+ else:
+ async = 1 if async is True else 0
+ self.connections[my_id] = Connection(
+ self, my_id, database, auto_reconnect, async,
+ use_binary_placeholder=use_binary_placeholder,
+ array_to_string=array_to_string
+ )
+
+ return self.connections[my_id]
+
+ def _restore(self, data):
+ """
+ Helps restoring to reconnect the auto-connect connections smoothly on
+ reload/restart of the app server..
+ """
+ # restore server version from flask session if flask server was
+ # restarted. As we need server version to resolve sql template paths.
+ from pgadmin.browser.server_groups.servers.types import ServerType
+
+ self.ver = data.get('ver', None)
+ self.sversion = data.get('sversion', None)
+
+ if self.ver and not self.server_type:
+ for st in ServerType.types():
+ if st.instanceOf(self.ver):
+ self.server_type = st.stype
+ self.server_cls = st
+ break
+
+ # Hmm.. we will not honour this request, when I already have
+ # connections
+ if len(self.connections) != 0:
+ return
+
+ # We need to know about the existing server variant supports during
+ # first connection for identifications.
+ self.pinged = datetime.datetime.now()
+ try:
+ if 'password' in data and data['password']:
+ data['password'] = data['password'].encode('utf-8')
+ except Exception as e:
+ current_app.logger.exception(e)
+
+ connections = data['connections']
+ for conn_id in connections:
+ conn_info = connections[conn_id]
+ conn = self.connections[conn_info['conn_id']] = Connection(
+ self, conn_info['conn_id'], conn_info['database'],
+ conn_info['auto_reconnect'], conn_info['async'],
+ use_binary_placeholder=conn_info['use_binary_placeholder'],
+ array_to_string=conn_info['array_to_string']
+ )
+
+ # only try to reconnect if connection was connected previously and
+ # auto_reconnect is true.
+ if conn_info['wasConnected'] and conn_info['auto_reconnect']:
+ try:
+ conn.connect(
+ password=data['password'],
+ server_types=ServerType.types()
+ )
+ # This will also update wasConnected flag in connection so
+ # no need to update the flag manually.
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.connections.pop(conn_info['conn_id'])
+
+ def release(self, database=None, conn_id=None, did=None):
+ if did is not None:
+ if did in self.db_info and 'datname' in self.db_info[did]:
+ database = self.db_info[did]['datname']
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if database is None:
+ return False
+ else:
+ return False
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database)) if database is not None else None
+
+ if my_id is not None:
+ if my_id in self.connections:
+ self.connections[my_id]._release()
+ del self.connections[my_id]
+ if did is not None:
+ del self.db_info[did]
+
+ if len(self.connections) == 0:
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+ else:
+ return False
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.connections = dict()
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+
+ def _update_password(self, passwd):
+ self.password = passwd
+ for conn_id in self.connections:
+ conn = self.connections[conn_id]
+ if conn.conn is not None or conn.wasConnected is True:
+ conn.password = passwd
+
+ def update_session(self):
+ managers = session['__pgsql_server_managers'] \
+ if '__pgsql_server_managers' in session else dict()
+ updated_mgr = self.as_dict()
+
+ if not updated_mgr:
+ if self.sid in managers:
+ managers.pop(self.sid)
+ else:
+ managers[self.sid] = updated_mgr
+ session['__pgsql_server_managers'] = managers
+ session.force_write = True
+
+ def utility(self, operation):
+ """
+ utility(operation)
+
+ Returns: name of the utility which used for the operation
+ """
+ if self.server_cls is not None:
+ return self.server_cls.utility(operation, self.sversion)
+
+ return None
+
+ def export_password_env(self, env):
+ if self.password:
+ password = decrypt(
+ self.password, current_user.password
+ ).decode()
+ os.environ[str(env)] = password
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
@ 2018-03-09 15:55 ` Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Dave Page @ 2018-03-09 15:55 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; +Cc: pgadmin-hackers
HI
On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
[email protected]> wrote:
> Hi,
>
> PFA patch to add service parameter in server dialog.
> - Docs updated
> - Test case added for Service ID parameter
>
> Please note,
> I have extracted Connection class and Server manager class from our own
> custom Psycopg2 driver module.
>
> Patch also covers RM#3120
>
This patch seems a little confused. The "Service" and "Service ID" fields
from pgAdmin 3 are very different things. The Redmine ticket seems to be
asking for the Service field (the pg_service.conf service name), *not*
Service ID (the operating system's service ID, used to start/stop the
database server service).
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
@ 2018-03-09 15:59 ` Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Murtuza Zabuawala @ 2018-03-09 15:59 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: pgadmin-hackers
Hi Dave,
I'll change the name and send you updated patch.
On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
> HI
>
> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <murtuza.zabuawala@
> enterprisedb.com> wrote:
>
>> Hi,
>>
>> PFA patch to add service parameter in server dialog.
>> - Docs updated
>> - Test case added for Service ID parameter
>>
>> Please note,
>> I have extracted Connection class and Server manager class from our own
>> custom Psycopg2 driver module.
>>
>> Patch also covers RM#3120
>>
>
> This patch seems a little confused. The "Service" and "Service ID" fields
> from pgAdmin 3 are very different things. The Redmine ticket seems to be
> asking for the Service field (the pg_service.conf service name), *not*
> Service ID (the operating system's service ID, used to start/stop the
> database server service).
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
@ 2018-03-12 07:31 ` Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Murtuza Zabuawala @ 2018-03-12 07:31 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: pgadmin-hackers
Hi Dave,
PFA updated patch.
--
Regards,
Murtuza Zabuawala
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
[email protected]> wrote:
> Hi Dave,
>
> I'll change the name and send you updated patch.
>
>
> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>
>> HI
>>
>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>> [email protected]> wrote:
>>
>>> Hi,
>>>
>>> PFA patch to add service parameter in server dialog.
>>> - Docs updated
>>> - Test case added for Service ID parameter
>>>
>>> Please note,
>>> I have extracted Connection class and Server manager class from our own
>>> custom Psycopg2 driver module.
>>>
>>> Patch also covers RM#3120
>>>
>>
>> This patch seems a little confused. The "Service" and "Service ID"
>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>> to be asking for the Service field (the pg_service.conf service name),
>> *not* Service ID (the operating system's service ID, used to start/stop the
>> database server service).
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
Attachments:
[application/octet-stream] RM_3140_v1.diff (280.4K, 3-RM_3140_v1.diff)
download | inline diff:
diff --git a/docs/en_US/images/server_connection.png b/docs/en_US/images/server_connection.png
index 7ebf6b58b7bd260bcb3c1223907c3a22375fcd94..dd18376e5884effefabf45fc1bf6332f5522a73e 100644
GIT binary patch
literal 66243
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	-|aIZ;>ir
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
literal 39678
zcmce-Wmp|S(=EDj2_d)yx8R-xm*DOe+zIaP7Bsj7Cund9?gV#tcX#(QB;@_xbMN#0
zxj$~632gRsPj^>!SFKuA6C@)g{2Kl(JO~7OEh-`)2LgcuAHkVnp@ClxaX)TAATSSO
zetsEIettq38%slDGXoHaVn|P0J70u`qFYBtTf2LJniAf|Q7$krR8G6Ot!1F4jj&z2
zgD~~Gy81E}(sG*@s#|?$3v34co=?9Cd#;1yu2Ac?p2<>q!GXFh)-l9k%=oH4G_(*R
zAqQDDD;s7v^v;Ml9R(Fw`4(uVZi5Vp@DiI)Eem#=m+%fOFa=S^6Ra${#q1qoX<`JS
z5uqA%H-?B$sXgz$9qb%*yVRhF1X%f}$5HPZFoYT(4K##-*KYQB*gs#{*ox^mc=3gh
zf1rPlCGrZa41u4)_mwZWFOe^sFPJaAFCE12Y1DWyN`&s#y-#mO|9L11l(6=84i)Rx
z7Dh~57RC;R(<a&84h0rA)?2S}@7343_gjReuS0dWkaa&tV`I@QGB7YG6Sd35F*BHz
z8EGTE5em#3f@a_0f@ljn_G!-$s9D2qE<;+y%*Dp}G|ef6fKYh#`1DlR+0w!t<h^p#
z{`BNYfBW=wq?`WWh=ql?4gyt;o4OA#1d;;^fwL0%YzqP*wm<&|i={@y0o0RXte|42
zA}PVHXK7BSqi?BeK<8v`1+)f%IGxymkLCt;I)qN<W)`;WPF%!)TCfA3pTDLjCj8UH
z&XkK-MN)>4-_pi_@B`g@ItF5HctS!#P8)qgb~yo|m+rt{T*P1O?5x=7=^Y&%=^UBq
zENzVF8QIv_=oy&knV4vS7PPj`7Ir#Lv=+7`e;@MiIRXZ@dN#&ZcE*+#gwM~_(Y3U<
z<02-09_T;+{?60D$@qUpvao$w7O+72=R5R_bPV+Wc{b3M^Z8qL8Dl2{GZg`2a{~)o
zU<~dLj7*$=+W)`q{LhI0)${Xz_58^E-#!29&Pz{D`sW4w*Mj~^>(94<xVYgt>Hi~o
zZur&DVfG+Uyt}BtCj}?XgA^Dyg^t@RPa+~HzArx@`Hn=;!SXOlWfYKNT+xdO)0M#*
zF&mY^&}DhuOIox9O5ovx2`OMH1WVAsc1FedUeW#_tB8n5Z8+=SG;?VCx&ZvJIK|-(
z7)WK_NH|L7I_qa1>OTnz4ebYqfQtbAxp>QnWinJI;$q3bPze8A!;n8g$3lNU6ME~4
zc){~Bn%OBOK#F)hU%*ZL|MjNZsVWqM<szq>a~>32<lhlMa88t9S_I40vIFK~(yzSY
zKT>tP`#YGo4^ld~43dk4I5;Wq^Q9@~1rId9{a+j4OSHf^TV9Hj^S-fQ4Ho*-O{M~N
zUiKq)Q6Je47n!$i?#XjOJS2TEjG|;^cVbA0oiK|iGNOu|C7EU%=VRb96^FEwU;iwo
zg&)qYzy>c|vHojfwp_~LeYQyks|bF$V)Ivv*pj94DidVhRBsH>n?_j*jK9(vAtEI4
z+_@)vZ!wRhGx2+QZ*NZlikXSY>TsX!r@=k@L;3e|XI1cfas8F}$O;fERASIyv5BOo
zLmtvNwvFGg7>6FeU6FEpoCQxugX!T%gM%yTBWEMS&jls_?iBbd2-0O%Z<9|u>^3SY
z-`9fn+uxt&=iI|3!%0a4rmOyA=Y@G#a4XngOo%uf<@XAG9t;bv9R-(n#4k#;Bx0!Q
zfuc3Jf~l)lSf6Mvn!J$=`>D&URV@LAmUV_nDQF$RiS;rwFG%<_uQ3d-@6xwP*^-iy
z9c6l5glU(2V>yy5M@N=nO786$oYD7{dU%|ZInepBZ+^$NE0}+{Hh*GstG@AT)aBg0
z4Q)LbW}p%1vtuJd_rz@q#C~FCvt;<Y$I#rt1$)7@XVgt63tKPtrXr)Fbgv?6!XS6v
z^SIT$u6th`+G_wYl40n~IiB&B{X^lp$f;WgvY_5vJ=a-m=SVlsq6@71+@!$3@aS#C
zE$8IB`H}Td;lH{^fNgJYUtVxr>Nr{J8mQ!QIwpK4ndnzj!&;4WjK^lPK38S4*zC#U
zKt7WyA8}oBZek);<$PxN-gFYd^YJDZqr#vs?x$uWJ0Cy4CA-7ne2`L+YQmV5hVypL
zYS)EvShIazK+PT{7Psvy&6H|Xf(I1u@pFlWMko5LHjnecx6?m`Mp3H8GpUYgSA3(b
zgKDZp(K#pbig&*hZ12UjijYr*UfVnjG|>(h3b8@PomYxE7Bt`*zotB*$1~B}3KHKw
zzoa)A_r$6nU+SVGU2aJz*rXBSp2k{8@+WH0H9otUw|7&W$h%5e5fPCZyLj)DWv)z*
zHW!{`fu!b)eG$Qar8I97zagT574UagJA-RbC8W;T?@d%D^n7R3LqbRQr&X&;cRt%>
zb-kon^n7v;)I~x@_J+ZCYB6LNCDGASFCExB{n+AtV0N`ZmjJF(@df#?b_-wMz+i`j
zVfb=Tz}VRM`r&G6q}g-u+`EVpg#BTvKu*Z<zN-J>mDX<X;5RCg+tb&6$`skuv^Rkh
z>B@Un*LMjREP>!Br&Dt$uI*4*#t3{F5}_J3IPQn}KhDY#E>CUFF(RbY3$ZdloVP1=
zNl~I*_pS2g{q_;C;tt83-E4CMr>c{(#cG>mJ6qrOefUR|8I5-yInPkYb~<3*em=Q9
zuWn<1{nwOv-N8*Kfk$}Qt^D>^jTp@qC*k7KP#Y&hz+*>y!*gFMNn{y<nx0L1Fk9Ky
z`AzM3&Jw50<$^$^$*pogukGtVM9rno_j^5sEep%yX0yDg?{~szS<KOcGVD*x@hFw6
zgj;-lklJ=t*}6+R)OEJ8rbo0K>9Kyz4u#*q##px5ocV(y2A4E7FV|U$L+<6mKiX66
zQbO+p(zd<Im&bq-+C4rG{l@CCQdjItfhbg3n5k~5nb~kN%dD}t(=6C(O-fHGjs=?}
zSzAaFHW_sC_h_n1^Fnt@bDsDOvNAG`%-_7hW+ELLY)+S}(I@cYdE)$<DNIj&=DAB7
z&TND-@xGO@o+%!V#+W5cK;?D|yWw$xjw|Y5sVVMoJbH|&VlCY+=0w!^r|L(MEi&tm
zctL_GVy!Oi^cbKu)x+QW8wS{(`+5WRU36W?cR2H;SIt{T<M2Wo5xiXROLvbXvl8?_
z3}~Z6EBSp-t`5Id?8^nz#o9`+#o42g2^BX#O>x-Ya>VLdYe>S0Ea3Kt7sBXjL|kXQ
zf3X5ih+x)H)$@1^goL1PZ$H>1dC|#&KAKH`>%!j)&XF|T%Tz82!{V&T9pGP`eZ^#g
zD06ml`P+WN`R$LWU2U>?WKAiK*&bvR1(b<jxv(BMV;V_o!kp8vMJDkq(x}r^ZJL^K
z#+pv)KMtoiN^xs(tg|xFsoNrkKJJWEck5HYXKZ&n@@qzF_^@)mZ7mNQbq-k_&2fOz
zh*M#$qR1BVA#o<lHk{_~-TP~RtG!#iAz;pvo6XKlArrv653shyM$|JvkkFend$#^o
zQx;jfgS6E>_e#!7B8&&+q?_i9YijsRx)%BD%Gg1DH=QLk_1WL6d#N3NKRr4K#8Y+^
z<<JNqM<Cu$-#BToz>Ow^lt-&+%3__uM``X7k71%b=Ee!KC&g3G20OnP&g2$h0(X6b
z%j#rA;2aCJzABCM6;}ew%FgB&x>PPECH#1qpNPon%jFujXlJ(Ig5<b311p)h-fump
z3Kfx>;14qq)f`tzMOdhgq%S~c_&`2!=({0ylWAR9?jpJn_EUTs%Q&%_9(_5$HA!hA
z^?Y?et9MB`X<}~b&BUZ%oQVSMyW?0_U52}xu-P6Np(*Amybe^8Ti(-X#LuG|q0&9f
z^qHK#*@f>TY;E@0t!LoNzTd*w=Y_b$_I;sumldeXM~5xXZ8a(gPgnbu|0QczPB`H{
zItH8s|JL(p5vk81m9bC%vv5yCVScj;LVhfLwmY=u&84))X3=RB(_DTmcy6p`dxm-L
zybTGX{2zeg-2gYO1|?Ih&ely;+v*FGX*85tvlhm&ytd{`A{@4Svy~=XX)<AEs2QOM
zqALScSZGMos{IUQxA(Dhes78qjEI*w-6*V2N=vdEbB?g!9lWC{u^(P=aUOTJ7KbgG
zh+dz0p?>$hE7ADKNtAl?TuULBo*G=AtI158G}y=PQYgaN;l?7XUQyG*zdDc*(T(`m
zI`szPuDbG+@#btM`{X!!Km`=<YG4D3^d)<ZV#I4d{Y5RGYe=ltgoaaj%+9|^%Zk9e
zSfgI|aK5(gjKxO<tm*OAbl!eS!-8^}tPiv`lok#{;D(ftkRzpti)rSM!(yQgIAn)-
zFx8w`V=pF?RCS>}SxnT51dYvRL1ZN9%lSdG&JUuf?mJ(dySPsFkFt`V*6$bPbHe1S
zNIp8ON87E6kZ)l1t)2E#^U&%f1Q;wjPmKJGCb6%m3>}J!kBpaf^wsiMH&XvAW*^kj
zYJ}k#6!>z4VKR8HFBDgct|gjl^gq<9Ehyx3LI-YXH0scXIL|_FW{ax89WY?WUI3pL
zIr6apR129rXk}%k#_fiQ-SM#S&53#p9+)jbd56wM?_|=UlN^LB#tY)Kg7FTesU%B-
zkJEgFq9x~-q!bq!8suocsTBbGveeZ0@viR<WMLmeA<L%*^AEM8vm&!5t>+fF9wr-a
z#8LZ1PR)>ZT@LnW<cz_4H6#qdwzjsKJb21Cb~QJ)ol1JeIoE(gpS>VgFlo}#y?o=j
z+wMRSfwUmH2r}M6*eGHBtev9r`jYP-8~QA*W?{6wRHImmEo~1(wj?43Jx8M+RBA4=
zn0pfb!bdO2hG5=ms;QBt->>6)8xG_s3v0j3ZArnO(ntDh300MTex8cBX-oe<2#AGL
zlWE4J8ZXXwk^cVrR-a8#?}R*1*3c9Ch9I>1;gpL+j|wD3WJvrXm}3j@YB^ByCD^eA
zX-w)101svXqXKo*KYVumt0-LnGxIW<J1HgoX^`%OT9b{cy-;T#!h$#XuT#T!UPC1e
zcKiJ=zo4rtK!KjbZ<v<3T3o%ZKu2JU5R1-l=EWRqv!S%RBZxE$uVn0AW<aqBsXvz9
zP(_gQhWDh2Mdg1T6TtBeb7__2e*h>&4&*k3Rmj(?iwnV8n++s1G`}da6xTU9#>SHV
z2`Opmu5UOjRu{Vy;EnegHMaT>qCjZTf(?ESX62J;_o$<z0}BVIvvmZ?LlTshmS#vn
z<4^hG=8ce+(RlF>02Cf}tKk?8FRs)3CvqTN#s)%@^%2Buv4q8i{$+M>rr6*A_Z{6j
zLn~S^RR#6F=-TJ4%KHgc28;rhE9YP6Lr=yF)gq1)rW^ed8~uN1AQOt_B|+NWNkTLx
zt`@B8O6EQ8=cFDceTg_T7G=f#v6VwVI|gtuQ16yGY}UK0%{R(DpFAoo7G^EL^2#ue
z?+8Y&Pp@oMs`iKx1v^M6xgb^)#o972-Nowyy?+gXei)2oWIUL=_r=p&Rv!xap{gmZ
z77TH>#0)ieTB===5LI%%eGl(jO+(6gb%q0*V1r)5GunQ@)EC!s$^TC3h{OAHK}Ol~
zP{=f@J$l|}?w@Ki6)gOGcJDn@?BRb^X|@v}$45kb|9bq^U^&ehst+SgNNxfi_SI>b
zR_HIrQM#1?LGnnMGTh&N^S5+7Wpv;}_i=vB^BN<+Jl=fVO?4rLf56ac*Pt!Ph<Zp1
z{B{043{oT=2@XJ#pAgl4@M3tKaUlNKYOr;pCw^V@7G|yN*1KgwprQH_UhY+y4<dTD
z%UV8FMzi>xzka``l)VI=s}8Mdrq+_j7mY{GRF8AiWN}B4{5d4ip8Iu%{VLoTXy_kC
z*}Oj|&jPJ(uY8>xir%upz~sA9!M4JQI$4J7q>6DlBe-L9=r}~tySzu_8}mYNYdAom
z%gqa+%u_*6@#Bw|pH~g{BPqMSSEuScsL`&*_0+mRJ+s}BG^&=!CAkh_k_Ug3GHUSy
z)V3(l;tWN&LbJG-h2LZ1SZ%3g5=PEG^{LtwXE|1<sPr~l2zF4A=mRBoUNuAW(y2?L
zSW+CpoO@0%J(rKYRi&JbeIL1#%R7eiBz_e}W60FoJ$bB?O`2@;0Y1FGm1a5UsOW?5
z-eb{`Zq(T;N>c|}l~f#D-~^9Wh~5~d{#*=FJxgcrG}%*rF|j>Ri*z1BV9T_*OYAQ3
zF34Xnnv~Y%ew~=Y8{27P8GrItVXvWdu`3SGy<_Rx^?WZZiCtzm)zgTl!u0X5+oJC$
zHLLrXIkbMxr$ZyW)43b*S+#||>RZ}v8*}&=k|D{13=itlIfbo<#t!|B^Tv+n^~XUb
zD}kh*oZ-60zpU^`v+zyK*Q$6(*La2VEcvFU31@7bbA8XqftMh9ZMu2T7BeBMG-YI2
z`-Mmge3E81;4tlHMcRH_^sDd{n<{*(+i+j3E7SK5Pjv(fPnK`$hJxhYl#!a<T}0eA
zNq6x4<Wa`^<|rRRuD4NN0?xIm)0zIxIS%>c4DCARe8dv%{G=B3%n|DXMvBH6qH!@L
z{O0IV&e{hlYxTm_kqkkxb<y6yLY8SgtI>AeO}%ovv_z;_fQKN{jG&?H3xp=EGr5Bv
zSz>;<o7*iBAG*Wgmh#HkeE6nK(P*BNW&eVCahiLAx|f>$Cc2w>c;bUWM80r-HKlr(
ze<E#b#3j*}aD@BPr{Gw^tt&jis~wn&heh|!Pxn?5l+5|uc_XsB9`oS~an*Xi*&4DA
z7Zk~(Cn`FzB@(g^^+XE}FPZ^i2OM7sc9r(8Rv(XMpmrw|Da*f8Q*f?RuRXfbmaN#J
z6Jy2pZ|*z#v>sVb<MAf1Y{ZHFPF4DX%|Jd%Y=vwxcy9cjCJr(f1&kcs>pkwbNLf`1
z)h+y8wb7o2EPRu#(lT|}Cig1trUMO(xl0Ov?RZWo=7@-xAVru}WH%$MIBqS1v7^(j
z26LmbcS=Hx3Dc}cSc@D-0Xq=8x;tZv!-8xSTsL|Zd4B5>W+8!X&4W;@zra2mA7oC9
z!^(0sxILVtaaWcqe8YMg>xYq7O8&ibkM#!EeBs=7x)T1{if@Qha%*c+l5b2{tD~?o
zAR@*Ns@`KiAa_o&?OtT8jg4)EQ>p3VJx07Mlu-%!%6@T&h(`hD;`K}x>2!UQgPrra
z>L_ykpn7YpZT+z2#2CfXqM`ij!4konca2^<rdA&>;StTXd_5ei+i--Ze)a@Us@R?v
zik*=qb{7szz%CE%htY6o+|W-h2X|Mv<10NLNS)tFby)Mh6F%9%qem*jxmi6OdF6C8
zCC{4H=Ur@~w*eE88fdhI*YU&F|ESc#EWqkiI2_Ub<O^1*Nx<o+?qZ4ET2!o>kH`y&
zib_|uByJNUZwl5kMwRc|`qmijhlZ3qn1ooH$q@q-G>c53`OuOQFZd^pvXJ8tVFd*h
zt0|M#%8xD(a6^=Otz<sCaTFpq9iV5R1)E+w^^wWT9|gDnnu}u0lE-R#QhP0N^fs7-
zD*tPv8?<{|XJ)I0;`!0augk~XD<psO3_+t#o!$o4!aTIXv->9L&$wK<9%Bf=8SVJ$
z6%FsT+c05U>cKlGLi6GFyD@2TxG!rCG-{qv$)}4*tkHL9tvd;@#kfAXvA;|Aw?Ck2
zJ1<hmA4OCtBH^Ni=HT&<DBypY8fW-;z~m7GHCvq@n3%1o31%={>rF+^j=wc9uoi9!
z!@~Pe8vIlBlVK^uNRz93MKMaCg27m!%IDD5&mvZ1F~^FruC+|g1Igri3)GhB7v~yS
zE?TIN&~f~GO>3psaM|zg4R<BLb|kpl<1-J1zb76cnm(|m9)cBeQL@d|rh0&%<>u`K
zun-8N%Kir5OW%4G4q;j3`}2*iGw<H#@8r$_as1I$SDE%sguN4pzK^-weTuY3)|p9~
z<9b<1jQ1Wfs0p*&{^&f29pTdEgH=e=(Y4FXDT!B$-X(R=YwGRj2=`*&)Q#|dAF4!;
zt`leyh~Fs-++_(gWC`^!<!joHyP#=W!JT%0qO9m6W|D0dbykp(J*<&m_$~@JjbS><
zFmzO3T!@-|B6el6>w(BOz=5L^C5LkEj+hOqIPD;s*wLCSLoMAMLKwHqIn=nIK7Ui-
zO@64jGa7q}gA|z4i|a`_QqGUqzOGp*^MMss5of$bFTd773&U2Lsp>$6rT!L=tH?tb
zok~GqjH69fQG=&Z{4g`hsn8hv!nIpq;&$Ww$n>rUGAp&o=R>a#%R|pC`w)Dk=}Oz(
z0*CWzm4i+vup^|9t>nOD-q1`O;feP+^WG@ug<0MeMAp3BP?nMA<XS3bcZZXY6JD9Q
zk=;I<z%G;%-7N?*(Boc|3*r@uGp3Wv9q@8V)}2e+I^JiIbvmUsc+HyN55L<Qm!HcP
ztE?B%?16;yDe1it1=RXk8`j+#oW!CwlX2ZvSzKZLQNffYV|7UbVjFl7y5FQbLD%6-
zeL07GY?b|Yf2h_N<?n1gw8aI^o|I30T}c^h4_Ew3v<Mnep_5!u5@R3-&fC*d7FdK>
z{62QS56xOBtlrcvF0HBkQVRjKD6ujrx#h4AnXy&(2(peH&6{{#H)i5eR!*;@OL|IG
zvE-E(w0e7gZ6+7hS}J!(MGOBvVJ#ZQ52;B<U*M)jx#%FMGU9+Rai5;`!&eea>BlJA
z)IgtjNDTfql25!fsFxc2;8sty`}F=ptU+S=?%9B-{ZVR%SqJ-8lj2pK3!CDg<JeJw
zPe-!*SVY6&>HGVKB)wgk=4OA)$o$c)#fb5DY!9?Kq4(6^(F~zc2)$Q7wTMFTCKw;d
zpeD&;PEe5I#3Z&AzbzJGwin;B>)pu-=s8C&I(`tjXi<7|K9s85<;8x<v0=8AN7RUu
zH0JT-_R8(aO+LCU9<sp5<LyT}`F8`$NQE&rUq5six$A$~&w)wQLPfwsY`DGW{D7L~
z*qv~~(h;O`MWXtqc1-ih?CG%2<9cX1-cO_6VaEW!Ua457OW=K^se>CLZ3>9?m3UYm
zZP{+YWr?EfZzMaM<aQrw|COCXldPhc-lXI^-@eSbOvM<!JfS5x46e@}ct`tVlPq%|
z6G%$XG7eiiRq}Hx;_`=fOcXr#w+*-kVoch6JSZ2}A=3G+6hreN-5M@pY)BPsAvStf
z!;ntiu=;!E#lF|OWl?I5#Q4r#h^>h%I7L(|(d>JM(OVNqqewZ<Yy9APt%+AKVR_7K
z-!RIiKsi0r{3_(5<OBxPEr>NZ<wT@q4vsYs^pni~pkT#V@y^rz7Yq+YS5@DMs7Ro6
z%gq{GFWWHM{hpFn`Ij}a4_dAg154yTC1s@uIDPqD!j_~cu!G6R+iWgRGpC(SgT)F?
zxt%fQPTybM64!+G%74bABd5rvs^n?iM`r@tYl2Z7s#c=l*cXX8VuMQ}8Op0YFJN_~
z0Z*z94B|n$dN3t+;F%g<zs?of*VR1AR(c(}om@SZ&PB7iCkBRnBCV|-5~)(qD<vgk
zAbi+4yO4v}(BsY@Yt|M6v1F*OxOlG|e$aWSr(B6EBjrWSk}6|tF^Q6R?oP(BV9`jm
zVzRCp>UJ-FexE_o%SNOAi8z1cn4l}Buvek@Ft@J^v&<|hw8CAMh_`=VR^`_b_@I{3
zmHa)rnHpa1QFVT6PO3y_X_u5{=gl^2GPkoVL40h9s5gJZsi@4gkbQnmp^P^yBAk+;
zhe5t2K|UJN#&EvdXOl&uM>x1^w?3$irKr#|;n)c(v5?e-gm~UNDKSdLvn$l`n=KCI
z50t;l6=eMVOoMt!c;LFKa9h!`O<4_V<$9e8qH1t|T&_%G=6aF?oKDxywS<9SB!K|&
zyFhw>Rj~xC8+eB!cHgtztu3vBLL1%w_Gtt1@eenR$!%LW5?8)$!NT4@m>_^7G$-0(
z3<V<V)$10wkA<7WOtY*@mpBiI4SK?YE)Ep-w*h;1rXLjSv@t{FL_>z`sf_6EZkU(n
zwM4g~e)&bv)lQAg?k+uJc$lOHW&2b)*3${&9Sv9ZXnsSyX8Y``nKqgry~@v`a21CN
z7#9-|R?CGV=dyRGq~nOAnf&q$8wW?dBRWpfq$x9g)=m&9i3($e*CwwMGsfOeIOmuD
zKEw&oSPC5r^IYAaNAzZX#Pt7BBzbwm%X5*J16SyiI>eGv=J@dV<OmhA#1>$Yzn{*O
zzyHorH~W-g>c`_6YrQkZ<YN(PZ^6y>>FdMxc3LquSTi5x#hoDKqmqJAyYV|Pl~`04
zo??ow=E~j^X(h54%TY-V6gqs1S+w3%iCUAj@u|LCl;e_(5trGm9%rMI6UB|`8#{h3
z<<xw%fvd^XHeQ3t!?Ov>P>zT&**D{S)eK_;gc^&9Tqa|=f+J22JD=SPgDGoJyLU90
zR!{=zDs2afAoD1Lt+W^EHcE5vQJYK%exk+MZf7<e!W4`A*4w=zYTfRCFr`kzw62yh
zUlKg9PA~CSY;!_CT3Z^VuzYB{zH600C`%5i-O*Gwrt#qTz`@e)nHbaFWtw?$v_6Bb
zn;`D*nazH!$%debq80pk_v+?b`FpcK7W-o|YQ0dK`-1cs<gud-@cX^*IpgK_l)1BE
z&zZG`0555M{VYkGn+E4I2K{cNE)(LtaQocDS5Ke|!m1xi+ZR1zl*jZt?HGC{lFc4p
zZh&Z2BUW(oL&0=!WUt43LxFOx6kgY^MY~+4*^Y}nOv0Q3-5uy>Wc7gQ4RwcuSzqO?
za||tBSnQ0}`hYu~@}Ua>h{O7n`Y{NV99Nv!y!f71zh_DEDs<xkW4--h{X67f7t^{t
z26&gy$&6sn+b26kEu%eshWeew_$9y6ii^9x$Ht!ymsh+ogDl|kQ6M;2@6b>L_i1>C
zdiemPI2pWOocrsovBF_C19^j5kTPPvS@wne${YznG{t!d{1a1=F~79ZS?^~O$(6TG
z8#bNboV#L}zL7W<?w4C8o?$v4^zVf9U#`~hxg|M^xQcVk%3;wSu~fs!c!j>cIZYDm
z*<X_J7{fk!ZOF24cbe3_cNPEO3a~9S_BkYAC1f(w3t3Y>8Os1}c)ny}o>>yHqMo{!
zjIoz5K(w&J29Nw>wJ4AQ@+U?$+Meho`v?Ok17?MUmifX5^2!qPLZwqwzT4yb^F&^7
zM3kBhZqQM~S(xt(!`pwUXn;42pj|b1<F(cmK8F^90g^VM?fuuqUjSgah`bFMU#7Jz
z3e3Vu&Q#_fiAX^Tt^k6Edmr*|7Sh0|EzzpJ<uCK*C3>ExNcv}lmjRtn03t1Q{B4Z?
z3$q3v1VZ=X10Mi#-hVS`(Xdt|gwiIj=_AO9h`>fhMrvFxsF;|U8+mnuMV{BPD4s45
zghHWQjB7If(;LV-Z50q-egw||kDyQP;3&6ygh9mR1$Zvm90~01Z#^%36gN22aGl2k
zyRNS8aN%cWNEjFnbu4sHfGlGM7kFf3q$<Sg*RTCcO6c9(-1f;<?Emt7;7APYCqQ14
z^xeA*>$OhAEg`5D9fi;@;VqLzY9kqfuOJ~IzY9MpfbYw${pn&A=4H2x(heVyoE-Xa
zceQp{f8@Z*><SMuuR+Yz7pR4MxWBjCqQ~Rm@LWWF7V9!Mc&DvWYAu;~oL%h_Iy&g8
zQX{f|X8S%we?x`xc^1dK;D>LmWWkPGI&(x`G@&ipf`_3@1#0N}w;`}l06`<*q$s|m
z`N66wKqn_BthSp7a&mGO#BR6e#_gS*QJnuY-(pU>i~*&+HJGwJQGiLOUSlOc;R58C
zYHhdbvU)Ql_UEdH$ebBb|B;eXkn<TJG>jI(5DXnay8eo|eJ@(~CgxwRq6RTZ9s}9R
zwS(%V$*y2@>cSjE{g|hXcrD%WpR(USnkR-)%fr6Hq-#%Rvn@5~!*KVwIo%M<kySx{
z8NrIhdoCX&vh}oEP@=oq9t6;lBV9lW=6KQdXvZalzLmP9!Dpe~aqUL{zR$i1skH$)
z`wZr1A2TVv{kU}tmHxDUWGy^cH5G_yiC(Qs6+dqIa(~7b$bjo@_9tL*xm0j--)@uK
zU$2F`SZFP>I$j^^AiRAK8x^m<RHhf8gtl%8m%?s8>e*)xK*yJ})dgFl2!_RCk$gBz
z`$oJ>DwQ*@xyo{B(XCE0>Eo>><AZXZ9F+UrWykSKTV;Y)3^?`!^UDDM!J&nEsiAuz
z;;}C`-5vg7r|J06e;v1_Z%4m!e{)v%i-5zSP>JyIdhHDYE~^ZXP*v4bVL*_Od^xop
zAwX}CE5%0RDT;pa60C3-?uh4PfwHt`Fbbhq`)WtXz!*dli)B^(hmsct6$Cf$(_*d&
zYUd-=*&WZD{iPLlbGEf-%Y;t2G*zT#w8;8XAy4i$@nk0b6GSqrHJ=(}O)b)q?LRdn
zKX*2nEDW-2eo*3gu-}`MBsgu-V<VRb6BQM8y4i@oaJD8z(*m8qeVNFQEnex4B&kHz
znX59l$Ng`aT?+^a8O{W=1EnbqXM-GJ8&TA8-_JXIwUV!GI1YkrwPa+Fs1)<{rN~YU
z2DSJm@)hm3EFnosY_|r9XlM{J-m5@k-v|o|&NOVs981KIgC)2aPZi_c-`|S_{bk*e
z;IeXa_3kbYwx`Q7+p>F6ZUCc>%i}R`__kUFlm!tVHXGN~lw7@kFGv-%RI0=8uHx_r
z<ZDQ%p==x<Z8T1M_Y&J;up<BM8|>#p(-BHk@SoBcVW2c->H{{^e`;q?fm3Gnwcq=t
zOG6k49aLh<1TXQTFDP=r>@FiD{HyhG!UOD`=9E6#_m>{<Qh-fu#b$ze5%)1Wc!wNV
z#DMQ$GTWMhUXPGaIrR@S*<|I2qHkn~|9K3Rw{9Lt;yVKvf!ncnZdFiMuVRJ(Tw?*+
z-xj<pC=o^_oW2^^>Pbn|WVZ5}Og_MZA)sHsUC%)Ywy4_Zjcv;ok0+cHXZpJ{(GbC$
zH+z36r?wU&SU6+0G`ZiE8V<Z|G%DLCpm)9Kd*z1jelC0Vv1Jtb>2l7}P~Hv5gq=+5
zg(p<M!C<a4d+oOJ8)|>%S6A|5Z!EP+nXdl;CshCGMxXWhwyb)+gJd)5PuHtMB{dX!
z^%|t;capyeJnw&0<5iIdG=wY%5O@sR#L=o@oI?KwDqnV$QTt4T$;{@eB&XziV_a}K
zosh)iXuPc=cA%YWBO408bcNwv1GeONGs$w75_~N&E4tzCV$W#tP^CAvCIAs{)BvzD
z5E^xMBN7Nrzbh>k>TcB!UFuHXF`kNvh&-p2WeSvN7s8sFnvPFStc7L!qn{NL%mt=L
z@3})QmBQ|CIFLxh$~vt>cew}!SVrE>t_@lb6zDhjg&eaxW4UXou7|B%1fGRM;==cU
zq01Bw$B!7a0MrIp23%+atn?Bb+if7in@D-eS1hQwSp%%W?KiJcBC*tPo=4V2k2~`e
zA$HH5WZDv-rU3Hkev9YA-r4*ZnB2)Q{~Mj@QXPj4hE?$d`o{ApCGaf0cx?)hkOu7r
z8kB&K-FSxf`Qd_^XFThuPM@Rm&dBf&{>X%cJnX<Ik~p|L--a{o`|FdsU+$+p<kk<j
z_8OjSm{?eWj}Lcy?7==HV*X#gG8^_=UFT4U2*`qGWM*FUr3yH_oYyPl2oGtY$I-1=
zIt_L$s2O<>p(R9x{ZH9^u|0K_dIxi*v(15DRTc{-gUefKo|9X2nhigP@?=XCH!T}4
zl;T_(OKy_e@fdaeZtfNvUF~n7USWJ6>RGa(V`KZgl{96aDM9k)=Kb%Yb)*|I>Aq{B
z1UX?iF|e{I`P1n4HX<^rNsnr~<Th`002_VVAlnovH7Fio`pa;jT$gm`b(K&GsgFU^
zU?rCPUvz`^0o)`aE;cUC@K<=e$5IhD==4^T1S?ToPA=*u`TJl8JcIe{d-Gk*W{<{L
z2a-$!5-eMSSZd{uY0Qst#^BBxrG9~duqkP{x|{LZ>S~QHl^<7FOvbzOlN;8jn?0Mu
zjCMwr>g-KzRT=xbzi3HZ2_cyVgbvyUxE?i5nNaZw-4l1kYkA}u#7e`7%?zjMzDA~Q
z`zf1wj^?XEvVIV(3m(PZ9ph7q`pUPcnr>5EQeXeiY4_F@ZYhHIDw?{2*V$q1xaV@a
zwlbHZlg~lOSIDzLb?Y0P)a(nyR`(fQ?iU8U&^ekPq+a4DiCv)pV`)UuXpTU*McBCB
zPy^0{p&`88i2@^gvHk~LzUmldee?vfZiT>W#BTc)Qh7|`lK#e_R4z4YSX6SU@iDjz
z*q4fs2wsn8XVxu_ggw&RP}lDmj?bk!)TLag!pLK@HPZ?>OZhEh=WbX{8VD42yKQYL
zQ)xu&V&)hal1LrNh@-Eg8T}_pr)IMiJr){Szv63C=F58>Ndo7Ys_B#crsivHFUZQ6
z0(}V2o>r3xgS}M1|K*NbWWlQi!5DHNvY2CKt5`>|2kpb)Q#n%YI#>0tRssl2el@V}
z_YhG-lZtFnPSF;jT&^=nJe9-q3N4NH^!}WO)fJ5hQ&&P4Nk9?a)nx@+%%^*H)G2kL
zi?2U-7#{;(h&_Q-RMkR>bPQ_rL>!k5Vb3o)8pwmL)35^|`&k1NUm-0*z=v}rak#>1
za}EYmIQGyXsx}AC#_&h6L*g4Kz?sOG3Y6*HL$R5K9Q$=`s*?-i8xDGidVLr%>1_{n
zEij`S3?d#Nv*@C#+av1&Gj!-HESK=3$)zVV9r=|Q&$`;%x3e-7x}9bDveXmkA`GIK
zCWub_k_y2i&WST6UjI{rS0?0qIf9H(Zn;FiO@(=KG@>fIskKS8{kL9Uyh_ul$y3#w
z$dM}6M{E-HAo@X%xGkG4N@uK-_Bxp+H|It}jX}{xp_w=<Kw{Q{-VqNl`j1+>G4W<{
zn4cGoK4bFZs~M<m(%yuOWsXFuP4XvG(vIPria6VROKHxm=2-iaiajABgixCKbbNiX
z_L2Gk11kM`*+B9+EBu@oW`>)V2U+x?c^PG7D7>If8Mo)Kgxh=|5s09k-H2BL;Qkh$
z`S0gT1_6jcD87Chr22<MfSVBif8W%CZg(Zrf<!?<LBhi;S}LX}p)mXl<bdiA6f&{x
z>g7qmRRGR#Q2EoPCbR$)p%!b1qyEnQ8(dZ|;80~sBrrwbME`a1E$)!|>Gps<1u7oX
zKmRg>MMQL{VW0vbX6g#KLK2gZR6@Uf`xahHOY8djdRqMVOC=K&p|;(swY9Z~goMCa
z=Qq5;Z}47iKe0yey^GYUGXMw;3k%B-{+I_jo!R-P%Q7z5P77HeIw2by_TAM{7f@Vz
zU}boT2AZ|P6Y2@HLA|}X*%{Pg_i%V@ApfHvcgWDKLQY%2P43t<Q&WKsP3wshevRB<
z_Vkv?Jqvx=8*=``at8Ra_czJNmoT9v%8P@hSOMe|{x41hVmqjGtdHyk{}7CYENFFY
z&3dyR`wSTeM||`}f4t!M0xh)gEx?%>0Zz!-NRb*#=-UtQ0Q#b~nvhQ8K8SLIr-5bu
zhneZ$0Y0>4nbCGrf<~(e_tg1v|1*Gak#zAWM4n3>l}a=<B51zHy}%;C5d)YVC_?!8
z`PaMIYRhn-vea}M85!AddvCI+t-^S$!hX4rzPUmUv0asLDh!t`gD1^t6|O(=!z<g(
z{#F1>oU_T&VR8EL3ULxUm~{beXrSw|Q8sX>lvZoL^<cV&q<2@>O#<y>@#mn}5M|wq
zxWJ^*9QB~z4r_H{NvlMPxmbUs_6>4AA2bb)f`*nQEpGI9+DE?^-#eZs4@0e7+zvS1
z0kIZf?=ZMtnw=jlX?5)k|A5e^7Jw@!`c}pt#llA)#gv=e68NKy;ma`L8Kc=SLvrbe
zcy6+0(A^U|0!gQZUk`qaPb{ixx@|Z&*2&P+$%;{l-uW5CaP0+_zyia_EVnD<?BW<q
z+xSJv#_cfom%A2(_VpjyXri5kXwRpvCgM4u08p~AFfaB%^W9<nm*I3Uld0m@*6_*P
z?(CW_J7mvD5dbKYB)4q=Z}b3Ytuz4?GbOVP?v*qbm)P%)i{|bu)K|ZfWhn4L5(%2|
zWqsc|FL_sLS#4b?*e6$O`>i!*F~>pR1_Zgy13rAc2<PN^NabEq_+U=pK$8|MQ`j=4
zrfU~EoJh-f>gzxKqnKV{>Ph9E+iK-iVIa+PjNGT){)#{H9C(%|0s*%=2YBziXDpSX
z9XSCC3b-r+Dd+XFub~5z;&7#@asefHEd_}uv?U>-MlDaTNWhV)^*W0C-T5aKFHJ0t
zjF5@gx~p@+p}2{GE%5v)L9!4m`+BZ`EtmH_N?2YIb>Bwh3J_lm(IApl0k}3RSg-m6
zV8<(XydW8m=HP)iW`drh6hY{%5mncFe^Fku2ckeWB=3V>OzpeYq^<3>E01qFzg8J^
zX6Ck0@`VnS&HCbPND$}ZJzk$+7%Q@j8)n6fMNSu#$Usr_D;a(URwo$n9G$hnF*uTI
zmgx#g%4}YKi-?NKz`Jj67+Q9RTn!82UlIz%dJk%gp>HW|&S$&D&?}wVUYE~cFdRmE
zf3u~2UWDou6EmJfh5f-Z!edT*uKuX26iyuJcF3RoaDWJ!IZb{HR#7Np3QqylaAfhL
z9T+y17l0AGn&@WZKO-+;kea;!Rhx7*14Y$4+dzQaOol1yxY`ak;hptG3mkfgP^Pg9
za%{diF*ANG+(157xCI`;MJvvXioCh{R$Fha>^qwP!W9+d^%AMmh45n6Xoq;2)gif4
zww8FiB#u{)sRTEOjR;>{=%NJ^Gf{i;YKW2t>;*X*!OjCvd6E2S#wg9$^3mhr_6M*t
zzq48szmrZa6!xly2kHacZ!h*ns?628q4^Q;1$Z|ffANc?96H^lNqT8826EIvmV{Vt
z#eTcqeCYXp|L7E&mX2_Z8Mk~e)v0%3JSOMu#WW#X{Ym4!@eI^ZXj+K$E!ZpWr7vak
z((F%zSk2sM^6{R&4jWuQ57GXxN)157jpp^ye0KZ8?do;BG7#>fY!0QBthwJERIB_l
z{1&U+94UNwezoA37G`>l>4bfxOKJg6JXP@7ZDZ$1E1&Lh^oZW+FmiXM?~$heGW(Qi
zC3m?_^CBUt2=sIC>BG)_z4qj7<+f&Y&XcYkb6Gy16AQv+RBY@71L`H9<a;H>anYJT
zvVT+!&TO#(8>oNwO-s`%rlLT7y0avyGS{-pQ`7i-C12`z;?%%xO8<CzqZ)c(_3MY`
z=G|TYp+8>p0~A;<kM`*q^N4=8)^NEL1)rOI>6RxmqsuJ3T|xy~8bycBg&#gvBP$~9
z6B~NTBDPP={sY)DX0U_#o(r{H?;ipI5m^MN`?@@s8!6Kh4p<c@ouMZuA%W-$C#X*7
zq{RRQWb3sP^AlFMapv6HVgXhD5M1M5xl4w`I;h9uX7}&yvmy9m><Q~{J-~iylEJSD
zQ)Z_-zL_r>4Nt9lJtUdvpBMg}{i+5vM>73V3a9AfTREw(Lq-If1Af5>VoH9`wjJId
zL<{owsr`Z$RSiX0?}6Q`p4ND##haS*pjIkVf190EfH0mbzY_v&IO+4;!Gf6g7042P
z=-qOG!=*f4^H@r?4KW&wZ0?xrM?A4VZy?|cddY+ECvegJ)da!^03_n?m43N@ZY~o5
zD#t%J(;1#k&@(h)1(-`TM5DNW)F$u<crW1(F#i8Itlva{AGBh{43ZOzq4>cPtNquJ
zQK*BT?#CLHQu~_P+Ggwb_pQOkP%BF{_kMPW*L`V%3x3)k`#1Xj395-QL>{8%<iyRD
zPU{4C7osFN8Dxsp*Mqx&$DLOKKMjpFD&n-=m{H3V0xB5WQa~EPzqXCyzJCv7HX2+8
z@W-ur*oz@MBq4k*QxxCQs<Yd1L)Smj04ab@{i7rQfiw<S0Hm=(E#n=CQ2zTa8(3)#
z8aeY{#7hWvL<mr_9J4`k|1g_4+%QZ_HfxB7AsifBU|3iVs`iMb$z%io`XFG3;f5jJ
zdJ}_2ZwOeeKobSZ!U;?U>P?hT*ncsd3TniJ(o~6-u!ICc;s?{O%gfpU$TPO!SpY8@
z6N-h6ofrSZ@9Wp!Q&X7S+}wVD#l0*9E<}!xi3#)h>q3pSUVwNe_%Z{PP=&BI;M>29
zWI%y~gZF28Qh{CmJYj!!aje2E(`J%|pq`!{ARgm6Y`U`uVE&a00%iw{Lc#I_C{P66
z99lEl8gl)!Y37AnoCbTDtMz-L=$707uyr?EEV5~4_b6Cy^Ve%S5rR3st^w$4Z9RF?
z7w)B$bQv*(8UU$!|F+5h6Tv<klIv@yht8VmX_X{Dc{#ardp6wH0N2&W%p-iJ&H3^q
zLNL^4?uf(jP@qJ!aTvfxflLt`06ikC72g&~=nVKjT)-~clh%ci97!!!Tc)+0VE>1A
z`qMWKB#7OOcRCg+J{u8%)St|T>I?narp6qDR@D#C0|#yz@{5XrD%_X1Y^Z2(5a%72
z+@;D`oli+UZZ<GeK2lP`ak*Z;-y$J71FDCI?jQUyHHSU>C_HD%4X&4-9s__)xdy~(
zK)m{O(OK&E)skm8z~E$x#ZtNG(_8C{&H}G)Sjpe}zrtKy3+I_M=(`Mxr_<nT`}*3R
z8>oPM=5~((7_qAnYci3aJyq3b*MX_!rW3oh(iR{KctLlOh`bP>)>L<AG`p^Uev0hq
znY0BE)a~t2^HW#|CVd9rCh8DK!U0A|wy)9YL{~$)mM&=ziz(17D%`AAt;+0k?raNQ
zkbbX_KFZ<CVel8`z0`?(l(_A<hnzv!2B!Qmy5>TZ+v!-lP^(!pY3<uHtNdWJ)Z|_S
ziIS}YFvv(cfI>pUxo=^g(;jR9Rby)>O!tW!s9rD1g(CIni=!=CeMJff-ajqcvwaV6
zOa)r{sQCC{jC!5RW70em)`FiIPGk0>wt$&>15`><E~5b0GXrYx63KXV0k}5p(Jawx
zfUvfEl&cySMT5>f^Sqc)zF?b~ECV>Dx=hSh0TjISP1hNN@}u}X9=MIJS5><4X+rci
z)+NF^t*gMnwL#bOf3{o8ACYD5>WyaD7v0VzCjk0pvRET&0!XnH0-bh;07_*$bX@7{
z=Y0N&rCf=Gj1ufm#XUNHX`YXtwVE9c61mAd#hXw<?<XcJSUQwRENtB<+hA=L<uKUj
zZMxZNbA%3VEk8_XOJxdO-zFQdedR>ysiC`w(H`%K_%zsfF{xHJh(b!xBfT5PAyON6
z5o=@9XDgq<$6aPTmQz?!QNc$yp`gBhR6>n!TW)h0F-Hvj>bZ#5dybDcweb{TP+G=U
z`=NiZAkDD=DqX$S#%U)z{`{wk(2y=ud?tSY;>QWR+IO2bz|)LT8zHgHx6pt8{@ouR
zng@>EGY7A05)Zr{_2Vp_UuX^`)b!M`(dDAQ2Lb}(Vu{6Ko*|9H^N|zCx#W(8Bxw2Q
zBrp&w<}38zYDrwn(ZlI(L<|G`Ws(3|SGUl&w<|!LFz(!!3z@AxT^OF5BEeP$2_fX}
zJ;5_HeDo?}&EtY=w=O&DM1{*W^u{HGyi0FwCz;V*tlyy@>jNSALo`XLz9p{i<dBSW
zkqoxvr#YBI;SiWk60s=Cf)>oo@vHq)Xasz}Pty4|T55TR)fAz!H<mngc+nGVL1!1A
zqWMkkM<%dNymr(>Fd#~`Lwa;ciRiI8zp-pQI~#8w6abzQ{sw}4sXmI7^nPc@gIV+s
zrjr#>YB3NeudrCH2!Z56vE-I6el@1emTkec5IL<gPCyLN7+PGb99VZs+BHUZjMq5-
zDU!VVVFU+H-(KgqU$&37IzajQI`{sLkUq8X^W~EAx8dS~Le9^`=b{5Y^5n++#hvQ4
zyX%@J#f)UMt6FwiIf%t&4khHl@JhTn$$v|Xwl#3kyo_DQ<qWM5n*~8j{Y#MaCSChH
zJx{CFA?zn!5$u?}F{qMrSVe0Wo!#{-M$=Q~pZF0zWSO_xLi&#Vs!bRLO|Arw^_RMq
zdCClX*@Lh$H-f@+t)(7qNY-1B5-FG5s)}$RU8M}$=8@gx;Hw4o2A+O`=(HnmC{?y@
zwXb|S-F(ky0jOTC(Ey8(RRU~a1?E!}nfP#tR&!E5xs>fH7WvsD>N|;+?UBp@Dc53M
z9lQP22#OmueB#VTLa|^KrY!kXMt*xcsyDKRr>F?R0&3zUWLnn6eY8n#S2@Q$v(m%A
z_{A&gbLkCfQ_}^MmAo_aeZyx0r<B1wZ=7gT_eK!ykG=#%+A4kG{A5TMKAvJ(h|pe8
z!B-zAIb3dz$;tA_bzX?-6_H3gl5JpbFX>)o9sr;&;$<OHjD7IbBu<nAldl;-5gFsk
z#k*d0)O1{Q{{C*&M_anhrbWz!`U`=+VUSt!MP08EX`II~!tBBb3YS)*zj8h(dTL%3
z+R_A~SL_+KgdthggA?*`-mFK@V$1L)eKfa->SL%3!ECS#TitlNn9|}P_Dr=F*kQF^
z^-Fb$a5K2@vYRIGxF!n2=Q3Fg6v@HGg<H+0|Fj;>kNX%(2@)O^$K@aS&TgMy>o&72
z!j~(RQV}bl#DSGW3I{{k<;sWZ7uOs4k=K5nHrrHM@>8$k&O%bUvKn>|&JT8c0_5Pp
z56Jm<Z$4d3r9n(KhgH!YcCF7)+e#8-sp?Un=bt@V$J@=<8I(SFBJ)zpbBIQuEpVtc
zad1E}l8^Vi>Sa1v2hBgVfDoL@X>-(NwmcRyZA!^~uPPwP{=5$03JEU=R`c|DSBbm&
zh7kX#*7y6VfufgFSBPVy1k`);+40o<>W1Pt7L)8u^SLS)e&U@L0Lzx3gO%}N;UM>j
zfrQ)Ed!j3|`SfG*K#UVx<RV-hy+qDf8+~HXaI2VvH?JVp60I0=UZ2W`k#}7TktH{K
zhBXUZSdf(IJ;*gYxj52%zpP8-{^CtGb9TuxmB*fXbIsQ7&P3x%*UbFxNNx-!+J)+@
zSx}}oKoQ9?j3ceh&$Gz~&N8&U8#!$`I-IAq@QrKUAwhQ<LaIb!`7_Dlg$I790!4_w
zzp7}tGeN&Z%eDP_$W=UCD2OEW)JAetbyFu{v3rTBsECOSu)d>jTf3D9Lol0(^bSu+
zo2==o=i{7Jd&xFF7lR@=%_igrMQ3bOHqG1P&!7YDo!sg;4vUn)NC|GFN>rzjOo=-3
z&~N9HA$lEo9DE<|&bV?E@p6!oF3Q-a?i<ZN3WN_PW)&}7&EUHqTsM&aO5{07nV|C8
zj<w9=T<T><!%ITPF-<6Wzxbn&+<qo*EBcpGsKSQ5qumGZN`%ABzWZkgLP!s;`3vA>
zZ<uC3^lxVPc02ZsG_(HRa9E8#VnT^CjhbY7(|0cP7+3f0&zgqHaY?k;=xtBGhSv5)
zH+-|L-KgpxlcS@gul7wVa_MEGR12HOKwe91ZJm2RjZ81qxj1GW$Wey37Lb4A@S$(1
zvFvShKvlaRhc~wGSG|oztjafhXi7~ye9o{S)Z3jPaI@qGD5b+dzWG?EcHD1X5^ohw
zhpMn+WaVLc^DGBiu?*0I#_qtf)Pv-ngmu>@zfIS8e48cL&OW_msl}Z|1lztGRrvtL
z0L7)L<`p<bRWc?!U2|<($gN`|(2$L=IEfV#|0gv0)FMm+swy5zVi__a!IvZT9!O%@
zQ^e~m{u(DB7e>cB=Rtjpxv^HOT~=mS_KN%S2Xu{mBb*I+-Qx>2Uj~EN5mA=b>7m?J
zdb}9lv_h2c!w@C(gF7w!N6t{TvA;C!PpE6km1*~I{Lq=SO5ET!ExreT({TL)m9deM
z!J9>FsO+a{eLTNq%6K}cf?&b{YQ#8d(R!;Q2~TL)1s;mej@_$A$l<s8U{Da#Mr1(?
zVflyku91Ws9UHr#stv>AV5jjee@93{!f#1qL=9AoV3CoqXMkuH7%}Y?jZ&k2&`@9?
zIxC&W=;f$m25{{ae?IrxH*MOTmBgIbz%j`xrfV<6_2XXK)t#`YoFAU*=7M?^Bfo^X
zu&H}+mVq|UzTKcuBPzwgG~E|J^}JVFCLrg3fY5?b(+fT^@JN$InCwm~!lW2&mAI;p
z+2cI84t_|&luWVrvb%QiB~ncF57)XM`KFSA0i{4M-$}&FPOl~wOr(X+#2Uj!^5-}O
z04dnzmDlSZR<nDB*pXd!G<5vx-h}aC7V@Umf5UrF-G1$%>l)fo0cQV-A2ROKvmw=#
z1fo4<mb5Kc0-0P)43BE~Z?u<SRt0uH4vu@8@Kh?z@~9k%Fz7UcE0Y|w4`4h${d5*<
zDMDfRlN3KT0jgXY>TWniuH`01*28wI25v1-<qvL^bBn2};S%5f3>jvsn5lpLff>jX
zQ`kI*nzio?{(!n?5pa9+$%EEkyOg6J0(ry(@eKh45>N6dCb;KN2e{BU!2NST>+yyA
zzDINhM9t+4O+j7_g(8zN|8li-V9uAl%+s;0m9@1pOfxV%B?GV2RS<sl!g$+k^nTJT
zph*&c$t?VLUbksCo6T~r*Xrr<q2{RRE}2eT=t#@`0^Q@`LU|!ltH}-fbhAD;)de14
zjwUki09tw_H_hWWfYK2wdAN6{O2Prs@EHhqq;k1N#KdGRMJU(V89$&9l)-o7T(3u*
znUr{p=Vew|%C7)+_c=j(9^%<_vx%co9GP2Yo6^u>`4V;Hp0PR}J{Rl&2rq)eGqM(V
z<KJbA!?_^6;fKunLZfT-Wc%;7wv^4aZ*aaoKIchj{sH#~YipTrUWleCR0fCB=q8C<
z3Fp3|<}$82-wsiH<zn?R>^WKvu;6q6BTxRm+#ca~+{F24HVh7s&gw@4%~mUX54&o$
zba-a~_-C+~XYEdcFzWyA4V2q#PhA0k67a{4Q;!g*UwPnw>~ZV=Y40tgs_Nc;VNyVp
zRzM^q1f&F{8z})v>F$ySk=}qxcZqa&NcTobNoffI>F(O(ncExg=Xu8aj&sKO@P2s5
z`1`?6?7i2TYpyGPam|iU66cgzuk+&Or-BG9(2MeBc!P0C?<xo+8t(yq)JNK={&3L^
zC}3DEhN!0<2Lw)mmgLCFW$C{6zN)CJ-yC?$Dm!S(r6Ra6f@Jq4Ub^a(UNG4nGXz8}
z>Yuy5scdT#)448GE2U&-AI>z+{0mVijG%YvD}#{fry{vwZp)xwyI>6gc)*CQSB1y$
z?*Lq?L!N~`kGfgklQAt@lYA^)DEg~;scBF|YBmx5U<+-`m!D@;FB9&JtgRa+T3Ot3
z!=nyeBDw2*1v9HilF;s{ff3s;^?5{{z=837DbJYiEoy2vBmhJbPr<K}(KT3K_p(vT
z0|7jnY{`i@+j{rYKmN8#n#R(?MF@cYZ%u#cUj}+Nt!JEupW-90;2dZ!26bCo7vF+|
zD7;|T3xP5%bd7uiJ)%<r+iMCDiHg229K#PW6GU+m+JX!k=@oJasf#o!Byyt{zJJ7Y
zdn8&!c*`HnS;HkpUHtaj`n?rBFwk+s2MGWGThhttMR#7)lu6jNs>t$6w&TO-#&VQ2
z%_>XI%%17WVv6ooz|5j0AVqLJ3$$q4C674We>5{uVYXor02G7k^!F4|sEO`=$YKox
zIvjHv$$gVsn!=XqFV#ONpTup@{(UG@PvVNr?V&ehFjLY95b}BIU+0-7m~<P}!+*BB
z$`2i`4R@M)$e5wo<HaZxyrIvtK>aHvn(_Mn0fRUE(|r=nr6&)A5s~k#!n_kU2*rgN
z5dq^fwg-e5S;HD+8QlL8fHk;w!AN#HO(&L)&7U_+Dp?--fas7oK{ST2_ni>Gz70Q@
zmx?I!G%t_NQ+hul7C9=sEq-VI!i)j1tK&-%P?{GOLnuBzKFf{N*k9=a)S$SF4Ua#)
zn*H32qhahO{GB${BZG8q5l2HT(%|3QrJjpZK`?xn%I{jPlFLkXIbX$+>9uG6n9Vp~
zTP&196<HA}3qS6;^ZJ{ub`}xYEyE~67Eh`G4hlv_*;2d++&VhkPo2#s%^lAWUzxq%
zUtv}@#>4#hHK?VQc^J_UH-f?;aH)mOHd3bhhIFGql&8|SeE@(I1Z3aO1MT-f*m2#$
zp$l_l8)xZv&Y%v^&@CA+(b1yCHRIQyG*8eiLK*rIoUbFXG9N*k>8g=x8!{GZ6_p!f
zlu(*Ym9facM(nyA_p0a)+eZqvK!?bwMwX?(kPOBEzx3>)l(F3ye|V~vf?IL{NWLdA
zFnPm9wro-9%OlzaW4CM-GRxe`<nZi101i`<juq=R8nc3EAEfSA&U*t2&7lJcPnd$?
z;~T7p&JZ5pzuqx%zC{~x!6zQ4w{hR1NNsSM)bM`y`akhK0zw5o!7UnSfAfK~0Jti$
z?NQ9=t!`2he8TPn-oniXgmI|=8X3SrSNQTTd<ky_K-NR2>9~a}DJ5EpZIKxbi_}TQ
zDgMz(o)rfqBqSLB3%Suq#|XoXzyQyfo1629HQq51hQo&y?<M2<uYu|WK7pk>h90?&
zmH3|vo0b&5qxoOpUK|SzNSo)A*}s+^6B7r*=X<<(LGX`)hW_0>bp!Z<(8>OT?fHYS
z1_t!}`ZJ%8z#S5VyM_C!-kCwO(%c6EK{!}Vm4*mdHp;>8rXS2>&<5uFsOadgz=k{R
zdGZ^}%v2sR?f&@x8QeQ*0EA|9HA55+NwRlj(PIX2_*ariwdf!-KSLn<^^4#iZ5RZF
z5u>(sUK$m0LUj9JU&0@(L1snw?Sp-YN+BpnX4ao1A}!th`U4;_LqX7q`o~WV?yVMZ
zff)#!({=bDwt;9xXj7oMGt<QCvOPVeZu0O}qX*YfLey`-IS2}WB4h`M5oW7tGTw6A
z&3c~j&|LnArriUm#9V%EiYUm4#rvl_-q*n(&Vq=F<rbt5IAZmBCwjnAgoAju!LeMv
z^7R8y6s{dWug)04$c4fHg|hM5oEpxh!Zm<XR3M3ut2e%9bvxb+%va7`zmE^_qF{K?
z<6m+I$_OW3EfduubzBD`*!D~l3D8J=H%s-*8SdfGK!)mnLvl=sv)KHxP9TOYOFhh6
ze*bw5!f{JSI$U8Y{snR<GBi7yr%0uD6w?PO(XKOO@3XE#xByD9zXHJK{bW9uvE)WN
zY`iQ-fE1?*(%$_BAURg2RfP&YA8oj*Q4Y|2zJQlqcLD_M;7S&LyknGZb1<6$9m2kj
zt|^tt)m|t0>MT^K&Gs9e3&7FTYwgl`nzX(5+6ntyrN02ZtwwfTY!S?oN#a(2CrV)l
z2)DJi9WW>dF55)DbFT$(d_QyL$#u2>YL{j;UUD{FX3+ZF>*Bq@quC0x@CR`~$M_IV
zhT4ZvtmOdUY~V-khpnla4SB_fS;(o+4SbmonoeehSMImBcD*b@i}KoE{BiaMFqJk@
zOF*Lui>ktzj26FrmtUgT6e9g>a(98Fj^T$uIg>6qVmKc^0DfY<*fjTbVh$YG95j_=
zavxs70<<HC`~xE8J00kH^U|G7Hotk3OJ!N5YMs`iR5&x}i0=A%NT*0dsIg1qILdk%
zJLicL-EQH#J}uUY2qFC{ytWBFgx(0md%>@CWseL~;wc;nJDym;wP~{wW+*Tl24H6A
zm3K0nB#dr8y(cyE3F>3w+qhp9o1a<!P*kfN{U&zVeTD~OdxB_vr?5{i%F!SnKqZra
z3KDV-=bvB=UD*@E`w@<KZ5j$C8f4`mKPM=#AZ$<;(neV(sb(cp5#I)5rU2Q0M~5Aw
z*NP+NsT4kZ^uinhaA>vg)pmDn*)vdN#>V9Wxx|@r@2<nlujf)T>?~{4jfX=rp8-Q%
zZ9N6{;S+B{WBeyQYY42rgRy;CQT4G$ic1<^j`?=A{$-8<ZKEF!zuYx*6EB^|E5>3h
ziAX@mD-Z2aPdWz5(X1src~72MKrC8c#k+dhA4gM!`LHRG3wqSbvG`vk$HW+y%GES7
z%Xun=#Q^nc$9WuPRQ~_GgBr>k+U`35T6g>Tb_fDeLN-mfgcI)Kbn+;ohbY!YewD@S
zI%;VVep9Te;=ZPJXQ4i+P^Y09R&RjuJO-oZB8#!dgE~AdjiO@yiA82c2F}BWIk_Z@
z^^qN_&2_^hN98JEtXl^3Rz4JN3iF_2%mgCA-GhSzihR1ef9rowq_wp*Hx4ZTK$Dgf
z-LnI;%?=&jYkdP+u~sFv3^jP{f6yoP1caC%Kx5D$*d!p)`EF{^+-4*tct*-!Ha5A<
zNbnr?c%e=GCekTn!84Ll-;eIwU2Fu;NF9M+Z!?k<JR@a&iH^O^NS*ME6oPMbQz8mS
z0%d=i6bCKCzZnU9VvdNmLkR^=n7PE>vSI&E!mMko6A%avKhE*_OB@IS;OUt^2J&`|
zVL;dQP6HeG9hyGM6g4oQ=+GU%KUD~OB5vpbi_)|CE_DI;9ol_?@Ea{@Fbrt_(*^nL
zAc0R9MyMGYW_WQ_G#=8ivsVy}d{&|U3r9-|_g=B>$M-{>)`nnsA6SsP37P*5AQ>Vp
zDM&5!kc`3pBGjA2uow*3Lytd(|A_QJN_zxf&d_(pOgF0o<V33|pINj+k|H7?tE=X`
zi2JJWipocnLYTHi6w*t%i68ObQ|iM0Yx+%@mcN1$g+}x)%fgNEQmUs<?|@?XrJY?_
zE5arZ;t(Lgq$NT^LPmNNz#z0g8ylG9<m96NJODc?#7z9~;9x(-R-5VCudUgNpjUrD
zHAz+GyrEiYK8S>fh`2l`6pP?K@;({9qDfTxZ>yiGLKYW|H)o;b;AO07O#nj9Rg_im
zD=~>b2L|L_u7>t14ps#I_Qcy%`ut`~30mdGui|Z+fv>^ST8-L~llPiZgGV*!??Z}!
zG~CgCgTP<*l^*YAt<>@C{M%#iBJ8l<?!8a&#cGu>i@iyJKqMsW;nC=2lw=S51`jQU
zlyl`qXZHRr@|pfyPGutpBU9kY$uiqnLEAr~JU8EwCWQ}*V||=N9>=F~EP7ZlelSDr
z4%TDzuUNPBGayrSk(vPm<1rP7UjC;AR1qsNPPFnVZJ=xO1tL{40I7?;f6x8pg@rGO
z$GD`tPCsYiDgM8;xwmALTe7RP^j%Jy=>Q<@F}%8faeJJW>AEwoiTS8kza{oM{ezag
z4P<g|Mql8fc-t5S;9p{@P$!|bZV!5J^h6{k@&SXYIWcvSR!zTl2e42$!J^c*4`)3N
z?zi=_@Eg-M0=#l<-y1aEOf|C3UCgd5jk~_ejg2&sBg)l?an-kUGe1%}cJNy??M#VK
zj|a~Z5q0Vu+D*TRdPkw5qFRVlzn$;}I@WbL0cvU#xUw_(>j%)R-iJ4;Gq%luVXTiP
z%ohC~L$6#0I^SXiDa{{LfbdsTRMh<T{5x<9umauE+h1khJZv_9eSQ8;6>zw5%nN{Z
z*0hN5_=H7jS*@U5@AN8nXBG+#qmuTcL(6yp7w_~?F9kCKt=SJXzvpZ>dturJ$wmdi
z_zhF01aatp(;*daOlL}BOVJ9v{q;PAOyCjR83CR^dMOwUh;lORhz**d51>WT?+77Y
z9nKAJewu6z#HvTGuNYikSdO3r{_*)(sa`3c9Z!gyhQu0P+dM@&*hM(GZEziScDycV
zw(y-`pd%fQ`=nB+s!{c$)_$SaUQ7{JqyF7ifl#)GBHq8c(<!*%H6pT{8~Ij$#E_ha
z+ZJ;LMBEz(aP2s0?T78(4I5CJ<j<SgFS%q7hMssI4oXhuD-88G`Dk1HDodARh(l`C
zq_EFcCj2NWv5B`nR*b1A`;UZ4!?-|{_ZTRO3propU@m6SVF78lIMnO1*FH-yqw#jf
zLbz~mbV<_<A)m9OIe3*=Yl3|#cH#(?p>*gH5j4(G61=mZidnTlFhcbSxiD$glZ8d;
z#-OZywgOaZv<w<xx!ze@KQFq|;Ii}Su|tVcwlvN`GiZQ)P6gHPyfo#B2T6}z2b7pi
z`F->ZHxCM~76LOZw-1^46b+cET!<57XjztBShGpO+$0jE9@RY^Mq*=&51UaE!TBgU
zBqInm(x@r~4EowQHJtGVit^(Az65s3)M)(}8pm@992wuXNP6E!-}J12g;bMYl@|E6
zx4CHd8GKXhQ$9&K#A_p9S@yIqs&LAR-U0lcoQ7|sx30tvUfY`7#oZ7f5$>oI1qy(^
zVf6hPl(nCs`Kb1fV<;@Bh4y43971AI9bV^<@8A6t1xH$v<ruL)73q$y6Z|6?O#6jj
z3nC5RM4ZqVpL($F1ambn@z;Y+I;9D`YiITEPr}KCbh+Z3n&;IhhEd@7P^qNH=GV@{
zDxmaVlm9Wbi}9S-lgE~uLR#_)$d}eCL7DKR5C;gC-nvK%*~`XU&?LXLqb?8r==-nV
zBM=g<2~9DPnY&f_6fm=8JUpoX+<SkeX`1-Q>J=cm-L{$sS!Es>)eAQsFS+aMQxvEc
zTaqow)C2pI`G$`y>mu{Zp&v>w6E?Vfzamkby!j9rA4BD;k;A$k!56w{k%d1Y&WRCM
z&WGBEiD&(ffN0pA;}+o~^Eo_p+L|4MYkDvr=<#SgwdMY{BDV_kmIiA?gM}b-?-nd)
zl))?c00<ueWQq+{;ltzymD}823N)WB42hwlH=VKYH+XqKB$RX86#swr2T;S<dSKpj
zQ=|U}*7NyJL-9`)P|L}sCf(7<9|rGP4+~>V++W@P$rFlyGyh!(gT=$WMt~GP*-_yJ
zl(4au^fM&!#%~&+Qt+9Q7EbxPUleOr!tcIByxHC2f(Ya|tX&K@4f2xYKBxE%o_K<b
zQvnURu)4JnQZ(PA)2k)Aowyrx2N9U|auI?g_$nUWR8bKupsL#c7<?<gNi&B}(tR-*
zSp_c!;Prq9sEHy#op7tA754*`MaE#*|5d30CTd|4Qr$k9K_K5rGq0qw`kS=DmB2tD
zM)5zd^cjM9CaDJIO-Ak_1b7APdwbVy@%Q3y030W@nX0}IZ+3yhagbv9TNU5|pMeJ5
zAns=r6fOAmjknYW8L_4ji<^`4ArMVqfjDfdlAXYAmI0!8P5PJiOesYK;L`8|*>w)+
zKH7ljqwfK9CB=OgX!t29C|WFt_Mgppx}W^%8l-|ZIei|_REq*W-G7uJ-t!PHpRQ+o
z3NO?^okfJ`Zj0;`jP%@Uj8QgKkQ9!)dWG9NIIsbIuP>O?M@dbcrO#ow&W}NK7w%A^
zu%>7Pk^uYdX&$(uhTQWI7Z@xBb1{J5M@7p4=utl2eemp8?+WlCaXYWa*sL_{G*7LS
ze+J?vI3}e3SN>nBC*-s~f&u!qYWgN8O6bKtCUAwqC$@ry{Li2yInaGrz%6C5*}^%v
zoVGI|z&?}#LM<3v$^)3yAkbu{gBY_q)6~!xS?u(I?NQvteJe3gz&Z(bNU8TOLNAXK
zQ*t7HR$3_YAOA8sbCZ@1Vs-&JPP4z>dE<)-`?}rg&nL-p`&&cVnD2IG?i15vlk&bv
zCA10{d1d^ZSMMcrrXYxn?D0?bQh>_2BEz_C3%E2vKcBt?I~<nhK=TW>!>Pk2;|~Bx
zw(!s*78}RzkPHjlL?kz>D0H!l+!jS)at<`<y^=QE`4ziyZa3j$nhXiZ%4<#$4gpx6
zn33&Q{s0&n0|8Dd`e9?bu7WI~JI2R}(feZ0fBQL8E8NlHnt1>kdd3pnrl8$jyT9Iu
zfNI~*`9Mb#3NT;|mplh44S-~(@SK5uzszAtIyqjOur7{Sr=T?0kiw!ThCXwN-|K=~
z>>Y-3j_fx8r3_}h!cSJ+$C2?zp>N%F#koW2mmLB?@v)UGK{lm$wk$y)QXYf_?CWQ7
zt^b62dz0n@q(qYkH(NyFh%!2crCe&omLcFfbvkIKQ0p;=hlgw@U^2@YL{d%E%r|rE
z(C`Dm-0gDbM%Z#<f%iLRZ2*^8wFTg6fF^7}iBxTDvdXIB&5C)y>{a!})xm(!My(}v
zqR9o5t5k%1-P4s&C?TV|e!t9B+=hgQOklQlUA4=O?t#TZ(iigal`PqwT{svY`j|&4
zRVlggimztV;q|^sa*c{+AT3NY&cd!NM)6AZ3TOGOYgR&G*)e%0FY`&MNixkg5LT6d
zne*#U))vd|-ASEZOSaJXK?Mi_8>PU7p#9B<ZRr2Jy{^F5XRoRGrB6`z%oCUzjZx^R
zjP1_~m9m4#{sh%cInY43^8#PFPgYu-0zE)g5H*!{LQPy6uVA4(pj;>LC@CqOczN2`
zD8AL2%8cU)p-_zg_vkhKFt$X5;u#Qsl^)<nH?!(cC}Zx|bTO9)U383m1J%@_ls9nH
zO%6R725ES?^FA@n6+pKs>}jO<6C5Jacr7b>u?0`lG~z%7X?aIUvIk5DqzVYs$q_a#
zXh%0}TAivXRe3~j1#q4E3sCI+5{)i(bWOOlHi=5Dh2i<$1W|2K&@Vuwo+hWizZm$;
z`jP=@=#LC718Mf>xoqX%&7KV@cW{PXw!_{6v-Md_R%F;~UhdWd?I9xnEK>gWyG#Zs
zsHhW-%%u6Br`lu4fZ^lyWkwm1$Z}u12Q$D~IJ#aySfR|M1+ppukHN@SD%h(gaa~2(
z5V?!1^PTY(u|eGECX_weuAXS!lC4?jthA}(y%0ck8CM7bQml&CHfAZPf0o9Dg#Dl7
zxl4p(qm004WzKjRjr;X|z%|@Re8_L`9c7CSyn8PhDA>v$d=y!(Vv?-ySZxAt2ygO|
zLEV)A*Kgn-_$(h7D<Yb&lzkYW-5n{XyN`#)qtRhXoV5^ZHI&}drOoRmgDOJs_!SE*
zpw#yRAT`vqu*5_nUKrxC_P_ca>L{V{Nj6HAfO;<#hqfDDf8|+V7t(<usQiq%@dNv<
zU3BrU`C_%Uy+Rj@HC4v^cYT#^+*1*Wo(!C>fHNT8tBxvE%#=85_*_auG@HE;-LE`W
z5o{R2cis5K1++i){5)C6aTNXTpdA|X?m6`7q6~A63v=4n0avXdYHm&k0H3t$INnDL
zX!nRiX~I5bP)fJ7Vha^5a9NGXdUx|^FzIJSUd(u&=;(xXmA#!{^ZZ-O8Jge}`tL?M
zDY3j%?RoKCb-m~H=Y6psnDEW6@D>B0NfBYMB=*XkUc;~3pQdNdSd3}eZm*g!x;`Y&
z1S8QT0;~TsSG>H@jE*2&`q6h-cK}tv4B}}5BT~;IuoNYhhcs#|h9z894Zjm=w5fe_
zAk&FEe8p2fqEI5ENw4QSqPYGv&L6N~FL|_9m;CUuz{TsU3$@+VKjjM8e<HGe@NX@s
z4KD?&GE<A|?>~AW75`){OP8`C5{l~$fEvv|WtGnf7UkmU@iZLFS)?8Z_WUC?o~r}S
zF5HL^fEU!J@sSbl#k1(?CVpe`<IelhrM29U%^r6;^pzyJDMXW|iF<cxhQF&zZ%Afl
z{=J!%KOgY}nNB*=bXkEewk8mrw3cXHHFWWbv$c)-brxT&D^^*mIBLoe1La(iaXi2A
z8)nC&ILDI?o*p-8Z5iFDcp3&jKNG<_l2pkfGR8ej4KcLmgte9@?Z{|Y2pYogL+uui
zxt<$G^$VTJR%&8@Hagm!o=S~^gFM26G!%lC#Kgp=vlY`E&cR4?^xXGRTgE!LQz$dJ
zemuDw%Kfl=q!R#e)<Miv-V+3`azyqWnh$e$l~v){d{-4zM=_{?F{qhoj`?RUYKz8A
zuHj}VDjW`sZ)&3PEn$v^-SUoHICMjIA>6A=M}kPU=8wDo&UXNNVVhDR>j8p`h*9%R
z4Kud|P=@#Jm`YwZMUI^>U~OpASZ_J>_k5t}nVIp8xg7$K1eZNyn9JS@zGr~%8d6h3
zv2=^>)4~BRq({-Wh*%9=SFpt-1-r#DFu?sJK{}}a>k@(vdmxQX_WX)hA3mUnNlI>T
zQFmK~{7as|$5NaX*X~DE{T)g{;l6_eF9-h_O0h2u*LJ_0IiuxzdXt{c(Yu6YW||-2
z<NMuskN<oJsN7%j1SGDJN9N!~^YHZiv#?;qf^lQ-CxAc(13TN>pRlsx0+H=IZmwH1
z8nEZyb^WF!EDUiyl&5JS<Iw$kt1IYIXv`x2FIwtP8~<lu=v)WjPI^-hek6tVL#0&1
zV*eg?fK5GDf=KuP0sHOSAh8>QDh_4Zz&;zP#XaP2sRfYAC*X;+mknx|+&;2kBph;b
zFg3kjofIa131kHp;f@;DUH`l`;Qu*IBw#*+9|xe@<fcIiX0W3G>%xlWS^~*UBO3ux
zhNDMBQE+)Q&?5VLsDjB0U~h}PeOsjL%ZB^!-)+EaUUW>%mjH1aZ1!}AL(iJ)fJ?17
z1oX)_;3s=-IYM{odUm*`H73i5j<*BmR&;=31|z}p28#iEx=RS|N{gHCVwS+`j%O(q
zs<EE>1a!a)01L`f&JDZT{N2DMMql<|u~8j|S{I;AswLWifYYI*p`p31GQE3q>vYR-
z6I9pOSGboAuAc@~YX+zy%>l9?admw)dp!)7^!ftUd1C;eBXgiWsGguU>IfO!gR2z`
zT0a>8cQ-(#p2H2gy@?T{H{00f0v~`1&+X^M8ZY*HBKjLZl~f-cPmyyD%G(S*sF$0b
zE5<$$5Xu`~o*i+8u7{qM<NC@XcI|<mScl_fz23zMe>L==LJs1A-|svE-N7*&v;mTd
zUebm}nHkkQ?YeRek+Mdcws=AkB1tJJbih3%?VL~%9vqt<-Mlfv@JrEu9#`jvF<(oY
z4kW;52LN(X?b|)yfo|*oLZzISCF}zp;O(<>F&VM<NdDVOllzD&!bdi!AXJzQjLCU*
zta^gd0mvR@FZSz<f8d3XaP^zsr@jOXyfeR1^b=TO&)tLZxsY2DPBcw{0D)d=`Q$#W
z(B3pTu%f3tA`<lAhR?wkYR{Sl3{fHzJMiRr>dX@--up2NW?8&_b@O~c=Z3cnKcl5Q
z)^w)P_5Dh+%YH&KeSOHP$&4=NHEYtvVq?fyc55|1i`6wvi7X~jj#nVV3Uw-YrCZ0x
z#~J-`_2fnXiLHfCbl1OSxi4|BM2GJb274gq!}!mn8)5waZp8@<7|@oG_@pan%TU{n
zM3G$Xv>cZVC17?uuN&xbnK+m&KZvjWB&t6F>v5%O&H{sic0F4HZX%JsIH;KrZVVY9
ze7)xcZ(7Q4!5A?17j!uglxa~A>$J+)ly7ct_bmuz{0*LTq&M$J?MDzjYxGTWZe!a7
zJiNX;SKz&US24j?+I;Jly$yyHz}maJd7SQa`-z9(E2n*{%uO}_H*+YAbjc)z-<dUn
zHqeXaraBS+gA)O7&_lV_>I>dHLnxutxVMjijSa>^z$c=(=~+HQYEK)~5QjJP692QI
zpNoq}tWy$RUedVr_v($sBqXe9d{=w~zj8p$w{5aeT|eTmOoOBU1<6hEh>D0^gmSk8
z0I<UV5x^V)#HErd`=2(H0x=%U&Y4q6>b(tJji7Bu*fFP-esvSvnBhaJv$+$9Z`SWU
z8a&$!%Ed(8thhG(3>_JJHul>U#{$dKn3+sRbGza(;49<jjZJUYj}*S*l6;ENw<}H$
zzLL59$L-wm7CtzP6ZS46=Jpdx;48I*q7!bDrwg1GBHa@Ezj6M6lt(XT`p*$QV3Lvu
zxN4V5{QoS(e-`4OUG|?Har4IiXCeNx5dR65@ZJa_1D`DTpRDo!--Y;R5F6eR=t^f4
zk^e1fs3T~7Krb@cC;|M|f6FI$1@#GKQxgFUlhC>Mwd`M^+;0^4%sU2QOo&SjRrYNM
z9ghd<sQ-P@0~r=X0NNJ#7!*y-N9E;jnW^pa(`veVJ3G+;a4iNbqJLIaT$D#*VhGwg
zJAn+2gzSkG0LXBFb_@c6h#Ix|O10o)h$YoSuC*F5&ch_A1gKi^#Tm<Fy_%cN2Pqz3
zua0B9wp6L3hmu74+k17GN&TuJ*I2L9oc0TM`-E2WHmrc^DoiPq<o5?EhGU$vORBjG
zG79$M)A?tLczKg|%}>}oo^e$D_?ChVQN9atx>76E6$hN%h0GbUU`^fqn5)PyaX~;<
z0es*6(ijnrge<yZ#hO(Lbq*%rbR49N=K(_we1ac!w6z~+8HkY?fQGg`vL!uwg>~_A
z2NzG3Ft%V6;uNzkiE*^>LO|JATM?x>H#eK=_=31CHTZkdc#2r`q)K03G7)az!a%5S
z_PPO`9{Xb&k4u{#$F+9?*w(R$36Wt8xtX{mj#x*{rzqKpFE|gn8{gvy3SmnqEb*e}
zy+IQk!uYa-yD&pfmZ~8Qdle|)lNC%GakM&4XYdjOt~B17<Tx!LFN8wZF9hhaa&jnN
zhEjINF%@P<CCcz>t(h0Tsna-UPR`J)wG&}@nx3A1hBg51BoK1Jd3}6EnIwN-^hdc$
z#OElVkltR(3D}R)23dtyS=s{rmWV%2&#JDL-6ZaN<7VTPP-SUr7}EzAY~zeV8IJ~v
zkRR0S2wFy{))UN+QXEJm;EY_(4=0_^=YB(Hj6}JPP@vG!a0*@(ScnkKxl^&r@Nwjn
zo7yZ^bd0LPc3ozZ9zhO<5HO(^ej&gZAWNBu%Qo5<3Csxb!srU}ykbuy!FVk&<Xb{`
z`<yDb|9h#dc-yR$3KZGoDn8OX?o5E-t9-uIMJT~3s{j0MzT@(2W0E|-2MRc<sH@RS
zM@47d)=e%!@Q5zw)Kg9SgTLLY5PrjU28L~%3%{Nx3`_NF*CZz)folCt%CgOcXP9Vx
zRL5z`o`GPmG>Zt>|14k<;<*9@_C*(HJ%pGS8dOni4o>i-dmc>K)Le_%Z@gF68=%m?
zcxbZ7f^9?mV@<vofuL`P>x~Y3?8l+gzKxX#5BYi@+24bOVe&%pBeYNk=2uSzCkC86
zZKZNkQ(`<ziISV`cL;`ezQ2q?r1jWKQbOQgz@4upIP-scT^VPtgvbA<VSz(m7LV==
zN+oLVi;*g_Zx6ohOm-VksfS)1Id&T^>Lo!}Oh`vpMhOC8&8)wL+K=54i@Mq+5PxP`
zXX}`)D}MUHX4e2E?33iR5=Vkj=!cx$w^CQUwvj>n`7nHwzWXgE0yMFxlJmjSR_QDz
zq=%}R+Ows!xqs%?Hn@pANxzOO;owF?UYg0yQf1)VU0Lok4JR^UxvuOejvwqrSA=N3
zB|wn>a?NFIU*7jQ1SJRy(P(?juq`zb$N%sYQwsV{mvkbvy17$N%+$g6hl9Qe>+al6
zwfhT(YigsJ*^8vECFmRNl0?}joE$luJl?Gi#=Wo=k9fqHrL+$7%kT&0JP!08k4_|m
zp)A5ZiakFa+LT^+8{l4i!EOsBsM;L*K0rAo_Lj>#dO&1miSW#sHWgit2{|M*l*4_J
zPlLHcyV;jXt7er%uTeL##ykvkklcyMJjq20V0uW@6|>AX7@0Q+$h#zuqan&*Lz{&Y
zU>u?1=bv7lK}Egu?VePNA4ZpLMQbp5E7T2Bc_n%4K$nv0{<T{ceSunjnxtNi)x@|-
zRmgtwph&`Sw9@e2R9Ne&iF}AdS#(YE(xTDVu%DUzxgDFGaq^ODvyIQWe^v2W=Fvdz
zRbwP6${_CiD&tL5d*oqOBPn%sHc?0&GK$CLo${K_XVQ<5GA$|4I!;sbv%?`F3g@=<
zAffS}pK1dwD@rH@s^}k5EewAdK9HD!xob9_em3eND!<HWZeuD$(6+DW04I5ZL5187
zi|AP7JRtxfQ!}>W!#0``a^IfaM2iUD$xwtF*S)td@W&ToJ4@1IH+su2@9syYvSX2K
zBex~JntO-)+yg;2m1+G=+bsD81SlaZi}db?JH0Ik<2%ePN<BHi78BOTadR>EefQe(
z&w<*x(f47}$daNvIk*YVUduZP^j)&A#7r%Hd6i-YDW?4ROHXb1Y=bMZbD^*Pu&N>j
zu&1$FTae{zYReC(MmeZrspoPDn3W$yt0+pZ1<1>{avUWj88~oOG7dazW@W3U8#o$g
zx6@?uYy4vHNO7hA`XTlqzvLR161zhEmYw5@Z8sGr@j3KmcYYs_#<P#*rs3nbS3j+G
z8Hk;N6`}JvkGcE&yM>tC+dp-xBPLzIW~%8CCL~+f7O%FAJ>NbZ7NWo`y}s~>&vx5s
zJjZf7T=jvwsHh}DTKupD0s?pB+Ln6at5`(5_A5emx_Q}H@OGt<CLCwhC9|4lFFjv}
zXL%3I1US3PtE;RP>YdEBUzfBxrWSpR-FS6sILaJy;URE!{8Q)i?i<>yK)eVBAy`U+
z8*0-wIC`h^@LQ0*=ksgw<lSR|SlOPOw{2W~TVEFKVY#6aY(k0U0@tBCKe~~xYA5Fy
zrJZ%TM$pI9iS@OiY45lN6o&}SZ1R@cG!>u!c*_9Isx)pBytw!M%J$J0kD~`)y!O2F
z&PrDzG`N17dTh};2VZEvYueI!|9*#g8s>JA*xln85o6UJP)(pwL?KdlmN)Z<o?(dI
z9V*@tOP6&8OBjXCS-w`>pTw<7VxDP4qH*y`h(-Ih(i|wf!ZkMxdmXcScn6Px>s$T?
zo2ULhe%Z6x_F4RrT*|(3uj+l5{Z8*G0*GdnRXU*F%x4<w1c7#M-uS^Lt>e2Zy=t7R
z6X<m)(AljfV~0{;hb=M083BrNdFx{`eQWb#hzOE+GQ^_IM)N9Oz;_&(6K131GTfVt
zD?g7=_)g+aHj7v1ELk(=4*zeB(EZUfyTXg1=f|<>pILY1oGb}GnI5(N%r{ZKNA?So
zysGCbk$P;Z7FoO(v`(OS<?QvC81aN&0YQ+zmu^rBv_w?&@=Que8e*}|gGm_sAhY}D
zlhp<<(l4IRiC2s<4VcWrIEGGBQglfS4%8N!_gZmg(n-5#2Z@%ns>R<!Ma)^ZRVp~x
zOAo6Td%ki{YwLO~SW%YeYcs<Rs-rY}VP;y-jbX;0%FU&|MEqPA*32Mwa=YNk+D<8c
zkX*Y;m!-pJ)*Scl4^IEqDXtkay?+b;@{tUu8&PcHJISTErbFC<urHp?V#6ZQc2s?R
zUI@fZIEN!#=tb`<QvG5Z<piJFBqHHtynDYsRl@>|1-ap61>1>e?C$4U?<Y0GCJ*Yb
zysuBZe}RF>#MZ$2pir#Iap01GcM9bHj;i>J?B29fAwQ(jx^SM4U6WAJRjkzf(Kk^X
zW?UkVNKd+Ai&mj6^?M&=X1J7^fl{O_Ev$8Dp~a`=`rOsl>|;%<-j{g961^6vf4``;
z^Wm3r;@P*q!V{*-Pe>H_x=r#n%8M<7CRI5E6gig^$A|DE-1dAOxVnh{tR1`}jH!C^
zEyi0SNSeg&&8xz&L+UlDq6IClno+y@)?Qsy0}s;}42khl(~LKx1-mTDcXWiV-!LF@
zy^$>CyEgMrn+WzEpPCzczl-?7)ha2UY9KaT*7<{M>ZeEdMG5?<YIjW~H?3ze+$Tf7
zf6VRyigIjc-U;c;#>w!xh)Kvw8ru&&+1Ko$T{J@Nmrf4P@P=YJD9_K*Nq*Em8Q=@v
zj~X5MZn1E7B-WOlcp7vsJ{sS-IqJIoMV0CABstUJZ2C|YoE*97nEq<~v?T$fP2xWb
z8{r3B7iBxLcvlymJk|1RK8wgg>(x@f_r0Z`NGWYo(crvO(=$$<2&ioBiFA0&6n4pv
zOML$<P4t8-B2D$a`nHHcCCT9iu~I~1GwHmLRdCyAD*eg`LDi9s%bQh?+&Go_r?lAx
zp(%J}=ZnspbrHT|D&ADktTqC%WTlm(BeA6Bf_KFn68q9$gBzS)>agnwS*brH7|7d2
zQ!J6jr`C<$2(0}QS0VWc*1TVB0vRvSeJN&)a&>VafAFN=efw^INIy+q#eiTi-*)Xw
z+fp!!K#WnhkqAC5&ktMB{Gl_LFm8IE7rIni*xVbB()bC&x~`NeXvzS|e;I*RZDOum
z%0_0RiqWL`G2JN*Iy2Wpgjzg}`!RM3D|7t!`V#Ae>-vQ+@)O&s+IPp)54)Jt*C&B8
zac?ZCnESKNOtic}P_o`N=1N>saK$?J6-=3EiWfEc&ea3fo~w^LWQxhT8qGKtC#~L3
zmV2NX@^MO1la;MGrL!Yz)R#W5PSTpz86D?WLZ&PEIejf&SU-A2LTXU=g=5s~B)0FX
zOo7f`z1N-|^^3e*iK~GOSO8&>L&4-RbK&9g#iKB>J}k?r>6mElVe9ApmxiZ=n{9PJ
zqH$|pH**P}(qh`r9_yTLd}})0@M)?w8}z5ke~`);<8?%GINgYF)U55>JTZP<Z^UGI
zbt#x-<}$su=8SW;=CW#{EmTWFDnyda*AfiEd3B=8h6l}c3FcEu4PLh5#U&I5p*x{%
z)kqQdWf&m!#iM2zM<G}n$MQ}Iwb;qCzx_C`FHp@my*_?Gm8ZCD(^TfKa`9Sk-if&A
z^p2=JwtjrMrOa_HbmxfN6WPG}oJuguwOW=3_GNzni)~VEmOacf@Pj~(Pr5wh9(qNL
zSNgjOk3-tZWDgvjC87pI#Mhb>8Zn<k#&$g&cr+i4Ki%mhrGmaxpoyf%UnvL^uTncn
z9Lmp}X*t13j6j?{PEB8;+H&TJO^w|+SKLbRFIu-AnVpzDxBiMyq*L<{uy{4RjFOZ6
zs!vOuI?RQjso>nD7BD%9)vW)q)6SR<RB=~nnx=3KVNa*aw%Ih7Oie8Y773}r?Mj&%
z$+W1zP))Jd_~md)sWf6fh<>?k{gFlYoZ;$xJ-Pt-M@tEQyew7G=7Oatp0D3U->kD2
znxWmBqjM9Is&7FCeS>szOiZ$>e7y-hJz|$`>2!$+ANsVy4cERgprhx1SV7|tcum4G
z|7HKa?Qn@k7?p0OX{}H%HvKwJ*^5o)$3%m7IFOag99I;J`0PndtbY5H($1R2LZ9gA
zKDCy?%(56s{<yK;2U&rh4i#{SW7J-6OmI105wDXX(RMaQS_i(!<`RmwRQbTa7|qEd
znk1OIQGZ1iGS@MxFh6ggt+U5aLasd3Gq&YBsZ2J$cRBKKqd9v$L%K&{`qwT)Nzqhz
zF9hd}`!RXm7XeM|%XMhpR0%Uq?!I~rF;~B?=-5YmsX{KKlg8hK-?B@n?0=WmzFTFy
zAaQy*wbNN>ffdb<H=@V6SX2C**xIH};#$IxA1_u#!lirtqqLWZi9$}_Y{cW-q@LpA
zxaURe8s-;m=XA=MbqV&N{+)A5z81Q(ZlTau9U~PMwO&-OVX0YjO2pCa^<tWpL8*wT
zl3W><zwL~5CW0%eyeWohQ66kxqlh@ODa>zsQ@klY?hg5#&WK$Y;l(O}k^fkc#gtND
zk!ED$oEzz~l!~g`sjsvfIgfo;$Cz4-a>1dInO0yKJ1bc`IOKrAdaQFI^%8ia-tt_)
zV(eyTzQafpNcNW6MPGG)%V}n73KW7c<`Z#x5P?ZLHp{;&%;Yr@lA6xJ!I)0DsRE_8
zMJwY1zHD1N+@B;`%`(R3t_v2}FE#kiE7}7lxZm#^9-GhZVs?=gYb?h*qm+7j6A-dr
z{A{!`yEq#rdYf{Jb4H&^W;EkH^g$!<dNcR^{I#b)uC2!h@=O(4g|A3=k|rU}ghRMQ
zy2z!biH?#^h3QOp6R%H6sji1mOh#uJa=jpuw9G9nYJwvc$W|)tvz76+;!PoLljimt
zk+%JJAr+|uZFLD_(h%Dc!m?K@-f!D_q+--CwA69#$%y7pj#6xoB^3De7A6%H3E{fx
znHB9EsTFx|&7RI0Mccn#)5+e*>sRDGeVb4^;DHqh4zQxtB@I*Vk<Y6z!8whnpRpq-
zs8LdlD)gT3dg=Xgq%ac~CM7S{c!XbYm5H`k%*uMb9x_U$*SaGw7!s{Wzia&y=|MRY
z`NSg#IL+kL_49-F#~ad`cBF4MxVxT-#BzGtwW=}ER^jSRK3xjOE}D<WvO>9j>S-HR
zYJ^HL=fi{U>VWl3@BnT7w}yP^raXnS-#R9no^XO&!IfH^(SW3Pd&*8qXKIDF>a*7f
z7aIZ?vlPQ5deLEC;lF1=Y+pH%xY+zArEGl3ck<?0j`Z0}_s!IUXUE=?I!)AauUXz$
zW2E{G+bvW&yj2;-w(t%U9qLw#^q|JpL!eEi24kn+XPr=)PgNJkc5Qo5r7V?bhV@>q
zgr(*zx5}mRgaY*{7Oh-YDE1|18=>xd#Bl!ofHCdY2dX?@YJTsy_S*7Hgvm-E>Aq_D
zbw10#&L&%Oc9J!NLnm{hG|~v=Wh)P#o~;;CF6Yxyd>j(S)yZZz=Y9Qh$}IlR!s}eS
z<$BFCl^Wa}W;fi0M?Kny_bWGM5E+CX`{aEz!mU`f<|iv5`%!=_tD4-_o4k*}>_^tR
zkyjhLYlpccAMrSt<tmpEZAljUIE5u#d?bVQ*(HjyW{hNKgJU<>!03$O-If=tWxsHr
zpPx_23w7=dG?z_O&->fW?NCKV{Dw_Q>rwtX9I7rNn1c1NGYC*Upy`wqnX1~@>+0WH
zt=SQXQPK2!bZsm&MUBRR*33t_qLCZ1;juKK#;ndj9O%vXGGmXiH%_Q5V?JB-=%i*L
z(sSy~)wx204J1hf=Kmp5kz;CqqNw&`_{VS69@e7<aa|f3arR7)xKyu@V;{ZiYUXly
zcNqPm98tlo7c1+rrlpG5$2*+!W}WDHLw8LIY9#DTMkjK~J7eX~DOHRu^p(mLnz=Na
zzLHpXZizM+gq!!s2rtD+@m1?Uzf;sMc>mn4az<afdKRW5tvpPdu*(1rEzi1qm$@|6
z@a7Et=vea)ept($go0Go2iy4OOy2!F?Z$(4oM1|6OKDcBYXzAZ>VQ1&pn)U5@@q#K
z*kv=&rcu)}ppidVWKz-dzT`-8T_#>Sjof_W<?3F$oNF2)gQP1*QL0$ZyOWp_;7+2V
zkZkjrw_q8j!Jsanb0m2F35cXt&+lCwzy?<N=b`T%drY#w#ecg@PNXO_)4SNMsW#Wm
zrVb+u{x%fqZ`We%JlfsngX(Yh#^iUJtM!YfLgFulSBJgdUjAaFk|L!iMWZw<<C*7w
zM;6vm|9kLP<2m}6^;g!j0ge?Aq&Hoih9TUY*uu@7&sG}6g1M5GZMF{be^@$0*2JGz
z4|VSF(QiDRn=M0(db`N7q;htsR_RvGAbn2Hep&4tpjE9JD)*YZc==CkU26o3Bo^tb
z=o5Oy<)^UMOi{icNG-2pNY5i{&*inJ(ynH9F-wZe`-N&QTpJLiN+#T|&BC|Y<%(E~
z5h5AZb)~eA?y|{Qi(0nHKh6G0c}2qkP0?-s>g6Kbos-|4KQb~G#k|oqz}3$6Bb^{E
zRdarlmTBY}(&)Zd_w;N_<-VxzuTu6>af*fBcUh@Z!A?u&pG&Uff+z1JrG#u;lYSb>
z+v?iS{}5p+pV?<Mo0;W`lP7V?Iu(gqK}fvv<_zDVNVq=aICRcPXLkB%z*CR=?6R5;
z!BBHNrRpO=(ecH=Sb5(Lirucec2Wq6tU(I|F)u&K$%eXHtI?rJnSy*4Gyjb76AFJk
zgmwRcxUjo;qF5}?A3|~q|2kXjZ@sL@N30Cz>?bFBIiVk*sf~AE@D;Z|W{%0!b(`lf
z&U4<?D4^<-mhVuz=0jCnGPw}^n!G9a<rAuHzwkhykl{#faHKXHNqpQ2g%?keVSMSw
z>%-z~<}CfKs3x~cveHZZa=XQI+ufQUvqmk8<h#9S2fK@CJzrb2Q#T`v_kK&r5FPAy
zpwFC=6*gYBrjUm{%#`@`R%i=<cS)9-(=tsJEdWnnAiM-x`X*C*R1e0r<G0=vJ#t5w
zSW59oS~0c!iGt>`PTq+0R?-)|kC!L>zJ64lhC7L7<%(a5zDRHxbI9AKzcn$<UdMVq
z7nUP!@I$zuUx+ktKS^E%EAm;J*B<3D{|P@<p3!XApI?H8j}5CDCsZoRAfGP@_IGV+
zs?fa0LK&ncIJC^(xt<tq{&qodm!q8*KD_@pq|S$GMb?YFen(OFn*PBTqni5Wl`f6P
zmlk;QSQnqHpv~#LGtPC(*RdfII-_*jTG(U73T8+Of0{o=)M%KR3=C6k(S<R;CsQL7
z3TBuU3SYHFTUaL#9u>${wP~~Mh)o@V$#1tD;Y<HP&-0J6{IHYBnHObea%87{`bR@s
z=LL#=FdEgFzqi$f@1{82lO<_hC6&a4Gp1qN_KRg%+X@7N<KeDuw3cqMSWmx1j@?FR
zPY?vS&J@VY=@km<zusquR=@~3(c&bLQY2a$uLRv=o+gL9LhceZG0~i9yhgFax}X~{
zYT?6F;H5*zMs7PSv38#rM#xdr8zVh2=-DJJJ0w3@@Pp0-Vzjy$^gy*X`mcK@Fn@WA
zj0MMw0axGdykFnmZ||#*4I?>rQJlhBciHz+opayRkbhX_@QrY|qMqTmcxE(zLOTkJ
zdhc;jcxt2W-nf67d<q@EWB5DqBvCT5Zo@3v`}m-fx}JWAq#(24_+({d=4dP#I+*?l
zW=<o_L?~-B{qqIhfLv-txjubAQ}~;~mkKMth3v<BT@>s`%-^`4AXjSgqjBualstZ<
zLQ9}Ps?t&L`yhqH|D9IK;K6Z@>2v#-v^(j`7mk%k&JI0jwGN_v8{_Mt?ofBMZv*#U
zRt+?NC*8qrUt8;bA{SZ2?nw2h>EN!fWT@Dw_e$nvd}#s`+62vSMsMAx2rqLs>mN~F
zRQ{gLqe)naHS<ERr?Wll-w{vyeM(w;P<3R!|J|^UgOdiGgZxqGS$NY&_p4b}Cm03Z
z{d0K<^9r;%m#x~-CZkiV#rL@;vs$^cqU&qwgYOmoq$5f-aAR0}wVV+=RD40SPZO2c
z1gY%bp>kTKL>X6*i%Euxh8pNm7LHcbj;x9s$2`P`vWe!O(b&$UUELL4GONx!6e#^8
zvEiN3mMN4rsn=nlQ9g1QqK#dD?m4Xh%kekq(RWQ<G0||C>-5rMM8Nf@EHfJ*W7q?u
zeoU9NS0I~MlJs44I$j7-Uf{%~_wWgz235yG=k@HWcl;TEa7*D0#;T%zznjIubKS=8
z&S(_Ub?ryfd!hcRWW)))&o(YRaMbIOwk^GBQ``>8?$tptEYBE9FRQFr_191twY~32
zjv_!sHr~CzOxRx4W<o~m&%;t|auk;6b}MlnPW|Po!g8n6hpBs_pBrjRlcM2pZ=B8V
zC+te@RPx$=_x;9t-X_affc6nv^OU)y(d$O9m-+mMqh=rD`fS%PhVk)mS&g|3ug>dK
zm@`nEVCG-e?&nUd9XT?byH8Ka|2ciTB%Xo?DR()M$TW~o_ew=mNH@?qmC4O3LRlk5
z%;TNgF&8K&mu6V0&SJRhGr7=`C1vd56YsdsrJI-G(xbNY*!>W>EmhT0>0riO@N6e<
zIG^L$Nb#qlz6HC*imLS{v?qC!EA%hO1;uo%e1%8RzAUmYy%9~Icj!qS7er8fdqkjg
z?1gN>WoHDVLs3!ej#ymN!lRLiRntOJ^Yfl4;MHAnmrOTa+c)(Rq`9cv5#EoyezNf0
zCrPntup_Ew`$KkXKe03I<tZ`SC=>3l3ym1u!j{IB$anN)UMhSPAjiJ3y3X5wfG&v<
zIY5ix6U&#j-jq3*)`=dY?07+(yRy(>5IdXioo^988)N>2Zrl{da)$3-EXj;dl;KAu
zi(xOlEDyaH)ZDSmy!`#*U$RLAOxwkqXME@HYnfP(qmB;2IOyhA%fLkR2ey*xQq9bp
zTpW)$T3i^HyZY+cCBOap0kCg$0?(5ZF*rA_)3G(x@fmr~ex{t*o{2E3rS;QeRius{
zh{wzCPLeDvDA5BAB&zM$wnCv}1KvUL=hGiQ{YfX<or&Vt`#GezwOwO(p=^FMO?zrK
zTb-vTK;Z3qy?hF6FCw0KgU$P|w8~+JuVK#X>dMwJnT_Ws!7#pSmJe$qUonhFui7gj
ze`PVENO(GUn|7o^*Gz;TjN}Ln4Spe)w|w1TTUT!W_0;`Tt>5cwg2O4_#YJ#!vkP=4
zKI;r~fa;<>$;@PPpa+*@`StLExXwUw>RyxV02z91LELI=&v%;9y_GiG59I=*kFR1k
zYMYPjQr5PX^a@^L$0#l38W*=6F-?R|y>;y_C<o)``O=rKsfsod;~V2+hoa9-guj{e
z%$A4x`WN}=X;hJ<iAyzS6>&R~fAXGM!5vv69}^-O4b)9NM`vS^<QY9ywbsrTxM0ZR
zlnD}_T(f=%<M8pOs&WwiC|iXQIXS$S<DO-lDJgr>vb5Blq3`*-M$!G0T1U;5tkTt%
zM!R8NfCc&PeH9YrU+51Qx=EU}$&>bn#=kg)sX@7~h(!n<H0WN{4NV2@oPA-F#M@;?
z8av<W$(wyKDO=~D+Lv5l!O4Lq)8urAK3=YHV0a>xXLcBGR6%84Pi3z{(vs)=gybMw
z;}>)XCvH<9kj9b*QoNBk0(-rF_IiI6raJrv;yN?haq>Fx&6W4#wjBJLFxx`s-iCP-
zA@)W0FK$}v>EB*Py6HAyX>6qs?j@~XoNTi9{V3cp6NIs32&tS;7QTr0)(Ty)ZA@mT
zu~0{1+-J|BFJ;mT{PlVYr$Lk_OG78Tg~NG!^6B2;0NL7<lx>V{%5mlNeT8=UU(~SD
zma4vqu(5~ZrNs*4Z(!qCJSot=h6$o0vzoxsXP9+*oL-P363cM(;JMsm*9cD8i9OVz
zg|LKDWBNjo{W0RMTBniej=F=M@e|C%$cHd|rBNCG5ciIlD+Hw_k2W^r#V1g?+>+eF
zOT=E?tBJUPL#f!EeB|X5ti|T-&C;uCbp%mS(Zi``2mcpKSyU{)=%va`5c6~j-^uxz
zS?P9Qyqu~zY4X)1d`x7nc^qvgAYyx$`rF`P^}dknIyDmls>R!!BTb}FsA*zlA&Gq{
z-j*nOxEncG&h@%N)?qCHYu(XPhYKTGOPb5x4N$YE9W%y=bZXC5G|Y+U?jr~yBGRSn
zk}_^w_NkO_{(2T8S+DfaA%%tPpxQBUf4L;JX-92TkdaFRVT5x(XYGmd)FZ0h#O7dE
zp|pF_C~?s+gwY>`JYElGOEpPON16#Ow1kvME!}3oX5SGR$1myRGeHFYWymnp1`Tgg
z3Bhu-$b`*;IENJ*7Sn|KeH2S^-^T6^9-SOY?_J@wpNU?H#Uq&R-(U7DPd!e|dA!0&
z&L4W(W*mNAOfmpchz-o?&GGrxuzi`8ON<Ih`)Zw?J?V3y+*6(U6D!L#XB*VC%IXeY
zp7_M%{u)EiY@7g^G=126X}1YfR->-t#525!MmbU4u_MEDgpne};R6B$=Y?b*$4_^K
z?bnag)z}d$kQ53;`>B4dqJ)0c!M#X{b%H%br4Zwv50VgC+;?lE&Hbf-Aia@~U4b9>
z%JJFQEPib12ll%y)*7e^zXn8F8%8n(>+tmrX74Z#AY=T}9!_!(b;LchDa4Q!l<r`L
zTvRR^9K<n+p7<4Rdz*Prwu>T`Y38zoF_AfNPnpX~B8h*3ENR+oJ&tYa*YJM$ZHMzw
ze`9d!sN;_pq!Eq;(48E`rq{7Q&IEA}0+&3MA4B<I!gkV#KhKX>Mmgo~&PN$f8k=7#
zJ%QQi>RA>|)SMa+^C3MFs{DS&%vN`BiN^frhpbdfD2i<90mqiU*|Fu=1}u2p<;b*F
zyOcXljcg-%r~HKVd8zum%Mw%SdF;>iiIQWp=R2+XMLytAiPG2(!(*L;viI1cUS8#L
z^awPg%&7U6;g<54l+j(L#7HrOTfnZ-`=#;~Pw!xn?YK+iD`O)dTaZo@xnG4u(Rz3`
zN+|@UmdK7)rH@QdqHq?K>IPd);z(Yxzhhf;If_GV7e7`1R1_3Ck|UmTE^Zrbg``ph
z&FyKRc=p0!opnA2NdZ2a(eUvE);JX-V!@^OL+}&qz9D5MeO`^4l#vm6GjusNc~e_x
z6z+<&1vCQS!Xy|V$|p?tt~`i!U@~Mz^dzQ^z)Q9fvdeC~2qV-kT^HNxJMYh3hHV@K
z*PBr4`zKVTVMT}gQkV0|FI+i^H@1(=_AZ$p1JwksU|gfpRCB>`o0{$O7|%V&&_hS9
zJ3(hI`A|D472Zla`y3W~%LfpC_qJd)UN(&e+qd6b8&X~=*~1Swu&v9ITf~DIS2A^r
z1Wnou;Ty+BLO1Ct?n1h@WLtkTk1@uhXp6L*T9bwF2FrWT_#o5vulZ{z-t+QmHdm#+
z$09iS&}Agw^s0~KUI}?f%Tj_Km<{~oa{lu?na9YKq2EaG$7lRp1SVfN>jKrzV92|(
z=X$5Q)4Ah&?%s9|f=yztjNjP!jHt4-luENFdIoC6A-dt)MaN~$Toz#&9eVRE#rNwj
zBZ@0VQk^8tZN-F|L<<|>?N-j_G^~3S&U)AzM37f6E)g+Fo<$j!BA`q65E^lR8f*F*
z@@b)Kwkav0VFmy1UkChN=hD+ATJ&df<&o&o)e}W+VGRrUJH-r0aK(cl0;nzYv@Mk4
z0k@-gd<>-kjIGaV{QdS^E#b%~5#tRA=7@A`uX}@T4NMCt`;`dXDX2y_2R#5|043st
zdk63*<d|0?yf+M1e0nQ3XNSxC@APio%KP!*^8Pl}blF?6xhY)UfA90ejSU?_3zx_T
z7K<kQ<1mnFK}E5tS||#i{G<d86c#jwUVvi~fM4VeFy)P>?(6jb*8!;SX%RIrC|EM0
j<ClK~uloA>?p`AvQ8Kx+hB@RQfPb&V<-|%v^?m;rYGjaT
diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst
index 24a726b..b1159c5 100644
--- a/docs/en_US/server_dialog.rst
+++ b/docs/en_US/server_dialog.rst
@@ -32,6 +32,7 @@ Use the fields in the *Connection* tab to configure a connection:
* Use the *Password* field to provide a password that will be supplied when authenticating with the server.
* Check the box next to *Save password?* to instruct pgAdmin to save the password for future use.
* Use the *Role* field to specify the name of a role that has privileges that will be conveyed to the client after authentication with the server. This selection allows you to connect as one role, and then assume the permissions of this specified role after the connection is established. Note that the connecting role must be a member of the role specified.
+* Use the *Service* field to specify the service name. For more information, see `Section 33.16 of the Postgres documentation <https://www.postgresql.org/docs/10/static/libpq-pgservice.html>`_.
Click the *SSL* tab to continue.
diff --git a/web/migrations/versions/50aad68f99c2_.py b/web/migrations/versions/50aad68f99c2_.py
new file mode 100644
index 0000000..4cda56b
--- /dev/null
+++ b/web/migrations/versions/50aad68f99c2_.py
@@ -0,0 +1,82 @@
+
+"""Added service field option in server table (RM#3140)
+
+Revision ID: 50aad68f99c2
+Revises: 02b9dccdcfcb
+Create Date: 2018-03-07 11:53:57.584280
+
+"""
+from pgadmin.model import db
+
+
+# revision identifiers, used by Alembic.
+revision = '50aad68f99c2'
+down_revision = '02b9dccdcfcb'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # To Save previous data
+ db.engine.execute("ALTER TABLE server RENAME TO server_old")
+
+ # With service file some fields won't be mandatory as user can provide
+ # them using service file. Removed NOT NULL constraint from few columns
+ db.engine.execute("""
+ CREATE TABLE server (
+ id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ servergroup_id INTEGER NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ host VARCHAR(128),
+ port INTEGER NOT NULL CHECK(port >= 1024 AND port <= 65534),
+ maintenance_db VARCHAR(64),
+ username VARCHAR(64) NOT NULL,
+ password VARCHAR(64),
+ role VARCHAR(64),
+ ssl_mode VARCHAR(16) NOT NULL CHECK(ssl_mode IN
+ ( 'allow' , 'prefer' , 'require' , 'disable' ,
+ 'verify-ca' , 'verify-full' )
+ ),
+ comment VARCHAR(1024),
+ discovery_id VARCHAR(128),
+ hostaddr TEXT(1024),
+ db_res TEXT,
+ passfile TEXT,
+ sslcert TEXT,
+ sslkey TEXT,
+ sslrootcert TEXT,
+ sslcrl TEXT,
+ sslcompression INTEGER DEFAULT 0,
+ bgcolor TEXT(10),
+ fgcolor TEXT(10),
+ PRIMARY KEY(id),
+ FOREIGN KEY(user_id) REFERENCES user(id),
+ FOREIGN KEY(servergroup_id) REFERENCES servergroup(id)
+ )
+ """)
+
+ # Copy old data again into table
+ db.engine.execute("""
+ INSERT INTO server (
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ ) SELECT
+ id,user_id, servergroup_id, name, host, port, maintenance_db,
+ username, ssl_mode, comment, password, role, discovery_id,
+ hostaddr, db_res, passfile, sslcert, sslkey, sslrootcert, sslcrl,
+ bgcolor, fgcolor
+ FROM server_old""")
+
+ # Remove old data
+ db.engine.execute("DROP TABLE server_old")
+
+ # Add column for Service
+ db.engine.execute(
+ 'ALTER TABLE server ADD COLUMN service TEXT'
+ )
+
+def downgrade():
+ pass
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index dfa9d62..fecb66d 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -478,7 +478,8 @@ class ServerNode(PGChildNodeView):
'sslcrl': 'sslcrl',
'sslcompression': 'sslcompression',
'bgcolor': 'bgcolor',
- 'fgcolor': 'fgcolor'
+ 'fgcolor': 'fgcolor',
+ 'service': 'service'
}
disp_lbl = {
@@ -515,7 +516,7 @@ class ServerNode(PGChildNodeView):
if connected:
for arg in (
'host', 'hostaddr', 'port', 'db', 'username', 'sslmode',
- 'role'
+ 'role', 'service'
):
if arg in data:
return forbidden(
@@ -663,7 +664,8 @@ class ServerNode(PGChildNodeView):
'sslrootcert': server.sslrootcert if is_ssl else None,
'sslcrl': server.sslcrl if is_ssl else None,
'sslcompression': True if is_ssl and server.sslcompression
- else False
+ else False,
+ 'service': server.service if server.service else None
}
)
@@ -672,18 +674,22 @@ class ServerNode(PGChildNodeView):
"""Add a server node to the settings database"""
required_args = [
u'name',
- u'host',
u'port',
- u'db',
- u'username',
u'sslmode',
- u'role'
+ u'username'
]
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
+ # Some fields can be provided with service file so they are optional
+ if 'service' in data and not data['service']:
+ required_args.extend([
+ u'host',
+ u'db',
+ u'role'
+ ])
for arg in required_args:
if arg not in data:
return make_json_response(
@@ -711,29 +717,26 @@ class ServerNode(PGChildNodeView):
try:
server = Server(
user_id=current_user.id,
- servergroup_id=data[u'gid'] if u'gid' in data else gid,
- name=data[u'name'],
- host=data[u'host'],
- hostaddr=data[u'hostaddr'] if u'hostaddr' in data else None,
- port=data[u'port'],
- maintenance_db=data[u'db'],
- username=data[u'username'],
- ssl_mode=data[u'sslmode'],
- comment=data[u'comment'] if u'comment' in data else None,
- role=data[u'role'] if u'role' in data else None,
+ servergroup_id=data.get('gid', gid),
+ name=data.get('name'),
+ host=data.get('host', None),
+ hostaddr=data.get('hostaddr', None),
+ port=data.get('port'),
+ maintenance_db=data.get('db', None),
+ username=data.get('username'),
+ ssl_mode=data.get('sslmode'),
+ comment=data.get('comment', None),
+ role=data.get('role', None),
db_res=','.join(data[u'db_res'])
- if u'db_res' in data
- else None,
- sslcert=data['sslcert'] if is_ssl else None,
- sslkey=data['sslkey'] if is_ssl else None,
- sslrootcert=data['sslrootcert'] if is_ssl else None,
- sslcrl=data['sslcrl'] if is_ssl else None,
+ if u'db_res' in data else None,
+ sslcert=data.get('sslcert', None),
+ sslkey=data.get('sslkey', None),
+ sslrootcert=data.get('sslrootcert', None),
+ sslcrl=data.get('sslcrl', None),
sslcompression=1 if is_ssl and data['sslcompression'] else 0,
- bgcolor=data['bgcolor'] if u'bgcolor' in data
- else None,
- fgcolor=data['fgcolor'] if u'fgcolor' in data
- else None
-
+ bgcolor=data.get('bgcolor', None),
+ fgcolor=data.get('fgcolor', None),
+ service=data.get('service', None)
)
db.session.add(server)
db.session.commit()
@@ -930,7 +933,7 @@ class ServerNode(PGChildNodeView):
if 'password' not in data:
conn_passwd = getattr(conn, 'password', None)
if conn_passwd is None and server.password is None and \
- server.passfile is None:
+ server.passfile is None and server.service is None:
# Return the password template in case password is not
# provided, or password has not been saved earlier.
return make_json_response(
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 9932808..53be225 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -665,6 +665,7 @@ define('pgadmin.node.server', [
sslkey: undefined,
sslrootcert: undefined,
sslcrl: undefined,
+ service: undefined,
},
// Default values!
initialize: function(attrs, args) {
@@ -841,12 +842,18 @@ define('pgadmin.node.server', [
var passfile = m.get('passfile');
return !_.isUndefined(passfile) && !_.isNull(passfile);
},
+ },{
+ id: 'service', label: gettext('Service'), type: 'text',
+ mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
+ group: gettext('Connection'),
}],
validate: function() {
var err = {},
errmsg,
self = this;
+ var service_id = this.get('service');
+
var check_for_empty = function(id, msg) {
var v = self.get(id);
if (
@@ -903,26 +910,41 @@ define('pgadmin.node.server', [
}
check_for_empty('name', gettext('Name must be specified.'));
- if (check_for_empty(
- 'host', gettext('Either Host name or Host address must be specified.')
- ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
- errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ // If no service id then only check
+ if (
+ _.isUndefined(service_id) || _.isNull(service_id) ||
+ String(service_id).replace(/^\s+|\s+$/g, '') == ''
+ ) {
+ if (check_for_empty(
+ 'host', gettext('Either Host name or Host address must be specified.')
+ ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
+ errmsg = errmsg || gettext('Either Host name or Host address must be specified');
+ } else {
+ errmsg = undefined;
+ delete err['host'];
+ delete err['hostaddr'];
+ }
+
+ check_for_empty(
+ 'db', gettext('Maintenance database must be specified.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
+ check_for_valid_ip(
+ 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
+ );
} else {
- errmsg = undefined;
- delete err['host'];
- delete err['hostaddr'];
+ _.each(['host', 'hostaddr', 'db'], (item) => {
+ self.errorModel.unset(item);
+ });
}
check_for_empty(
- 'db', gettext('Maintenance database must be specified.')
- );
- check_for_empty(
'username', gettext('Username must be specified.')
);
check_for_empty('port', gettext('Port must be specified.'));
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
+
this.errorModel.set(err);
if (_.size(err)) {
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
new file mode 100644
index 0000000..3b03d49
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
@@ -0,0 +1,47 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import json
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+
+
+class ServersWithServiceIDAddTestCase(BaseTestGenerator):
+ """ This class will add the servers under default server group. """
+
+ scenarios = [
+ # Fetch the default url for server object
+ (
+ 'Default Server Node url', dict(
+ url='/browser/server/obj/'
+ )
+ )
+ ]
+
+ def setUp(self):
+ pass
+
+ def runTest(self):
+ """ This function will add the server under default server group."""
+ url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+ # Add service name in the config
+ self.server['service'] = "TestDB"
+ response = self.tester.post(
+ url,
+ data=json.dumps(self.server),
+ content_type='html/json'
+ )
+ self.assertEquals(response.status_code, 200)
+ response_data = json.loads(response.data.decode('utf-8'))
+ self.server_id = response_data['node']['_id']
+
+ def tearDown(self):
+ """This function delete the server from SQLite """
+ utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index 674f945..11bc9f0 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -107,13 +107,13 @@ class Server(db.Model):
nullable=False
)
name = db.Column(db.String(128), nullable=False)
- host = db.Column(db.String(128), nullable=False)
+ host = db.Column(db.String(128), nullable=True)
hostaddr = db.Column(db.String(128), nullable=True)
port = db.Column(
db.Integer(),
db.CheckConstraint('port >= 1024 AND port <= 65534'),
nullable=False)
- maintenance_db = db.Column(db.String(64), nullable=False)
+ maintenance_db = db.Column(db.String(64), nullable=True)
username = db.Column(db.String(64), nullable=False)
password = db.Column(db.String(64), nullable=True)
role = db.Column(db.String(64), nullable=True)
@@ -144,6 +144,7 @@ class Server(db.Model):
)
bgcolor = db.Column(db.Text(10), nullable=True)
fgcolor = db.Column(db.Text(10), nullable=True)
+ service = db.Column(db.Text(), nullable=True)
class ModulePreference(db.Model):
diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py
index 941a694..95a49fb 100644
--- a/web/pgadmin/utils/driver/psycopg2/__init__.py
+++ b/web/pgadmin/utils/driver/psycopg2/__init__.py
@@ -8,1985 +8,23 @@
##########################################################################
"""
-Implementation of Connection, ServerManager and Driver classes using the
-psycopg2. It is a wrapper around the actual psycopg2 driver, and connection
+Implementation of Driver class
+It is a wrapper around the actual psycopg2 driver, and connection
object.
-"""
+"""
import datetime
-import os
-import random
-import select
-import sys
-
-import simplejson as json
-import psycopg2
-from flask import g, current_app, session
+from flask import session
from flask_babel import gettext
-from flask_security import current_user
-from pgadmin.utils.crypto import decrypt
-from psycopg2.extensions import adapt, encodings
+import psycopg2
+from psycopg2.extensions import adapt
import config
from pgadmin.model import Server, User
-from pgadmin.utils.exception import ConnectionLost
-from pgadmin.utils import get_complete_file_path
from .keywords import ScanKeyword
-from ..abstract import BaseDriver, BaseConnection
-from .cursor import DictCursor
-from .typecast import register_global_typecasters, \
- register_string_typecasters, register_binary_typecasters, \
- register_array_to_string_typecasters, ALL_JSON_TYPES
-from collections import deque
-
-
-if sys.version_info < (3,):
- # Python2 in-built csv module do not handle unicode
- # backports.csv module ported from PY3 csv module for unicode handling
- from backports import csv
- from StringIO import StringIO
- IS_PY2 = True
-else:
- from io import StringIO
- import csv
- IS_PY2 = False
-
-_ = gettext
-
-
-# Register global type caster which will be applicable to all connections.
-register_global_typecasters()
-
-
-class Connection(BaseConnection):
- """
- class Connection(object)
-
- A wrapper class, which wraps the psycopg2 connection object, and
- delegate the execution to the actual connection object, when required.
-
- Methods:
- -------
- * connect(**kwargs)
- - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
- driver
-
- * execute_scalar(query, params, formatted_exception_msg)
- - Execute the given query and returns single datum result
-
- * execute_async(query, params, formatted_exception_msg)
- - Execute the given query asynchronously and returns result.
-
- * execute_void(query, params, formatted_exception_msg)
- - Execute the given query with no result.
-
- * execute_2darray(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as a 2 dimensional
- array.
-
- * execute_dict(query, params, formatted_exception_msg)
- - Execute the given query and returns the result as an array of dict
- (column name -> value) format.
-
- * connected()
- - Get the status of the connection.
- Returns True if connected, otherwise False.
-
- * reset()
- - Reconnect the database server (if possible)
-
- * transaction_status()
- - Transaction Status
-
- * ping()
- - Ping the server.
-
- * _release()
- - Release the connection object of psycopg2
-
- * _reconnect()
- - Attempt to reconnect to the database
-
- * _wait(conn)
- - This method is used to wait for asynchronous connection. This is a
- blocking call.
-
- * _wait_timeout(conn)
- - This method is used to wait for asynchronous connection with timeout.
- This is a non blocking call.
-
- * poll(formatted_exception_msg)
- - This method is used to poll the data of query running on asynchronous
- connection.
-
- * status_message()
- - Returns the status message returned by the last command executed on
- the server.
-
- * rows_affected()
- - Returns the no of rows affected by the last command executed on
- the server.
-
- * cancel_transaction(conn_id, did=None)
- - This method is used to cancel the transaction for the
- specified connection id and database id.
-
- * messages()
- - Returns the list of messages/notices sends from the PostgreSQL database
- server.
-
- * _formatted_exception_msg(exception_obj, formatted_msg)
- - This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- """
-
- def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
- use_binary_placeholder=False, array_to_string=False):
- assert (manager is not None)
- assert (conn_id is not None)
-
- self.conn_id = conn_id
- self.manager = manager
- self.db = db if db is not None else manager.db
- self.conn = None
- self.auto_reconnect = auto_reconnect
- self.async = async
- self.__async_cursor = None
- self.__async_query_id = None
- self.__backend_pid = None
- self.execution_aborted = False
- self.row_count = 0
- self.__notices = None
- self.password = None
- # This flag indicates the connection status (connected/disconnected).
- self.wasConnected = False
- # This flag indicates the connection reconnecting status.
- self.reconnecting = False
- self.use_binary_placeholder = use_binary_placeholder
- self.array_to_string = array_to_string
-
- super(Connection, self).__init__()
-
- def as_dict(self):
- """
- Returns the dictionary object representing this object.
- """
- # In case, it cannot be auto reconnectable, or already been released,
- # then we will return None.
- if not self.auto_reconnect and not self.conn:
- return None
-
- res = dict()
- res['conn_id'] = self.conn_id
- res['database'] = self.db
- res['async'] = self.async
- res['wasConnected'] = self.wasConnected
- res['auto_reconnect'] = self.auto_reconnect
- res['use_binary_placeholder'] = self.use_binary_placeholder
- res['array_to_string'] = self.array_to_string
-
- return res
-
- def __repr__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def __str__(self):
- return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
- self.conn_id, self.db,
- 'Connected' if self.conn and not self.conn.closed else
- "Disconnected",
- self.async
- )
-
- def connect(self, **kwargs):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- else:
- return True, None
-
- pg_conn = None
- password = None
- passfile = None
- mgr = self.manager
-
- encpass = kwargs['password'] if 'password' in kwargs else None
- passfile = kwargs['passfile'] if 'passfile' in kwargs else None
-
- if encpass is None:
- encpass = self.password or getattr(mgr, 'password', None)
-
- # Reset the existing connection password
- if self.reconnecting is not False:
- self.password = None
-
- if encpass:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- try:
- password = decrypt(encpass, user.password)
- # Handling of non ascii password (Python2)
- if hasattr(str, 'decode'):
- password = password.decode('utf-8').encode('utf-8')
- # password is in bytes, for python3 we need it in string
- elif isinstance(password, bytes):
- password = password.decode()
-
- except Exception as e:
- 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
- # come from Query tool, ViewData grid, debugger etc tools.
- # we will check for pgpass file availability from connection manager
- # if it's present then we will use it
- if not password and not encpass and not passfile:
- passfile = mgr.passfile if mgr.passfile else None
-
- try:
- if hasattr(str, 'decode'):
- database = self.db.encode('utf-8')
- user = mgr.user.encode('utf-8')
- conn_id = self.conn_id.encode('utf-8')
- else:
- database = self.db
- user = mgr.user
- conn_id = self.conn_id
-
- import os
- os.environ['PGAPPNAME'] = '{0} - {1}'.format(
- config.APP_NAME, conn_id)
-
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=database,
- user=user,
- password=password,
- async=self.async,
- passfile=get_complete_file_path(passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- # If connection is asynchronous then we will have to wait
- # until the connection is ready to use.
- if self.async == 1:
- self._wait(pg_conn)
-
- except psycopg2.Error as e:
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- current_app.logger.info(
- u"Failed to connect to the database server(#{server_id}) for "
- u"connection ({conn_id}) with error message as below"
- u":{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
- )
- )
- return False, msg
-
- # Overwrite connection notice attr to support
- # more than 50 notices at a time
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.wasConnected = True
- try:
- status, msg = self._initialize(conn_id, **kwargs)
- except Exception as e:
- current_app.logger.exception(e)
- self.conn = None
- if not self.reconnecting:
- self.wasConnected = False
- raise e
-
- if status:
- mgr._update_password(encpass)
- else:
- if not self.reconnecting:
- self.wasConnected = False
-
- return status, msg
-
- def _initialize(self, conn_id, **kwargs):
- self.execution_aborted = False
- self.__backend_pid = self.conn.get_backend_pid()
-
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- status, cur = self.__cursor()
- formatted_exception_msg = self._formatted_exception_msg
- mgr = self.manager
-
- def _execute(cur, query, params=None):
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- return formatted_exception_msg(pe, False)
- return None
-
- # autocommit flag does not work with asynchronous connections.
- # By default asynchronous connection runs in autocommit mode.
- if self.async == 0:
- if 'autocommit' in kwargs and kwargs['autocommit'] is False:
- self.conn.autocommit = False
- else:
- self.conn.autocommit = True
-
- register_string_typecasters(self.conn)
-
- if self.array_to_string:
- register_array_to_string_typecasters(self.conn)
-
- # Register type casters for binary data only after registering array to
- # string type casters.
- if self.use_binary_placeholder:
- register_binary_typecasters(self.conn)
-
- status = _execute(cur, "SET DateStyle=ISO;"
- "SET client_min_messages=notice;"
- "SET bytea_output=escape;"
- "SET client_encoding='UNICODE';")
-
- if status is not None:
- self.conn.close()
- self.conn = None
-
- return False, status
-
- if mgr.role:
- status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
-
- if status is not None:
- self.conn.close()
- self.conn = None
- current_app.logger.error(
- "Connect to the database server (#{server_id}) for "
- "connection ({conn_id}), but - failed to setup the role "
- "with error message as below:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status
- )
- )
- return False, \
- _(
- "Failed to setup the role with error message:\n{0}"
- ).format(status)
-
- if mgr.ver is None:
- status = _execute(cur, "SELECT version()")
-
- if status is not None:
- self.conn.close()
- self.conn = None
- self.wasConnected = False
- current_app.logger.error(
- "Failed to fetch the version information on the "
- "established connection to the database server "
- "(#{server_id}) for '{conn_id}' with below error "
- "message:{msg}".format(
- server_id=self.manager.sid,
- conn_id=conn_id,
- msg=status)
- )
- return False, status
-
- if cur.rowcount > 0:
- row = cur.fetchmany(1)[0]
- mgr.ver = row['version']
- mgr.sversion = self.conn.server_version
-
- status = _execute(cur, """
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.datname = current_database()""")
-
- if status is None:
- mgr.db_info = mgr.db_info or dict()
- if cur.rowcount > 0:
- res = cur.fetchmany(1)[0]
- mgr.db_info[res['did']] = res.copy()
-
- # We do not have database oid for the maintenance database.
- if len(mgr.db_info) == 1:
- mgr.did = res['did']
-
- status = _execute(cur, """
-SELECT
- oid as id, rolname as name, rolsuper as is_superuser,
- rolcreaterole as can_create_role, rolcreatedb as can_create_db
-FROM
- pg_catalog.pg_roles
-WHERE
- rolname = current_user""")
-
- if status is None:
- mgr.user_info = dict()
- if cur.rowcount > 0:
- mgr.user_info = cur.fetchmany(1)[0]
-
- if 'password' in kwargs:
- mgr.password = kwargs['password']
-
- server_types = None
- if 'server_types' in kwargs and isinstance(
- kwargs['server_types'], list):
- server_types = mgr.server_types = kwargs['server_types']
-
- if server_types is None:
- from pgadmin.browser.server_groups.servers.types import ServerType
- server_types = ServerType.types()
-
- for st in server_types:
- if st.instanceOf(mgr.ver):
- mgr.server_type = st.stype
- mgr.server_cls = st
- break
-
- mgr.update_session()
-
- return True, None
-
- def __cursor(self, server_cursor=False):
- if self.wasConnected is False:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- cur = getattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
-
- if self.connected() and cur and not cur.closed:
- if not server_cursor or (server_cursor and cur.name):
- return True, cur
-
- if not self.connected():
- errmsg = ""
-
- current_app.logger.warning(
- "Connection to database server (#{server_id}) for the "
- "connection - '{conn_id}' has been lost.".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
-
- if self.auto_reconnect and not self.reconnecting:
- self.__attempt_execution_reconnect(None)
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- try:
- if server_cursor:
- # Providing name to cursor will create server side cursor.
- cursor_name = "CURSOR:{0}".format(self.conn_id)
- cur = self.conn.cursor(
- name=cursor_name, cursor_factory=DictCursor
- )
- else:
- cur = self.conn.cursor(cursor_factory=DictCursor)
- except psycopg2.Error as pe:
- current_app.logger.exception(pe)
- errmsg = gettext(
- "Failed to create cursor for psycopg2 connection with error "
- "message for the server#{1}:{2}:\n{0}"
- ).format(
- str(pe), self.manager.sid, self.db
- )
-
- current_app.logger.error(errmsg)
- if self.conn.closed:
- self.conn = None
- if self.auto_reconnect and not self.reconnecting:
- current_app.logger.info(
- gettext(
- "Attempting to reconnect to the database server "
- "(#{server_id}) for the connection - '{conn_id}'."
- ).format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- return self.__attempt_execution_reconnect(
- self.__cursor, server_cursor
- )
- else:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:'
- else self.conn_id[5:]
- )
-
- setattr(
- g, "{0}#{1}".format(
- self.manager.sid, self.conn_id.encode('utf-8')
- ), cur
- )
-
- return True, cur
-
- def __internal_blocking_execute(self, cur, query, params):
- """
- This function executes the query using cursor's execute function,
- but in case of asynchronous connection we need to wait for the
- transaction to be completed. If self.async is 1 then it is a
- blocking call.
-
- Args:
- cur: Cursor object
- query: SQL query to run.
- params: Extra parameters
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- cur.execute(query, params)
- if self.async == 1:
- self._wait(cur.connection)
-
- def execute_on_server_as_csv(self,
- query, params=None,
- formatted_exception_msg=False,
- records=2000):
- """
- To fetch query result and generate CSV output
-
- Args:
- query: SQL
- params: Additional parameters
- formatted_exception_msg: For exception
- records: Number of initial records
- Returns:
- Generator response
- """
- status, cur = self.__cursor(server_cursor=True)
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- if IS_PY2 and type(query) == unicode:
- query = query.encode('utf-8')
-
- current_app.logger.log(
- 25,
- u"Execute (with server cursor) for server #{server_id} - "
- u"{conn_id} (Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8') if
- sys.version_info < (3,) else query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"failed to execute query ((with server cursor) "
- u"for the server #{server_id} - {conn_id} "
- u"(query-id: {query_id}):\nerror message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- def handle_json_data(json_columns, results):
- """
- [ This is only for Python2.x]
- This function will be useful to handle json data types.
- We will dump json data as proper json instead of unicode values
-
- Args:
- json_columns: Columns which contains json data
- results: Query result
-
- Returns:
- results
- """
- # Only if Python2 and there are columns with JSON type
- if IS_PY2 and len(json_columns) > 0:
- temp_results = []
- for row in results:
- res = dict()
- for k, v in row.items():
- if k in json_columns:
- res[k] = json.dumps(v)
- else:
- res[k] = v
- temp_results.append(res)
- results = temp_results
- return results
-
- def convert_keys_to_unicode(results, conn_encoding):
- """
- [ This is only for Python2.x]
- We need to convert all keys to unicode as psycopg2
- sends them as string
-
- Args:
- res: Query result set from psycopg2
- conn_encoding: Connection encoding
-
- Returns:
- Result set (With all the keys converted to unicode)
- """
- new_results = []
- for row in results:
- new_results.append(
- dict([(k.decode(conn_encoding), v)
- for k, v in row.items()])
- )
- return new_results
-
- def gen(quote='strings', quote_char="'", field_separator=','):
-
- results = cur.fetchmany(records)
- if not results:
- if not cur.closed:
- cur.close()
- yield gettext('The query executed did not return any data.')
- return
-
- header = []
- json_columns = []
- conn_encoding = cur.connection.encoding
-
- for c in cur.ordered_description():
- # This is to handle the case in which column name is non-ascii
- column_name = c.to_dict()['name']
- if IS_PY2:
- column_name = column_name.decode(conn_encoding)
- header.append(column_name)
- if c.to_dict()['type_code'] in ALL_JSON_TYPES:
- json_columns.append(column_name)
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- res_io = StringIO()
-
- if quote == 'strings':
- quote = csv.QUOTE_NONNUMERIC
- elif quote == 'all':
- quote = csv.QUOTE_ALL
- else:
- quote = csv.QUOTE_NONE
-
- if hasattr(str, 'decode'):
- # Decode the field_separator
- try:
- field_separator = field_separator.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- # Decode the quote_char
- try:
- quote_char = quote_char.decode('utf-8')
- except Exception as e:
- current_app.logger.error(e)
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- csv_writer.writeheader()
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
-
- yield res_io.getvalue()
-
- while True:
- results = cur.fetchmany(records)
-
- if not results:
- if not cur.closed:
- cur.close()
- break
- res_io = StringIO()
-
- csv_writer = csv.DictWriter(
- res_io, fieldnames=header, delimiter=field_separator,
- quoting=quote,
- quotechar=quote_char
- )
-
- if IS_PY2:
- results = convert_keys_to_unicode(results, conn_encoding)
-
- results = handle_json_data(json_columns, results)
- csv_writer.writerows(results)
- yield res_io.getvalue()
-
- return True, gen
-
- def execute_scalar(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_scalar) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- res = cur.fetchone()
- if len(res) > 0:
- return True, res[0]
-
- return True, None
-
- def execute_async(self, query, params=None, formatted_exception_msg=True):
- """
- This function executes the given query asynchronously and returns
- result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
-
- if sys.version_info < (3,):
- if type(query) == unicode:
- query = query.encode('utf-8')
- else:
- query = query.encode('utf-8')
-
- self.__async_cursor = None
- status, cur = self.__cursor()
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- query_id=query_id
- )
- )
-
- try:
- self.__notices = []
- self.execution_aborted = False
- cur.execute(query, params)
- res = self._wait_timeout(cur.connection)
- except psycopg2.Error as pe:
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_async) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query.decode('utf-8'),
- errmsg=errmsg,
- query_id=query_id
- )
- )
-
- if self.is_disconnected(pe):
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- return False, errmsg
-
- self.__async_cursor = cur
- self.__async_query_id = query_id
-
- return True, res
-
- def execute_void(self, query, params=None, formatted_exception_msg=False):
- """
- This function executes the given query with no result.
-
- Args:
- query: SQL query to run.
- params: extra parameters to the function
- formatted_exception_msg: if True then function return the
- formatted exception message
- """
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
-
- current_app.logger.log(
- 25,
- u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
-
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_void, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_void) for the server "
- u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- self.row_count = cur.rowcount
-
- return True, None
-
- def __attempt_execution_reconnect(self, fn, *args, **kwargs):
- self.reconnecting = True
- setattr(g, "{0}#{1}".format(
- self.manager.sid,
- self.conn_id.encode('utf-8')
- ), None)
- try:
- status, res = self.connect()
- if status:
- if fn:
- status, res = fn(*args, **kwargs)
- self.reconnecting = False
- return status, res
- except Exception as e:
- current_app.logger.exception(e)
- self.reconnecting = False
-
- current_app.warning(
- "Failed to reconnect the database server "
- "(#{server_id})".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id
- )
- )
- self.reconnecting = False
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
-
- def execute_2darray(self, query, params=None,
- formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
-
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (2darray) for server #{server_id} - {conn_id} "
- u"(Query-id: {query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and \
- not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_2darray, query, params,
- formatted_exception_msg
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_2darray) for the server "
- u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- errmsg=errmsg,
- query_id=query_id
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(row)
-
- return True, {'columns': columns, 'rows': rows}
-
- def execute_dict(self, query, params=None, formatted_exception_msg=False):
- status, cur = self.__cursor()
- self.row_count = 0
-
- if not status:
- return False, str(cur)
- query_id = random.randint(1, 9999999)
- current_app.logger.log(
- 25,
- u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
- u"{query_id}):\n{query}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query=query,
- query_id=query_id
- )
- )
- try:
- self.__internal_blocking_execute(cur, query, params)
- except psycopg2.Error as pe:
- cur.close()
- if not self.connected():
- if self.auto_reconnect and not self.reconnecting:
- return self.__attempt_execution_reconnect(
- self.execute_dict, query, params,
- formatted_exception_msg
- )
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- current_app.logger.error(
- u"Failed to execute query (execute_dict) for the server "
- u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
- u"Error Message:{errmsg}".format(
- server_id=self.manager.sid,
- conn_id=self.conn_id,
- query_id=query_id,
- errmsg=errmsg
- )
- )
- return False, errmsg
-
- # Get Resultset Column Name, Type and size
- columns = cur.description and [
- desc.to_dict() for desc in cur.ordered_description()
- ] or []
-
- rows = []
- self.row_count = cur.rowcount
- if cur.rowcount > 0:
- for row in cur:
- rows.append(dict(row))
-
- return True, {'columns': columns, 'rows': rows}
-
- def async_fetchmany_2darray(self, records=2000,
- formatted_exception_msg=False):
- """
- User should poll and check if status is ASYNC_OK before calling this
- function
- Args:
- records: no of records to fetch. use -1 to fetchall.
- formatted_exception_msg:
-
- Returns:
-
- """
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- if self.conn.isexecuting():
- return False, gettext(
- "Asynchronous query execution/operation underway."
- )
-
- if self.row_count > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML and
- # DDL operations, we need to rely on exception to figure
- # that out at the moment.
- try:
- if records == -1:
- res = cur.fetchall()
- else:
- res = cur.fetchmany(records)
- for row in res:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
- except psycopg2.ProgrammingError as e:
- result = None
- else:
- # User performed operation which dose not produce record/s as
- # result.
- # for eg. DDL operations.
- return True, None
-
- return True, result
-
- def connected(self):
- if self.conn:
- if not self.conn.closed:
- return True
- self.conn = None
- return False
-
- def reset(self):
- if self.conn:
- if self.conn.closed:
- self.conn = None
- pg_conn = None
- mgr = self.manager
-
- password = getattr(mgr, 'password', None)
-
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
-
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=mgr.host,
- hostaddr=mgr.hostaddr,
- port=mgr.port,
- database=self.db,
- user=mgr.user,
- password=password,
- passfile=get_complete_file_path(mgr.passfile),
- sslmode=mgr.ssl_mode,
- sslcert=get_complete_file_path(mgr.sslcert),
- sslkey=get_complete_file_path(mgr.sslkey),
- sslrootcert=get_complete_file_path(mgr.sslrootcert),
- sslcrl=get_complete_file_path(mgr.sslcrl),
- sslcompression=True if mgr.sslcompression else False
- )
-
- except psycopg2.Error as e:
- msg = e.pgerror if e.pgerror else e.message \
- if e.message else e.diag.message_detail \
- if e.diag.message_detail else str(e)
-
- current_app.logger.error(
- gettext(
- """
-Failed to reset the connection to the server due to following error:
-{0}"""
- ).Format(msg)
- )
- return False, msg
-
- pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
- self.conn = pg_conn
- self.__backend_pid = pg_conn.get_backend_pid()
-
- return True, None
-
- def transaction_status(self):
- if self.conn:
- return self.conn.get_transaction_status()
- return None
-
- def ping(self):
- return self.execute_scalar('SELECT 1')
-
- def _release(self):
- if self.wasConnected:
- if self.conn:
- self.conn.close()
- self.conn = None
- self.password = None
- self.wasConnected = False
-
- def _wait(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method in a infinite loop till poll
- returns psycopg2.extensions.POLL_OK. This is a blocking
- call.
-
- Args:
- conn: connection object
- """
-
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- break
- elif state == psycopg2.extensions.POLL_WRITE:
- select.select([], [conn.fileno()], [])
- elif state == psycopg2.extensions.POLL_READ:
- select.select([conn.fileno()], [], [])
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait function" % state)
-
- def _wait_timeout(self, conn):
- """
- This function is used for the asynchronous connection,
- it will call poll method and return the status. If state is
- psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
- function will wait for the given timeout.This is not a blocking call.
-
- Args:
- conn: connection object
- """
- while 1:
- state = conn.poll()
- if state == psycopg2.extensions.POLL_OK:
- return self.ASYNC_OK
- elif state == psycopg2.extensions.POLL_WRITE:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([], [conn.fileno()], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_WRITE_TIMEOUT
- elif state == psycopg2.extensions.POLL_READ:
- # Wait for the given time and then check the return status
- # If three empty lists are returned then the time-out is
- # reached.
- timeout_status = select.select([conn.fileno()], [], [],
- self.ASYNC_TIMEOUT)
- if timeout_status == ([], [], []):
- return self.ASYNC_READ_TIMEOUT
- else:
- raise psycopg2.OperationalError(
- "poll() returned %s from _wait_timeout function" % state
- )
-
- def poll(self, formatted_exception_msg=False, no_result=False):
- """
- This function is a wrapper around connection's poll function.
- It internally uses the _wait_timeout method to poll the
- result on the connection object. In case of success it
- returns the result of the query.
-
- Args:
- formatted_exception_msg: if True then function return the formatted
- exception message, otherwise error string.
- no_result: If True then only poll status will be returned.
- """
-
- cur = self.__async_cursor
- if not cur:
- return False, gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Polling result for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- is_error = False
- try:
- status = self._wait_timeout(self.conn)
- except psycopg2.Error as pe:
- if self.conn.closed:
- raise ConnectionLost(
- self.manager.sid,
- self.db,
- self.conn_id[5:]
- )
- errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
- is_error = True
-
- if self.conn.notices and self.__notices is not None:
- self.__notices.extend(self.conn.notices)
- self.conn.notices.clear()
-
- # We also need to fetch notices before we return from function in case
- # of any Exception, To avoid code duplication we will return after
- # fetching the notices in case of any Exception
- if is_error:
- return False, errmsg
-
- result = None
- self.row_count = 0
- self.column_info = None
-
- if status == self.ASYNC_OK:
-
- # if user has cancelled the transaction then changed the status
- if self.execution_aborted:
- status = self.ASYNC_EXECUTION_ABORTED
- self.execution_aborted = False
- return status, result
-
- # Fetch the column information
- if cur.description is not None:
- self.column_info = [
- desc.to_dict() for desc in cur.ordered_description()
- ]
-
- pos = 0
- for col in self.column_info:
- col['pos'] = pos
- pos += 1
-
- self.row_count = cur.rowcount
- if not no_result:
- if cur.rowcount > 0:
- result = []
- # For DDL operation, we may not have result.
- #
- # Because - there is not direct way to differentiate DML
- # and DDL operations, we need to rely on exception to
- # figure that out at the moment.
- try:
- for row in cur:
- new_row = []
- for col in self.column_info:
- new_row.append(row[col['name']])
- result.append(new_row)
-
- except psycopg2.ProgrammingError:
- result = None
-
- return status, result
-
- def status_message(self):
- """
- This function will return the status message returned by the last
- command executed on the server.
- """
- cur = self.__async_cursor
- if not cur:
- return gettext(
- "Cursor could not be found for the async connection."
- )
-
- current_app.logger.log(
- 25,
- "Status message for (Query-id: {query_id})".format(
- query_id=self.__async_query_id
- )
- )
-
- return cur.statusmessage
-
- def rows_affected(self):
- """
- This function will return the no of rows affected by the last command
- executed on the server.
- """
-
- return self.row_count
-
- def get_column_info(self):
- """
- This function will returns list of columns for last async sql command
- executed on the server.
- """
-
- return self.column_info
-
- def cancel_transaction(self, conn_id, did=None):
- """
- This function is used to cancel the running transaction
- of the given connection id and database id using
- PostgreSQL's pg_cancel_backend.
-
- Args:
- conn_id: Connection id
- did: Database id (optional)
- """
- cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
- query = """SELECT pg_cancel_backend({0});""".format(
- cancel_conn.__backend_pid)
-
- status = True
- msg = ''
-
- # if backend pid is same then create a new connection
- # to cancel the query and release it.
- if cancel_conn.__backend_pid == self.__backend_pid:
- password = getattr(self.manager, 'password', None)
- if password:
- # Fetch Logged in User Details.
- user = User.query.filter_by(id=current_user.id).first()
- if user is None:
- return False, gettext("Unauthorized request.")
-
- password = decrypt(password, user.password).decode()
-
- try:
- pg_conn = psycopg2.connect(
- host=self.manager.host,
- hostaddr=self.manager.hostaddr,
- port=self.manager.port,
- database=self.db,
- user=self.manager.user,
- password=password,
- passfile=get_complete_file_path(self.manager.passfile),
- sslmode=self.manager.ssl_mode,
- sslcert=get_complete_file_path(self.manager.sslcert),
- sslkey=get_complete_file_path(self.manager.sslkey),
- sslrootcert=get_complete_file_path(
- self.manager.sslrootcert
- ),
- sslcrl=get_complete_file_path(self.manager.sslcrl),
- sslcompression=True if self.manager.sslcompression
- else False
- )
-
- # Get the cursor and run the query
- cur = pg_conn.cursor()
- cur.execute(query)
-
- # Close the connection
- pg_conn.close()
- pg_conn = None
-
- except psycopg2.Error as e:
- status = False
- if e.pgerror:
- msg = e.pgerror
- elif e.diag.message_detail:
- msg = e.diag.message_detail
- else:
- msg = str(e)
- return status, msg
- else:
- if self.connected():
- status, msg = self.execute_void(query)
-
- if status:
- cancel_conn.execution_aborted = True
- else:
- status = False
- msg = gettext("Not connected to the database server.")
-
- return status, msg
-
- def messages(self):
- """
- Returns the list of the messages/notices send from the database server.
- """
- resp = []
- while self.__notices:
- resp.append(self.__notices.pop(0))
- return resp
-
- def decode_to_utf8(self, value):
- """
- This method will decode values to utf-8
- Args:
- value: String to be decode
-
- Returns:
- Decoded string
- """
- is_error = False
- if hasattr(str, 'decode'):
- try:
- value = value.decode('utf-8')
- except UnicodeDecodeError:
- # Let's try with python's preferred encoding
- # On Windows lc_messages mostly has environment dependent
- # encoding like 'French_France.1252'
- try:
- import locale
- pref_encoding = locale.getpreferredencoding()
- value = value.decode(pref_encoding)\
- .encode('utf-8')\
- .decode('utf-8')
- except Exception:
- is_error = True
- except Exception:
- is_error = True
-
- # If still not able to decode then
- if is_error:
- value = value.decode('ascii', 'ignore')
-
- return value
-
- def _formatted_exception_msg(self, exception_obj, formatted_msg):
- """
- This method is used to parse the psycopg2.Error object and returns the
- formatted error message if flag is set to true else return
- normal error message.
-
- Args:
- exception_obj: exception object
- formatted_msg: if True then function return the formatted exception
- message
-
- """
- if exception_obj.pgerror:
- errmsg = exception_obj.pgerror
- elif exception_obj.diag.message_detail:
- errmsg = exception_obj.diag.message_detail
- else:
- errmsg = str(exception_obj)
- # errmsg might contains encoded value, lets decode it
- errmsg = self.decode_to_utf8(errmsg)
-
- # if formatted_msg is false then return from the function
- if not formatted_msg:
- return errmsg
-
- # Do not append if error starts with `ERROR:` as most pg related
- # error starts with `ERROR:`
- if not errmsg.startswith(u'ERROR:'):
- errmsg = u'ERROR: ' + errmsg + u'\n\n'
-
- if exception_obj.diag.severity is not None \
- and exception_obj.diag.message_primary is not None:
- ex_diag_message = u"{0}: {1}".format(
- exception_obj.diag.severity,
- self.decode_to_utf8(exception_obj.diag.message_primary)
- )
- # If both errors are different then only append it
- if errmsg and ex_diag_message and \
- ex_diag_message.strip().strip('\n').lower() not in \
- errmsg.strip().strip('\n').lower():
- errmsg += ex_diag_message
- elif exception_obj.diag.message_primary is not None:
- message_primary = self.decode_to_utf8(
- exception_obj.diag.message_primary
- )
- if message_primary.lower() not in errmsg.lower():
- errmsg += message_primary
-
- if exception_obj.diag.sqlstate is not None:
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('SQL state: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
-
- if exception_obj.diag.message_detail is not None:
- if 'Detail:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Detail: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.message_detail
- )
-
- if exception_obj.diag.message_hint is not None:
- if 'Hint:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Hint: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
-
- if exception_obj.diag.statement_position is not None:
- if 'Character:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Character: ')
- errmsg += self.decode_to_utf8(
- exception_obj.diag.statement_position
- )
-
- if exception_obj.diag.context is not None:
- if 'Context:'.lower() not in errmsg.lower():
- if not errmsg.endswith('\n'):
- errmsg += '\n'
- errmsg += gettext('Context: ')
- errmsg += self.decode_to_utf8(exception_obj.diag.context)
-
- return errmsg
-
- #####
- # As per issue reported on pgsycopg2 github repository link is shared below
- # conn.closed is not reliable enough to identify the disconnection from the
- # database server for some unknown reasons.
- #
- # (https://github.com/psycopg/psycopg2/issues/263)
- #
- # In order to resolve the issue, sqlalchamey follows the below logic to
- # identify the disconnection. It relies on exception message to identify
- # the error.
- #
- # Reference (MIT license):
- # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
- #
- def is_disconnected(self, err):
- if not self.conn.closed:
- # checks based on strings. in the case that .closed
- # didn't cut it, fall back onto these.
- str_e = str(err).partition("\n")[0]
- for msg in [
- # these error messages from libpq: interfaces/libpq/fe-misc.c
- # and interfaces/libpq/fe-secure.c.
- 'terminating connection',
- 'closed the connection',
- 'connection not open',
- 'could not receive data from server',
- 'could not send data to server',
- # psycopg2 client errors, psycopg2/conenction.h,
- # psycopg2/cursor.h
- 'connection already closed',
- 'cursor already closed',
- # not sure where this path is originally from, it may
- # be obsolete. It really says "losed", not "closed".
- 'losed the connection unexpectedly',
- # these can occur in newer SSL
- 'connection has been closed unexpectedly',
- 'SSL SYSCALL error: Bad file descriptor',
- 'SSL SYSCALL error: EOF detected',
- ]:
- idx = str_e.find(msg)
- if idx >= 0 and '"' not in str_e[:idx]:
- return True
-
- return False
- return True
-
-
-class ServerManager(object):
- """
- class ServerManager
-
- This class contains the information about the given server.
- And, acts as connection manager for that particular session.
- """
-
- def __init__(self, server):
- self.connections = dict()
-
- self.update(server)
-
- def update(self, server):
- assert (server is not None)
- assert (isinstance(server, Server))
-
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.sid = server.id
- self.host = server.host
- self.hostaddr = server.hostaddr
- self.port = server.port
- self.db = server.maintenance_db
- self.did = None
- self.user = server.username
- self.password = server.password
- self.role = server.role
- self.ssl_mode = server.ssl_mode
- self.pinged = datetime.datetime.now()
- self.db_info = dict()
- self.server_types = None
- self.db_res = server.db_res
- self.passfile = server.passfile
- self.sslcert = server.sslcert
- self.sslkey = server.sslkey
- self.sslrootcert = server.sslrootcert
- self.sslcrl = server.sslcrl
- self.sslcompression = True if server.sslcompression else False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.update_session()
-
- self.connections = dict()
-
- def as_dict(self):
- """
- Returns a dictionary object representing the server manager.
- """
- if self.ver is None or len(self.connections) == 0:
- return None
-
- res = dict()
- res['sid'] = self.sid
- res['ver'] = self.ver
- res['sversion'] = self.sversion
- if hasattr(self, 'password') and self.password:
- # If running under PY2
- if hasattr(self.password, 'decode'):
- res['password'] = self.password.decode('utf-8')
- else:
- res['password'] = str(self.password)
- else:
- res['password'] = self.password
-
- connections = res['connections'] = dict()
-
- for conn_id in self.connections:
- conn = self.connections[conn_id].as_dict()
-
- if conn is not None:
- connections[conn_id] = conn
-
- return res
-
- def ServerVersion(self):
- return self.ver
-
- @property
- def version(self):
- return self.sversion
-
- def MajorVersion(self):
- if self.sversion is not None:
- return int(self.sversion / 10000)
- raise Exception("Information is not available.")
-
- def MinorVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) % 100)
- raise Exception("Information is not available.")
-
- def PatchVersion(self):
- if self.sversion:
- return int(int(self.sversion / 100) / 100)
- raise Exception("Information is not available.")
-
- def connection(
- self, database=None, conn_id=None, auto_reconnect=True, did=None,
- async=None, use_binary_placeholder=False, array_to_string=False
- ):
- if database is not None:
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if did is not None:
- if did in self.db_info:
- self.db_info[did]['datname'] = database
- else:
- if did is None:
- database = self.db
- elif did in self.db_info:
- database = self.db_info[did]['datname']
- else:
- maintenance_db_id = u'DB:{0}'.format(self.db)
- if maintenance_db_id in self.connections:
- conn = self.connections[maintenance_db_id]
- if conn.connected():
- status, res = conn.execute_dict(u"""
-SELECT
- db.oid as did, db.datname, db.datallowconn,
- pg_encoding_to_char(db.encoding) AS serverencoding,
- has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
-FROM
- pg_database db
-WHERE db.oid = {0}""".format(did))
-
- if status and len(res['rows']) > 0:
- for row in res['rows']:
- self.db_info[did] = row
- database = self.db_info[did]['datname']
-
- if did not in self.db_info:
- raise Exception(gettext(
- "Could not find the specified database."
- ))
-
- if database is None:
- raise ConnectionLost(self.sid, None, None)
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database))
-
- self.pinged = datetime.datetime.now()
-
- if my_id in self.connections:
- return self.connections[my_id]
- else:
- if async is None:
- async = 1 if conn_id is not None else 0
- else:
- async = 1 if async is True else 0
- self.connections[my_id] = Connection(
- self, my_id, database, auto_reconnect, async,
- use_binary_placeholder=use_binary_placeholder,
- array_to_string=array_to_string
- )
-
- return self.connections[my_id]
-
- def _restore(self, data):
- """
- Helps restoring to reconnect the auto-connect connections smoothly on
- reload/restart of the app server..
- """
- # restore server version from flask session if flask server was
- # restarted. As we need server version to resolve sql template paths.
-
- self.ver = data.get('ver', None)
- self.sversion = data.get('sversion', None)
-
- if self.ver and not self.server_type:
- from pgadmin.browser.server_groups.servers.types import ServerType
- for st in ServerType.types():
- if st.instanceOf(self.ver):
- self.server_type = st.stype
- self.server_cls = st
- break
-
- # Hmm.. we will not honour this request, when I already have
- # connections
- if len(self.connections) != 0:
- return
-
- # We need to know about the existing server variant supports during
- # first connection for identifications.
- from pgadmin.browser.server_groups.servers.types import ServerType
- self.pinged = datetime.datetime.now()
- try:
- if 'password' in data and data['password']:
- data['password'] = data['password'].encode('utf-8')
- except Exception as e:
- current_app.logger.exception(e)
-
- connections = data['connections']
- for conn_id in connections:
- conn_info = connections[conn_id]
- conn = self.connections[conn_info['conn_id']] = Connection(
- self, conn_info['conn_id'], conn_info['database'],
- conn_info['auto_reconnect'], conn_info['async'],
- use_binary_placeholder=conn_info['use_binary_placeholder'],
- array_to_string=conn_info['array_to_string']
- )
-
- # only try to reconnect if connection was connected previously and
- # auto_reconnect is true.
- if conn_info['wasConnected'] and conn_info['auto_reconnect']:
- try:
- conn.connect(
- password=data['password'],
- server_types=ServerType.types()
- )
- # This will also update wasConnected flag in connection so
- # no need to update the flag manually.
- except Exception as e:
- current_app.logger.exception(e)
- self.connections.pop(conn_info['conn_id'])
-
- def release(self, database=None, conn_id=None, did=None):
- if did is not None:
- if did in self.db_info and 'datname' in self.db_info[did]:
- database = self.db_info[did]['datname']
- if hasattr(str, 'decode') and \
- not isinstance(database, unicode):
- database = database.decode('utf-8')
- if database is None:
- return False
- else:
- return False
-
- my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
- (u'DB:{0}'.format(database)) if database is not None else None
-
- if my_id is not None:
- if my_id in self.connections:
- self.connections[my_id]._release()
- del self.connections[my_id]
- if did is not None:
- del self.db_info[did]
-
- if len(self.connections) == 0:
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
- else:
- return False
-
- for con in self.connections:
- self.connections[con]._release()
-
- self.connections = dict()
- self.ver = None
- self.sversion = None
- self.server_type = None
- self.server_cls = None
- self.password = None
-
- self.update_session()
-
- return True
-
- def _update_password(self, passwd):
- self.password = passwd
- for conn_id in self.connections:
- conn = self.connections[conn_id]
- if conn.conn is not None or conn.wasConnected is True:
- conn.password = passwd
-
- def update_session(self):
- managers = session['__pgsql_server_managers'] \
- if '__pgsql_server_managers' in session else dict()
- updated_mgr = self.as_dict()
-
- if not updated_mgr:
- if self.sid in managers:
- managers.pop(self.sid)
- else:
- managers[self.sid] = updated_mgr
- session['__pgsql_server_managers'] = managers
- session.force_write = True
-
- def utility(self, operation):
- """
- utility(operation)
-
- Returns: name of the utility which used for the operation
- """
- if self.server_cls is not None:
- return self.server_cls.utility(operation, self.sversion)
-
- return None
-
- def export_password_env(self, env):
- if self.password:
- password = decrypt(
- self.password, current_user.password
- ).decode()
- os.environ[str(env)] = password
+from ..abstract import BaseDriver
+from .connection import Connection
+from .server_manager import ServerManager
class Driver(BaseDriver):
@@ -2164,8 +202,9 @@ class Driver(BaseDriver):
continue
if curr_time - sess_mgr['pinged'] >= session_idle_timeout:
- for mgr in [m for m in sess_mgr if isinstance(m,
- ServerManager)]:
+ for mgr in [
+ m for m in sess_mgr if isinstance(m, ServerManager)
+ ]:
mgr.release()
@staticmethod
diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py
new file mode 100644
index 0000000..d4c9573
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/connection.py
@@ -0,0 +1,1682 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of Connection.
+It is a wrapper around the actual psycopg2 driver, and connection
+object.
+"""
+
+import random
+import select
+import sys
+from collections import deque
+import simplejson as json
+import psycopg2
+from flask import g, current_app
+from flask_babel import gettext
+from flask_security import current_user
+from pgadmin.utils.crypto import decrypt
+from psycopg2.extensions import adapt, encodings
+
+import config
+from pgadmin.model import Server, User
+from pgadmin.utils.exception import ConnectionLost
+from pgadmin.utils import get_complete_file_path
+from ..abstract import BaseDriver, BaseConnection
+from .cursor import DictCursor
+from .typecast import register_global_typecasters, \
+ register_string_typecasters, register_binary_typecasters, \
+ register_array_to_string_typecasters, ALL_JSON_TYPES
+
+
+if sys.version_info < (3,):
+ # Python2 in-built csv module do not handle unicode
+ # backports.csv module ported from PY3 csv module for unicode handling
+ from backports import csv
+ from StringIO import StringIO
+ IS_PY2 = True
+else:
+ from io import StringIO
+ import csv
+ IS_PY2 = False
+
+_ = gettext
+
+
+# Register global type caster which will be applicable to all connections.
+register_global_typecasters()
+
+
+class Connection(BaseConnection):
+ """
+ class Connection(object)
+
+ A wrapper class, which wraps the psycopg2 connection object, and
+ delegate the execution to the actual connection object, when required.
+
+ Methods:
+ -------
+ * connect(**kwargs)
+ - Connect the PostgreSQL/EDB Postgres Advanced Server using the psycopg2
+ driver
+
+ * execute_scalar(query, params, formatted_exception_msg)
+ - Execute the given query and returns single datum result
+
+ * execute_async(query, params, formatted_exception_msg)
+ - Execute the given query asynchronously and returns result.
+
+ * execute_void(query, params, formatted_exception_msg)
+ - Execute the given query with no result.
+
+ * execute_2darray(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as a 2 dimensional
+ array.
+
+ * execute_dict(query, params, formatted_exception_msg)
+ - Execute the given query and returns the result as an array of dict
+ (column name -> value) format.
+
+ * connected()
+ - Get the status of the connection.
+ Returns True if connected, otherwise False.
+
+ * reset()
+ - Reconnect the database server (if possible)
+
+ * transaction_status()
+ - Transaction Status
+
+ * ping()
+ - Ping the server.
+
+ * _release()
+ - Release the connection object of psycopg2
+
+ * _reconnect()
+ - Attempt to reconnect to the database
+
+ * _wait(conn)
+ - This method is used to wait for asynchronous connection. This is a
+ blocking call.
+
+ * _wait_timeout(conn)
+ - This method is used to wait for asynchronous connection with timeout.
+ This is a non blocking call.
+
+ * poll(formatted_exception_msg)
+ - This method is used to poll the data of query running on asynchronous
+ connection.
+
+ * status_message()
+ - Returns the status message returned by the last command executed on
+ the server.
+
+ * rows_affected()
+ - Returns the no of rows affected by the last command executed on
+ the server.
+
+ * cancel_transaction(conn_id, did=None)
+ - This method is used to cancel the transaction for the
+ specified connection id and database id.
+
+ * messages()
+ - Returns the list of messages/notices sends from the PostgreSQL database
+ server.
+
+ * _formatted_exception_msg(exception_obj, formatted_msg)
+ - This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ """
+
+ def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0,
+ use_binary_placeholder=False, array_to_string=False):
+ assert (manager is not None)
+ assert (conn_id is not None)
+
+ self.conn_id = conn_id
+ self.manager = manager
+ self.db = db if db is not None else manager.db
+ self.conn = None
+ self.auto_reconnect = auto_reconnect
+ self.async = async
+ self.__async_cursor = None
+ self.__async_query_id = None
+ self.__backend_pid = None
+ self.execution_aborted = False
+ self.row_count = 0
+ self.__notices = None
+ self.password = None
+ # This flag indicates the connection status (connected/disconnected).
+ self.wasConnected = False
+ # This flag indicates the connection reconnecting status.
+ self.reconnecting = False
+ self.use_binary_placeholder = use_binary_placeholder
+ self.array_to_string = array_to_string
+
+ super(Connection, self).__init__()
+
+ def as_dict(self):
+ """
+ Returns the dictionary object representing this object.
+ """
+ # In case, it cannot be auto reconnectable, or already been released,
+ # then we will return None.
+ if not self.auto_reconnect and not self.conn:
+ return None
+
+ res = dict()
+ res['conn_id'] = self.conn_id
+ res['database'] = self.db
+ res['async'] = self.async
+ res['wasConnected'] = self.wasConnected
+ res['auto_reconnect'] = self.auto_reconnect
+ res['use_binary_placeholder'] = self.use_binary_placeholder
+ res['array_to_string'] = self.array_to_string
+
+ return res
+
+ def __repr__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def __str__(self):
+ return "PG Connection: {0} ({1}) -> {2} (ajax:{3})".format(
+ self.conn_id, self.db,
+ 'Connected' if self.conn and not self.conn.closed else
+ "Disconnected",
+ self.async
+ )
+
+ def connect(self, **kwargs):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ else:
+ return True, None
+
+ pg_conn = None
+ password = None
+ passfile = None
+ mgr = self.manager
+
+ encpass = kwargs['password'] if 'password' in kwargs else None
+ passfile = kwargs['passfile'] if 'passfile' in kwargs else None
+
+ if encpass is None:
+ encpass = self.password or getattr(mgr, 'password', None)
+
+ # Reset the existing connection password
+ if self.reconnecting is not False:
+ self.password = None
+
+ if encpass:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ try:
+ password = decrypt(encpass, user.password)
+ # Handling of non ascii password (Python2)
+ if hasattr(str, 'decode'):
+ password = password.decode('utf-8').encode('utf-8')
+ # password is in bytes, for python3 we need it in string
+ elif isinstance(password, bytes):
+ password = password.decode()
+
+ except Exception as e:
+ 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
+ # come from Query tool, ViewData grid, debugger etc tools.
+ # we will check for pgpass file availability from connection manager
+ # if it's present then we will use it
+ if not password and not encpass and not passfile:
+ passfile = mgr.passfile if mgr.passfile else None
+
+ try:
+ if hasattr(str, 'decode'):
+ database = self.db.encode('utf-8')
+ user = mgr.user.encode('utf-8')
+ conn_id = self.conn_id.encode('utf-8')
+ else:
+ database = self.db
+ user = mgr.user
+ conn_id = self.conn_id
+
+ import os
+ os.environ['PGAPPNAME'] = '{0} - {1}'.format(
+ config.APP_NAME, conn_id)
+
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=database,
+ user=user,
+ password=password,
+ async=self.async,
+ passfile=get_complete_file_path(passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ # If connection is asynchronous then we will have to wait
+ # until the connection is ready to use.
+ if self.async == 1:
+ self._wait(pg_conn)
+
+ except psycopg2.Error as e:
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ current_app.logger.info(
+ u"Failed to connect to the database server(#{server_id}) for "
+ u"connection ({conn_id}) with error message as below"
+ u":{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=msg.decode('utf-8') if hasattr(str, 'decode') else msg
+ )
+ )
+ return False, msg
+
+ # Overwrite connection notice attr to support
+ # more than 50 notices at a time
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+
+ self.conn = pg_conn
+ self.wasConnected = True
+ try:
+ status, msg = self._initialize(conn_id, **kwargs)
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.conn = None
+ if not self.reconnecting:
+ self.wasConnected = False
+ raise e
+
+ if status:
+ mgr._update_password(encpass)
+ else:
+ if not self.reconnecting:
+ self.wasConnected = False
+
+ return status, msg
+
+ def _initialize(self, conn_id, **kwargs):
+ self.execution_aborted = False
+ self.__backend_pid = self.conn.get_backend_pid()
+
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ status, cur = self.__cursor()
+ formatted_exception_msg = self._formatted_exception_msg
+ mgr = self.manager
+
+ def _execute(cur, query, params=None):
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ return formatted_exception_msg(pe, False)
+ return None
+
+ # autocommit flag does not work with asynchronous connections.
+ # By default asynchronous connection runs in autocommit mode.
+ if self.async == 0:
+ if 'autocommit' in kwargs and kwargs['autocommit'] is False:
+ self.conn.autocommit = False
+ else:
+ self.conn.autocommit = True
+
+ register_string_typecasters(self.conn)
+
+ if self.array_to_string:
+ register_array_to_string_typecasters(self.conn)
+
+ # Register type casters for binary data only after registering array to
+ # string type casters.
+ if self.use_binary_placeholder:
+ register_binary_typecasters(self.conn)
+
+ status = _execute(cur, "SET DateStyle=ISO;"
+ "SET client_min_messages=notice;"
+ "SET bytea_output=escape;"
+ "SET client_encoding='UNICODE';")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+
+ return False, status
+
+ if mgr.role:
+ status = _execute(cur, u"SET ROLE TO %s", [mgr.role])
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ current_app.logger.error(
+ "Connect to the database server (#{server_id}) for "
+ "connection ({conn_id}), but - failed to setup the role "
+ "with error message as below:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status
+ )
+ )
+ return False, \
+ _(
+ "Failed to setup the role with error message:\n{0}"
+ ).format(status)
+
+ if mgr.ver is None:
+ status = _execute(cur, "SELECT version()")
+
+ if status is not None:
+ self.conn.close()
+ self.conn = None
+ self.wasConnected = False
+ current_app.logger.error(
+ "Failed to fetch the version information on the "
+ "established connection to the database server "
+ "(#{server_id}) for '{conn_id}' with below error "
+ "message:{msg}".format(
+ server_id=self.manager.sid,
+ conn_id=conn_id,
+ msg=status)
+ )
+ return False, status
+
+ if cur.rowcount > 0:
+ row = cur.fetchmany(1)[0]
+ mgr.ver = row['version']
+ mgr.sversion = self.conn.server_version
+
+ status = _execute(cur, """
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.datname = current_database()""")
+
+ if status is None:
+ mgr.db_info = mgr.db_info or dict()
+ if cur.rowcount > 0:
+ res = cur.fetchmany(1)[0]
+ mgr.db_info[res['did']] = res.copy()
+
+ # We do not have database oid for the maintenance database.
+ if len(mgr.db_info) == 1:
+ mgr.did = res['did']
+
+ status = _execute(cur, """
+SELECT
+ oid as id, rolname as name, rolsuper as is_superuser,
+ rolcreaterole as can_create_role, rolcreatedb as can_create_db
+FROM
+ pg_catalog.pg_roles
+WHERE
+ rolname = current_user""")
+
+ if status is None:
+ mgr.user_info = dict()
+ if cur.rowcount > 0:
+ mgr.user_info = cur.fetchmany(1)[0]
+
+ if 'password' in kwargs:
+ mgr.password = kwargs['password']
+
+ server_types = None
+ if 'server_types' in kwargs and isinstance(
+ kwargs['server_types'], list):
+ server_types = mgr.server_types = kwargs['server_types']
+
+ if server_types is None:
+ from pgadmin.browser.server_groups.servers.types import ServerType
+ server_types = ServerType.types()
+
+ for st in server_types:
+ if st.instanceOf(mgr.ver):
+ mgr.server_type = st.stype
+ mgr.server_cls = st
+ break
+
+ mgr.update_session()
+
+ return True, None
+
+ def __cursor(self, server_cursor=False):
+ if self.wasConnected is False:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ cur = getattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+
+ if self.connected() and cur and not cur.closed:
+ if not server_cursor or (server_cursor and cur.name):
+ return True, cur
+
+ if not self.connected():
+ errmsg = ""
+
+ current_app.logger.warning(
+ "Connection to database server (#{server_id}) for the "
+ "connection - '{conn_id}' has been lost.".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+
+ if self.auto_reconnect and not self.reconnecting:
+ self.__attempt_execution_reconnect(None)
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ try:
+ if server_cursor:
+ # Providing name to cursor will create server side cursor.
+ cursor_name = "CURSOR:{0}".format(self.conn_id)
+ cur = self.conn.cursor(
+ name=cursor_name, cursor_factory=DictCursor
+ )
+ else:
+ cur = self.conn.cursor(cursor_factory=DictCursor)
+ except psycopg2.Error as pe:
+ current_app.logger.exception(pe)
+ errmsg = gettext(
+ "Failed to create cursor for psycopg2 connection with error "
+ "message for the server#{1}:{2}:\n{0}"
+ ).format(
+ str(pe), self.manager.sid, self.db
+ )
+
+ current_app.logger.error(errmsg)
+ if self.conn.closed:
+ self.conn = None
+ if self.auto_reconnect and not self.reconnecting:
+ current_app.logger.info(
+ gettext(
+ "Attempting to reconnect to the database server "
+ "(#{server_id}) for the connection - '{conn_id}'."
+ ).format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ return self.__attempt_execution_reconnect(
+ self.__cursor, server_cursor
+ )
+ else:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:'
+ else self.conn_id[5:]
+ )
+
+ setattr(
+ g, "{0}#{1}".format(
+ self.manager.sid, self.conn_id.encode('utf-8')
+ ), cur
+ )
+
+ return True, cur
+
+ def __internal_blocking_execute(self, cur, query, params):
+ """
+ This function executes the query using cursor's execute function,
+ but in case of asynchronous connection we need to wait for the
+ transaction to be completed. If self.async is 1 then it is a
+ blocking call.
+
+ Args:
+ cur: Cursor object
+ query: SQL query to run.
+ params: Extra parameters
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ cur.execute(query, params)
+ if self.async == 1:
+ self._wait(cur.connection)
+
+ def execute_on_server_as_csv(self,
+ query, params=None,
+ formatted_exception_msg=False,
+ records=2000):
+ """
+ To fetch query result and generate CSV output
+
+ Args:
+ query: SQL
+ params: Additional parameters
+ formatted_exception_msg: For exception
+ records: Number of initial records
+ Returns:
+ Generator response
+ """
+ status, cur = self.__cursor(server_cursor=True)
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ if IS_PY2 and type(query) == unicode:
+ query = query.encode('utf-8')
+
+ current_app.logger.log(
+ 25,
+ u"Execute (with server cursor) for server #{server_id} - "
+ u"{conn_id} (Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8') if
+ sys.version_info < (3,) else query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"failed to execute query ((with server cursor) "
+ u"for the server #{server_id} - {conn_id} "
+ u"(query-id: {query_id}):\nerror message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ def handle_json_data(json_columns, results):
+ """
+ [ This is only for Python2.x]
+ This function will be useful to handle json data types.
+ We will dump json data as proper json instead of unicode values
+
+ Args:
+ json_columns: Columns which contains json data
+ results: Query result
+
+ Returns:
+ results
+ """
+ # Only if Python2 and there are columns with JSON type
+ if IS_PY2 and len(json_columns) > 0:
+ temp_results = []
+ for row in results:
+ res = dict()
+ for k, v in row.items():
+ if k in json_columns:
+ res[k] = json.dumps(v)
+ else:
+ res[k] = v
+ temp_results.append(res)
+ results = temp_results
+ return results
+
+ def convert_keys_to_unicode(results, conn_encoding):
+ """
+ [ This is only for Python2.x]
+ We need to convert all keys to unicode as psycopg2
+ sends them as string
+
+ Args:
+ res: Query result set from psycopg2
+ conn_encoding: Connection encoding
+
+ Returns:
+ Result set (With all the keys converted to unicode)
+ """
+ new_results = []
+ for row in results:
+ new_results.append(
+ dict([(k.decode(conn_encoding), v)
+ for k, v in row.items()])
+ )
+ return new_results
+
+ def gen(quote='strings', quote_char="'", field_separator=','):
+
+ results = cur.fetchmany(records)
+ if not results:
+ if not cur.closed:
+ cur.close()
+ yield gettext('The query executed did not return any data.')
+ return
+
+ header = []
+ json_columns = []
+ conn_encoding = cur.connection.encoding
+
+ for c in cur.ordered_description():
+ # This is to handle the case in which column name is non-ascii
+ column_name = c.to_dict()['name']
+ if IS_PY2:
+ column_name = column_name.decode(conn_encoding)
+ header.append(column_name)
+ if c.to_dict()['type_code'] in ALL_JSON_TYPES:
+ json_columns.append(column_name)
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ res_io = StringIO()
+
+ if quote == 'strings':
+ quote = csv.QUOTE_NONNUMERIC
+ elif quote == 'all':
+ quote = csv.QUOTE_ALL
+ else:
+ quote = csv.QUOTE_NONE
+
+ if hasattr(str, 'decode'):
+ # Decode the field_separator
+ try:
+ field_separator = field_separator.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ # Decode the quote_char
+ try:
+ quote_char = quote_char.decode('utf-8')
+ except Exception as e:
+ current_app.logger.error(e)
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ csv_writer.writeheader()
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+
+ yield res_io.getvalue()
+
+ while True:
+ results = cur.fetchmany(records)
+
+ if not results:
+ if not cur.closed:
+ cur.close()
+ break
+ res_io = StringIO()
+
+ csv_writer = csv.DictWriter(
+ res_io, fieldnames=header, delimiter=field_separator,
+ quoting=quote,
+ quotechar=quote_char
+ )
+
+ if IS_PY2:
+ results = convert_keys_to_unicode(results, conn_encoding)
+
+ results = handle_json_data(json_columns, results)
+ csv_writer.writerows(results)
+ yield res_io.getvalue()
+
+ return True, gen
+
+ def execute_scalar(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (scalar) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_scalar) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ res = cur.fetchone()
+ if len(res) > 0:
+ return True, res[0]
+
+ return True, None
+
+ def execute_async(self, query, params=None, formatted_exception_msg=True):
+ """
+ This function executes the given query asynchronously and returns
+ result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+
+ if sys.version_info < (3,):
+ if type(query) == unicode:
+ query = query.encode('utf-8')
+ else:
+ query = query.encode('utf-8')
+
+ self.__async_cursor = None
+ status, cur = self.__cursor()
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (async) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__notices = []
+ self.execution_aborted = False
+ cur.execute(query, params)
+ res = self._wait_timeout(cur.connection)
+ except psycopg2.Error as pe:
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_async) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query.decode('utf-8'),
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+
+ if self.is_disconnected(pe):
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ return False, errmsg
+
+ self.__async_cursor = cur
+ self.__async_query_id = query_id
+
+ return True, res
+
+ def execute_void(self, query, params=None, formatted_exception_msg=False):
+ """
+ This function executes the given query with no result.
+
+ Args:
+ query: SQL query to run.
+ params: extra parameters to the function
+ formatted_exception_msg: if True then function return the
+ formatted exception message
+ """
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+
+ current_app.logger.log(
+ 25,
+ u"Execute (void) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_void, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_void) for the server "
+ u"#{server_id} - {conn_id}(Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ self.row_count = cur.rowcount
+
+ return True, None
+
+ def __attempt_execution_reconnect(self, fn, *args, **kwargs):
+ self.reconnecting = True
+ setattr(g, "{0}#{1}".format(
+ self.manager.sid,
+ self.conn_id.encode('utf-8')
+ ), None)
+ try:
+ status, res = self.connect()
+ if status:
+ if fn:
+ status, res = fn(*args, **kwargs)
+ self.reconnecting = False
+ return status, res
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.reconnecting = False
+
+ current_app.warning(
+ "Failed to reconnect the database server "
+ "(#{server_id})".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id
+ )
+ )
+ self.reconnecting = False
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+
+ def execute_2darray(self, query, params=None,
+ formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (2darray) for server #{server_id} - {conn_id} "
+ u"(Query-id: {query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and \
+ not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_2darray, query, params,
+ formatted_exception_msg
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_2darray) for the server "
+ u"#{server_id} - {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ errmsg=errmsg,
+ query_id=query_id
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(row)
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def execute_dict(self, query, params=None, formatted_exception_msg=False):
+ status, cur = self.__cursor()
+ self.row_count = 0
+
+ if not status:
+ return False, str(cur)
+ query_id = random.randint(1, 9999999)
+ current_app.logger.log(
+ 25,
+ u"Execute (dict) for server #{server_id} - {conn_id} (Query-id: "
+ u"{query_id}):\n{query}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query=query,
+ query_id=query_id
+ )
+ )
+ try:
+ self.__internal_blocking_execute(cur, query, params)
+ except psycopg2.Error as pe:
+ cur.close()
+ if not self.connected():
+ if self.auto_reconnect and not self.reconnecting:
+ return self.__attempt_execution_reconnect(
+ self.execute_dict, query, params,
+ formatted_exception_msg
+ )
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ None if self.conn_id[0:3] == u'DB:' else self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ current_app.logger.error(
+ u"Failed to execute query (execute_dict) for the server "
+ u"#{server_id}- {conn_id} (Query-id: {query_id}):\n"
+ u"Error Message:{errmsg}".format(
+ server_id=self.manager.sid,
+ conn_id=self.conn_id,
+ query_id=query_id,
+ errmsg=errmsg
+ )
+ )
+ return False, errmsg
+
+ # Get Resultset Column Name, Type and size
+ columns = cur.description and [
+ desc.to_dict() for desc in cur.ordered_description()
+ ] or []
+
+ rows = []
+ self.row_count = cur.rowcount
+ if cur.rowcount > 0:
+ for row in cur:
+ rows.append(dict(row))
+
+ return True, {'columns': columns, 'rows': rows}
+
+ def async_fetchmany_2darray(self, records=2000,
+ formatted_exception_msg=False):
+ """
+ User should poll and check if status is ASYNC_OK before calling this
+ function
+ Args:
+ records: no of records to fetch. use -1 to fetchall.
+ formatted_exception_msg:
+
+ Returns:
+
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ if self.conn.isexecuting():
+ return False, gettext(
+ "Asynchronous query execution/operation underway."
+ )
+
+ if self.row_count > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML and
+ # DDL operations, we need to rely on exception to figure
+ # that out at the moment.
+ try:
+ if records == -1:
+ res = cur.fetchall()
+ else:
+ res = cur.fetchmany(records)
+ for row in res:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+ except psycopg2.ProgrammingError as e:
+ result = None
+ else:
+ # User performed operation which dose not produce record/s as
+ # result.
+ # for eg. DDL operations.
+ return True, None
+
+ return True, result
+
+ def connected(self):
+ if self.conn:
+ if not self.conn.closed:
+ return True
+ self.conn = None
+ return False
+
+ def reset(self):
+ if self.conn:
+ if self.conn.closed:
+ self.conn = None
+ pg_conn = None
+ mgr = self.manager
+
+ password = getattr(mgr, 'password', None)
+
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=mgr.host,
+ hostaddr=mgr.hostaddr,
+ port=mgr.port,
+ database=self.db,
+ user=mgr.user,
+ password=password,
+ passfile=get_complete_file_path(mgr.passfile),
+ sslmode=mgr.ssl_mode,
+ sslcert=get_complete_file_path(mgr.sslcert),
+ sslkey=get_complete_file_path(mgr.sslkey),
+ sslrootcert=get_complete_file_path(mgr.sslrootcert),
+ sslcrl=get_complete_file_path(mgr.sslcrl),
+ sslcompression=True if mgr.sslcompression else False,
+ service=mgr.service
+ )
+
+ except psycopg2.Error as e:
+ msg = e.pgerror if e.pgerror else e.message \
+ if e.message else e.diag.message_detail \
+ if e.diag.message_detail else str(e)
+
+ current_app.logger.error(
+ gettext(
+ """
+Failed to reset the connection to the server due to following error:
+{0}"""
+ ).Format(msg)
+ )
+ return False, msg
+
+ pg_conn.notices = deque([], self.ASYNC_NOTICE_MAXLENGTH)
+ self.conn = pg_conn
+ self.__backend_pid = pg_conn.get_backend_pid()
+
+ return True, None
+
+ def transaction_status(self):
+ if self.conn:
+ return self.conn.get_transaction_status()
+ return None
+
+ def ping(self):
+ return self.execute_scalar('SELECT 1')
+
+ def _release(self):
+ if self.wasConnected:
+ if self.conn:
+ self.conn.close()
+ self.conn = None
+ self.password = None
+ self.wasConnected = False
+
+ def _wait(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method in a infinite loop till poll
+ returns psycopg2.extensions.POLL_OK. This is a blocking
+ call.
+
+ Args:
+ conn: connection object
+ """
+
+ while 1:
+ state = conn.poll()
+ if state == psycopg2.extensions.POLL_OK:
+ break
+ elif state == psycopg2.extensions.POLL_WRITE:
+ select.select([], [conn.fileno()], [])
+ elif state == psycopg2.extensions.POLL_READ:
+ select.select([conn.fileno()], [], [])
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait function" % state)
+
+ def _wait_timeout(self, conn):
+ """
+ This function is used for the asynchronous connection,
+ it will call poll method and return the status. If state is
+ psycopg2.extensions.POLL_WRITE and psycopg2.extensions.POLL_READ
+ function will wait for the given timeout.This is not a blocking call.
+
+ Args:
+ conn: connection object
+ time: wait time
+ """
+
+ while 1:
+ state = conn.poll()
+
+ if state == psycopg2.extensions.POLL_OK:
+ return self.ASYNC_OK
+ elif state == psycopg2.extensions.POLL_WRITE:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [], [conn.fileno()], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_WRITE_TIMEOUT
+ elif state == psycopg2.extensions.POLL_READ:
+ # Wait for the given time and then check the return status
+ # If three empty lists are returned then the time-out is
+ # reached.
+ timeout_status = select.select(
+ [conn.fileno()], [], [], self.ASYNC_TIMEOUT
+ )
+ if timeout_status == ([], [], []):
+ return self.ASYNC_READ_TIMEOUT
+ else:
+ raise psycopg2.OperationalError(
+ "poll() returned %s from _wait_timeout function" % state
+ )
+
+ def poll(self, formatted_exception_msg=False, no_result=False):
+ """
+ This function is a wrapper around connection's poll function.
+ It internally uses the _wait_timeout method to poll the
+ result on the connection object. In case of success it
+ returns the result of the query.
+
+ Args:
+ formatted_exception_msg: if True then function return the formatted
+ exception message, otherwise error string.
+ no_result: If True then only poll status will be returned.
+ """
+
+ cur = self.__async_cursor
+ if not cur:
+ return False, gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Polling result for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ is_error = False
+ try:
+ status = self._wait_timeout(self.conn)
+ except psycopg2.Error as pe:
+ if self.conn.closed:
+ raise ConnectionLost(
+ self.manager.sid,
+ self.db,
+ self.conn_id[5:]
+ )
+ errmsg = self._formatted_exception_msg(pe, formatted_exception_msg)
+ is_error = True
+
+ if self.conn.notices and self.__notices is not None:
+ self.__notices.extend(self.conn.notices)
+ self.conn.notices.clear()
+
+ # We also need to fetch notices before we return from function in case
+ # of any Exception, To avoid code duplication we will return after
+ # fetching the notices in case of any Exception
+ if is_error:
+ return False, errmsg
+
+ result = None
+ self.row_count = 0
+ self.column_info = None
+
+ if status == self.ASYNC_OK:
+
+ # if user has cancelled the transaction then changed the status
+ if self.execution_aborted:
+ status = self.ASYNC_EXECUTION_ABORTED
+ self.execution_aborted = False
+ return status, result
+
+ # Fetch the column information
+ if cur.description is not None:
+ self.column_info = [
+ desc.to_dict() for desc in cur.ordered_description()
+ ]
+
+ pos = 0
+ for col in self.column_info:
+ col['pos'] = pos
+ pos += 1
+
+ self.row_count = cur.rowcount
+ if not no_result:
+ if cur.rowcount > 0:
+ result = []
+ # For DDL operation, we may not have result.
+ #
+ # Because - there is not direct way to differentiate DML
+ # and DDL operations, we need to rely on exception to
+ # figure that out at the moment.
+ try:
+ for row in cur:
+ new_row = []
+ for col in self.column_info:
+ new_row.append(row[col['name']])
+ result.append(new_row)
+
+ except psycopg2.ProgrammingError:
+ result = None
+
+ return status, result
+
+ def status_message(self):
+ """
+ This function will return the status message returned by the last
+ command executed on the server.
+ """
+ cur = self.__async_cursor
+ if not cur:
+ return gettext(
+ "Cursor could not be found for the async connection."
+ )
+
+ current_app.logger.log(
+ 25,
+ "Status message for (Query-id: {query_id})".format(
+ query_id=self.__async_query_id
+ )
+ )
+
+ return cur.statusmessage
+
+ def rows_affected(self):
+ """
+ This function will return the no of rows affected by the last command
+ executed on the server.
+ """
+
+ return self.row_count
+
+ def get_column_info(self):
+ """
+ This function will returns list of columns for last async sql command
+ executed on the server.
+ """
+
+ return self.column_info
+
+ def cancel_transaction(self, conn_id, did=None):
+ """
+ This function is used to cancel the running transaction
+ of the given connection id and database id using
+ PostgreSQL's pg_cancel_backend.
+
+ Args:
+ conn_id: Connection id
+ did: Database id (optional)
+ """
+ cancel_conn = self.manager.connection(did=did, conn_id=conn_id)
+ query = """SELECT pg_cancel_backend({0});""".format(
+ cancel_conn.__backend_pid)
+
+ status = True
+ msg = ''
+
+ # if backend pid is same then create a new connection
+ # to cancel the query and release it.
+ if cancel_conn.__backend_pid == self.__backend_pid:
+ password = getattr(self.manager, 'password', None)
+ if password:
+ # Fetch Logged in User Details.
+ user = User.query.filter_by(id=current_user.id).first()
+ if user is None:
+ return False, gettext("Unauthorized request.")
+
+ password = decrypt(password, user.password).decode()
+
+ try:
+ pg_conn = psycopg2.connect(
+ host=self.manager.host,
+ hostaddr=self.manager.hostaddr,
+ port=self.manager.port,
+ database=self.db,
+ user=self.manager.user,
+ password=password,
+ passfile=get_complete_file_path(self.manager.passfile),
+ sslmode=self.manager.ssl_mode,
+ sslcert=get_complete_file_path(self.manager.sslcert),
+ sslkey=get_complete_file_path(self.manager.sslkey),
+ sslrootcert=get_complete_file_path(
+ self.manager.sslrootcert
+ ),
+ sslcrl=get_complete_file_path(self.manager.sslcrl),
+ sslcompression=True if self.manager.sslcompression
+ else False,
+ service=self.manager.service
+ )
+
+ # Get the cursor and run the query
+ cur = pg_conn.cursor()
+ cur.execute(query)
+
+ # Close the connection
+ pg_conn.close()
+ pg_conn = None
+
+ except psycopg2.Error as e:
+ status = False
+ if e.pgerror:
+ msg = e.pgerror
+ elif e.diag.message_detail:
+ msg = e.diag.message_detail
+ else:
+ msg = str(e)
+ return status, msg
+ else:
+ if self.connected():
+ status, msg = self.execute_void(query)
+
+ if status:
+ cancel_conn.execution_aborted = True
+ else:
+ status = False
+ msg = gettext("Not connected to the database server.")
+
+ return status, msg
+
+ def messages(self):
+ """
+ Returns the list of the messages/notices send from the database server.
+ """
+ resp = []
+ while self.__notices:
+ resp.append(self.__notices.pop(0))
+ return resp
+
+ def decode_to_utf8(self, value):
+ """
+ This method will decode values to utf-8
+ Args:
+ value: String to be decode
+
+ Returns:
+ Decoded string
+ """
+ is_error = False
+ if hasattr(str, 'decode'):
+ try:
+ value = value.decode('utf-8')
+ except UnicodeDecodeError:
+ # Let's try with python's preferred encoding
+ # On Windows lc_messages mostly has environment dependent
+ # encoding like 'French_France.1252'
+ try:
+ import locale
+ pref_encoding = locale.getpreferredencoding()
+ value = value.decode(pref_encoding)\
+ .encode('utf-8')\
+ .decode('utf-8')
+ except Exception:
+ is_error = True
+ except Exception:
+ is_error = True
+
+ # If still not able to decode then
+ if is_error:
+ value = value.decode('ascii', 'ignore')
+
+ return value
+
+ def _formatted_exception_msg(self, exception_obj, formatted_msg):
+ """
+ This method is used to parse the psycopg2.Error object and returns the
+ formatted error message if flag is set to true else return
+ normal error message.
+
+ Args:
+ exception_obj: exception object
+ formatted_msg: if True then function return the formatted exception
+ message
+
+ """
+ if exception_obj.pgerror:
+ errmsg = exception_obj.pgerror
+ elif exception_obj.diag.message_detail:
+ errmsg = exception_obj.diag.message_detail
+ else:
+ errmsg = str(exception_obj)
+ # errmsg might contains encoded value, lets decode it
+ errmsg = self.decode_to_utf8(errmsg)
+
+ # if formatted_msg is false then return from the function
+ if not formatted_msg:
+ return errmsg
+
+ # Do not append if error starts with `ERROR:` as most pg related
+ # error starts with `ERROR:`
+ if not errmsg.startswith(u'ERROR:'):
+ errmsg = u'ERROR: ' + errmsg + u'\n\n'
+
+ if exception_obj.diag.severity is not None \
+ and exception_obj.diag.message_primary is not None:
+ ex_diag_message = u"{0}: {1}".format(
+ exception_obj.diag.severity,
+ self.decode_to_utf8(exception_obj.diag.message_primary)
+ )
+ # If both errors are different then only append it
+ if errmsg and ex_diag_message and \
+ ex_diag_message.strip().strip('\n').lower() not in \
+ errmsg.strip().strip('\n').lower():
+ errmsg += ex_diag_message
+ elif exception_obj.diag.message_primary is not None:
+ message_primary = self.decode_to_utf8(
+ exception_obj.diag.message_primary
+ )
+ if message_primary.lower() not in errmsg.lower():
+ errmsg += message_primary
+
+ if exception_obj.diag.sqlstate is not None:
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('SQL state: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.sqlstate)
+
+ if exception_obj.diag.message_detail is not None:
+ if 'Detail:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Detail: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.message_detail
+ )
+
+ if exception_obj.diag.message_hint is not None:
+ if 'Hint:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Hint: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.message_hint)
+
+ if exception_obj.diag.statement_position is not None:
+ if 'Character:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Character: ')
+ errmsg += self.decode_to_utf8(
+ exception_obj.diag.statement_position
+ )
+
+ if exception_obj.diag.context is not None:
+ if 'Context:'.lower() not in errmsg.lower():
+ if not errmsg.endswith('\n'):
+ errmsg += '\n'
+ errmsg += gettext('Context: ')
+ errmsg += self.decode_to_utf8(exception_obj.diag.context)
+
+ return errmsg
+
+ #####
+ # As per issue reported on pgsycopg2 github repository link is shared below
+ # conn.closed is not reliable enough to identify the disconnection from the
+ # database server for some unknown reasons.
+ #
+ # (https://github.com/psycopg/psycopg2/issues/263)
+ #
+ # In order to resolve the issue, sqlalchamey follows the below logic to
+ # identify the disconnection. It relies on exception message to identify
+ # the error.
+ #
+ # Reference (MIT license):
+ # https://github.com/zzzeek/sqlalchemy/blob/master/lib/sqlalchemy/dialects/postgresql/psycopg2.py
+ #
+ def is_disconnected(self, err):
+ if not self.conn.closed:
+ # checks based on strings. in the case that .closed
+ # didn't cut it, fall back onto these.
+ str_e = str(err).partition("\n")[0]
+ for msg in [
+ # these error messages from libpq: interfaces/libpq/fe-misc.c
+ # and interfaces/libpq/fe-secure.c.
+ 'terminating connection',
+ 'closed the connection',
+ 'connection not open',
+ 'could not receive data from server',
+ 'could not send data to server',
+ # psycopg2 client errors, psycopg2/conenction.h,
+ # psycopg2/cursor.h
+ 'connection already closed',
+ 'cursor already closed',
+ # not sure where this path is originally from, it may
+ # be obsolete. It really says "losed", not "closed".
+ 'losed the connection unexpectedly',
+ # these can occur in newer SSL
+ 'connection has been closed unexpectedly',
+ 'SSL SYSCALL error: Bad file descriptor',
+ 'SSL SYSCALL error: EOF detected',
+ ]:
+ idx = str_e.find(msg)
+ if idx >= 0 and '"' not in str_e[:idx]:
+ return True
+
+ return False
+ return True
diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
new file mode 100644
index 0000000..2299e28
--- /dev/null
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -0,0 +1,333 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""
+Implementation of ServerManager
+"""
+import os
+import datetime
+from flask import current_app, session
+from flask_security import current_user
+from flask_babel import gettext
+
+from pgadmin.utils.crypto import decrypt
+from .connection import Connection
+from pgadmin.model import Server
+
+
+class ServerManager(object):
+ """
+ class ServerManager
+
+ This class contains the information about the given server.
+ And, acts as connection manager for that particular session.
+ """
+
+ def __init__(self, server):
+ self.connections = dict()
+
+ self.update(server)
+
+ def update(self, server):
+ assert (server is not None)
+ assert (isinstance(server, Server))
+
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.sid = server.id
+ self.host = server.host
+ self.hostaddr = server.hostaddr
+ self.port = server.port
+ self.db = server.maintenance_db
+ self.did = None
+ self.user = server.username
+ self.password = server.password
+ self.role = server.role
+ self.ssl_mode = server.ssl_mode
+ self.pinged = datetime.datetime.now()
+ self.db_info = dict()
+ self.server_types = None
+ self.db_res = server.db_res
+ self.passfile = server.passfile
+ self.sslcert = server.sslcert
+ self.sslkey = server.sslkey
+ self.sslrootcert = server.sslrootcert
+ self.sslcrl = server.sslcrl
+ self.sslcompression = True if server.sslcompression else False
+ self.service = server.service
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.update_session()
+
+ self.connections = dict()
+
+ def as_dict(self):
+ """
+ Returns a dictionary object representing the server manager.
+ """
+ if self.ver is None or len(self.connections) == 0:
+ return None
+
+ res = dict()
+ res['sid'] = self.sid
+ res['ver'] = self.ver
+ res['sversion'] = self.sversion
+ if hasattr(self, 'password') and self.password:
+ # If running under PY2
+ if hasattr(self.password, 'decode'):
+ res['password'] = self.password.decode('utf-8')
+ else:
+ res['password'] = str(self.password)
+ else:
+ res['password'] = self.password
+
+ connections = res['connections'] = dict()
+
+ for conn_id in self.connections:
+ conn = self.connections[conn_id].as_dict()
+
+ if conn is not None:
+ connections[conn_id] = conn
+
+ return res
+
+ def ServerVersion(self):
+ return self.ver
+
+ @property
+ def version(self):
+ return self.sversion
+
+ def MajorVersion(self):
+ if self.sversion is not None:
+ return int(self.sversion / 10000)
+ raise Exception("Information is not available.")
+
+ def MinorVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) % 100)
+ raise Exception("Information is not available.")
+
+ def PatchVersion(self):
+ if self.sversion:
+ return int(int(self.sversion / 100) / 100)
+ raise Exception("Information is not available.")
+
+ def connection(
+ self, database=None, conn_id=None, auto_reconnect=True, did=None,
+ async=None, use_binary_placeholder=False, array_to_string=False
+ ):
+ if database is not None:
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if did is not None:
+ if did in self.db_info:
+ self.db_info[did]['datname'] = database
+ else:
+ if did is None:
+ database = self.db
+ elif did in self.db_info:
+ database = self.db_info[did]['datname']
+ else:
+ maintenance_db_id = u'DB:{0}'.format(self.db)
+ if maintenance_db_id in self.connections:
+ conn = self.connections[maintenance_db_id]
+ if conn.connected():
+ status, res = conn.execute_dict(u"""
+SELECT
+ db.oid as did, db.datname, db.datallowconn,
+ pg_encoding_to_char(db.encoding) AS serverencoding,
+ has_database_privilege(db.oid, 'CREATE') as cancreate, datlastsysoid
+FROM
+ pg_database db
+WHERE db.oid = {0}""".format(did))
+
+ if status and len(res['rows']) > 0:
+ for row in res['rows']:
+ self.db_info[did] = row
+ database = self.db_info[did]['datname']
+
+ if did not in self.db_info:
+ raise Exception(gettext(
+ "Could not find the specified database."
+ ))
+
+ if database is None:
+ raise ConnectionLost(self.sid, None, None)
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database))
+
+ self.pinged = datetime.datetime.now()
+
+ if my_id in self.connections:
+ return self.connections[my_id]
+ else:
+ if async is None:
+ async = 1 if conn_id is not None else 0
+ else:
+ async = 1 if async is True else 0
+ self.connections[my_id] = Connection(
+ self, my_id, database, auto_reconnect, async,
+ use_binary_placeholder=use_binary_placeholder,
+ array_to_string=array_to_string
+ )
+
+ return self.connections[my_id]
+
+ def _restore(self, data):
+ """
+ Helps restoring to reconnect the auto-connect connections smoothly on
+ reload/restart of the app server..
+ """
+ # restore server version from flask session if flask server was
+ # restarted. As we need server version to resolve sql template paths.
+ from pgadmin.browser.server_groups.servers.types import ServerType
+
+ self.ver = data.get('ver', None)
+ self.sversion = data.get('sversion', None)
+
+ if self.ver and not self.server_type:
+ for st in ServerType.types():
+ if st.instanceOf(self.ver):
+ self.server_type = st.stype
+ self.server_cls = st
+ break
+
+ # Hmm.. we will not honour this request, when I already have
+ # connections
+ if len(self.connections) != 0:
+ return
+
+ # We need to know about the existing server variant supports during
+ # first connection for identifications.
+ self.pinged = datetime.datetime.now()
+ try:
+ if 'password' in data and data['password']:
+ data['password'] = data['password'].encode('utf-8')
+ except Exception as e:
+ current_app.logger.exception(e)
+
+ connections = data['connections']
+ for conn_id in connections:
+ conn_info = connections[conn_id]
+ conn = self.connections[conn_info['conn_id']] = Connection(
+ self, conn_info['conn_id'], conn_info['database'],
+ conn_info['auto_reconnect'], conn_info['async'],
+ use_binary_placeholder=conn_info['use_binary_placeholder'],
+ array_to_string=conn_info['array_to_string']
+ )
+
+ # only try to reconnect if connection was connected previously and
+ # auto_reconnect is true.
+ if conn_info['wasConnected'] and conn_info['auto_reconnect']:
+ try:
+ conn.connect(
+ password=data['password'],
+ server_types=ServerType.types()
+ )
+ # This will also update wasConnected flag in connection so
+ # no need to update the flag manually.
+ except Exception as e:
+ current_app.logger.exception(e)
+ self.connections.pop(conn_info['conn_id'])
+
+ def release(self, database=None, conn_id=None, did=None):
+ if did is not None:
+ if did in self.db_info and 'datname' in self.db_info[did]:
+ database = self.db_info[did]['datname']
+ if hasattr(str, 'decode') and \
+ not isinstance(database, unicode):
+ database = database.decode('utf-8')
+ if database is None:
+ return False
+ else:
+ return False
+
+ my_id = (u'CONN:{0}'.format(conn_id)) if conn_id is not None else \
+ (u'DB:{0}'.format(database)) if database is not None else None
+
+ if my_id is not None:
+ if my_id in self.connections:
+ self.connections[my_id]._release()
+ del self.connections[my_id]
+ if did is not None:
+ del self.db_info[did]
+
+ if len(self.connections) == 0:
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+ else:
+ return False
+
+ for con in self.connections:
+ self.connections[con]._release()
+
+ self.connections = dict()
+ self.ver = None
+ self.sversion = None
+ self.server_type = None
+ self.server_cls = None
+ self.password = None
+
+ self.update_session()
+
+ return True
+
+ def _update_password(self, passwd):
+ self.password = passwd
+ for conn_id in self.connections:
+ conn = self.connections[conn_id]
+ if conn.conn is not None or conn.wasConnected is True:
+ conn.password = passwd
+
+ def update_session(self):
+ managers = session['__pgsql_server_managers'] \
+ if '__pgsql_server_managers' in session else dict()
+ updated_mgr = self.as_dict()
+
+ if not updated_mgr:
+ if self.sid in managers:
+ managers.pop(self.sid)
+ else:
+ managers[self.sid] = updated_mgr
+ session['__pgsql_server_managers'] = managers
+ session.force_write = True
+
+ def utility(self, operation):
+ """
+ utility(operation)
+
+ Returns: name of the utility which used for the operation
+ """
+ if self.server_cls is not None:
+ return self.server_cls.utility(operation, self.sversion)
+
+ return None
+
+ def export_password_env(self, env):
+ if self.password:
+ password = decrypt(
+ self.password, current_user.password
+ ).decode()
+ os.environ[str(env)] = password
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
@ 2018-03-12 20:46 ` Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Dave Page @ 2018-03-12 20:46 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; +Cc: pgadmin-hackers
Thanks, patch applied!
On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
[email protected]> wrote:
> Hi Dave,
>
> PFA updated patch.
>
> --
> Regards,
> Murtuza Zabuawala
> EnterpriseDB: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
>
> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <murtuza.zabuawala@
> enterprisedb.com> wrote:
>
>> Hi Dave,
>>
>> I'll change the name and send you updated patch.
>>
>>
>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>
>>> HI
>>>
>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>> [email protected]> wrote:
>>>
>>>> Hi,
>>>>
>>>> PFA patch to add service parameter in server dialog.
>>>> - Docs updated
>>>> - Test case added for Service ID parameter
>>>>
>>>> Please note,
>>>> I have extracted Connection class and Server manager class from our own
>>>> custom Psycopg2 driver module.
>>>>
>>>> Patch also covers RM#3120
>>>>
>>>
>>> This patch seems a little confused. The "Service" and "Service ID"
>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>> to be asking for the Service field (the pg_service.conf service name),
>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>> database server service).
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
@ 2018-03-12 21:18 ` Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Joao De Almeida Pereira @ 2018-03-12 21:18 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Dave and Murtuza,
Regarding this patch we refactored the Javascript code so that is lives in
a different file and added some tests.
Also we found an issue with karma-jasmine that does not allow us to use
jasmine 3.1 yet. You can find attached a patch that reverts that commit.
Thanks
Victoria && Joao
On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
> Thanks, patch applied!
>
> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
> [email protected]> wrote:
>
>> Hi Dave,
>>
>> PFA updated patch.
>>
>> --
>> Regards,
>> Murtuza Zabuawala
>> EnterpriseDB: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>>
>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>> [email protected]> wrote:
>>
>>> Hi Dave,
>>>
>>> I'll change the name and send you updated patch.
>>>
>>>
>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>
>>>> HI
>>>>
>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> PFA patch to add service parameter in server dialog.
>>>>> - Docs updated
>>>>> - Test case added for Service ID parameter
>>>>>
>>>>> Please note,
>>>>> I have extracted Connection class and Server manager class from our
>>>>> own custom Psycopg2 driver module.
>>>>>
>>>>> Patch also covers RM#3120
>>>>>
>>>>
>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>> to be asking for the Service field (the pg_service.conf service name),
>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>> database server service).
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
Attachments:
[text/x-patch] refactor-javascript.diff (19.9K, 3-refactor-javascript.diff)
download | inline diff:
diff --git a/web/package.json b/web/package.json
index ad0f3e16..66684fc6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -69,6 +69,7 @@
"hard-source-webpack-plugin": "^0.4.9",
"immutability-helper": "^2.2.0",
"imports-loader": "^0.7.1",
+ "ip-address": "^5.8.9",
"jquery": "1.11.2",
"jquery-contextmenu": "^2.5.0",
"jquery-ui": "^1.12.1",
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 53be2255..302fe458 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -2,10 +2,13 @@ define('pgadmin.node.server', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user',
- 'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.alertifyjs', 'pgadmin.backform',
+ 'sources/browser/server_groups/servers/model_validation',
+ 'pgadmin.browser.server.privilege',
], function(
gettext, url_for, $, _, Backbone, S, pgAdmin, pgBrowser,
- supported_servers, current_user, Alertify, Backform
+ supported_servers, current_user, Alertify, Backform,
+ modelValidation
) {
if (!pgBrowser.Nodes['server']) {
@@ -848,110 +851,8 @@ define('pgadmin.node.server', [
group: gettext('Connection'),
}],
validate: function() {
- var err = {},
- errmsg,
- self = this;
-
- var service_id = this.get('service');
-
- var check_for_empty = function(id, msg) {
- var v = self.get(id);
- if (
- _.isUndefined(v) || v === null || String(v).replace(/^\s+|\s+$/g, '') == ''
- ) {
- err[id] = msg;
- errmsg = errmsg || msg;
- return true;
- } else {
- self.errorModel.unset(id);
- return false;
- }
- };
- var check_for_valid_ipv6 = function(val){
- // Regular expression for validating IPv6 address formats
- var exps = ['^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|',
- '(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|',
- '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
- '(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|',
- ':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
- '(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|',
- '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
- '(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|',
- '[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|',
- '((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
- '(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|',
- '1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|',
- '((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$'];
-
- var exp = new RegExp(exps.join(''));
- return exp.test(val.trim());
- };
- var check_for_valid_ip = function(id, msg) {
- var v4exps = '(^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)';
- var v4exp = new RegExp(v4exps);
- var v = self.get(id);
- if (
- v && !(v4exp.test(v.trim()))
- ) {
- if(!check_for_valid_ipv6(v)){
- err[id] = msg;
- errmsg = msg;
- }
- } else {
- self.errorModel.unset(id);
- }
- };
-
- if (!self.isNew() && 'id' in self.sessAttrs) {
- err['id'] = gettext('The ID cannot be changed.');
- errmsg = err['id'];
- } else {
- self.errorModel.unset('id');
- }
- check_for_empty('name', gettext('Name must be specified.'));
-
- // If no service id then only check
- if (
- _.isUndefined(service_id) || _.isNull(service_id) ||
- String(service_id).replace(/^\s+|\s+$/g, '') == ''
- ) {
- if (check_for_empty(
- 'host', gettext('Either Host name or Host address must be specified.')
- ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){
- errmsg = errmsg || gettext('Either Host name or Host address must be specified');
- } else {
- errmsg = undefined;
- delete err['host'];
- delete err['hostaddr'];
- }
-
- check_for_empty(
- 'db', gettext('Maintenance database must be specified.')
- );
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
- } else {
- _.each(['host', 'hostaddr', 'db'], (item) => {
- self.errorModel.unset(item);
- });
- }
-
- check_for_empty(
- 'username', gettext('Username must be specified.')
- );
- check_for_empty('port', gettext('Port must be specified.'));
-
- this.errorModel.set(err);
-
- if (_.size(err)) {
- return errmsg;
- }
-
- return null;
+ const validateModel = new modelValidation.ModelValidation(this);
+ return validateModel.validate();
},
isConnected: function(model) {
return model.get('connected');
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 3fcc69d8..83b2ad8b 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -1,6 +1,6 @@
define('bundled_browser',[
'pgadmin.browser',
- 'sources/browser/server_groups/servers/databases/external_tables/index',
+ 'sources/browser/index',
], function(pgBrowser) {
pgBrowser.init();
});
diff --git a/web/pgadmin/static/js/browser/index.js b/web/pgadmin/static/js/browser/index.js
new file mode 100644
index 00000000..297e8bf9
--- /dev/null
+++ b/web/pgadmin/static/js/browser/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'server_groups';
diff --git a/web/pgadmin/static/js/browser/server_groups/index.js b/web/pgadmin/static/js/browser/server_groups/index.js
new file mode 100644
index 00000000..b151b6f6
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'servers';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js
new file mode 100644
index 00000000..ef17c0ad
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'external_tables';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/index.js b/web/pgadmin/static/js/browser/server_groups/servers/index.js
new file mode 100644
index 00000000..242a1919
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/index.js
@@ -0,0 +1,11 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'databases';
+import 'model_validation';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js b/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js
new file mode 100644
index 00000000..feb4e6bd
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js
@@ -0,0 +1,104 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import _ from 'underscore';
+import {Address4, Address6} from 'ip-address';
+
+export class ModelValidation {
+ constructor(model) {
+ this.err = {};
+ this.errmsg = '';
+ this.model = model;
+ }
+
+ validate() {
+ const serviceId = this.model.get('service');
+
+ if (!this.model.isNew() && 'id' in this.model.sessAttrs) {
+ this.err['id'] = gettext('The ID cannot be changed.');
+ this.errmsg = this.err['id'];
+ } else {
+ this.model.errorModel.unset('id');
+ }
+
+ this.checkForEmpty('name', gettext('Name must be specified.'));
+
+ if (ModelValidation.isEmptyString(serviceId)) {
+ this.checkHostAndHostAddress();
+
+ this.checkForEmpty('db', gettext('Maintenance database must be specified.'));
+ } else {
+ this.clearHostAddressAndDbErrors();
+ }
+
+ this.checkForEmpty('username', gettext('Username must be specified.'));
+ this.checkForEmpty('port', gettext('Port must be specified.'));
+
+ this.model.errorModel.set(this.err);
+
+ if (_.size(this.err)) {
+ return this.errmsg;
+ }
+
+ return null;
+ }
+
+ clearHostAddressAndDbErrors() {
+ _.each(['host', 'hostaddr', 'db'], (item) => {
+ this.model.errorModel.unset(item);
+ });
+ }
+
+ checkHostAndHostAddress() {
+ const translatedStr = gettext('Either Host name or Host address must be' +
+ ' specified.');
+ if (this.checkForEmpty('host', translatedStr) &&
+ this.checkForEmpty('hostaddr', translatedStr)) {
+ this.errmsg = this.errmsg || translatedStr;
+ } else {
+ this.errmsg = undefined;
+ delete this.err['host'];
+ delete this.err['hostaddr'];
+ }
+
+ this.checkForValidIp(this.model.get('hostaddr'),
+ gettext('Host address must be valid IPv4 or IPv6 address.'));
+ }
+
+ checkForValidIp(ipAddress, msg) {
+ if (ipAddress) {
+ const isIpv6Address = new Address6(ipAddress).isValid();
+ const isIpv4Address = new Address4(ipAddress).isValid();
+ if (!isIpv4Address && !isIpv6Address) {
+ this.err['hostaddr'] = msg;
+ this.errmsg = msg;
+ }
+ } else {
+ this.model.errorModel.unset('hostaddr');
+ }
+ }
+
+ checkForEmpty(id, msg) {
+ const value = this.model.get(id);
+
+ if (ModelValidation.isEmptyString(value)) {
+ this.err[id] = msg;
+ this.errmsg = this.errmsg || msg;
+ return true;
+ } else {
+ this.model.errorModel.unset(id);
+ return false;
+ }
+ }
+
+ static isEmptyString(string) {
+ return _.isUndefined(string) || _.isNull(string) || string.trim() === '';
+ }
+}
diff --git a/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
new file mode 100644
index 00000000..f0b13a8d
--- /dev/null
+++ b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
@@ -0,0 +1,101 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {ModelValidation} from 'sources/browser/server_groups/servers/model_validation';
+
+describe('Server#ModelValidation', () => {
+ describe('When validating a server parameters', () => {
+ let model;
+ let modelValidation;
+ beforeEach(() => {
+ model = {
+ errorModel: jasmine.createSpyObj('errorModel', ['set', 'unset']),
+ allValues: {},
+ get: function (key) {
+ return this.allValues[key];
+ },
+ sessAttrs: {},
+ };
+ model.isNew = jasmine.createSpy('isNew');
+ modelValidation = new ModelValidation(model);
+ });
+
+ describe('When all parameters are valid', () => {
+ beforeEach(() => {
+ model.isNew.and.returnValue(true);
+ model.allValues['name'] = 'some name';
+ model.allValues['username'] = 'some username';
+ model.allValues['port'] = 'some port';
+ });
+
+ describe('No service id', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['host'] = 'some host';
+ model.allValues['db'] = 'some db';
+ model.allValues['hostaddr'] = '1.1.1.1';
+ expect(modelValidation.validate()).toBeNull();
+ expect(model.errorModel.set).toHaveBeenCalledWith({});
+ });
+ });
+
+ describe('Service id present', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['service'] = 'asdfg';
+ expect(modelValidation.validate()).toBeNull();
+ expect(model.errorModel.set).toHaveBeenCalledWith({});
+ });
+ });
+ });
+
+ describe('When no parameters are valid', () => {
+ describe('Service id not present', () => {
+ it('does not set any error in the model', () => {
+ expect(modelValidation.validate()).toBe('Name must be specified.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ host: 'Either Host name or Host address must be specified.',
+ hostaddr: 'Either Host name or Host address must be specified.',
+ db: 'Maintenance database must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+
+ describe('Host address is not valid', () => {
+ it('sets the "Host address must be a valid IPv4 or IPv6 address" error', () => {
+ model.allValues['hostaddr'] = 'something that is not an ip address';
+ expect(modelValidation.validate()).toBe('Host address must be valid IPv4 or IPv6 address.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ hostaddr: 'Host address must be valid IPv4 or IPv6 address.',
+ db: 'Maintenance database must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+
+ describe('Service id present', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['service'] = 'asdfg';
+ expect(modelValidation.validate()).toBe('Name must be specified.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/yarn.lock b/web/yarn.lock
index 85ccbc8b..2dc4c5c2 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -3938,6 +3938,18 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ip-address@^5.8.9:
+ version "5.8.9"
+ resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-5.8.9.tgz#6379277c23fc5adb20511e4d23ec2c1bde105dfd"
+ dependencies:
+ jsbn "1.1.0"
+ lodash.find "^4.6.0"
+ lodash.max "^4.0.1"
+ lodash.merge "^4.6.0"
+ lodash.padstart "^4.6.1"
+ lodash.repeat "^4.1.0"
+ sprintf-js "1.1.0"
+
ip-regex@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
@@ -4329,6 +4341,10 @@ js-yaml@~3.7.0:
argparse "^1.0.7"
esprima "^2.6.0"
[email protected]:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
+
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -4738,6 +4754,10 @@ lodash.escape@^3.0.0:
dependencies:
lodash._root "^3.0.0"
+lodash.find@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
+
lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
@@ -4777,6 +4797,10 @@ lodash.keys@^3.0.0:
lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0"
+lodash.max@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.max/-/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a"
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -4785,10 +4809,22 @@ lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
+lodash.merge@^4.6.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
+
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+lodash.padstart@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b"
+
+lodash.repeat@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44"
+
lodash.restparam@^3.0.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
@@ -6863,6 +6899,10 @@ spectrum-colorpicker@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/spectrum-colorpicker/-/spectrum-colorpicker-1.8.0.tgz#b926cf5002c0a77860b5f8351e1c093c65200107"
[email protected]:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.0.tgz#cffcaf702daf65ea39bb4e0fa2b299cec1a1be46"
+
sprintf-js@^1.0.3:
version "1.1.1"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c"
[text/x-patch] revert-upgrade-of-jasmine.diff (998B, 4-revert-upgrade-of-jasmine.diff)
download | inline diff:
diff --git a/web/package.json b/web/package.json
index ad0f3e16..a4cb58d5 100644
--- a/web/package.json
+++ b/web/package.json
@@ -18,7 +18,7 @@
"file-loader": "^0.11.2",
"image-webpack-loader": "^3.3.1",
"is-docker": "^1.1.0",
- "jasmine-core": "~2.99.0",
+ "jasmine-core": "~3.1.0",
"jasmine-enzyme": "~4.1.1",
"karma": "~1.5.0",
"karma-babel-preprocessor": "^6.0.1",
diff --git a/web/yarn.lock b/web/yarn.lock
index 85ccbc8b..5e310d79 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -4267,9 +4267,9 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
-jasmine-core@~2.99.0:
- version "2.99.1"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.99.1.tgz#e6400df1e6b56e130b61c4bcd093daa7f6e8ca15"
+jasmine-core@~3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.1.0.tgz#a4785e135d5df65024dfc9224953df585bd2766c"
jasmine-enzyme@~4.1.1:
version "4.1.1"
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
@ 2018-03-13 00:48 ` Dave Page <[email protected]>
2018-03-13 03:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
2018-03-13 13:53 ` Re: [pgAdmin4][RM#3140] Add service parameter Victoria Henry <[email protected]>
0 siblings, 2 replies; 14+ messages in thread
From: Dave Page @ 2018-03-13 00:48 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi
On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Dave and Murtuza,
>
> Regarding this patch we refactored the Javascript code so that is lives in
> a different file and added some tests.
>
> Also we found an issue with karma-jasmine that does not allow us to use
> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>
Sounds good, but neither patch will apply (in fact, the Jasmine one looks
entirely backwards). One of the error messages was changed in Murtuza's
patch, and wasn't reflected in your update for example.
Can you rebase please?
Thanks.
>
> Thanks
> Victoria && Joao
>
> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>
>> Thanks, patch applied!
>>
>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <murtuza.zabuawala@
>> enterprisedb.com> wrote:
>>
>>> Hi Dave,
>>>
>>> PFA updated patch.
>>>
>>> --
>>> Regards,
>>> Murtuza Zabuawala
>>> EnterpriseDB: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>>
>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <murtuza.zabuawala@
>>> enterprisedb.com> wrote:
>>>
>>>> Hi Dave,
>>>>
>>>> I'll change the name and send you updated patch.
>>>>
>>>>
>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>
>>>>> HI
>>>>>
>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <murtuza.zabuawala@
>>>>> enterprisedb.com> wrote:
>>>>>
>>>>>> Hi,
>>>>>>
>>>>>> PFA patch to add service parameter in server dialog.
>>>>>> - Docs updated
>>>>>> - Test case added for Service ID parameter
>>>>>>
>>>>>> Please note,
>>>>>> I have extracted Connection class and Server manager class from our
>>>>>> own custom Psycopg2 driver module.
>>>>>>
>>>>>> Patch also covers RM#3120
>>>>>>
>>>>>
>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>> database server service).
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>>>
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
@ 2018-03-13 03:31 ` Ashesh Vashi <[email protected]>
2018-03-13 04:12 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
1 sibling, 1 reply; 14+ messages in thread
From: Ashesh Vashi @ 2018-03-13 03:31 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; Dave Page <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; pgadmin-hackers
Murtuza/Dave,
I have to reviewed/seen the patch yet.
But - I have a question.
Have we used the service file in the external tools for backup, restore,
and import/export functionalities?
If not - we should fix that asap.
We had missed that during SSL support, and now - we're fixing that.
--
Thanks & Regards,
Ashesh Vashi
EnterpriseDB INDIA: Enterprise PostgreSQL Company
<http://www.enterprisedb.com;
*http://www.linkedin.com/in/asheshvashi*
<http://www.linkedin.com/in/asheshvashi;
On Tue, Mar 13, 2018 at 6:18 AM, Dave Page <[email protected]> wrote:
> Hi
>
> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Dave and Murtuza,
>>
>> Regarding this patch we refactored the Javascript code so that is lives
>> in a different file and added some tests.
>>
>> Also we found an issue with karma-jasmine that does not allow us to use
>> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>
>
> Sounds good, but neither patch will apply (in fact, the Jasmine one looks
> entirely backwards). One of the error messages was changed in Murtuza's
> patch, and wasn't reflected in your update for example.
>
> Can you rebase please?
>
> Thanks.
>
>
>>
>> Thanks
>> Victoria && Joao
>>
>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>
>>> Thanks, patch applied!
>>>
>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>> [email protected]> wrote:
>>>
>>>> Hi Dave,
>>>>
>>>> PFA updated patch.
>>>>
>>>> --
>>>> Regards,
>>>> Murtuza Zabuawala
>>>> EnterpriseDB: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>>
>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave,
>>>>>
>>>>> I'll change the name and send you updated patch.
>>>>>
>>>>>
>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>>
>>>>>> HI
>>>>>>
>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi,
>>>>>>>
>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>> - Docs updated
>>>>>>> - Test case added for Service ID parameter
>>>>>>>
>>>>>>> Please note,
>>>>>>> I have extracted Connection class and Server manager class from our
>>>>>>> own custom Psycopg2 driver module.
>>>>>>>
>>>>>>> Patch also covers RM#3120
>>>>>>>
>>>>>>
>>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>>> database server service).
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>>>
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-13 03:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
@ 2018-03-13 04:12 ` Murtuza Zabuawala <[email protected]>
2018-03-13 04:15 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
2018-03-13 11:45 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
0 siblings, 2 replies; 14+ messages in thread
From: Murtuza Zabuawala @ 2018-03-13 04:12 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers
Hi Ashesh,
I haven't implemented that intentionally because Khushboo is working on the
same for SSL and our code will conflict, So once Khushboo's patch gets
committed, I'll make changes for Service file as well.
--
Regards,
Murtuza Zabuawala
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Tue, Mar 13, 2018 at 9:01 AM, Ashesh Vashi <[email protected]
> wrote:
> Murtuza/Dave,
>
> I have to reviewed/seen the patch yet.
> But - I have a question.
> Have we used the service file in the external tools for backup, restore,
> and import/export functionalities?
> If not - we should fix that asap.
>
> We had missed that during SSL support, and now - we're fixing that.
>
> --
>
> Thanks & Regards,
>
> Ashesh Vashi
> EnterpriseDB INDIA: Enterprise PostgreSQL Company
> <http://www.enterprisedb.com;
>
>
> *http://www.linkedin.com/in/asheshvashi*
> <http://www.linkedin.com/in/asheshvashi;
>
> On Tue, Mar 13, 2018 at 6:18 AM, Dave Page <[email protected]> wrote:
>
>> Hi
>>
>> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Dave and Murtuza,
>>>
>>> Regarding this patch we refactored the Javascript code so that is lives
>>> in a different file and added some tests.
>>>
>>> Also we found an issue with karma-jasmine that does not allow us to use
>>> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>>
>>
>> Sounds good, but neither patch will apply (in fact, the Jasmine one looks
>> entirely backwards). One of the error messages was changed in Murtuza's
>> patch, and wasn't reflected in your update for example.
>>
>> Can you rebase please?
>>
>> Thanks.
>>
>>
>>>
>>> Thanks
>>> Victoria && Joao
>>>
>>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>>
>>>> Thanks, patch applied!
>>>>
>>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave,
>>>>>
>>>>> PFA updated patch.
>>>>>
>>>>> --
>>>>> Regards,
>>>>> Murtuza Zabuawala
>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>>
>>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Dave,
>>>>>>
>>>>>> I'll change the name and send you updated patch.
>>>>>>
>>>>>>
>>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> HI
>>>>>>>
>>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi,
>>>>>>>>
>>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>>> - Docs updated
>>>>>>>> - Test case added for Service ID parameter
>>>>>>>>
>>>>>>>> Please note,
>>>>>>>> I have extracted Connection class and Server manager class from our
>>>>>>>> own custom Psycopg2 driver module.
>>>>>>>>
>>>>>>>> Patch also covers RM#3120
>>>>>>>>
>>>>>>>
>>>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>>>> database server service).
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>
>>>>>>
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-13 03:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
2018-03-13 04:12 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
@ 2018-03-13 04:15 ` Ashesh Vashi <[email protected]>
2018-03-13 04:35 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
1 sibling, 1 reply; 14+ messages in thread
From: Ashesh Vashi @ 2018-03-13 04:15 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers
On Tue, Mar 13, 2018 at 9:42 AM, Murtuza Zabuawala <
[email protected]> wrote:
> Hi Ashesh,
>
> I haven't implemented that intentionally because Khushboo is working on
> the same for SSL and our code will conflict, So once Khushboo's patch gets
> committed, I'll make changes for Service file as well.
>
No - that's a bad practice.
You need to work on full feature set, not partial.
If you have intentionally skipped that, you should have informed the list.
-- Thanks, Ashesh
>
>
> --
> Regards,
> Murtuza Zabuawala
> EnterpriseDB: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
>
> On Tue, Mar 13, 2018 at 9:01 AM, Ashesh Vashi <
> [email protected]> wrote:
>
>> Murtuza/Dave,
>>
>> I have to reviewed/seen the patch yet.
>> But - I have a question.
>> Have we used the service file in the external tools for backup, restore,
>> and import/export functionalities?
>> If not - we should fix that asap.
>>
>> We had missed that during SSL support, and now - we're fixing that.
>>
>> --
>>
>> Thanks & Regards,
>>
>> Ashesh Vashi
>> EnterpriseDB INDIA: Enterprise PostgreSQL Company
>> <http://www.enterprisedb.com;
>>
>>
>> *http://www.linkedin.com/in/asheshvashi*
>> <http://www.linkedin.com/in/asheshvashi;
>>
>> On Tue, Mar 13, 2018 at 6:18 AM, Dave Page <[email protected]> wrote:
>>
>>> Hi
>>>
>>> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Dave and Murtuza,
>>>>
>>>> Regarding this patch we refactored the Javascript code so that is lives
>>>> in a different file and added some tests.
>>>>
>>>> Also we found an issue with karma-jasmine that does not allow us to use
>>>> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>>>
>>>
>>> Sounds good, but neither patch will apply (in fact, the Jasmine one
>>> looks entirely backwards). One of the error messages was changed in
>>> Murtuza's patch, and wasn't reflected in your update for example.
>>>
>>> Can you rebase please?
>>>
>>> Thanks.
>>>
>>>
>>>>
>>>> Thanks
>>>> Victoria && Joao
>>>>
>>>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>>>
>>>>> Thanks, patch applied!
>>>>>
>>>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Dave,
>>>>>>
>>>>>> PFA updated patch.
>>>>>>
>>>>>> --
>>>>>> Regards,
>>>>>> Murtuza Zabuawala
>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>>
>>>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Dave,
>>>>>>>
>>>>>>> I'll change the name and send you updated patch.
>>>>>>>
>>>>>>>
>>>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>>>>
>>>>>>>> HI
>>>>>>>>
>>>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi,
>>>>>>>>>
>>>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>>>> - Docs updated
>>>>>>>>> - Test case added for Service ID parameter
>>>>>>>>>
>>>>>>>>> Please note,
>>>>>>>>> I have extracted Connection class and Server manager class from
>>>>>>>>> our own custom Psycopg2 driver module.
>>>>>>>>>
>>>>>>>>> Patch also covers RM#3120
>>>>>>>>>
>>>>>>>>
>>>>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>>>>> database server service).
>>>>>>>>
>>>>>>>> --
>>>>>>>> Dave Page
>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>> Twitter: @pgsnake
>>>>>>>>
>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>>
>
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-13 03:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
2018-03-13 04:12 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-13 04:15 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
@ 2018-03-13 04:35 ` Murtuza Zabuawala <[email protected]>
0 siblings, 0 replies; 14+ messages in thread
From: Murtuza Zabuawala @ 2018-03-13 04:35 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Dave Page <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers
On Tue, Mar 13, 2018 at 9:45 AM, Ashesh Vashi <[email protected]
> wrote:
>
> On Tue, Mar 13, 2018 at 9:42 AM, Murtuza Zabuawala <murtuza.zabuawala@
> enterprisedb.com> wrote:
>
>> Hi Ashesh,
>>
>> I haven't implemented that intentionally because Khushboo is working on
>> the same for SSL and our code will conflict, So once Khushboo's patch gets
>> committed, I'll make changes for Service file as well.
>>
> No - that's a bad practice.
> You need to work on full feature set, not partial.
>
> If you have intentionally skipped that, you should have informed the list.
>
​I forgot to create sub-task, will keep that in mind next time onwards.​
https://redmine.postgresql.org/issues/3195
>
> -- Thanks, Ashesh
>
>>
>>
>> --
>> Regards,
>> Murtuza Zabuawala
>> EnterpriseDB: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>>
>> On Tue, Mar 13, 2018 at 9:01 AM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> Murtuza/Dave,
>>>
>>> I have to reviewed/seen the patch yet.
>>> But - I have a question.
>>> Have we used the service file in the external tools for backup, restore,
>>> and import/export functionalities?
>>> If not - we should fix that asap.
>>>
>>> We had missed that during SSL support, and now - we're fixing that.
>>>
>>> --
>>>
>>> Thanks & Regards,
>>>
>>> Ashesh Vashi
>>> EnterpriseDB INDIA: Enterprise PostgreSQL Company
>>> <http://www.enterprisedb.com;
>>>
>>>
>>> *http://www.linkedin.com/in/asheshvashi*
>>> <http://www.linkedin.com/in/asheshvashi;
>>>
>>> On Tue, Mar 13, 2018 at 6:18 AM, Dave Page <[email protected]> wrote:
>>>
>>>> Hi
>>>>
>>>> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave and Murtuza,
>>>>>
>>>>> Regarding this patch we refactored the Javascript code so that is
>>>>> lives in a different file and added some tests.
>>>>>
>>>>> Also we found an issue with karma-jasmine that does not allow us to
>>>>> use jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>>>>
>>>>
>>>> Sounds good, but neither patch will apply (in fact, the Jasmine one
>>>> looks entirely backwards). One of the error messages was changed in
>>>> Murtuza's patch, and wasn't reflected in your update for example.
>>>>
>>>> Can you rebase please?
>>>>
>>>> Thanks.
>>>>
>>>>
>>>>>
>>>>> Thanks
>>>>> Victoria && Joao
>>>>>
>>>>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>>>>
>>>>>> Thanks, patch applied!
>>>>>>
>>>>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Dave,
>>>>>>>
>>>>>>> PFA updated patch.
>>>>>>>
>>>>>>> --
>>>>>>> Regards,
>>>>>>> Murtuza Zabuawala
>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>>
>>>>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Dave,
>>>>>>>>
>>>>>>>> I'll change the name and send you updated patch.
>>>>>>>>
>>>>>>>>
>>>>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]>
>>>>>>>> wrote:
>>>>>>>>
>>>>>>>>> HI
>>>>>>>>>
>>>>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi,
>>>>>>>>>>
>>>>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>>>>> - Docs updated
>>>>>>>>>> - Test case added for Service ID parameter
>>>>>>>>>>
>>>>>>>>>> Please note,
>>>>>>>>>> I have extracted Connection class and Server manager class from
>>>>>>>>>> our own custom Psycopg2 driver module.
>>>>>>>>>>
>>>>>>>>>> Patch also covers RM#3120
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> This patch seems a little confused. The "Service" and "Service
>>>>>>>>> ID" fields from pgAdmin 3 are very different things. The Redmine ticket
>>>>>>>>> seems to be asking for the Service field (the pg_service.conf service
>>>>>>>>> name), *not* Service ID (the operating system's service ID, used to
>>>>>>>>> start/stop the database server service).
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Dave Page
>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>
>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>>
>
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-13 03:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Ashesh Vashi <[email protected]>
2018-03-13 04:12 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
@ 2018-03-13 11:45 ` Dave Page <[email protected]>
1 sibling, 0 replies; 14+ messages in thread
From: Dave Page @ 2018-03-13 11:45 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Joao De Almeida Pereira <[email protected]>; pgadmin-hackers
On Tue, Mar 13, 2018 at 12:12 AM, Murtuza Zabuawala <
[email protected]> wrote:
> Hi Ashesh,
>
> I haven't implemented that intentionally because Khushboo is working on
> the same for SSL and our code will conflict, So once Khushboo's patch gets
> committed, I'll make changes for Service file as well.
>
Ashesh is right - this should have been finished (and I should have thought
about this issue).
Please fix ASAP.
Thanks.
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
@ 2018-03-13 13:53 ` Victoria Henry <[email protected]>
2018-03-13 18:47 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
1 sibling, 1 reply; 14+ messages in thread
From: Victoria Henry @ 2018-03-13 13:53 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Dave,
We've made updated our previous patch to fix the error messages. Attached
are our updated patches.
On Mon, Mar 12, 2018 at 8:48 PM, Dave Page <[email protected]> wrote:
> Hi
>
> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Dave and Murtuza,
>>
>> Regarding this patch we refactored the Javascript code so that is lives
>> in a different file and added some tests.
>>
>> Also we found an issue with karma-jasmine that does not allow us to use
>> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>
>
> Sounds good, but neither patch will apply (in fact, the Jasmine one looks
> entirely backwards). One of the error messages was changed in Murtuza's
> patch, and wasn't reflected in your update for example.
>
> Can you rebase please?
>
> Thanks.
>
>
>>
>> Thanks
>> Victoria && Joao
>>
>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>
>>> Thanks, patch applied!
>>>
>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>> [email protected]> wrote:
>>>
>>>> Hi Dave,
>>>>
>>>> PFA updated patch.
>>>>
>>>> --
>>>> Regards,
>>>> Murtuza Zabuawala
>>>> EnterpriseDB: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>>
>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave,
>>>>>
>>>>> I'll change the name and send you updated patch.
>>>>>
>>>>>
>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>>
>>>>>> HI
>>>>>>
>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi,
>>>>>>>
>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>> - Docs updated
>>>>>>> - Test case added for Service ID parameter
>>>>>>>
>>>>>>> Please note,
>>>>>>> I have extracted Connection class and Server manager class from our
>>>>>>> own custom Psycopg2 driver module.
>>>>>>>
>>>>>>> Patch also covers RM#3120
>>>>>>>
>>>>>>
>>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>>> database server service).
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>>>
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
Attachments:
[text/x-patch] refactor-javascript.diff (19.9K, 3-refactor-javascript.diff)
download | inline diff:
diff --git a/web/package.json b/web/package.json
index ad0f3e16..66684fc6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -69,6 +69,7 @@
"hard-source-webpack-plugin": "^0.4.9",
"immutability-helper": "^2.2.0",
"imports-loader": "^0.7.1",
+ "ip-address": "^5.8.9",
"jquery": "1.11.2",
"jquery-contextmenu": "^2.5.0",
"jquery-ui": "^1.12.1",
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 36908733..302fe458 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -2,10 +2,13 @@ define('pgadmin.node.server', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user',
- 'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.alertifyjs', 'pgadmin.backform',
+ 'sources/browser/server_groups/servers/model_validation',
+ 'pgadmin.browser.server.privilege',
], function(
gettext, url_for, $, _, Backbone, S, pgAdmin, pgBrowser,
- supported_servers, current_user, Alertify, Backform
+ supported_servers, current_user, Alertify, Backform,
+ modelValidation
) {
if (!pgBrowser.Nodes['server']) {
@@ -848,110 +851,8 @@ define('pgadmin.node.server', [
group: gettext('Connection'),
}],
validate: function() {
- var err = {},
- errmsg,
- self = this;
-
- var service_id = this.get('service');
-
- var check_for_empty = function(id, msg) {
- var v = self.get(id);
- if (
- _.isUndefined(v) || v === null || String(v).replace(/^\s+|\s+$/g, '') == ''
- ) {
- err[id] = msg;
- errmsg = errmsg || msg;
- return true;
- } else {
- self.errorModel.unset(id);
- return false;
- }
- };
- var check_for_valid_ipv6 = function(val){
- // Regular expression for validating IPv6 address formats
- var exps = ['^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|',
- '(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|',
- '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
- '(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|',
- ':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|',
- '(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|',
- '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
- '(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|',
- '[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|',
- '((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|',
- '(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|',
- '1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|',
- '((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$'];
-
- var exp = new RegExp(exps.join(''));
- return exp.test(val.trim());
- };
- var check_for_valid_ip = function(id, msg) {
- var v4exps = '(^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)';
- var v4exp = new RegExp(v4exps);
- var v = self.get(id);
- if (
- v && !(v4exp.test(v.trim()))
- ) {
- if(!check_for_valid_ipv6(v)){
- err[id] = msg;
- errmsg = msg;
- }
- } else {
- self.errorModel.unset(id);
- }
- };
-
- if (!self.isNew() && 'id' in self.sessAttrs) {
- err['id'] = gettext('The ID cannot be changed.');
- errmsg = err['id'];
- } else {
- self.errorModel.unset('id');
- }
- check_for_empty('name', gettext('Name must be specified.'));
-
- // If no service id then only check
- if (
- _.isUndefined(service_id) || _.isNull(service_id) ||
- String(service_id).replace(/^\s+|\s+$/g, '') == ''
- ) {
- if (check_for_empty(
- 'host', gettext('Either Host name, Address or Service must be specified.')
- ) && check_for_empty('hostaddr', gettext('Either Host name, Address or Service must be specified.'))){
- errmsg = errmsg || gettext('Either Host name, Address or Service must be specified.');
- } else {
- errmsg = undefined;
- delete err['host'];
- delete err['hostaddr'];
- }
-
- check_for_empty(
- 'db', gettext('Maintenance database must be specified.')
- );
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
- check_for_valid_ip(
- 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.')
- );
- } else {
- _.each(['host', 'hostaddr', 'db'], (item) => {
- self.errorModel.unset(item);
- });
- }
-
- check_for_empty(
- 'username', gettext('Username must be specified.')
- );
- check_for_empty('port', gettext('Port must be specified.'));
-
- this.errorModel.set(err);
-
- if (_.size(err)) {
- return errmsg;
- }
-
- return null;
+ const validateModel = new modelValidation.ModelValidation(this);
+ return validateModel.validate();
},
isConnected: function(model) {
return model.get('connected');
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 3fcc69d8..83b2ad8b 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -1,6 +1,6 @@
define('bundled_browser',[
'pgadmin.browser',
- 'sources/browser/server_groups/servers/databases/external_tables/index',
+ 'sources/browser/index',
], function(pgBrowser) {
pgBrowser.init();
});
diff --git a/web/pgadmin/static/js/browser/index.js b/web/pgadmin/static/js/browser/index.js
new file mode 100644
index 00000000..297e8bf9
--- /dev/null
+++ b/web/pgadmin/static/js/browser/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'server_groups';
diff --git a/web/pgadmin/static/js/browser/server_groups/index.js b/web/pgadmin/static/js/browser/server_groups/index.js
new file mode 100644
index 00000000..b151b6f6
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'servers';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js
new file mode 100644
index 00000000..ef17c0ad
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'external_tables';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/index.js b/web/pgadmin/static/js/browser/server_groups/servers/index.js
new file mode 100644
index 00000000..242a1919
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/index.js
@@ -0,0 +1,11 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import 'databases';
+import 'model_validation';
diff --git a/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js b/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js
new file mode 100644
index 00000000..7ab129ba
--- /dev/null
+++ b/web/pgadmin/static/js/browser/server_groups/servers/model_validation.js
@@ -0,0 +1,104 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import _ from 'underscore';
+import {Address4, Address6} from 'ip-address';
+
+export class ModelValidation {
+ constructor(model) {
+ this.err = {};
+ this.errmsg = '';
+ this.model = model;
+ }
+
+ validate() {
+ const serviceId = this.model.get('service');
+
+ if (!this.model.isNew() && 'id' in this.model.sessAttrs) {
+ this.err['id'] = gettext('The ID cannot be changed.');
+ this.errmsg = this.err['id'];
+ } else {
+ this.model.errorModel.unset('id');
+ }
+
+ this.checkForEmpty('name', gettext('Name must be specified.'));
+
+ if (ModelValidation.isEmptyString(serviceId)) {
+ this.checkHostAndHostAddress();
+
+ this.checkForEmpty('db', gettext('Maintenance database must be specified.'));
+ } else {
+ this.clearHostAddressAndDbErrors();
+ }
+
+ this.checkForEmpty('username', gettext('Username must be specified.'));
+ this.checkForEmpty('port', gettext('Port must be specified.'));
+
+ this.model.errorModel.set(this.err);
+
+ if (_.size(this.err)) {
+ return this.errmsg;
+ }
+
+ return null;
+ }
+
+ clearHostAddressAndDbErrors() {
+ _.each(['host', 'hostaddr', 'db'], (item) => {
+ this.model.errorModel.unset(item);
+ });
+ }
+
+ checkHostAndHostAddress() {
+ const translatedStr = gettext('Either Host name, Address or Service must ' +
+ 'be specified.');
+ if (this.checkForEmpty('host', translatedStr) &&
+ this.checkForEmpty('hostaddr', translatedStr)) {
+ this.errmsg = this.errmsg || translatedStr;
+ } else {
+ this.errmsg = undefined;
+ delete this.err['host'];
+ delete this.err['hostaddr'];
+ }
+
+ this.checkForValidIp(this.model.get('hostaddr'),
+ gettext('Host address must be valid IPv4 or IPv6 address.'));
+ }
+
+ checkForValidIp(ipAddress, msg) {
+ if (ipAddress) {
+ const isIpv6Address = new Address6(ipAddress).isValid();
+ const isIpv4Address = new Address4(ipAddress).isValid();
+ if (!isIpv4Address && !isIpv6Address) {
+ this.err['hostaddr'] = msg;
+ this.errmsg = msg;
+ }
+ } else {
+ this.model.errorModel.unset('hostaddr');
+ }
+ }
+
+ checkForEmpty(id, msg) {
+ const value = this.model.get(id);
+
+ if (ModelValidation.isEmptyString(value)) {
+ this.err[id] = msg;
+ this.errmsg = this.errmsg || msg;
+ return true;
+ } else {
+ this.model.errorModel.unset(id);
+ return false;
+ }
+ }
+
+ static isEmptyString(string) {
+ return _.isUndefined(string) || _.isNull(string) || string.trim() === '';
+ }
+}
diff --git a/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
new file mode 100644
index 00000000..a05cd455
--- /dev/null
+++ b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
@@ -0,0 +1,101 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {ModelValidation} from 'sources/browser/server_groups/servers/model_validation';
+
+describe('Server#ModelValidation', () => {
+ describe('When validating a server parameters', () => {
+ let model;
+ let modelValidation;
+ beforeEach(() => {
+ model = {
+ errorModel: jasmine.createSpyObj('errorModel', ['set', 'unset']),
+ allValues: {},
+ get: function (key) {
+ return this.allValues[key];
+ },
+ sessAttrs: {},
+ };
+ model.isNew = jasmine.createSpy('isNew');
+ modelValidation = new ModelValidation(model);
+ });
+
+ describe('When all parameters are valid', () => {
+ beforeEach(() => {
+ model.isNew.and.returnValue(true);
+ model.allValues['name'] = 'some name';
+ model.allValues['username'] = 'some username';
+ model.allValues['port'] = 'some port';
+ });
+
+ describe('No service id', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['host'] = 'some host';
+ model.allValues['db'] = 'some db';
+ model.allValues['hostaddr'] = '1.1.1.1';
+ expect(modelValidation.validate()).toBeNull();
+ expect(model.errorModel.set).toHaveBeenCalledWith({});
+ });
+ });
+
+ describe('Service id present', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['service'] = 'asdfg';
+ expect(modelValidation.validate()).toBeNull();
+ expect(model.errorModel.set).toHaveBeenCalledWith({});
+ });
+ });
+ });
+
+ describe('When no parameters are valid', () => {
+ describe('Service id not present', () => {
+ it('does not set any error in the model', () => {
+ expect(modelValidation.validate()).toBe('Name must be specified.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ host: 'Either Host name, Address or Service must be specified.',
+ hostaddr: 'Either Host name, Address or Service must be specified.',
+ db: 'Maintenance database must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+
+ describe('Host address is not valid', () => {
+ it('sets the "Host address must be a valid IPv4 or IPv6 address" error', () => {
+ model.allValues['hostaddr'] = 'something that is not an ip address';
+ expect(modelValidation.validate()).toBe('Host address must be valid IPv4 or IPv6 address.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ hostaddr: 'Host address must be valid IPv4 or IPv6 address.',
+ db: 'Maintenance database must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+
+ describe('Service id present', () => {
+ it('does not set any error in the model', () => {
+ model.allValues['service'] = 'asdfg';
+ expect(modelValidation.validate()).toBe('Name must be specified.');
+ expect(model.errorModel.set).toHaveBeenCalledTimes(1);
+ expect(model.errorModel.set).toHaveBeenCalledWith({
+ name: 'Name must be specified.',
+ username: 'Username must be specified.',
+ port: 'Port must be specified.'
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/yarn.lock b/web/yarn.lock
index 85ccbc8b..2dc4c5c2 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -3938,6 +3938,18 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ip-address@^5.8.9:
+ version "5.8.9"
+ resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-5.8.9.tgz#6379277c23fc5adb20511e4d23ec2c1bde105dfd"
+ dependencies:
+ jsbn "1.1.0"
+ lodash.find "^4.6.0"
+ lodash.max "^4.0.1"
+ lodash.merge "^4.6.0"
+ lodash.padstart "^4.6.1"
+ lodash.repeat "^4.1.0"
+ sprintf-js "1.1.0"
+
ip-regex@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
@@ -4329,6 +4341,10 @@ js-yaml@~3.7.0:
argparse "^1.0.7"
esprima "^2.6.0"
[email protected]:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
+
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -4738,6 +4754,10 @@ lodash.escape@^3.0.0:
dependencies:
lodash._root "^3.0.0"
+lodash.find@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
+
lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
@@ -4777,6 +4797,10 @@ lodash.keys@^3.0.0:
lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0"
+lodash.max@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.max/-/lodash.max-4.0.1.tgz#8735566c618b35a9f760520b487ae79658af136a"
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -4785,10 +4809,22 @@ lodash.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
+lodash.merge@^4.6.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
+
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+lodash.padstart@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.padstart/-/lodash.padstart-4.6.1.tgz#d2e3eebff0d9d39ad50f5cbd1b52a7bce6bb611b"
+
+lodash.repeat@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.repeat/-/lodash.repeat-4.1.0.tgz#fc7de8131d8c8ac07e4b49f74ffe829d1f2bec44"
+
lodash.restparam@^3.0.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
@@ -6863,6 +6899,10 @@ spectrum-colorpicker@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/spectrum-colorpicker/-/spectrum-colorpicker-1.8.0.tgz#b926cf5002c0a77860b5f8351e1c093c65200107"
[email protected]:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.0.tgz#cffcaf702daf65ea39bb4e0fa2b299cec1a1be46"
+
sprintf-js@^1.0.3:
version "1.1.1"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c"
[text/x-patch] revert-upgrade-of-jasmine.diff (998B, 4-revert-upgrade-of-jasmine.diff)
download | inline diff:
diff --git a/web/package.json b/web/package.json
index a4cb58d5..ad0f3e16 100644
--- a/web/package.json
+++ b/web/package.json
@@ -18,7 +18,7 @@
"file-loader": "^0.11.2",
"image-webpack-loader": "^3.3.1",
"is-docker": "^1.1.0",
- "jasmine-core": "~3.1.0",
+ "jasmine-core": "~2.99.0",
"jasmine-enzyme": "~4.1.1",
"karma": "~1.5.0",
"karma-babel-preprocessor": "^6.0.1",
diff --git a/web/yarn.lock b/web/yarn.lock
index 5e310d79..85ccbc8b 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -4267,9 +4267,9 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
-jasmine-core@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.1.0.tgz#a4785e135d5df65024dfc9224953df585bd2766c"
+jasmine-core@~2.99.0:
+ version "2.99.1"
+ resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.99.1.tgz#e6400df1e6b56e130b61c4bcd093daa7f6e8ca15"
jasmine-enzyme@~4.1.1:
version "4.1.1"
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: [pgAdmin4][RM#3140] Add service parameter
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-09 15:59 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Re: [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-12 21:18 ` Re: [pgAdmin4][RM#3140] Add service parameter Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Re: [pgAdmin4][RM#3140] Add service parameter Dave Page <[email protected]>
2018-03-13 13:53 ` Re: [pgAdmin4][RM#3140] Add service parameter Victoria Henry <[email protected]>
@ 2018-03-13 18:47 ` Dave Page <[email protected]>
0 siblings, 0 replies; 14+ messages in thread
From: Dave Page @ 2018-03-13 18:47 UTC (permalink / raw)
To: Victoria Henry <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Thanks, patches applied!
On Tue, Mar 13, 2018 at 9:53 AM, Victoria Henry <[email protected]> wrote:
> Hi Dave,
>
> We've made updated our previous patch to fix the error messages. Attached
> are our updated patches.
>
> On Mon, Mar 12, 2018 at 8:48 PM, Dave Page <[email protected]> wrote:
>
>> Hi
>>
>> On Mon, Mar 12, 2018 at 5:18 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Dave and Murtuza,
>>>
>>> Regarding this patch we refactored the Javascript code so that is lives
>>> in a different file and added some tests.
>>>
>>> Also we found an issue with karma-jasmine that does not allow us to use
>>> jasmine 3.1 yet. You can find attached a patch that reverts that commit.
>>>
>>
>> Sounds good, but neither patch will apply (in fact, the Jasmine one looks
>> entirely backwards). One of the error messages was changed in Murtuza's
>> patch, and wasn't reflected in your update for example.
>>
>> Can you rebase please?
>>
>> Thanks.
>>
>>
>>>
>>> Thanks
>>> Victoria && Joao
>>>
>>> On Mon, Mar 12, 2018 at 4:46 PM Dave Page <[email protected]> wrote:
>>>
>>>> Thanks, patch applied!
>>>>
>>>> On Mon, Mar 12, 2018 at 3:31 AM, Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Dave,
>>>>>
>>>>> PFA updated patch.
>>>>>
>>>>> --
>>>>> Regards,
>>>>> Murtuza Zabuawala
>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>>
>>>>> On Fri, Mar 9, 2018 at 9:29 PM, Murtuza Zabuawala <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Dave,
>>>>>>
>>>>>> I'll change the name and send you updated patch.
>>>>>>
>>>>>>
>>>>>> On Fri, Mar 9, 2018 at 9:25 PM, Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> HI
>>>>>>>
>>>>>>> On Fri, Mar 9, 2018 at 11:47 AM, Murtuza Zabuawala <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi,
>>>>>>>>
>>>>>>>> PFA patch to add service parameter in server dialog.
>>>>>>>> - Docs updated
>>>>>>>> - Test case added for Service ID parameter
>>>>>>>>
>>>>>>>> Please note,
>>>>>>>> I have extracted Connection class and Server manager class from our
>>>>>>>> own custom Psycopg2 driver module.
>>>>>>>>
>>>>>>>> Patch also covers RM#3120
>>>>>>>>
>>>>>>>
>>>>>>> This patch seems a little confused. The "Service" and "Service ID"
>>>>>>> fields from pgAdmin 3 are very different things. The Redmine ticket seems
>>>>>>> to be asking for the Service field (the pg_service.conf service name),
>>>>>>> *not* Service ID (the operating system's service ID, used to start/stop the
>>>>>>> database server service).
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>
>>>>>>
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 14+ messages in thread
end of thread, other threads:[~2018-03-13 18:47 UTC | newest]
Thread overview: 14+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-03-09 11:47 [pgAdmin4][RM#3140] Add service parameter Murtuza Zabuawala <[email protected]>
2018-03-09 15:55 ` Dave Page <[email protected]>
2018-03-09 15:59 ` Murtuza Zabuawala <[email protected]>
2018-03-12 07:31 ` Murtuza Zabuawala <[email protected]>
2018-03-12 20:46 ` Dave Page <[email protected]>
2018-03-12 21:18 ` Joao De Almeida Pereira <[email protected]>
2018-03-13 00:48 ` Dave Page <[email protected]>
2018-03-13 03:31 ` Ashesh Vashi <[email protected]>
2018-03-13 04:12 ` Murtuza Zabuawala <[email protected]>
2018-03-13 04:15 ` Ashesh Vashi <[email protected]>
2018-03-13 04:35 ` Murtuza Zabuawala <[email protected]>
2018-03-13 11:45 ` Dave Page <[email protected]>
2018-03-13 13:53 ` Victoria Henry <[email protected]>
2018-03-13 18:47 ` Dave Page <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox