From fa1854bd85502ea9db2716f18d5edf0bd79cacdc Mon Sep 17 00:00:00 2001 From: Murtuza Zabuawala Date: Thu, 5 Apr 2018 16:25:17 +0100 Subject: [PATCH] Allow sorting when viewing/editing data. Fixes #1894 --- docs/en_US/editgrid.rst | 23 ++ docs/en_US/images/editgrid_filter_dialog.png | Bin 0 -> 68460 bytes docs/en_US/release_notes_3_0.rst | 2 +- .../static/js/sqleditor/filter_dialog.js | 243 ++++++++++++++++++ .../js/sqleditor/filter_dialog_model.js | 133 ++++++++++ .../datagrid/templates/datagrid/index.html | 33 +-- web/pgadmin/tools/sqleditor/__init__.py | 115 +++------ web/pgadmin/tools/sqleditor/command.py | 187 +++++++++++++- .../tools/sqleditor/static/css/sqleditor.css | 23 ++ .../tools/sqleditor/static/js/sqleditor.js | 85 +----- .../sqleditor/sql/default/get_columns.sql | 9 + .../sqleditor/sql/default/objectquery.sql | 6 +- .../tools/sqleditor/utils/filter_dialog.py | 95 +++++++ .../tests/test_filter_dialog_callbacks.py | 103 ++++++++ .../sqleditor/filter_dialog_specs.js | 31 +++ 15 files changed, 894 insertions(+), 194 deletions(-) create mode 100644 docs/en_US/images/editgrid_filter_dialog.png create mode 100644 web/pgadmin/static/js/sqleditor/filter_dialog.js create mode 100644 web/pgadmin/static/js/sqleditor/filter_dialog_model.js create mode 100644 web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql create mode 100644 web/pgadmin/tools/sqleditor/utils/filter_dialog.py create mode 100644 web/pgadmin/tools/sqleditor/utils/tests/test_filter_dialog_callbacks.py create mode 100644 web/regression/javascript/sqleditor/filter_dialog_specs.js diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf97..45ef597a3 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -95,6 +95,29 @@ 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 Sort/Filter button. This allows you to specify an SQL Filter to limit the data displayed and data sorting options in the edit grid window: +.. image:: images/editgrid_filter_dialog.png + :alt: Edit grid filter dialog window + +* Use *SQL Filter* to provide SQL filtering criteria. These will be added to the "WHERE" clause of the query used to retrieve the data. For example, you might enter: + +.. code-block:: sql + + id > 25 AND created > '2018-01-01' + +* 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 delete a 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 0000000000000000000000000000000000000000..046d9a124363a621e618efc524d745a8a526ebed GIT binary patch literal 68460 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 BJDmUk literal 0 HcmV?d00001 diff --git a/docs/en_US/release_notes_3_0.rst b/docs/en_US/release_notes_3_0.rst index b41bd37f0..ade1c0d28 100644 --- a/docs/en_US/release_notes_3_0.rst +++ b/docs/en_US/release_notes_3_0.rst @@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release Features ******** -| `Feature #1305 `_ - Enable building of the runtime from the top level Makefile +| `Feature #1894 `_ - Allow sorting when viewing/editing data | `Feature #1978 `_ - Add the ability to enable/disable UI animations | `Feature #2895 `_ - Add keyboard navigation options for the main browser windows | `Feature #2896 `_ - Add keyboard navigation in Query tool module via Tab/Shift-Tab key 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 000000000..0ba9e357a --- /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 = $( + '').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 000000000..c3146a40a --- /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 2f3bb0578..b28f185c3 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -79,7 +79,7 @@
  • - {{ _('Find next') }}{% if client_platform == 'macos' -%} + {{ _('Find Next') }}{% if client_platform == 'macos' -%} {{ _(' (Cmd+G)') }} {% else %} {{ _(' (Ctrl+G)') }}{%- endif %} @@ -87,7 +87,7 @@
  • - {{ _('Find previous') }}{% if client_platform == 'macos' -%} + {{ _('Find Previous') }}{% if client_platform == 'macos' -%} {{ _(' (Cmd+Shift+G)') }} {% else %} {{ _(' (Ctrl+Shift+G)') }}{%- endif %} @@ -95,7 +95,7 @@
  • - {{ _('Persistent find') }} + {{ _('Persistent Find') }}
  • @@ -109,7 +109,7 @@
  • - {{ _('Replace all') }} + {{ _('Replace All') }}
  • @@ -194,10 +194,10 @@ @@ -341,23 +341,6 @@
    - -
    diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index f8d7d97bf..fae527c7d 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): @@ -783,80 +784,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 +1488,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 7ec03c581..fbe37df69 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 46588dcec..c54590d3d 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 60dacbb20..f8cb05af0 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 000000000..610747dfb --- /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 1cb60d913..add1658ec 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 000000000..d7064979f --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/filter_dialog.py @@ -0,0 +1,95 @@ +########################################################################## +# +# 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 000000000..97479782e --- /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 000000000..e13fa0974 --- /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({}); + }); + + }); + }); +});