Allow sorting when viewing/editing data. Fixes #1894

This commit is contained in:
Murtuza Zabuawala 2018-04-05 16:25:17 +01:00 committed by Dave Page
parent 659390493d
commit fa1854bd85
15 changed files with 894 additions and 194 deletions

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -10,7 +10,7 @@ This release contains a number of features and fixes reported since the release
Features
********
| `Feature #1305 <https://redmine.postgresql.org/issues/1305>`_ - Enable building of the runtime from the top level Makefile
| `Feature #1894 <https://redmine.postgresql.org/issues/1894>`_ - Allow sorting when viewing/editing data
| `Feature #1978 <https://redmine.postgresql.org/issues/1978>`_ - Add the ability to enable/disable UI animations
| `Feature #2895 <https://redmine.postgresql.org/issues/2895>`_ - Add keyboard navigation options for the main browser windows
| `Feature #2896 <https://redmine.postgresql.org/issues/2896>`_ - Add keyboard navigation in Query tool module via Tab/Shift-Tab key

View File

@ -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 = $('<div class=\'data_sorting_dialog\'></div>');
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 = $('<div class=\'pg-prop-status-bar pg-el-xs-12 hide\'>' +
' <div class=\'media error-in-footer bg-red-1 border-red-2 font-red-3 text-14\'>' +
' <div class=\'media-body media-middle\'>' +
' <div class=\'alert-icon error-icon\'>' +
' <i class=\'fa fa-exclamation-triangle\' aria-hidden=\'true\'></i>' +
' </div>' +
' <div class=\'alert-text\'>' +
' </div>' +
' </div>' +
' </div>' +
'</div>', {
text: '',
}).appendTo($container);
// To show progress on filter Saving/Updating on AJAX
this.showFilterProgress = $(
'<div id="show_filter_progress" class="wcLoadingIconContainer busy-fetching hidden">' +
'<div class="wcLoadingBackground"></div>' +
'<span class="wcLoadingIcon fa fa-spinner fa-pulse"></span>' +
'<span class="busy-text wcLoadingLabel">' + gettext('Loading data...') + '</span>' +
'</div>').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: '<div></div>',
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;
});

View File

@ -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;
});

View File

@ -79,7 +79,7 @@
</li>
<li>
<a id="btn-find-menu-find-next" href="#" tabindex="0">
<span> {{ _('Find next') }}{% if client_platform == 'macos' -%}
<span> {{ _('Find Next') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+G)') }}
{% else %}
{{ _(' (Ctrl+G)') }}{%- endif %}</span>
@ -87,7 +87,7 @@
</li>
<li>
<a id="btn-find-menu-find-previous" href="#" tabindex="0">
<span> {{ _('Find previous') }}{% if client_platform == 'macos' -%}
<span> {{ _('Find Previous') }}{% if client_platform == 'macos' -%}
{{ _(' (Cmd+Shift+G)') }}
{% else %}
{{ _(' (Ctrl+Shift+G)') }}{%- endif %}</span>
@ -95,7 +95,7 @@
</li>
<li>
<a id="btn-find-menu-find-persistent" href="#" tabindex="0">
<span>{{ _('Persistent find') }}</span>
<span>{{ _('Persistent Find') }}</span>
</a>
</li>
<li class="divider"></li>
@ -109,7 +109,7 @@
</li>
<li>
<a id="btn-find-menu-replace-all" href="#" tabindex="0">
<span>{{ _('Replace all') }}</span>
<span>{{ _('Replace All') }}</span>
</a>
</li>
<li class="divider"></li>
@ -194,10 +194,10 @@
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a id="btn-filter-menu" href="#" tabindex="0">{{ _('Filter') }}</a>
<a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Filter') }}</a>
<a id="btn-include-filter" href="#" tabindex="0">{{ _('By Selection') }}</a>
<a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude Selection') }}</a>
<a id="btn-filter-menu" href="#" tabindex="0">{{ _('Sort/Filter') }}</a>
<a id="btn-include-filter" href="#" tabindex="0">{{ _('Filter by Selection') }}</a>
<a id="btn-exclude-filter" href="#" tabindex="0">{{ _('Exclude by Selection') }}</a>
<a id="btn-remove-filter" href="#" tabindex="0">{{ _('Remove Sort/Filter') }}</a>
</li>
</ul>
</div>
@ -341,23 +341,6 @@
<div class="editor-title"
style="background-color: {% if fgcolor %}{{ bgcolor or '#FFFFFF' }}{% else %}{{ bgcolor or '#2C76B4' }}{% endif %}; color: {{ fgcolor or 'white' }};"></div>
</div>
<div id="filter" class="filter-container hidden">
<div class="filter-title">Filter</div>
<div class="sql-textarea">
<textarea id="sql_filter" rows="5"></textarea>
</div>
<div class="btn-group">
<button id="btn-cancel" type="button" class="btn btn-danger" title="{{ _('Cancel') }}" tabindex="0">
<i class="fa fa-times" aria-hidden="true"></i> {{ _('Cancel') }}
</button>
</div>
<div class="btn-group">
<button id="btn-apply" type="button" class="btn btn-primary" title="{{ _('Apply') }}" tabindex="0">
<i class="fa fa-check" aria-hidden="true"></i> {{ _('Apply') }}
</button>
</div>
</div>
<div id="editor-panel" tabindex="0"></div>
<iframe id="download-csv" style="display:none"></iframe>
</div>

View File

@ -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/<int:trans_id>',
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/<int:trans_id>',
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/<int:trans_id>',
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/<int:trans_id>',
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/<int:trans_id>',
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
)

View File

@ -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

View File

@ -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;
}

View File

@ -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.

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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
}
)

View File

@ -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
)

View File

@ -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({});
});
});
});
});