diff --git a/web/pgadmin/feature_tests/query_tool_tests.py b/web/pgadmin/feature_tests/query_tool_tests.py index a0b3713..2dbe876 100644 --- a/web/pgadmin/feature_tests/query_tool_tests.py +++ b/web/pgadmin/feature_tests/query_tool_tests.py @@ -96,6 +96,11 @@ class QueryToolFeatureTest(BaseFeatureTest): print("OK.", file=sys.stderr) self._clear_query_tool() + # Notify Statements. + print("Capture Notify Statements... ", file=sys.stderr, end="") + self._query_tool_notify_statements() + self._clear_query_tool() + def after(self): self.page.remove_server(self.server) connection = test_utils.get_db_connection( @@ -615,3 +620,41 @@ SELECT 1, pg_sleep(300)""" self.server['sslmode'] ) return connection.server_version > 90100 + + def _query_tool_notify_statements(self): + print("\n\tListen on an event... ", file=sys.stderr, end="") + self.page.fill_codemirror_area_with("LISTEN foo;") + self.page.find_by_id("btn-flash").click() + self.page.wait_for_query_tool_loading_indicator_to_disappear() + self.page.click_tab('Messages') + self.page.find_by_xpath( + '//div[contains(@class, "sql-editor-message") and ' + 'contains(string(), "LISTEN")]' + ) + print("OK.", file=sys.stderr) + self._clear_query_tool() + + print("\tNotify event without data... ", file=sys.stderr, end="") + self.page.fill_codemirror_area_with("NOTIFY foo;") + self.page.find_by_id("btn-flash").click() + self.page.wait_for_query_tool_loading_indicator_to_disappear() + self.page.find_by_xpath( + '//div[contains(@class, "ajs-content") and ' + 'contains(string(), "Asynchronous notification")]' + ) + self.page.click_modal("OK") + print("OK.", file=sys.stderr) + self._clear_query_tool() + + print("\tNotify event with data... ", file=sys.stderr, end="") + self.page.fill_codemirror_area_with("SELECT pg_notify('foo', " + "'This is bar')") + self.page.find_by_id("btn-flash").click() + self.page.wait_for_query_tool_loading_indicator_to_disappear() + self.page.find_by_xpath( + '//div[contains(@class, "ajs-content") and ' + 'contains(string(), "with payload")]' + ) + self.page.click_modal("OK") + print("OK.", file=sys.stderr) + self._clear_query_tool() diff --git a/web/pgadmin/static/js/sqleditor/execute_query.js b/web/pgadmin/static/js/sqleditor/execute_query.js index 333a3a2..2005b6a 100644 --- a/web/pgadmin/static/js/sqleditor/execute_query.js +++ b/web/pgadmin/static/js/sqleditor/execute_query.js @@ -82,6 +82,8 @@ class ExecuteQuery { self.loadingScreen.hide(); self.enableSQLEditorButtons(); self.sqlServerObject.update_msg_history(false, httpMessageData.data.result); + if ('notify_messages' in httpMessageData.data) + self.sqlServerObject.show_notify_messages(httpMessageData.data.notify_messages); // Highlight the error in the sql panel self.sqlServerObject._highlight_error(httpMessageData.data.result); @@ -116,6 +118,8 @@ class ExecuteQuery { self.loadingScreen.setMessage('Loading data from the database server and rendering...'); self.sqlServerObject.call_render_after_poll(httpMessage.data.data); + if ('notify_messages' in httpMessage.data.data) + self.sqlServerObject.show_notify_messages(httpMessage.data.data.notify_messages); } else if (ExecuteQuery.isQueryStillRunning(httpMessage)) { // If status is Busy then poll the result by recursive call to the poll function this.delayedPoll(); diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index c72505a..82dcc6b 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -543,10 +543,12 @@ def poll(trans_id): # There may be additional messages even if result is present # eg: Function can provide result as well as RAISE messages additional_messages = None + notify_messages = None if status == 'Success': messages = conn.messages() if messages: additional_messages = ''.join(messages) + notify_messages = conn.notify_messages() # Procedure/Function output may comes in the form of Notices from the # database server, so we need to append those outputs with the @@ -564,6 +566,7 @@ def poll(trans_id): 'rows_fetched_from': rows_fetched_from, 'rows_fetched_to': rows_fetched_to, 'additional_messages': additional_messages, + 'notify_messages': notify_messages, 'has_more_rows': has_more_rows, 'colinfo': columns_info, 'primary_keys': primary_keys, diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index f95389c..650fa43 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -3832,6 +3832,17 @@ define('tools.querytool', [ } }); }, + // This function is used to raise notify message. + show_notify_messages: function(notify_messages) { + if (notify_messages.length > 0) { + let notify_msgs = ''; + for (let i in notify_messages) { + notify_msgs += notify_messages[i] + '
'; + } + + alertify.alert('Notifications', notify_msgs); + } + }, }); pgAdmin.SqlEditor = { diff --git a/web/pgadmin/tools/sqleditor/utils/start_running_query.py b/web/pgadmin/tools/sqleditor/utils/start_running_query.py index 8c598ff..f6631b9 100644 --- a/web/pgadmin/tools/sqleditor/utils/start_running_query.py +++ b/web/pgadmin/tools/sqleditor/utils/start_running_query.py @@ -47,6 +47,7 @@ class StartRunningQuery: transaction_object = pickle.loads(session_obj['command_obj']) can_edit = False can_filter = False + notify_messages = None if transaction_object is not None and session_obj is not None: # set fetched row count to 0 as we are executing query again. transaction_object.update_fetched_row_cnt(0) @@ -88,6 +89,8 @@ class StartRunningQuery: can_edit = transaction_object.can_edit() can_filter = transaction_object.can_filter() + # Get the notify messages + notify_messages = conn.notify_messages() else: status = False result = gettext( @@ -97,7 +100,8 @@ class StartRunningQuery: 'status': status, 'result': result, 'can_edit': can_edit, 'can_filter': can_filter, 'info_notifier_timeout': - self.blueprint_object.info_notifier_timeout.get() + self.blueprint_object.info_notifier_timeout.get(), + 'notify_messages': notify_messages } ) diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index 315631c..86d1a14 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -136,6 +136,9 @@ class Connection(BaseConnection): formatted error message if flag is set to true else return normal error message. + * notify_messages() + - Returns the list of notify messages sends from the PostgreSQL database + server. """ def __init__(self, manager, conn_id, db, auto_reconnect=True, async=0, @@ -155,6 +158,7 @@ class Connection(BaseConnection): self.execution_aborted = False self.row_count = 0 self.__notices = None + self.__notifies = None self.password = None # This flag indicates the connection status (connected/disconnected). self.wasConnected = False @@ -891,6 +895,7 @@ WHERE try: self.__notices = [] + self.__notifies = [] self.execution_aborted = False cur.execute(query, params) res = self._wait_timeout(cur.connection) @@ -908,6 +913,11 @@ WHERE ) ) + # Get the notify messages send by database server. + if self.conn.notifies and self.__notifies is not None: + self.__notifies.extend(self.conn.notifies) + self.conn.notifies = [] + if self.is_disconnected(pe): raise ConnectionLost( self.manager.sid, @@ -1366,6 +1376,10 @@ Failed to reset the connection to the server due to following error: self.__notices.extend(self.conn.notices) self.conn.notices.clear() + if self.conn.notifies and self.__notifies is not None: + self.__notifies.extend(self.conn.notifies) + self.conn.notifies = [] + # 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 @@ -1711,3 +1725,26 @@ Failed to reset the connection to the server due to following error: return False return True + + def notify_messages(self): + """ + Returns the list of the notify messages send from the database server. + """ + resp = [] + + while self.__notifies: + notify = self.__notifies.pop(0) + if notify.payload is not None and notify.payload is not '': + notify_msg = gettext( + "Asynchronous notification \"{0}\" with payload \"{1}\" " + "received from server process with PID {2}\n" + ).format(notify.channel, notify.payload, notify.pid) + + else: + notify_msg = gettext( + "Asynchronous notification \"{0}\" received from " + "server process with PID {1}\n" + ).format(notify.channel, notify.pid) + resp.append(notify_msg) + + return resp diff --git a/web/regression/javascript/sqleditor/execute_query_spec.js b/web/regression/javascript/sqleditor/execute_query_spec.js index 06fefff..f9fc869 100644 --- a/web/regression/javascript/sqleditor/execute_query_spec.js +++ b/web/regression/javascript/sqleditor/execute_query_spec.js @@ -43,6 +43,7 @@ describe('ExecuteQuery', () => { 'saveState', 'initTransaction', 'handle_connection_lost', + 'show_notify_messages', ]); sqlEditorMock.transId = 123; sqlEditorMock.rows_affected = 1000; @@ -76,7 +77,7 @@ describe('ExecuteQuery', () => { describe('when query was successful', () => { beforeEach(() => { response = { - data: {status: 'Success'}, + data: {status: 'Success', notify_messages: ['Asynchronous notification']}, }; networkMock.onGet('/sqleditor/query_tool/poll/123').reply(200, response); @@ -97,7 +98,15 @@ describe('ExecuteQuery', () => { it('should render the results', (done) => { setTimeout(() => { expect(sqlEditorMock.call_render_after_poll) - .toHaveBeenCalledWith({status: 'Success'}); + .toHaveBeenCalledWith({status: 'Success', notify_messages: ['Asynchronous notification']}); + done(); + }, 0); + }); + + it('should notify the messages', (done) => { + setTimeout(() => { + expect(sqlEditorMock.show_notify_messages) + .toHaveBeenCalledWith(['Asynchronous notification']); done(); }, 0); });