diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst
index 1cb23cf..b1a9551 100644
--- a/docs/en_US/editgrid.rst
+++ b/docs/en_US/editgrid.rst
@@ -95,6 +95,24 @@ To delete a row, press the *Delete* toolbar button. A popup will open, asking y
To commit the changes to the server, select the *Save* toolbar button. Modifications to a row are written to the server automatically when you select a different row.
+**Sort/Filter options dialog**
+You can access *Sort/Filter options dialog* by clicking on Filter button, This dialog provides information about the sql filter and data sorting in the edit grid window:
+.. image:: images/editgrid_filter_dialog.png
+ :alt: Edit grid filter dialog window
+
+* Use *SQL filter* to provide where clause conditions
+* Use *Data sorting* to sort the data in the output grid
+
+To add new column(s) in data sorting grid, click on the [+] icon.
+
+* Use the drop-down *Column* to select the column you want to sort.
+* Use the drop-down *Order* to select the sort order for the column.
+
+To discard a data sorting, and delete the row from the grid, click the trash icon.
+
+* Click the *Help* button (?) to access online help.
+* Click the *Ok* button to save work.
+* Click the *Close* button to discard current changes and close the dialog.
diff --git a/docs/en_US/images/editgrid_filter_dialog.png b/docs/en_US/images/editgrid_filter_dialog.png
new file mode 100644
index 0000000..046d9a1
Binary files /dev/null and b/docs/en_US/images/editgrid_filter_dialog.png differ
diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog.js b/web/pgadmin/static/js/sqleditor/filter_dialog.js
new file mode 100644
index 0000000..0ba9e35
--- /dev/null
+++ b/web/pgadmin/static/js/sqleditor/filter_dialog.js
@@ -0,0 +1,243 @@
+define([
+ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string',
+ 'pgadmin.alertifyjs', 'sources/pgadmin', 'backbone',
+ 'pgadmin.backgrid', 'pgadmin.backform', 'axios',
+ 'sources/sqleditor/query_tool_actions',
+ 'sources/sqleditor/filter_dialog_model',
+ //'pgadmin.browser.node.ui',
+], function(
+ gettext, url_for, $, _, S, Alertify, pgAdmin, Backbone,
+ Backgrid, Backform, axios, queryToolActions, filterDialogModel
+) {
+
+ let FilterDialog = {
+ 'dialog': function(handler) {
+ let title = gettext('Sort/Filter options');
+ axios.get(
+ url_for('sqleditor.get_filter_data', {
+ 'trans_id': handler.transId,
+ }),
+ { headers: {'Cache-Control' : 'no-cache'} }
+ ).then(function (res) {
+ let response = res.data.data.result;
+
+ // Check the alertify dialog already loaded then delete it to clear
+ // the cache
+ if (Alertify.filterDialog) {
+ delete Alertify.filterDialog;
+ }
+
+ // Create Dialog
+ Alertify.dialog('filterDialog', function factory() {
+ let $container = $('
');
+ return {
+ main: function() {
+ this.set('title', gettext('Sort/Filter options'));
+ },
+ build: function() {
+ this.elements.content.appendChild($container.get(0));
+ Alertify.pgDialogBuild.apply(this);
+ },
+ setup: function() {
+ return {
+ buttons: [{
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Help'),
+ url: url_for('help.static', {
+ 'filename': 'editgrid.html',
+ }),
+ },
+ }, {
+ text: gettext('Ok'),
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'ok',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: title,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ autoReset: false,
+ },
+ };
+ },
+ hooks: {
+ // triggered when the dialog is closed
+ onclose: function() {
+ if (this.view) {
+ this.filterCollectionModel.stopSession();
+ this.view.model.stopSession();
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ },
+ prepare: function() {
+ let self = this;
+ $container.html('');
+ // Disable Ok button
+ this.__internal.buttons[1].element.disabled = true;
+
+ // Status bar
+ this.statusBar = $('', {
+ text: '',
+ }).appendTo($container);
+
+ // To show progress on filter Saving/Updating on AJAX
+ this.showFilterProgress = $(
+ '' +
+ '
' +
+ '
' +
+ '
' + gettext('Loading data...') + ' ' +
+ '
').appendTo($container);
+
+ $(
+ self.showFilterProgress[0]
+ ).removeClass('hidden');
+
+ self.filterCollectionModel = filterDialogModel(response);
+
+ let fields = Backform.generateViewSchema(
+ null, self.filterCollectionModel, 'create', null, null, true
+ );
+
+ let view = this.view = new Backform.Dialog({
+ el: '
',
+ model: self.filterCollectionModel,
+ schema: fields,
+ });
+
+ $(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+
+ $container.append(view.render().$el);
+
+ // Enable/disable save button and show/hide statusbar based on session
+ view.listenTo(view.model, 'pgadmin-session:start', function() {
+ view.listenTo(view.model, 'pgadmin-session:invalid', function(msg) {
+ self.statusBar.removeClass('hide');
+ $(self.statusBar.find('.alert-text')).html(msg);
+ // Disable Okay button
+ self.__internal.buttons[1].element.disabled = true;
+ });
+
+ view.listenTo(view.model, 'pgadmin-session:valid', function() {
+ self.statusBar.addClass('hide');
+ $(self.statusBar.find('.alert-text')).html('');
+ // Enable Okay button
+ self.__internal.buttons[1].element.disabled = false;
+ });
+ });
+
+ view.listenTo(view.model, 'pgadmin-session:stop', function() {
+ view.stopListening(view.model, 'pgadmin-session:invalid');
+ view.stopListening(view.model, 'pgadmin-session:valid');
+ });
+
+ // Starts monitoring changes to model
+ view.model.startNewSession();
+
+ // Set data in collection
+ let viewDataSortingModel = view.model.get('data_sorting');
+ viewDataSortingModel.add(response['data_sorting']);
+
+ // Hide Progress ...
+ $(
+ self.showFilterProgress[0]
+ ).addClass('hidden');
+
+ },
+ // Callback functions when click on the buttons of the Alertify dialogs
+ callback: function(e) {
+ let self = this;
+
+ if (e.button.element.name == 'dialog_help') {
+ e.cancel = true;
+ pgAdmin.Browser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
+ null, null, e.button.element.getAttribute('label'));
+ return;
+ } else if (e.button['data-btn-name'] === 'ok') {
+ e.cancel = true; // Do not close dialog
+
+ let filterCollectionModel = this.filterCollectionModel.toJSON();
+
+ // Show Progress ...
+ $(
+ self.showFilterProgress[0]
+ ).removeClass('hidden');
+
+ axios.put(
+ url_for('sqleditor.set_filter_data', {
+ 'trans_id': handler.transId,
+ }),
+ filterCollectionModel
+ ).then(function () {
+ // Hide Progress ...
+ $(
+ self.showFilterProgress[0]
+ ).addClass('hidden');
+ setTimeout(
+ function() {
+ self.close(); // Close the dialog now
+ Alertify.success(gettext('Filter updated successfully'));
+ queryToolActions.executeQuery(handler);
+ }, 10
+ );
+
+ }).catch(function (error) {
+ // Hide Progress ...
+ $(
+ self.showFilterProgress[0]
+ ).addClass('hidden');
+ handler.onExecuteHTTPError(error);
+
+ setTimeout(
+ function() {
+ Alertify.error(error);
+ }, 10
+ );
+ });
+ } else {
+ self.close();
+ }
+ },
+ };
+ });
+
+ Alertify.filterDialog(title).resizeTo('65%', '60%');
+ });
+ },
+ };
+ return FilterDialog;
+});
diff --git a/web/pgadmin/static/js/sqleditor/filter_dialog_model.js b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js
new file mode 100644
index 0000000..c3146a4
--- /dev/null
+++ b/web/pgadmin/static/js/sqleditor/filter_dialog_model.js
@@ -0,0 +1,133 @@
+define([
+ 'sources/gettext', 'underscore', 'sources/pgadmin',
+ 'pgadmin.backform', 'pgadmin.backgrid',
+], function(
+ gettext, _, pgAdmin, Backform, Backgrid
+) {
+
+ let initModel = function(response) {
+
+ let order_mapping = {
+ 'asc': gettext('ASC'),
+ 'desc': gettext('DESC'),
+ };
+
+ let DataSortingModel = pgAdmin.Browser.DataModel.extend({
+ idAttribute: 'name',
+ defaults: {
+ name: undefined,
+ order: 'asc',
+ },
+ schema: [{
+ id: 'name',
+ name: 'name',
+ label: gettext('Column'),
+ cell: 'select2',
+ editable: true,
+ cellHeaderClasses: 'width_percent_60',
+ headerCell: Backgrid.Extension.CustomHeaderCell,
+ disabled: false,
+ control: 'select2',
+ select2: {
+ allowClear: false,
+ },
+ options: function() {
+ return _.map(response.column_list, (obj) => {
+ return {
+ value: obj,
+ label: obj,
+ };
+ });
+ },
+ },
+ {
+ id: 'order',
+ name: 'order',
+ label: gettext('Order'),
+ control: 'select2',
+ cell: 'select2',
+ cellHeaderClasses: 'width_percent_40',
+ headerCell: Backgrid.Extension.CustomHeaderCell,
+ editable: true,
+ deps: ['type'],
+ select2: {
+ allowClear: false,
+ },
+ options: function() {
+ return _.map(order_mapping, (val, key) => {
+ return {
+ value: key,
+ label: val,
+ };
+ });
+ },
+ },
+ ],
+ validate: function() {
+ let msg = null;
+ this.errorModel.clear();
+ if (_.isUndefined(this.get('name')) ||
+ _.isNull(this.get('name')) ||
+ String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
+ msg = gettext('Please select a column.');
+ this.errorModel.set('name', msg);
+ return msg;
+ } else if (_.isUndefined(this.get('order')) ||
+ _.isNull(this.get('order')) ||
+ String(this.get('order')).replace(/^\s+|\s+$/g, '') == '') {
+ msg = gettext('Please select the order.');
+ this.errorModel.set('order', msg);
+ return msg;
+ }
+ return null;
+ },
+ });
+
+ let FilterCollectionModel = pgAdmin.Browser.DataModel.extend({
+ idAttribute: 'sql',
+ defaults: {
+ sql: response.sql || null,
+ },
+ schema: [{
+ id: 'sql',
+ label: gettext('SQL Filter'),
+ cell: 'string',
+ type: 'text', mode: ['create'],
+ control: Backform.SqlFieldControl.extend({
+ render: function() {
+ let obj = Backform.SqlFieldControl.prototype.render.apply(this, arguments);
+ // We need to set focus on editor after the dialog renders
+ setTimeout(() => {
+ obj.sqlCtrl.focus();
+ }, 1000);
+ return obj;
+ },
+ }),
+ extraClasses:['custom_height_css_class'],
+ },{
+ id: 'data_sorting',
+ name: 'data_sorting',
+ label: gettext('Data Sorting'),
+ model: DataSortingModel,
+ editable: true,
+ type: 'collection',
+ mode: ['create'],
+ control: 'unique-col-collection',
+ uniqueCol: ['name'],
+ canAdd: true,
+ canEdit: false,
+ canDelete: true,
+ visible: true,
+ version_compatible: true,
+ }],
+ validate: function() {
+ return null;
+ },
+ });
+
+ let model = new FilterCollectionModel();
+ return model;
+ };
+
+ return initModel;
+});
diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html
index 2f3bb05..29e6b81 100644
--- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html
+++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html
@@ -195,9 +195,9 @@
@@ -341,23 +341,6 @@
-
-
-
Filter
-
-
-
-
-
- {{ _('Cancel') }}
-
-
-
-
- {{ _('Apply') }}
-
-
-
diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py
index f8d7d97..622b7c3 100644
--- a/web/pgadmin/tools/sqleditor/__init__.py
+++ b/web/pgadmin/tools/sqleditor/__init__.py
@@ -40,6 +40,7 @@ from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
RegisterQueryToolPreferences
from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
read_file_generator
+from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
MODULE_NAME = 'sqleditor'
@@ -92,8 +93,6 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.fetch',
'sqleditor.fetch_all',
'sqleditor.save',
- 'sqleditor.get_filter',
- 'sqleditor.apply_filter',
'sqleditor.inclusive_filter',
'sqleditor.exclusive_filter',
'sqleditor.remove_filter',
@@ -106,7 +105,9 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.load_file',
'sqleditor.save_file',
'sqleditor.query_tool_download',
- 'sqleditor.connection_status'
+ 'sqleditor.connection_status',
+ 'sqleditor.get_filter_data',
+ 'sqleditor.set_filter_data'
]
def register_preferences(self):
@@ -782,81 +783,6 @@ def save(trans_id):
}
)
-
-@blueprint.route(
- '/filter/get/',
- methods=["GET"], endpoint='get_filter'
-)
-@login_required
-def get_filter(trans_id):
- """
- This method is used to get the existing filter.
-
- Args:
- trans_id: unique transaction id
- """
-
- # Check the transaction and connection status
- status, error_msg, conn, trans_obj, session_obj = \
- check_transaction_status(trans_id)
-
- if error_msg == gettext('Transaction ID not found in the session.'):
- return make_json_response(success=0, errormsg=error_msg,
- info='DATAGRID_TRANSACTION_REQUIRED',
- status=404)
- if status and conn is not None and \
- trans_obj is not None and session_obj is not None:
-
- res = trans_obj.get_filter()
- else:
- status = False
- res = error_msg
-
- return make_json_response(data={'status': status, 'result': res})
-
-
-@blueprint.route(
- '/filter/apply/',
- methods=["PUT", "POST"], endpoint='apply_filter'
-)
-@login_required
-def apply_filter(trans_id):
- """
- This method is used to apply the filter.
-
- Args:
- trans_id: unique transaction id
- """
- if request.data:
- filter_sql = json.loads(request.data, encoding='utf-8')
- else:
- filter_sql = request.args or request.form
-
- # Check the transaction and connection status
- status, error_msg, conn, trans_obj, session_obj = \
- check_transaction_status(trans_id)
-
- if error_msg == gettext('Transaction ID not found in the session.'):
- return make_json_response(success=0, errormsg=error_msg,
- info='DATAGRID_TRANSACTION_REQUIRED',
- status=404)
-
- if status and conn is not None and \
- trans_obj is not None and session_obj is not None:
-
- status, res = trans_obj.set_filter(filter_sql)
-
- # As we changed the transaction object we need to
- # restore it and update the session variable.
- session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
- update_session_grid_transaction(trans_id, session_obj)
- else:
- status = False
- res = error_msg
-
- return make_json_response(data={'status': status, 'result': res})
-
-
@blueprint.route(
'/filter/inclusive/',
methods=["PUT", "POST"], endpoint='inclusive_filter'
@@ -1561,3 +1487,37 @@ def query_tool_status(trans_id):
return internal_server_error(
errormsg=gettext("Transaction status check failed.")
)
+
+
+@blueprint.route(
+ '/filter_dialog/',
+ methods=["GET"], endpoint='get_filter_data'
+)
+@login_required
+def get_filter_data(trans_id):
+ """
+ This method is used to get all the columns for data sorting dialog.
+
+ Args:
+ trans_id: unique transaction id
+ """
+ return FilterDialog.get(*check_transaction_status(trans_id))
+
+
+@blueprint.route(
+ '/filter_dialog/',
+ methods=["PUT"], endpoint='set_filter_data'
+)
+@login_required
+def set_filter_data(trans_id):
+ """
+ This method is used to update the columns for data sorting dialog.
+
+ Args:
+ trans_id: unique transaction id
+ """
+ return FilterDialog.save(
+ *check_transaction_status(trans_id),
+ request=request,
+ trans_id=trans_id
+ )
diff --git a/web/pgadmin/tools/sqleditor/command.py b/web/pgadmin/tools/sqleditor/command.py
index 7ec03c5..fbe37df 100644
--- a/web/pgadmin/tools/sqleditor/command.py
+++ b/web/pgadmin/tools/sqleditor/command.py
@@ -141,6 +141,10 @@ class SQLFilter(object):
- This method removes the filter applied.
* validate_filter(row_filter)
- This method validates the given filter.
+ * get_data_sorting()
+ - This method returns columns for data sorting
+ * set_data_sorting()
+ - This method saves columns for data sorting
"""
def __init__(self, **kwargs):
@@ -160,8 +164,8 @@ class SQLFilter(object):
self.sid = kwargs['sid']
self.did = kwargs['did']
self.obj_id = kwargs['obj_id']
- self.__row_filter = kwargs['sql_filter'] if 'sql_filter' in kwargs \
- else None
+ self.__row_filter = kwargs.get('sql_filter', None)
+ self.__dara_sorting = kwargs.get('data_sorting', None)
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid)
conn = manager.connection(did=self.did)
@@ -210,20 +214,41 @@ class SQLFilter(object):
return status, msg
+ def get_data_sorting(self):
+ """
+ This function returns the filter.
+ """
+ if self.__dara_sorting and len(self.__dara_sorting) > 0:
+ return self.__dara_sorting
+ return None
+
+ def set_data_sorting(self, data_filter):
+ """
+ This function validates the filter and set the
+ given filter to member variable.
+ """
+ self.__dara_sorting = data_filter['data_sorting']
+
def is_filter_applied(self):
"""
This function returns True if filter is applied else False.
"""
+ is_filter_applied = True
if self.__row_filter is None or self.__row_filter == '':
- return False
+ is_filter_applied = False
- return True
+ if not is_filter_applied:
+ if self.__dara_sorting and len(self.__dara_sorting) > 0:
+ is_filter_applied = True
+
+ return is_filter_applied
def remove_filter(self):
"""
This function remove the filter by setting value to None.
"""
self.__row_filter = None
+ self.__dara_sorting = None
def append_filter(self, row_filter):
"""
@@ -325,13 +350,58 @@ class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker):
self.cmd_type = kwargs['cmd_type'] if 'cmd_type' in kwargs else None
self.limit = -1
- if self.cmd_type == VIEW_FIRST_100_ROWS or \
- self.cmd_type == VIEW_LAST_100_ROWS:
+ if self.cmd_type in (VIEW_FIRST_100_ROWS, VIEW_LAST_100_ROWS):
self.limit = 100
def get_primary_keys(self, *args, **kwargs):
return None, None
+ def get_all_columns_with_order(self, default_conn):
+ """
+ Responsible for fetching columns from given object
+
+ Args:
+ default_conn: Connection object
+
+ Returns:
+ all_sorted_columns: Columns which are already sorted which will
+ be used to populate the Grid in the dialog
+ all_columns: List of all the column for given object which will
+ be used to fill columns options
+ """
+ driver = get_driver(PG_DEFAULT_DRIVER)
+ if default_conn is None:
+ manager = driver.connection_manager(self.sid)
+ conn = manager.connection(did=self.did, conn_id=self.conn_id)
+ else:
+ conn = default_conn
+
+ all_sorted_columns = []
+ data_sorting = self.get_data_sorting()
+ all_columns = []
+ if conn.connected():
+ # Fetch the rest of the column names
+ query = render_template(
+ "/".join([self.sql_path, 'get_columns.sql']),
+ obj_id=self.obj_id
+ )
+ status, result = conn.execute_dict(query)
+ if not status:
+ raise Exception(result)
+
+ for row in result['rows']:
+ all_columns.append(row['attname'])
+ else:
+ raise Exception(
+ gettext('Not connected to server or connection with the '
+ 'server has been closed.')
+ )
+ # If user has custom data sorting then pass as it as it is
+ if data_sorting and len(data_sorting) > 0:
+ all_sorted_columns = data_sorting
+
+ return all_sorted_columns, all_columns
+
def save(self, changed_data, default_conn=None):
return forbidden(
errmsg=gettext("Data cannot be saved for the current object.")
@@ -351,6 +421,17 @@ class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker):
"""
self.limit = limit
+ def get_pk_order(self):
+ """
+ This function gets the order required for primary keys
+ """
+ if self.cmd_type in (VIEW_FIRST_100_ROWS, VIEW_ALL_ROWS):
+ return 'asc'
+ elif self.cmd_type == VIEW_LAST_100_ROWS:
+ return 'desc'
+ else:
+ return None
+
class TableCommand(GridCommand):
"""
@@ -385,6 +466,7 @@ class TableCommand(GridCommand):
has_oids = self.has_oids(default_conn)
sql_filter = self.get_filter()
+ data_sorting = self.get_data_sorting()
if sql_filter is None:
sql = render_template(
@@ -392,7 +474,8 @@ class TableCommand(GridCommand):
object_name=self.object_name,
nsp_name=self.nsp_name, pk_names=pk_names,
cmd_type=self.cmd_type, limit=self.limit,
- primary_keys=primary_keys, has_oids=has_oids
+ primary_keys=primary_keys, has_oids=has_oids,
+ data_sorting=data_sorting
)
else:
sql = render_template(
@@ -401,7 +484,7 @@ class TableCommand(GridCommand):
nsp_name=self.nsp_name, pk_names=pk_names,
cmd_type=self.cmd_type, sql_filter=sql_filter,
limit=self.limit, primary_keys=primary_keys,
- has_oids=has_oids
+ has_oids=has_oids, data_sorting=data_sorting
)
return sql
@@ -447,6 +530,73 @@ class TableCommand(GridCommand):
return pk_names, primary_keys
+ def get_all_columns_with_order(self, default_conn=None):
+ """
+ It is overridden method specially for Table because we all have to
+ fetch primary keys and rest of the columns both.
+
+ Args:
+ default_conn: Connection object
+
+ Returns:
+ all_sorted_columns: Sorted columns for the Grid
+ all_columns: List of columns for the select2 options
+ """
+ driver = get_driver(PG_DEFAULT_DRIVER)
+ if default_conn is None:
+ manager = driver.connection_manager(self.sid)
+ conn = manager.connection(did=self.did, conn_id=self.conn_id)
+ else:
+ conn = default_conn
+
+ all_sorted_columns = []
+ data_sorting = self.get_data_sorting()
+ all_columns = []
+ if conn.connected():
+
+ # Fetch the primary key column names
+ query = render_template(
+ "/".join([self.sql_path, 'primary_keys.sql']),
+ obj_id=self.obj_id
+ )
+
+ status, result = conn.execute_dict(query)
+ if not status:
+ raise Exception(result)
+
+ for row in result['rows']:
+ all_columns.append(row['attname'])
+ all_sorted_columns.append(
+ {
+ 'name': row['attname'],
+ 'order': self.get_pk_order()
+ }
+ )
+
+ # Fetch the rest of the column names
+ query = render_template(
+ "/".join([self.sql_path, 'get_columns.sql']),
+ obj_id=self.obj_id
+ )
+ status, result = conn.execute_dict(query)
+ if not status:
+ raise Exception(result)
+
+ for row in result['rows']:
+ # Only append if not already present in the list
+ if row['attname'] not in all_columns:
+ all_columns.append(row['attname'])
+ else:
+ raise Exception(
+ gettext('Not connected to server or connection with the '
+ 'server has been closed.')
+ )
+ # If user has custom data sorting then pass as it as it is
+ if data_sorting and len(data_sorting) > 0:
+ all_sorted_columns = data_sorting
+
+ return all_sorted_columns, all_columns
+
def can_edit(self):
return True
@@ -771,20 +921,22 @@ class ViewCommand(GridCommand):
to fetch the data for the specified view
"""
sql_filter = self.get_filter()
+ data_sorting = self.get_data_sorting()
if sql_filter is None:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- limit=self.limit
+ limit=self.limit, data_sorting=data_sorting
)
else:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- sql_filter=sql_filter, limit=self.limit
+ sql_filter=sql_filter, limit=self.limit,
+ data_sorting=data_sorting
)
return sql
@@ -832,20 +984,22 @@ class ForeignTableCommand(GridCommand):
to fetch the data for the specified foreign table
"""
sql_filter = self.get_filter()
+ data_sorting = self.get_data_sorting()
if sql_filter is None:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- limit=self.limit
+ limit=self.limit, data_sorting=data_sorting
)
else:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- sql_filter=sql_filter, limit=self.limit
+ sql_filter=sql_filter, limit=self.limit,
+ data_sorting=data_sorting
)
return sql
@@ -883,20 +1037,22 @@ class CatalogCommand(GridCommand):
to fetch the data for the specified catalog object
"""
sql_filter = self.get_filter()
+ data_sorting = self.get_data_sorting()
if sql_filter is None:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- limit=self.limit
+ limit=self.limit, data_sorting=data_sorting
)
else:
sql = render_template(
"/".join([self.sql_path, 'objectquery.sql']),
object_name=self.object_name,
nsp_name=self.nsp_name, cmd_type=self.cmd_type,
- sql_filter=sql_filter, limit=self.limit
+ sql_filter=sql_filter, limit=self.limit,
+ data_sorting=data_sorting
)
return sql
@@ -929,6 +1085,9 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker):
def get_sql(self, default_conn=None):
return None
+ def get_all_columns_with_order(self, default_conn=None):
+ return None
+
def can_edit(self):
return False
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 46588dc..c54590d 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -602,3 +602,26 @@ input.editor-checkbox:focus {
font-size: 13px;
line-height: 3em;
}
+
+/* For Filter status bar */
+.data_sorting_dialog .pg-prop-status-bar {
+ position: absolute;
+ bottom: 37px;
+ z-index: 5;
+}
+
+.data_sorting_dialog .CodeMirror-gutter-wrapper {
+ left: -30px !important;
+}
+
+.data_sorting_dialog .CodeMirror-gutters {
+ left: 0px !important;
+}
+
+.data_sorting_dialog .custom_height_css_class {
+ height: 100px;
+}
+
+.data_sorting_dialog .data_sorting {
+ padding: 10px 0px;
+}
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 60dacbb..f8cb05a 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -14,6 +14,7 @@ define('tools.querytool', [
'sources/sqleditor_utils',
'sources/sqleditor/execute_query',
'sources/sqleditor/query_tool_http_error_handler',
+ 'sources/sqleditor/filter_dialog',
'sources/history/index.js',
'sources/../jsx/history/query_history',
'react', 'react-dom',
@@ -33,7 +34,7 @@ define('tools.querytool', [
], function(
babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror,
pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
- XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler,
+ XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
HistoryBundle, queryHistory, React, ReactDOM,
keyboardShortcuts, queryToolActions, Datagrid, modifyAnimation,
calculateQueryRunTime, callRenderAfterPoll) {
@@ -112,8 +113,7 @@ define('tools.querytool', [
// This function is used to render the template.
render: function() {
- var self = this,
- filter = self.$el.find('#sql_filter');
+ var self = this;
$('.editor-title').text(_.unescape(self.editor_title));
self.checkConnectionStatus();
@@ -121,31 +121,6 @@ define('tools.querytool', [
// Fetch and assign the shortcuts to current instance
self.keyboardShortcutConfig = queryToolActions.getKeyboardShortcuts(self);
- self.filter_obj = CodeMirror.fromTextArea(filter.get(0), {
- tabindex: '0',
- lineNumbers: true,
- mode: self.handler.server_type === 'gpdb' ? 'text/x-gpsql' : 'text/x-pgsql',
- foldOptions: {
- widget: '\u2026',
- },
- foldGutter: {
- rangeFinder: CodeMirror.fold.combine(
- CodeMirror.pgadminBeginRangeFinder,
- CodeMirror.pgadminIfRangeFinder,
- CodeMirror.pgadminLoopRangeFinder,
- CodeMirror.pgadminCaseRangeFinder
- ),
- },
- gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
- extraKeys: pgBrowser.editor_shortcut_keys,
- indentWithTabs: pgAdmin.Browser.editor_options.indent_with_tabs,
- indentUnit: pgAdmin.Browser.editor_options.tabSize,
- tabSize: pgAdmin.Browser.editor_options.tabSize,
- lineWrapping: pgAdmin.Browser.editor_options.wrapCode,
- autoCloseBrackets: pgAdmin.Browser.editor_options.insert_pair_brackets,
- matchBrackets: pgAdmin.Browser.editor_options.brace_matching,
- });
-
// Updates connection status flag
self.gain_focus = function() {
setTimeout(function() {
@@ -2141,11 +2116,11 @@ define('tools.querytool', [
if (self.can_filter && res.data.filter_applied) {
$('#btn-filter').removeClass('btn-default');
$('#btn-filter-dropdown').removeClass('btn-default');
- $('#btn-filter').addClass('btn-warning');
- $('#btn-filter-dropdown').addClass('btn-warning');
+ $('#btn-filter').addClass('btn-primary');
+ $('#btn-filter-dropdown').addClass('btn-primary');
} else {
- $('#btn-filter').removeClass('btn-warning');
- $('#btn-filter-dropdown').removeClass('btn-warning');
+ $('#btn-filter').removeClass('btn-primary');
+ $('#btn-filter-dropdown').removeClass('btn-primary');
$('#btn-filter').addClass('btn-default');
$('#btn-filter-dropdown').addClass('btn-default');
}
@@ -3044,50 +3019,8 @@ define('tools.querytool', [
// This function will show the filter in the text area.
_show_filter: function() {
- var self = this;
-
- self.trigger(
- 'pgadmin-sqleditor:loading-icon:show',
- gettext('Loading the existing filter options...')
- );
- $.ajax({
- url: url_for('sqleditor.get_filter', {
- 'trans_id': self.transId,
- }),
- method: 'GET',
- success: function(res) {
- self.trigger('pgadmin-sqleditor:loading-icon:hide');
- if (res.data.status) {
- $('#filter').removeClass('hidden');
- $('#editor-panel').addClass('sql-editor-busy-fetching');
- self.gridView.filter_obj.refresh();
-
- if (res.data.result == null)
- self.gridView.filter_obj.setValue('');
- else
- self.gridView.filter_obj.setValue(res.data.result);
- // Set focus on filter area
- self.gridView.filter_obj.focus();
- } else {
- setTimeout(
- function() {
- alertify.alert(gettext('Get Filter Error'), res.data.result);
- }, 10
- );
- }
- },
- error: function(e) {
- self.trigger('pgadmin-sqleditor:loading-icon:hide');
- let msg = httpErrorHandler.handleQueryToolAjaxError(
- pgAdmin, self, e, '_show_filter', [], true
- );
- setTimeout(
- function() {
- alertify.alert(gettext('Get Filter Error'), msg);
- }, 10
- );
- },
- });
+ let self = this;
+ FilterHandler.dialog(self);
},
// This function will include the filter by selection.
diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql
new file mode 100644
index 0000000..610747d
--- /dev/null
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql
@@ -0,0 +1,9 @@
+{# ============= Fetch the columns ============= #}
+{% if obj_id %}
+SELECT at.attname, ty.typname
+ FROM pg_attribute at
+ LEFT JOIN pg_type ty ON (ty.oid = at.atttypid)
+WHERE attrelid={{obj_id}}::oid
+ AND at.attnum > 0
+ AND at.attisdropped = FALSE
+{% endif %}
diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql
index 1cb60d9..add1658 100644
--- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/objectquery.sql
@@ -3,7 +3,11 @@ SELECT {% if has_oids %}oid, {% endif %}* FROM {{ conn|qtIdent(nsp_name, object_
{% if sql_filter %}
WHERE {{ sql_filter }}
{% endif %}
-{% if primary_keys %}
+{% if data_sorting and data_sorting|length > 0 %}
+ORDER BY {% for obj in data_sorting %}
+{{ conn|qtIdent(obj.name) }} {{ obj.order|upper }}{% if not loop.last %}, {% else %} {% endif %}
+{% endfor %}
+{% elif primary_keys %}
ORDER BY {% for p in primary_keys %}{{conn|qtIdent(p)}}{% if cmd_type == 1 or cmd_type == 3 %} ASC{% elif cmd_type == 2 %} DESC{% endif %}
{% if not loop.last %}, {% else %} {% endif %}{% endfor %}
{% endif %}
diff --git a/web/pgadmin/tools/sqleditor/utils/filter_dialog.py b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py
new file mode 100644
index 0000000..368dbb4
--- /dev/null
+++ b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py
@@ -0,0 +1,96 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Code to handle data sorting in view data mode."""
+import pickle
+import simplejson as json
+from flask_babelex import gettext
+from pgadmin.utils.ajax import make_json_response, internal_server_error
+from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
+ update_session_grid_transaction
+
+
+class FilterDialog(object):
+ @staticmethod
+ def get(*args):
+ """To fetch the current sorted columns"""
+ status, error_msg, conn, trans_obj, session_obj = args
+ if error_msg == gettext('Transaction ID not found in the session.'):
+ return make_json_response(
+ success=0,
+ errormsg=error_msg,
+ info='DATAGRID_TRANSACTION_REQUIRED',
+ status=404
+ )
+ column_list = []
+ if status and conn is not None and \
+ trans_obj is not None and session_obj is not None:
+ msg = gettext('Success')
+ columns, column_list = trans_obj.get_all_columns_with_order(conn)
+ sql = trans_obj.get_filter()
+ else:
+ status = False
+ msg = error_msg
+ columns = None
+ sql = None
+
+
+ return make_json_response(
+ data={
+ 'status': status,
+ 'msg': msg,
+ 'result': {
+ 'data_sorting': columns,
+ 'column_list': column_list,
+ 'sql': sql
+ }
+ }
+ )
+
+ @staticmethod
+ def save(*args, **kwargs):
+ """To save the sorted columns"""
+ # Check the transaction and connection status
+ status, error_msg, conn, trans_obj, session_obj = args
+ trans_id = kwargs['trans_id']
+ request = kwargs['request']
+
+ if request.data:
+ data = json.loads(request.data, encoding='utf-8')
+ else:
+ data = request.args or request.form
+
+ if error_msg == gettext('Transaction ID not found in the session.'):
+ return make_json_response(
+ success=0,
+ errormsg=error_msg,
+ info='DATAGRID_TRANSACTION_REQUIRED',
+ status=404
+ )
+
+ if status and conn is not None and \
+ trans_obj is not None and session_obj is not None:
+ trans_obj.set_data_sorting(data)
+ trans_obj.set_filter(data.get('sql'))
+ # As we changed the transaction object we need to
+ # restore it and update the session variable.
+ session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
+ update_session_grid_transaction(trans_id, session_obj)
+ res = gettext('Data sorting object updated successfully')
+ else:
+ return internal_server_error(
+ errormsg=gettext('Failed to update the data on server.')
+ )
+
+ return make_json_response(
+ data={
+ 'status': status,
+ 'result': res
+ }
+ )
diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py b/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py
new file mode 100644
index 0000000..9747978
--- /dev/null
+++ b/web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py
@@ -0,0 +1,103 @@
+#######################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2018, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Apply Explain plan wrapper to sql object."""
+from pgadmin.utils.ajax import make_json_response, internal_server_error
+from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
+from pgadmin.utils.route import BaseTestGenerator
+
+TX_ID_ERROR_MSG = 'Transaction ID not found in the session.'
+FAILED_TX_MSG = 'Failed to update the data on server.'
+
+
+class MockRequest(object):
+ "To mock request object"
+ def __init__(self):
+ self.data = None
+ self.args = "Test data",
+
+
+class StartRunningDataSortingTest(BaseTestGenerator):
+ """
+ Check that the DataSorting methods works as
+ intended
+ """
+ scenarios = [
+ ('When we do not find Transaction ID in session in get', dict(
+ input_parameters=(None, TX_ID_ERROR_MSG, None, None, None),
+ expected_return_response={
+ 'success': 0,
+ 'errormsg': TX_ID_ERROR_MSG,
+ 'info': 'DATAGRID_TRANSACTION_REQUIRED',
+ 'status': 404
+ },
+ type='get'
+ )),
+ ('When we pass all the values as None in get', dict(
+ input_parameters=(None, None, None, None, None),
+ expected_return_response={
+ 'data': {
+ 'status': False,
+ 'msg': None,
+ 'result': {
+ 'data_sorting': None,
+ 'column_list': []
+ }
+ }
+ },
+ type='get'
+ )),
+
+ ('When we do not find Transaction ID in session in save', dict(
+ input_arg_parameters=(None, TX_ID_ERROR_MSG, None, None, None),
+ input_kwarg_parameters={
+ 'trans_id': None,
+ 'request': MockRequest()
+ },
+ expected_return_response={
+ 'success': 0,
+ 'errormsg': TX_ID_ERROR_MSG,
+ 'info': 'DATAGRID_TRANSACTION_REQUIRED',
+ 'status': 404
+ },
+ type='save'
+ )),
+
+ ('When we pass all the values as None in save', dict(
+ input_arg_parameters=(None, None, None, None, None),
+ input_kwarg_parameters={
+ 'trans_id': None,
+ 'request': MockRequest()
+ },
+ expected_return_response={
+ 'status': 500,
+ 'success': 0,
+ 'errormsg': FAILED_TX_MSG
+
+ },
+ type='save'
+ ))
+ ]
+
+ def runTest(self):
+ expected_response = make_json_response(
+ **self.expected_return_response
+ )
+ if self.type == 'get':
+ result = FilterDialog.get(*self.input_parameters)
+ self.assertEquals(
+ result.status_code, expected_response.status_code
+ )
+ else:
+ result = FilterDialog.save(
+ *self.input_arg_parameters, **self.input_kwarg_parameters
+ )
+ self.assertEquals(
+ result.status_code, expected_response.status_code
+ )
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
new file mode 100644
index 0000000..e13fa09
--- /dev/null
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -0,0 +1,31 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+import filterDialog from 'sources/sqleditor/filter_dialog';
+import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
+
+describe('filterDialog', () => {
+ let sqlEditorController;
+ sqlEditorController = jasmine.createSpy('sqlEditorController')
+ describe('filterDialog', () => {
+ describe('when using filter dialog', () => {
+ beforeEach(() => {
+ spyOn(filterDialog, 'dialog');
+ });
+
+ it("it should be defined as function", function() {
+ expect(filterDialog.dialog).toBeDefined();
+ });
+
+ it('it should call without proper handler', () => {
+ expect(filterDialog.dialog).not.toHaveBeenCalledWith({});
+ });
+
+ });
+ });
+});