Added Macro support. Fixes #1402

This commit is contained in:
Khushboo Vashi
2020-09-28 15:26:45 +05:30
committed by Akshay Joshi
parent 952197f130
commit 4616a74029
17 changed files with 1429 additions and 14 deletions

View File

@@ -33,6 +33,7 @@ from pgadmin.settings import get_setting
from pgadmin.browser.utils import underscore_unescape
from pgadmin.utils.exception import ObjectGone
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.tools.sqleditor.utils.macros import get_user_macros
MODULE_NAME = 'datagrid'
@@ -274,6 +275,8 @@ def panel(trans_id):
layout = get_setting('SQLEditor/Layout')
macros = get_user_macros()
return render_template(
"datagrid/index.html",
_=gettext,
@@ -286,6 +289,7 @@ def panel(trans_id):
bgcolor=bgcolor,
fgcolor=fgcolor,
layout=layout,
macros=macros
)

View File

@@ -377,6 +377,31 @@
<i class="fa fa-download sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
</div>
<div class="btn-group mr-1 user_macros" role="group" aria-label="">
<button id="btn-macro-dropdown" type="button" class="btn btn-sm btn-primary-icon dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-label="{{ _('Macros') }}" title="{{ _('Macros') }}" tabindex="0">
<i class="fa fa-scroll sql-icon-lg" aria-hidden="true" role="img"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" id="btn-manage-macros" href="#" tabindex="0">
<span> {{ _('Manage Macros...') }} </span>
</a>
</li>
{% if macros|length > 0 %}
<li class="dropdown-divider"></li>
{% endif %}
{% for i in macros %}
<li>
<a class="dropdown-item btn-macro" data-macro-id="{{ i.id }}" href="#" tabindex="0">
<span> {{ _(i.name) }} </span>
<span> ({{ i.key_label }}) </span>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="connection_status_wrapper d-flex">
@@ -459,7 +484,8 @@ require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sou
sqlEditorController.start(
{{ uniqueId }},
{{ url_params|safe}},
'{{ layout|safe }}'
'{{ layout|safe }}',
{{ macros|safe }}
);
// If opening from schema diff, set the generated script to the SQL Editor

View File

@@ -32,7 +32,7 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
from pgadmin.utils import PgAdminModule
from pgadmin.utils import get_storage_directory
from pgadmin.utils.ajax import make_json_response, bad_request, \
success_return, internal_server_error
success_return, internal_server_error, make_response as ajax_response
from pgadmin.utils.driver import get_driver
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
@@ -46,6 +46,8 @@ from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\
ERROR_MSG_TRANS_ID_NOT_FOUND
from pgadmin.tools.sqleditor.utils.macros import get_macros,\
get_user_macros, set_macros
MODULE_NAME = 'sqleditor'
@@ -109,6 +111,9 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.get_query_history',
'sqleditor.add_query_history',
'sqleditor.clear_query_history',
'sqleditor.get_macro',
'sqleditor.get_macros',
'sqleditor.set_macros'
]
def register_preferences(self):
@@ -1547,3 +1552,46 @@ def get_query_history(trans_id):
check_transaction_status(trans_id)
return QueryHistory.get(current_user.id, trans_obj.sid, conn.db)
@blueprint.route(
'/get_macros/<int:trans_id>',
methods=["GET"], endpoint='get_macros'
)
@blueprint.route(
'/get_macros/<int:macro_id>/<int:trans_id>',
methods=["GET"], endpoint='get_macro'
)
@login_required
def macros(trans_id, macro_id=None, json_resp=True):
"""
This method is used to get all the columns for data sorting dialog.
Args:
trans_id: unique transaction id
macro_id: Macro id
"""
status, error_msg, conn, trans_obj, session_ob = \
check_transaction_status(trans_id)
return get_macros(macro_id, json_resp)
@blueprint.route(
'/set_macros/<int:trans_id>',
methods=["PUT"], endpoint='set_macros'
)
@login_required
def update_macros(trans_id):
"""
This method is used to get all the columns for data sorting dialog.
Args:
trans_id: unique transaction id
"""
status, error_msg, conn, trans_obj, session_ob = \
check_transaction_status(trans_id)
return set_macros()

View File

@@ -395,3 +395,32 @@ input.editor-checkbox:focus {
.hide-vertical-scrollbar {
overflow-y: hidden;
}
/* Macros */
.macro-tab {
top: 0px !important;
}
.macro-tab .tab-pane {
padding: 0px !important;
}
.macro_dialog .CodeMirror {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .sql-cell > div {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .CodeMirror-cursor {
width: 1px !important;
height: 18px !important;
}
.macro_dialog .pg-prop-status-bar {
z-index: 1;
}

View File

@@ -43,6 +43,7 @@ define('tools.querytool', [
'tools/datagrid/static/js/datagrid_panel_title',
'sources/window',
'sources/is_native',
'sources/sqleditor/macro',
'sources/../bundle/slickgrid',
'pgadmin.file_manager',
'slick.pgadmin.formatters',
@@ -57,7 +58,7 @@ define('tools.querytool', [
GeometryViewer, historyColl, queryHist, querySources,
keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc,
pgWindow, isNative) {
pgWindow, isNative, MacroHandler) {
/* Return back, this has been called more than once */
if (pgAdmin.SqlEditor)
return pgAdmin.SqlEditor;
@@ -149,6 +150,9 @@ define('tools.querytool', [
// Transaction control
'click #btn-commit': 'on_commit_transaction',
'click #btn-rollback': 'on_rollback_transaction',
// Manage Macros
'click #btn-manage-macros': 'on_manage_macros',
'click .btn-macro': 'on_execute_macro',
},
reflectPreferences: function() {
@@ -2038,8 +2042,30 @@ define('tools.querytool', [
queryToolActions.executeRollback(this.handler);
},
// Callback function for manage macros button click.
on_manage_macros: function() {
var self = this;
// Trigger the show_filter signal to the SqlEditorController class
self.handler.trigger(
'pgadmin-sqleditor:button:manage_macros',
self,
self.handler
);
},
// Callback function for manage macros button click.
on_execute_macro: function(e) {
let macroId = $(e.currentTarget).data('macro-id');
this.handler.history_query_source = QuerySources.EXECUTE;
queryToolActions.executeMacro(this.handler, macroId);
},
});
/* Defining controller class for data grid, which actually
* perform the operations like executing the sql query, poll the result,
* render the data in the grid, Save/Refresh the data etc...
@@ -2308,7 +2334,7 @@ define('tools.querytool', [
* call the render method of the grid view to render the slickgrid
* header and loading icon and start execution of the sql query.
*/
start: function(transId, url_params, layout) {
start: function(transId, url_params, layout, macros) {
var self = this;
self.is_query_tool = url_params.is_query_tool==='true'?true:false;
@@ -2333,6 +2359,7 @@ define('tools.querytool', [
layout: layout,
});
self.transId = self.gridView.transId = transId;
self.macros = self.gridView.macros = macros;
self.gridView.current_file = undefined;
@@ -2474,12 +2501,14 @@ define('tools.querytool', [
self.on('pgadmin-sqleditor:unindent_selected_code', self._unindent_selected_code, self);
// Format
self.on('pgadmin-sqleditor:format_sql', self._format_sql, self);
self.on('pgadmin-sqleditor:button:manage_macros', self._manage_macros, self);
self.on('pgadmin-sqleditor:button:execute_macro', self._execute_macro, self);
window.parent.$(window.parent.document).on('pgadmin-sqleditor:rows-copied', self._copied_in_other_session);
},
// Checks if there is any dirty data in the grid before executing a query
check_data_changes_to_execute_query: function(explain_prefix=null, shouldReconnect=false) {
check_data_changes_to_execute_query: function(explain_prefix=null, shouldReconnect=false, macroId=undefined) {
var self = this;
// Check if the data grid has any changes before running query
@@ -2492,7 +2521,10 @@ define('tools.querytool', [
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
function() {
// The user does not want to save, just continue
if(self.is_query_tool) {
if (macroId !== undefined) {
self._execute_macro_query(explain_prefix, shouldReconnect, macroId);
}
else if(self.is_query_tool) {
self._execute_sql_query(explain_prefix, shouldReconnect);
}
else {
@@ -2508,7 +2540,10 @@ define('tools.querytool', [
cancel: gettext('No'),
});
} else {
if(self.is_query_tool) {
if (macroId !== undefined) {
self._execute_macro_query(explain_prefix, shouldReconnect, macroId);
}
else if(self.is_query_tool) {
self._execute_sql_query(explain_prefix, shouldReconnect);
}
else {
@@ -2602,6 +2637,37 @@ define('tools.querytool', [
});
},
// Executes sql query for macroin the editor in Query Tool mode
_execute_macro_query: function(explain_prefix, shouldReconnect, macroId) {
var self = this;
self.has_more_rows = false;
self.fetching_rows = false;
$.ajax({
url: url_for('sqleditor.get_macro', {'macro_id': macroId, 'trans_id': self.transId}),
method: 'GET',
contentType: 'application/json',
dataType: 'json',
})
.done(function(res) {
if (res) {
// Replace the place holder
let query = res.sql.replaceAll('$SELECTION$', self.gridView.query_tool_obj.getSelection());
const executeQuery = new ExecuteQuery.ExecuteQuery(self, pgAdmin.Browser.UserManagement);
executeQuery.poll = pgBrowser.override_activity_event_decorator(executeQuery.poll).bind(executeQuery);
executeQuery.execute(query, explain_prefix, shouldReconnect);
} else {
// Let it be for now
}
})
.fail(function() {
/* failure should not be ignored */
});
},
// Executes sql query in the editor in Query Tool mode
_execute_sql_query: function(explain_prefix, shouldReconnect) {
var self = this, sql = '';
@@ -3968,6 +4034,7 @@ define('tools.querytool', [
$('#btn-file-menu-dropdown').prop('disabled', mode_disabled);
$('#btn-find').prop('disabled', mode_disabled);
$('#btn-find-menu-dropdown').prop('disabled', mode_disabled);
$('#btn-macro-dropdown').prop('disabled', mode_disabled);
if (this.is_query_tool) {
@@ -4375,6 +4442,24 @@ define('tools.querytool', [
});
},
// This function will open the manage macro dialog
_manage_macros: function() {
let self = this;
/* When server is disconnected and connected, connection is lost,
* To reconnect pass true
*/
MacroHandler.dialog(self);
},
// This function will open the manage macro dialog
_execute_macro: function() {
queryToolActions.executeMacro(this.handler);
},
isQueryRunning: function() {
return is_query_running;
},

View File

@@ -0,0 +1,125 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import json
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from pgadmin.utils.route import BaseTestGenerator
from regression import parent_node_dict
from regression.python_test_utils import test_utils as utils
import random
class TestMacros(BaseTestGenerator):
""" This class will test the query tool polling. """
scenarios = [
('Get all macros',
dict(
url='get_macros',
method='get'
)),
('Set Macros',
dict(
url='set_macros',
method='put',
operation='update',
data={
'changed': [
{'id': 1,
'name': 'Test Macro 1',
'sql': 'SELECT 1;'
},
{'id': 2,
'name': 'Test Macro 2',
'sql': 'SELECT 2;'
},
{'id': 3,
'name': 'Test Macro 3',
'sql': 'SELECT 3;'
},
]
}
)),
('Clear Macros',
dict(
url='set_macros',
method='put',
operation='clear',
data={
'changed': [
{'id': 1,
'name': '',
'sql': ''
},
{'id': 2,
'name': '',
'sql': ''
},
{'id': 3,
'name': '',
'sql': ''
},
]
}
))
]
def setUp(self):
""" This function will check messages return by query tool polling. """
database_info = parent_node_dict["database"][-1]
self.server_id = database_info["server_id"]
self.db_id = database_info["db_id"]
db_con = database_utils.connect_database(self,
utils.SERVER_GROUP,
self.server_id,
self.db_id)
if not db_con["info"] == "Database connected.":
raise Exception("Could not connect to the database.")
# Initialize query tool
self.trans_id = str(random.randint(1, 9999999))
url = '/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}'.format(
self.trans_id, utils.SERVER_GROUP, self.server_id, self.db_id)
response = self.tester.post(url)
self.assertEqual(response.status_code, 200)
def runTest(self):
url = '/sqleditor/{0}/{1}'.format(self.url, self.trans_id)
if self.method == 'get':
response = self.tester.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.data.decode('utf-8'))
self.assertEqual(len(response_data['macro']), 22)
else:
response = self.tester.put(url,
data=json.dumps(self.data),
follow_redirects=True)
self.assertEqual(response.status_code, 200)
for m in self.data['changed']:
url = '/sqleditor/get_macros/{0}/{1}'.format(m['id'],
self.trans_id)
response = self.tester.get(url)
if self.operation == 'clear':
self.assertEqual(response.status_code, 410)
else:
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.data.decode('utf-8'))
self.assertEqual(response_data['name'], m['name'])
self.assertEqual(response_data['sql'], m['sql'])
def tearDown(self):
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)

View File

@@ -0,0 +1,189 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2020, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Handle Macros for SQL Editor."""
import simplejson as json
from flask_babelex import gettext
from flask import current_app, request
from flask_security import login_required, current_user
from pgadmin.utils.ajax import make_response as ajax_response,\
make_json_response
from pgadmin.model import db, Macros, UserMacros
from sqlalchemy import and_
def get_macros(macro_id, json_resp):
"""
This method is used to get all the macros/specific macro.
:param macro_id: Macro ID
:param json_resp: Set True to return json response
"""
if macro_id:
macro = UserMacros.query.filter_by(mid=macro_id,
uid=current_user.id).first()
if macro is None:
return make_json_response(
status=410,
success=0,
errormsg=gettext("Macro not found.")
)
else:
return ajax_response(
response={'id': macro.mid,
'name': macro.name,
'sql': macro.sql},
status=200
)
else:
macros = db.session.query(Macros.id, Macros.alt, Macros.control,
Macros.key, Macros.key_code,
UserMacros.name, UserMacros.sql
).outerjoin(
UserMacros, and_(Macros.id == UserMacros.mid,
UserMacros.uid == current_user.id)).all()
data = []
for m in macros:
key_label = 'Ctrl + ' + m[3] if m[2] is True else 'Alt + ' + m[3]
data.append({'id': m[0], 'alt': m[1],
'control': m[2], 'key': m[3],
'key_code': m[4], 'name': m[5],
'sql': m[6],
'key_label': key_label})
if not json_resp:
return data
return ajax_response(
response={'macro': data},
status=200
)
def get_user_macros():
"""
This method is used to get all the user macros.
"""
macros = db.session.query(UserMacros.name,
Macros.id,
Macros.alt, Macros.control,
Macros.key, Macros.key_code
).outerjoin(
Macros, UserMacros.mid == Macros.id).filter(
UserMacros.uid == current_user.id).order_by(UserMacros.name).all()
data = []
for m in macros:
key_label = 'Ctrl + ' + m[4] if m[3] is True else 'Alt + ' + m[4]
data.append({'name': m[0], 'id': m[1], 'key': m[4],
'key_label': key_label, 'alt': 1 if m[2] else 0,
'control': 1 if m[3] else 0, 'key_code': m[5]})
return data
def set_macros():
"""
This method is used to update the user defined macros.
"""
data = request.form if request.form else json.loads(
request.data, encoding='utf-8'
)
if 'changed' not in data:
return make_json_response(
success=1,
info=gettext('Nothing to update.')
)
for m in data['changed']:
if m['id']:
macro = UserMacros.query.filter_by(
uid=current_user.id,
mid=m['id']).first()
if macro:
status, msg = update_macro(m, macro)
else:
status, msg = create_macro(m)
if not status:
return make_json_response(
status=410, success=0, errormsg=msg
)
return ajax_response(status=200)
def create_macro(macro):
"""
This method is used to create the user defined macros.
:param macro: macro
"""
required_args = [
'name',
'sql'
]
for arg in required_args:
if arg not in macro:
return False, gettext(
"Could not find the required parameter ({}).").format(arg)
try:
new_macro = UserMacros(
uid=current_user.id,
mid=macro['id'],
name=macro['name'],
sql=macro['sql']
)
db.session.add(new_macro)
db.session.commit()
except Exception as e:
db.session.rollback()
return False, str(e)
return True, None
def update_macro(data, macro):
"""
This method is used to clear/update the user defined macros.
:param data: updated macro data
:param macro: macro
"""
name = getattr(data, 'name', None)
sql = getattr(data, 'sql', None)
if name or sql and macro.sql and name is None:
return False, gettext(
"Could not find the required parameter (name).")
elif name or sql and macro.name and sql is None:
return False, gettext(
"Could not find the required parameter (sql).")
try:
if name or sql:
if name:
macro.name = name
if sql:
macro.sql = sql
else:
db.session.delete(macro)
db.session.commit()
except Exception as e:
db.session.rollback()
return False, str(e)
return True, None