diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst index 1cb23cf..b8b58e5 100644 --- a/docs/en_US/editgrid.rst +++ b/docs/en_US/editgrid.rst @@ -46,16 +46,19 @@ Hover over an icon to display a tooltip that describes the icon's functionality. | | | | | | Use options on the *Filter* menu to quick-sort or quick-filter the data set: | | | | | | -| | * Filter: This option opens a dialog that allows you to define a filter. A filter is a | | +| | * SQL Filter: This option opens a dialog that allows you to define a filter. A filter is a | | | | condition that is supplied to an arbitrary WHERE clause that restricts the result set. | | | | | | -| | * Remove Filter: This option removes all selection / exclusion filter conditions. | | +| | * Data Sorting: This option allows user to sort their data as per respective columns order | | +| | using data sorting dialog. | | | | | | | | * By Selection: This option refreshes the data set and displays only those rows whose | | | | column value matches the value in the cell currently selected. | | | | | | | | * Exclude Selection: This option refreshes the data set and excludes those rows whose | | | | column value matches the value in the cell currently selected. | | +| | | | +| | * Remove Filter: This option removes any existing filter conditions. | | +----------------------+---------------------------------------------------------------------------------------------------+-------------+ | *No limit* | Use the *No limit* drop-down listbox to specify how many rows to display in the output panel. | | | | Select from: *No limit* (the default), *1000 rows*, *500 rows*, or *100 rows*. | | @@ -95,6 +98,20 @@ 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. +**The Data Sorting dialog** + +.. image:: images/editgrid_data_sorting_dialog.png + :alt: Edit grid data sorting dialog + +Provide information about the data sorting in the edit grid window: + +To add new column for data sorting, 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 newly added or an existing row(s) 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. \ No newline at end of file diff --git a/docs/en_US/images/editgrid_data_sorting_dialog.png b/docs/en_US/images/editgrid_data_sorting_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..4db53dbe0fc6be9b3e2168646b2add7f0f4f8701 GIT binary patch literal 42262 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 { + 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 DataSortingCollectionModel = pgAdmin.Browser.DataModel.extend({ + idAttribute: 'data_sorting', + schema: [{ + id: 'data_sorting', + name: 'data_sorting', + label: gettext('Please select column(s)'), + model: FilterModel, + 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; + }, + }); + + // Check the alertify dialog already loaded then delete it to clear + // the cache + if (Alertify.dataSorting) { + delete Alertify.dataSorting; + } + + // Create Dialog + Alertify.dialog('dataSorting', function factory() { + let $container = $('
'); + return { + main: function() { + this.set('title', gettext('Data Sorting')); + }, + build: function() { + this.elements.content.appendChild($container.get(0)); + Alertify.pgDialogBuild.apply(this); + }, + setup: function() { + return { + buttons: [{ + text: '', + key: 27, + 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'), + key: 27, + 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.dataSortingCollectionModel.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.dataSortingCollectionModel = new DataSortingCollectionModel(); + + let fields = Backform.generateViewSchema( + null, self.dataSortingCollectionModel, 'create', null, null, true + ); + + let view = this.view = new Backform.Dialog({ + el: '
', + model: self.dataSortingCollectionModel, + 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 viewModel = view.model.get('data_sorting'); + viewModel.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) { + 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; + } + let self = this; + + if (e.button['data-btn-name'] === 'ok') { + e.cancel = true; // Do not close dialog + + let dataSortingCollectionModel = this.dataSortingCollectionModel.toJSON(); + + // Show Progress ... + $( + self.showFilterProgress[0] + ).removeClass('hidden'); + + axios.put( + url_for('sqleditor.set_data_sorting', { + 'trans_id': handler.transId, + }), + dataSortingCollectionModel + ).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'); + + setTimeout( + function() { + Alertify.error(error); + }, 10 + ); + }); + } + }, + }; + }); + + Alertify.dataSorting(title).resizeTo('65%', '60%'); + }); + }, + }; + return dataSorting; +}); diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index 2f3bb05..1ad1c9a 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -194,10 +194,11 @@ diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 6f5d5b7..ed609d2 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.data_sorting import DataSorting MODULE_NAME = 'sqleditor' @@ -106,7 +107,9 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.load_file', 'sqleditor.save_file', 'sqleditor.query_tool_download', - 'sqleditor.connection_status' + 'sqleditor.connection_status', + 'sqleditor.get_data_sorting', + 'sqleditor.set_data_sorting' ] def register_preferences(self): @@ -1561,3 +1564,37 @@ def query_tool_status(trans_id): return internal_server_error( errormsg=gettext("Transaction status check failed.") ) + + +@blueprint.route( + '/data_sorting/', + methods=["GET"], endpoint='get_data_sorting' +) +@login_required +def get_data_sorting(trans_id): + """ + This method is used to get all the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return DataSorting.get(*check_transaction_status(trans_id)) + + +@blueprint.route( + '/data_sorting/', + methods=["PUT"], endpoint='set_data_sorting' +) +@login_required +def set_data_sorting(trans_id): + """ + This method is used to update the columns for data sorting dialog. + + Args: + trans_id: unique transaction id + """ + return DataSorting.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 8cc96e0..993b0d9 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..25599a4 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -602,3 +602,10 @@ 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; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 923ccea..e2b23be 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/is_new_transaction_required', + 'sources/sqleditor/data_sorting', 'sources/history/index.js', 'sources/../jsx/history/query_history', 'react', 'react-dom', @@ -30,7 +31,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, transaction, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, transaction, DataSortingHandler, HistoryBundle, queryHistory, React, ReactDOM, keyboardShortcuts, queryToolActions, Datagrid) { /* Return back, this has been called more than once */ @@ -71,6 +72,7 @@ define('tools.querytool', [ 'click #btn-delete-row': 'on_delete', 'click #btn-filter': 'on_show_filter', 'click #btn-filter-menu': 'on_show_filter', + 'click #btn-data-sorting': 'on_data_sorting', 'click #btn-include-filter': 'on_include_filter', 'click #btn-exclude-filter': 'on_exclude_filter', 'click #btn-remove-filter': 'on_remove_filter', @@ -1366,6 +1368,21 @@ define('tools.querytool', [ ); }, + // Callback function for data sorting button click. + on_data_sorting: function(ev) { + var self = this; + + this._stopEventPropogation(ev); + this._closeDropDown(ev); + + // Trigger the data_sorting signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:data_sorting', + self, + self.handler + ); + }, + // Callback function for include filter button click. on_include_filter: function(ev) { var self = this; @@ -2057,6 +2074,7 @@ define('tools.querytool', [ self.on('pgadmin-sqleditor:button:save', self._save, self); self.on('pgadmin-sqleditor:button:deleterow', self._delete, self); self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); + self.on('pgadmin-sqleditor:button:data_sorting', self._data_sorting, self); self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); @@ -3245,6 +3263,12 @@ define('tools.querytool', [ }); }, + // This function will used when user wants custom data sorting + _data_sorting: function() { + let self = this; + DataSortingHandler.dialog(self); + }, + // This function will include the filter by selection. _include_filter: function() { var self = this, 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/data_sorting.py b/web/pgadmin/tools/sqleditor/utils/data_sorting.py new file mode 100644 index 0000000..7a4c406 --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/data_sorting.py @@ -0,0 +1,91 @@ +########################################################################## +# +# 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_babel 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 DataSorting(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) + else: + status = False + msg = error_msg + columns = None + + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data_sorting': columns, + 'column_list': column_list + } + } + ) + + @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: + sorting_data = json.loads(request.data, encoding='utf-8') + else: + sorting_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(sorting_data) + # 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_data_sorting_callbacks.py b/web/pgadmin/tools/sqleditor/utils/tests/test_data_sorting_callbacks.py new file mode 100644 index 0000000..6364cfb --- /dev/null +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_data_sorting_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.data_sorting import DataSorting +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 = DataSorting.get(*self.input_parameters) + self.assertEquals( + result.status_code, expected_response.status_code + ) + else: + result = DataSorting.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/data_sorting_specs.js b/web/regression/javascript/sqleditor/data_sorting_specs.js new file mode 100644 index 0000000..cb19556 --- /dev/null +++ b/web/regression/javascript/sqleditor/data_sorting_specs.js @@ -0,0 +1,30 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// +import dataSorting from 'sources/sqleditor/data_sorting'; + +describe('dataSorting', () => { + let sqlEditorController; + sqlEditorController = jasmine.createSpy('sqlEditorController') + describe('dataSorting', () => { + describe('when data sorting filter dialog', () => { + beforeEach(() => { + spyOn(dataSorting, 'dialog'); + }); + + it("it should be defined as function", function() { + expect(dataSorting.dialog).toBeDefined(); + }); + + it('it should call without proper handler', () => { + expect(dataSorting.dialog).not.toHaveBeenCalledWith({}); + }); + + }); + }); +});