Re: [GSoC][New Feature] Editable and Read-only Columns
Yosry Muhammad <[email protected]>
| Newsgroups | gmane.comp.db.postgresql.pgadmin.devel |
|---|---|
| Message-ID | <CAFSMqn8CRKP=RrLzdko4n4RqAKHZ_cD2OeNGkhW9ukQe0tcUYA@mail.gmail.com> |
Hi Dave, I am now using the same versions of Python, Chrome and chromedriver. The tests still pass every time. Are you sure you applied the latest patch? I re-created the patch as a sanity check, please find it attached. I tried running the specific test a lot of times, passes every time. The only difference is that I am running Ubuntu, otherwise, everything is the same. On Fri, Aug 23, 2019 at 5:00 PM Dave Page <[email protected]> wrote: > Hi > > I've run it again 4 more times and it fails consistently for me. I'm > running on macOS 10.14.5 with Chrome 76.0.3809.100. > > On Fri, Aug 23, 2019 at 3:17 PM Yosry Muhammad <[email protected]> wrote: > >> Hi Dave, >> >> The tests pass on my device normally. I am using Python 3.6 on Ubuntu. I >> have ran all tests using "make check" and they all succeeded. I also ran >> feature_tests/query_tool_journey_test.py multiple times on its own using >> "cd web && python regression/runtests.py --pkg feature_tests --modules >> query_tool_journey_test" and it passed successfully. Could you try again? >> >> Generally, I have found that StaleElementReferenceException occurs >> intermittently on various feature tests. >> >> On Fri, Aug 23, 2019 at 3:46 PM Dave Page <[email protected]> wrote: >> >>> Thanks. Unfortunately this is failing the tests: >>> >>> ====================================================================== >>> ERROR: runTest >>> (pgadmin.feature_tests.query_tool_journey_test.QueryToolJourneyTest) >>> Tests the path through the query tool >>> ---------------------------------------------------------------------- >>> Traceback (most recent call last): >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_journey_test.py", >>> line 85, in runTest >>> self._test_updatable_resultset() >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_journey_test.py", >>> line 254, in _test_updatable_resultset >>> discard_changes_modal=True) >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_journey_test.py", >>> line 417, in _check_query_results_editable >>> is_editable = self._check_cell_editable(column_index) >>> File >>> "/Users/dpage/git/pgadmin4/web/pgadmin/feature_tests/query_tool_journey_test.py", >>> line 430, in _check_cell_editable >>> cell_value = int(cell_el.text) >>> File >>> "/Users/dpage/.virtualenvs/pgadmin4/lib/python3.7/site-packages/selenium/webdriver/remote/webelement.py", >>> line 76, in text >>> return self._execute(Command.GET_ELEMENT_TEXT)['value'] >>> File >>> "/Users/dpage/.virtualenvs/pgadmin4/lib/python3.7/site-packages/selenium/webdriver/remote/webelement.py", >>> line 628, in _execute >>> return self._parent.execute(command, params) >>> File >>> "/Users/dpage/.virtualenvs/pgadmin4/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", >>> line 312, in execute >>> self.error_handler.check_response(response) >>> File >>> "/Users/dpage/.virtualenvs/pgadmin4/lib/python3.7/site-packages/selenium/webdriver/remote/errorhandler.py", >>> line 242, in check_response >>> raise exception_class(message, screen, stacktrace) >>> selenium.common.exceptions.StaleElementReferenceException: Message: >>> stale element reference: element is not attached to the page document >>> (Session info: chrome=76.0.3809.100) >>> (Driver info: chromedriver=76.0.3809.126 >>> (d80a294506b4c9d18015e755cee48f953ddc3f2f-refs/branch-heads/3809@{#1024}),platform=Mac >>> OS X 10.14.5 x86_64) >>> >>> Without the patch, the tests pass. >>> >>> >>> On Fri, Aug 23, 2019 at 1:10 PM Yosry Muhammad <[email protected]> >>> wrote: >>> >>>> Please find an updated patch attached. >>>> >>>> On Fri, Aug 23, 2019 at 1:22 PM Dave Page <[email protected]> wrote: >>>> >>>>> Except it now doesn't apply as the async connection/rendering patch >>>>> was in the queue right ahead of it :-(. >>>>> >>>>> Can I get a rebased patch please? >>>>> >>>>> On Fri, Aug 23, 2019 at 11:57 AM Aditya Toshniwal < >>>>> [email protected]> wrote: >>>>> >>>>>> Excellent !! Looks good to me. >>>>>> >>>>>> On Fri, Aug 23, 2019 at 2:23 PM Yosry Muhammad <[email protected]> >>>>>> wrote: >>>>>> >>>>>>> I updated the patch for backwards compatibility anyway. >>>>>>> >>>>>>> On Fri, Aug 23, 2019 at 10:45 AM Dave Page <[email protected]> >>>>>>> wrote: >>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Fri, Aug 23, 2019 at 9:43 AM Yosry Muhammad <[email protected]> >>>>>>>> wrote: >>>>>>>> >>>>>>>>> Hi Aditya, >>>>>>>>> >>>>>>>>> On Fri, Aug 23, 2019 at 7:33 AM Aditya Toshniwal < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> Hi Yosry, >>>>>>>>>> >>>>>>>>>> The previously mentioned issues seems to be fixed. Below are few >>>>>>>>>> issues: >>>>>>>>>> 1) On Python 2.7, I get below error when opening query tool: >>>>>>>>>> 2019-08-23 10:49:09,329: ERROR flask.app: Object of type buffer >>>>>>>>>> is not JSON serializable >>>>>>>>>> Traceback (most recent call last): >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/flask/app.py", >>>>>>>>>> line 1813, in full_dispatch_request >>>>>>>>>> rv = self.dispatch_request() >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/flask/app.py", >>>>>>>>>> line 1799, in dispatch_request >>>>>>>>>> return self.view_functions[rule.endpoint](**req.view_args) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/flask_login/utils.py", >>>>>>>>>> line 261, in decorated_view >>>>>>>>>> return func(*args, **kwargs) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/tools/sqleditor/__init__.py", >>>>>>>>>> line 1544, in get_query_history >>>>>>>>>> return QueryHistory.get(current_user.id, trans_obj.sid, >>>>>>>>>> conn.db) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/tools/sqleditor/utils/query_history.py", >>>>>>>>>> line 21, in get >>>>>>>>>> 'result': [rec.query_info for rec in result] >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/utils/ajax.py", line >>>>>>>>>> 75, in make_json_response >>>>>>>>>> separators=(',', ':'), encoding=encoding), >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/simplejson/__init__.py", >>>>>>>>>> line 399, in dumps >>>>>>>>>> **kw).encode(obj) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/simplejson/encoder.py", >>>>>>>>>> line 296, in encode >>>>>>>>>> chunks = self.iterencode(o, _one_shot=True) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/simplejson/encoder.py", >>>>>>>>>> line 378, in iterencode >>>>>>>>>> return _iterencode(o, 0) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/utils/ajax.py", line >>>>>>>>>> 30, in default >>>>>>>>>> return json.JSONEncoder.default(self, obj) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg27/lib/python2.7/site-packages/simplejson/encoder.py", >>>>>>>>>> line 273, in default >>>>>>>>>> o.__class__.__name__) >>>>>>>>>> TypeError: Object of type buffer is not JSON serializable >>>>>>>>>> >>>>>>>>> >>>>>>>>> This error exists on the master branch on Python 2.7, it is not >>>>>>>>> caused by the patch. It is also not a regression of my previous Query >>>>>>>>> History Enhancement patch, I checked. >>>>>>>>> >>>>>>>>> >>>>>>>>>> >>>>>>>>>> 2) The patch is not compatible with older pyscopg2 (2.7.7). I get >>>>>>>>>> below error in View/Edit data. >>>>>>>>>> [image: Screenshot 2019-08-23 at 10.53.30.png] >>>>>>>>>> 2019-08-23 10:53:12,020: ERROR flask.app: 'table_column' >>>>>>>>>> Traceback (most recent call last): >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg35/lib/python3.5/site-packages/flask/app.py", >>>>>>>>>> line 1813, in full_dispatch_request >>>>>>>>>> rv = self.dispatch_request() >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg35/lib/python3.5/site-packages/flask/app.py", >>>>>>>>>> line 1799, in dispatch_request >>>>>>>>>> return self.view_functions[rule.endpoint](**req.view_args) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/venv/pypg35/lib/python3.5/site-packages/flask_login/utils.py", >>>>>>>>>> line 261, in decorated_view >>>>>>>>>> return func(*args, **kwargs) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/tools/sqleditor/__init__.py", >>>>>>>>>> line 435, in poll >>>>>>>>>> columns = trans_obj.get_columns_types(conn) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/tools/sqleditor/command.py", >>>>>>>>>> line 688, in get_columns_types >>>>>>>>>> table_oid=table_oid) >>>>>>>>>> File >>>>>>>>>> "/Users/adityatoshniwal/projects/pgadmin4/web/pgadmin/tools/sqleditor/utils/get_column_types.py", >>>>>>>>>> line 38, in get_columns_types >>>>>>>>>> if row['oid'] == col['table_column']: >>>>>>>>>> KeyError: 'table_column' >>>>>>>>>> >>>>>>>>> >>>>>>>>> I recall Dave mentioning that it is okay to use psycopg 2.8+ >>>>>>>>> features and that it was a requirement of pgAdmin 4 now (clearly mentioned >>>>>>>>> in requirements.txt too). >>>>>>>>> >>>>>>>> >>>>>>>> I did - then we had the whole table_oid issue on Debian/Ubuntu :-( >>>>>>>> >>>>>>>> >>>>>>>>> However, I updated the patch for backwards compatibility. Please >>>>>>>>> find an updated patch attached. >>>>>>>>> >>>>>>>>> >>>>>>>>> -- >>>>>>>>> *Yosry Muhammad Yosry* >>>>>>>>> >>>>>>>>> Computer Engineering student, >>>>>>>>> The Faculty of Engineering, >>>>>>>>> Cairo University (2021). >>>>>>>>> Class representative of CMP 2021. >>>>>>>>> https://www.linkedin.com/in/yosrym93/ >>>>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> -- >>>>>>>> Dave Page >>>>>>>> Blog: http://pgsnake.blogspot.com >>>>>>>> Twitter: @pgsnake >>>>>>>> >>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>>>>> The Enterprise PostgreSQL Company >>>>>>>> >>>>>>> >>>>>>> >>>>>>> -- >>>>>>> *Yosry Muhammad Yosry* >>>>>>> >>>>>>> Computer Engineering student, >>>>>>> The Faculty of Engineering, >>>>>>> Cairo University (2021). >>>>>>> Class representative of CMP 2021. >>>>>>> https://www.linkedin.com/in/yosrym93/ >>>>>>> >>>>>> >>>>>> >>>>>> -- >>>>>> Thanks and Regards, >>>>>> Aditya Toshniwal >>>>>> Software Engineer | EnterpriseDB India | Pune >>>>>> "Don't Complain about Heat, Plant a TREE" >>>>>> >>>>> >>>>> >>>>> -- >>>>> Dave Page >>>>> Blog: http://pgsnake.blogspot.com >>>>> Twitter: @pgsnake >>>>> >>>>> EnterpriseDB UK: http://www.enterprisedb.com >>>>> The Enterprise PostgreSQL Company >>>>> >>>> >>>> >>>> -- >>>> *Yosry Muhammad Yosry* >>>> >>>> Computer Engineering student, >>>> The Faculty of Engineering, >>>> Cairo University (2021). >>>> Class representative of CMP 2021. >>>> https://www.linkedin.com/in/yosrym93/ >>>> >>> >>> >>> -- >>> Dave Page >>> Blog: http://pgsnake.blogspot.com >>> Twitter: @pgsnake >>> >>> EnterpriseDB UK: http://www.enterprisedb.com >>> The Enterprise PostgreSQL Company >>> >> >> >> -- >> *Yosry Muhammad Yosry* >> >> Computer Engineering student, >> The Faculty of Engineering, >> Cairo University (2021). >> Class representative of CMP 2021. >> https://www.linkedin.com/in/yosrym93/ >> > > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > -- *Yosry Muhammad Yosry* Computer Engineering student, The Faculty of Engineering, Cairo University (2021). Class representative of CMP 2021. https://www.linkedin.com/in/yosrym93/
Screenshot 2019-08-23 at 10.53.30.png
(image/png, 74.9 KB) - not displayed
editable_readonly_columns_v5.patch
(text/x-patch, 75.8 KB)
diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst
index b697449d6..c9f6d4723 100644
--- a/docs/en_US/editgrid.rst
+++ b/docs/en_US/editgrid.rst
@@ -35,6 +35,8 @@ disabled in either mode. Please see
:ref:`The Query Tool Toolbar <query_tool_toolbar>` for a description of the
available controls.
+.. _data-grid:
+
The Data Grid
*************
@@ -42,8 +44,6 @@ The top row of the data grid displays the name of each column, the data type,
and if applicable, the number of characters allowed. A column that is part of
the primary key will additionally be marked with [PK].
-.. _modifying-data-grid:
-
To modify the displayed data:
* To change a numeric value within the grid, double-click the value to select
diff --git a/docs/en_US/images/query_tool_editable_columns.png b/docs/en_US/images/query_tool_editable_columns.png
new file mode 100644
index 0000000000000000000000000000000000000000..922c0280f9de576cb2201953d29f4a1ab9f87414
GIT binary patch
literal 31676
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
literal 0
HcmV?d00001
diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst
index 48f2c88f9..bf5162b64 100644
--- a/docs/en_US/query_tool.rst
+++ b/docs/en_US/query_tool.rst
@@ -130,15 +130,27 @@ You can:
A result set is updatable if:
-* All the columns belong to the same table.
-* All the primary keys or OIDs of the table are explicitly selected.
-* No columns are duplicated.
+* All columns are either selected directly from a single table, or
+ are not table columns at all (e.g. concatenation of 2 columns).
+ Only columns that are selected directly from the table are
+ editable, other columns are read-only.
+* All the primary key columns or OIDs of the table are selected in the
+ result set.
+
+Any columns that are renamed or selected more than once are also read-only.
+
+Editable and read-only columns are identified using pencil and lock icons
+(respectively) in the column headers.
+
+.. image:: images/query_tool_editable_columns.png
+ :alt: Query tool editable and read-only columns
+ :align: center
The psycopg2 driver version should be equal to or above 2.8 for updatable
query result sets to work.
-An updatable result set can be modified just like in
-:ref:`View/Edit Data <modifying-data-grid>` mode.
+An updatable result set is identical to the :ref:`Data Grid <data-grid>` in
+View/Edit Data mode, and can be modified in the same way.
If Auto-commit is off, the data changes are made as part of the ongoing
transaction, if no transaction is ongoing a new one is initiated. The data
diff --git a/web/pgadmin/feature_tests/query_tool_journey_test.py b/web/pgadmin/feature_tests/query_tool_journey_test.py
index 8d8bfe34f..f59c29bf7 100644
--- a/web/pgadmin/feature_tests/query_tool_journey_test.py
+++ b/web/pgadmin/feature_tests/query_tool_journey_test.py
@@ -74,10 +74,9 @@ class QueryToolJourneyTest(BaseFeatureTest):
self._test_history_tab()
print(" OK.", file=sys.stderr)
- # Insert data into test editable table
self._insert_data_into_test_editable_table()
- print("History query sources and generated queries toggle...",
+ print("History query source icons and generated queries toggle...",
file=sys.stderr, end="")
self._test_query_sources_and_generated_queries()
print(" OK.", file=sys.stderr)
@@ -86,6 +85,10 @@ class QueryToolJourneyTest(BaseFeatureTest):
self._test_updatable_resultset()
print(" OK.", file=sys.stderr)
+ print("Is editable column header icons...", file=sys.stderr, end="")
+ self._test_is_editable_columns_icons()
+ print(" OK.", file=sys.stderr)
+
def _test_copies_rows(self):
pyperclip.copy("old clipboard contents")
self.page.driver.switch_to.default_content()
@@ -237,16 +240,53 @@ class QueryToolJourneyTest(BaseFeatureTest):
return
self.page.click_tab("Query Editor")
- # Select all data (contains the primary key -> should be editable)
+ # Select all data
+ # (contains the primary key -> all columns should be editable)
self.page.clear_query_tool()
query = "SELECT pk_column, normal_column FROM %s" \
% self.test_editable_table_name
- self._check_query_results_editable(query, True)
+ self._check_query_results_editable(query, [True, True])
# Select data without primary keys -> should not be editable
self.page.clear_query_tool()
query = "SELECT normal_column FROM %s" % self.test_editable_table_name
- self._check_query_results_editable(query, False)
+ self._check_query_results_editable(query, [False],
+ discard_changes_modal=True)
+
+ # Select all data in addition to duplicate, renamed, and out-of-table
+ # columns
+ self.page.clear_query_tool()
+ query = """
+ SELECT pk_column, normal_column, normal_column,
+ normal_column as pk_column,
+ (normal_column::text || normal_column::text)::int
+ FROM %s
+ """ % self.test_editable_table_name
+ self._check_query_results_editable(query,
+ [True, True, False, False, False])
+
+ def _test_is_editable_columns_icons(self):
+ if self.driver_version < 2.8:
+ return
+ self.page.click_tab("Query Editor")
+
+ self.page.clear_query_tool()
+ query = "SELECT pk_column FROM %s" % self.test_editable_table_name
+ self.page.execute_query(query)
+ # Discard changes made by previous test to data grid
+ self.page.click_modal('Yes')
+ icon_exists = self.page.check_if_element_exist_by_xpath(
+ QueryToolLocators.editable_column_icon_xpath
+ )
+ self.assertTrue(icon_exists)
+
+ self.page.clear_query_tool()
+ query = "SELECT normal_column FROM %s" % self.test_editable_table_name
+ self.page.execute_query(query)
+ icon_exists = self.page.check_if_element_exist_by_xpath(
+ QueryToolLocators.read_only_column_icon_xpath
+ )
+ self.assertTrue(icon_exists)
def _execute_sources_test_queries(self):
self.page.clear_query_tool()
@@ -367,15 +407,18 @@ class QueryToolJourneyTest(BaseFeatureTest):
def _assert_clickable(self, element):
self.page.click_element(element)
- def _check_query_results_editable(self, query, should_be_editable):
+ def _check_query_results_editable(self, query, cols_should_be_editable,
+ discard_changes_modal=False):
self.page.execute_query(query)
- # Check if the first cell in the first row is editable
- is_editable = self._check_cell_editable(1)
- self.assertEqual(is_editable, should_be_editable)
+ if discard_changes_modal:
+ self.page.click_modal('Yes')
+ enumerated_should_be_editable = enumerate(cols_should_be_editable, 1)
+ for column_index, should_be_editable in enumerated_should_be_editable:
+ is_editable = self._check_cell_editable(column_index)
+ self.assertEqual(is_editable, should_be_editable)
def _check_cell_editable(self, cell_index):
"""Checks if a cell in the first row of the resultset is editable"""
-
self.page.check_if_element_exist_by_xpath(
"//div[contains(@style, 'top:0px')]//div[contains(@class, "
"'l{0} r{1}')]".format(cell_index, cell_index))
diff --git a/web/pgadmin/static/js/sqleditor_utils.js b/web/pgadmin/static/js/sqleditor_utils.js
index bd4356c02..fde7283b0 100644
--- a/web/pgadmin/static/js/sqleditor_utils.js
+++ b/web/pgadmin/static/js/sqleditor_utils.js
@@ -8,8 +8,8 @@
//////////////////////////////////////////////////////////////////////////
// This file contains common utilities functions used in sqleditor modules
-define(['jquery', 'sources/gettext', 'sources/url_for'],
- function ($, gettext, url_for) {
+define(['jquery', 'underscore', 'sources/gettext', 'sources/url_for'],
+ function ($, _, gettext, url_for) {
var sqlEditorUtils = {
/* Reference link http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
* Modified as per requirement.
@@ -198,6 +198,32 @@ define(['jquery', 'sources/gettext', 'sources/url_for'],
}
return '1em';
},
+
+ addEditableIcon: function(columnDefinition, is_editable) {
+ /* This uses Slickgrid.HeaderButtons plugin to add an icon to the
+ columns headers. Instead of a button, an icon is created */
+ let content = null;
+ if(is_editable) {
+ content = '<i class="fa fa-pencil"></i>';
+ }
+ else {
+ content = '<i class="fa fa-lock"></i>';
+ }
+ let button = {
+ cssClass: 'editable-column-header-icon',
+ content: content,
+ };
+ // Check for existing buttons
+ if(!_.isUndefined(columnDefinition.header) &&
+ !_.isUndefined(columnDefinition.header.buttons)) {
+ columnDefinition.header.buttons.push(button);
+ }
+ else {
+ columnDefinition.header = {
+ buttons: [button],
+ };
+ }
+ },
};
return sqlEditorUtils;
});
diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py
index b862f62cf..da612da1f 100644
--- a/web/pgadmin/tools/sqleditor/__init__.py
+++ b/web/pgadmin/tools/sqleditor/__init__.py
@@ -430,40 +430,17 @@ def poll(trans_id):
oids = {'oid': 'oid'}
if columns_info is not None:
- # If it is a QueryToolCommand that has obj_id attribute
- # then it should also be editable
- if hasattr(trans_obj, 'obj_id') and \
- (not isinstance(trans_obj, QueryToolCommand) or
- trans_obj.can_edit()):
- # Get the template path for the column
- template_path = 'columns/sql/#{0}#'.format(
- conn.manager.version
- )
-
- SQL = render_template(
- "/".join([template_path, 'nodes.sql']),
- tid=trans_obj.obj_id,
- has_oids=True
- )
- # rows with attribute not_null
- colst, rset = conn.execute_2darray(SQL)
- if not colst:
- return internal_server_error(errormsg=rset)
-
- for key, col in enumerate(columns_info):
- col_type = dict()
- col_type['type_code'] = col['type_code']
- col_type['type_name'] = None
- col_type['internal_size'] = col['internal_size']
- columns[col['name']] = col_type
-
- if rset:
- col_type['not_null'] = col['not_null'] = \
- rset['rows'][key]['not_null']
-
- col_type['has_default_val'] = \
- col['has_default_val'] = \
- rset['rows'][key]['has_default_val']
+ # Only QueryToolCommand or TableCommand can be editable
+ if hasattr(trans_obj, 'obj_id') and trans_obj.can_edit():
+ columns = trans_obj.get_columns_types(conn)
+
+ else:
+ for col in columns_info:
+ col_type = dict()
+ col_type['type_code'] = col['type_code']
+ col_type['type_name'] = None
+ col_type['internal_size'] = col['internal_size']
+ columns[col['name']] = col_type
if columns:
st, types = fetch_pg_types(columns, trans_obj)
diff --git a/web/pgadmin/tools/sqleditor/command.py b/web/pgadmin/tools/sqleditor/command.py
index 01815f8cf..05268f6d4 100644
--- a/web/pgadmin/tools/sqleditor/command.py
+++ b/web/pgadmin/tools/sqleditor/command.py
@@ -22,6 +22,7 @@ from pgadmin.utils.driver import get_driver
from pgadmin.tools.sqleditor.utils.is_query_resultset_updatable \
import is_query_resultset_updatable
from pgadmin.tools.sqleditor.utils.save_changed_data import save_changed_data
+from pgadmin.tools.sqleditor.utils.get_column_types import get_columns_types
from config import PG_DEFAULT_DRIVER
@@ -677,6 +678,16 @@ class TableCommand(GridCommand):
client_primary_key=client_primary_key,
conn=conn)
+ def get_columns_types(self, conn):
+ columns_info = conn.get_column_info()
+ has_oids = self.has_oids()
+ table_oid = self.obj_id
+ return get_columns_types(conn=conn,
+ columns_info=columns_info,
+ has_oids=has_oids,
+ table_oid=table_oid,
+ is_query_tool=False)
+
class ViewCommand(GridCommand):
"""
@@ -864,6 +875,7 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker):
self.primary_keys = None
self.pk_names = None
self.table_has_oids = False
+ self.columns_types = None
def get_sql(self, default_conn=None):
return None
@@ -874,6 +886,9 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker):
def get_primary_keys(self):
return self.pk_names, self.primary_keys
+ def get_columns_types(self, conn=None):
+ return self.columns_types
+
def has_oids(self):
return self.table_has_oids
@@ -906,8 +921,9 @@ class QueryToolCommand(BaseCommand, FetchedRowTracker):
# Get the path to the sql templates
sql_path = 'sqleditor/sql/#{0}#'.format(manager.version)
- self.is_updatable_resultset, self.table_has_oids, self.primary_keys, \
- pk_names, table_oid = is_query_resultset_updatable(conn, sql_path)
+ self.is_updatable_resultset, self.table_has_oids,\
+ self.primary_keys, pk_names, table_oid,\
+ self.columns_types = is_query_resultset_updatable(conn, sql_path)
# Create pk_names attribute in the required format
if pk_names is not None:
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index e078ea753..8831d5023 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -385,13 +385,13 @@ input.editor-checkbox:focus {
/* For geometry column button */
-.div-view-geometry-column {
+.div-view-geometry-column, .editable-column-header-icon {
float: right;
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
- padding-right: 4px;
+ padding-right: 6px;
}
/* For leaflet popup */
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 4433019c3..7e368ffba 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -778,6 +778,7 @@ define('tools.querytool', [
not_null: c.not_null,
has_default_val: c.has_default_val,
is_array: c.is_array,
+ can_edit: c.can_edit,
};
// Get the columns width based on longer string among data type or
@@ -795,17 +796,17 @@ define('tools.querytool', [
if (c.cell == 'oid' && c.name == 'oid') {
options['editor'] = null;
} else if (c.cell == 'Json') {
- options['editor'] = is_editable ? Slick.Editors.JsonText :
+ options['editor'] = c.can_edit ? Slick.Editors.JsonText :
Slick.Editors.ReadOnlyJsonText;
options['formatter'] = Slick.Formatters.JsonString;
} else if (c.cell == 'number' || c.cell == 'oid' ||
$.inArray(c.type, ['xid', 'real']) !== -1
) {
- options['editor'] = is_editable ? Slick.Editors.CustomNumber :
+ options['editor'] = c.can_edit ? Slick.Editors.CustomNumber :
Slick.Editors.ReadOnlyText;
options['formatter'] = Slick.Formatters.Numbers;
} else if (c.cell == 'boolean') {
- options['editor'] = is_editable ? Slick.Editors.Checkbox :
+ options['editor'] = c.can_edit ? Slick.Editors.Checkbox :
Slick.Editors.ReadOnlyCheckbox;
options['formatter'] = Slick.Formatters.Checkmark;
} else if (c.cell == 'binary') {
@@ -814,23 +815,41 @@ define('tools.querytool', [
} else if (c.cell == 'geometry' || c.cell == 'geography') {
// increase width to add 'view' button
options['width'] += 28;
+ options['can_edit'] = false;
} else {
- options['editor'] = is_editable ? Slick.Editors.pgText :
+ options['editor'] = c.can_edit ? Slick.Editors.pgText :
Slick.Editors.ReadOnlypgText;
options['formatter'] = Slick.Formatters.Text;
}
+ if(!_.isUndefined(c.can_edit)) {
+ // Increase width for editable/read-only icon
+ options['width'] += 12;
+
+ let tooltip = '';
+ if(c.can_edit)
+ tooltip = gettext('Editable column');
+ else
+ tooltip = gettext('Read-only column');
+
+ options['toolTip'] = tooltip;
+ }
+
grid_columns.push(options);
});
var gridSelector = new GridSelector();
grid_columns = self.grid_columns = gridSelector.getColumnDefinitions(grid_columns);
- // add 'view' button in geometry and geography type column header
_.each(grid_columns, function (c) {
+ // Add 'view' button in geometry and geography type column headers
if (c.column_type_internal == 'geometry' || c.column_type_internal == 'geography') {
GeometryViewer.add_header_button(c);
}
+ // Add editable/read-only icon to columns
+ if (!_.isUndefined(c.can_edit)) {
+ SqlEditorUtils.addEditableIcon(c, c.can_edit);
+ }
});
if (rows_affected) {
@@ -2634,10 +2653,11 @@ define('tools.querytool', [
// Create columns required by slick grid to render
_.each(colinfo, function(c) {
- var is_primary_key = false;
+ var is_primary_key = false,
+ is_editable = self.can_edit && (!self.is_query_tool || c.is_editable);
- // Check whether table have primary key
- if (_.size(primary_keys) > 0) {
+ // Check whether this column is a primary key
+ if (is_editable && _.size(primary_keys) > 0) {
_.each(primary_keys, function(value, key) {
if (key === c.name)
is_primary_key = true;
@@ -2738,7 +2758,7 @@ define('tools.querytool', [
'pos': c.pos,
'label': column_label,
'cell': col_cell,
- 'can_edit': (c.name == 'oid') ? false : self.can_edit,
+ 'can_edit': (c.name == 'oid') ? false : is_editable,
'type': type,
'not_null': c.not_null,
'has_default_val': c.has_default_val,
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
index 610747dfb..851b98523 100644
--- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/get_columns.sql
@@ -1,6 +1,6 @@
{# ============= Fetch the columns ============= #}
{% if obj_id %}
-SELECT at.attname, ty.typname
+SELECT at.attname, ty.typname, at.attnum
FROM pg_attribute at
LEFT JOIN pg_type ty ON (ty.oid = at.atttypid)
WHERE attrelid={{obj_id}}::oid
diff --git a/web/pgadmin/tools/sqleditor/utils/get_column_types.py b/web/pgadmin/tools/sqleditor/utils/get_column_types.py
new file mode 100644
index 000000000..99bcb9fa8
--- /dev/null
+++ b/web/pgadmin/tools/sqleditor/utils/get_column_types.py
@@ -0,0 +1,57 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2019, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+"""
+ Get the column types for QueryToolCommand or TableCommand when
+ the result-set is editable.
+"""
+
+from flask import render_template
+
+
+def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids):
+ nodes_sqlpath = 'columns/sql/#{0}#'.format(conn.manager.version)
+ query = render_template(
+ "/".join([nodes_sqlpath, 'nodes.sql']),
+ tid=table_oid,
+ has_oids=has_oids
+ )
+
+ colst, rset = conn.execute_2darray(query)
+ if not colst:
+ raise Exception(rset)
+
+ column_types = dict()
+ for key, col in enumerate(columns_info):
+ col_type = dict()
+ col_type['type_code'] = col['type_code']
+ col_type['type_name'] = None
+ col_type['internal_size'] = col['internal_size']
+ column_types[col['name']] = col_type
+
+ if not is_query_tool:
+ col_type['not_null'] = col['not_null'] = \
+ rset['rows'][key]['not_null']
+
+ col_type['has_default_val'] = \
+ col['has_default_val'] = \
+ rset['rows'][key]['has_default_val']
+
+ else:
+ for row in rset['rows']:
+ if row['oid'] == col['table_column']:
+ col_type['not_null'] = col['not_null'] = row['not_null']
+
+ col_type['has_default_val'] = \
+ col['has_default_val'] = row['has_default_val']
+
+ else:
+ col_type['not_null'] = col['not_null'] = None
+ col_type['has_default_val'] = col['has_default_val'] = None
+
+ return column_types
diff --git a/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py b/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py
index 9c2bd09a6..0582ca3ab 100644
--- a/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py
+++ b/web/pgadmin/tools/sqleditor/utils/is_query_resultset_updatable.py
@@ -8,11 +8,18 @@
##########################################################################
"""
- Check if the result-set of a query is updatable, A resultset is
- updatable (as of this version) if:
- - All columns belong to the same table.
- - All the primary key columns of the table are present in the resultset
- - No duplicate columns
+ Check if the result-set of a query is editable, A result-set is
+ editable if:
+ - All columns are either selected directly from a single table, or
+ are not table columns at all (e.g. concatenation of 2 columns).
+ Only columns that are selected directly from a the table are
+ editable, other columns are read-only.
+ - All the primary key columns or oids (if applicable) of the table are
+ present in the result-set.
+
+ Note:
+ - Duplicate columns (selected twice) or renamed columns are also
+ read-only.
"""
from flask import render_template
from flask_babelex import gettext
@@ -20,16 +27,18 @@ try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
+from pgadmin.tools.sqleditor.utils.get_column_types import get_columns_types
def is_query_resultset_updatable(conn, sql_path):
"""
This function is used to check whether the last successful query
- produced updatable results.
+ produced editable results.
Args:
conn: Connection object.
- sql_path: the path to the sql templates.
+ sql_path: the path to the sql templates
+ primary_keys.sql & columns.sql.
"""
columns_info = conn.get_column_info()
@@ -37,26 +46,43 @@ def is_query_resultset_updatable(conn, sql_path):
return return_not_updatable()
table_oid = _check_single_table(columns_info)
- if not table_oid:
- return return_not_updatable()
-
- if not _check_duplicate_columns(columns_info):
+ if table_oid is None:
return return_not_updatable()
if conn.connected():
- primary_keys, pk_names = _check_primary_keys(conn=conn,
- columns_info=columns_info,
- table_oid=table_oid,
- sql_path=sql_path)
+ # Get all the table columns
+ table_columns = _get_table_columns(conn=conn,
+ table_oid=table_oid,
+ sql_path=sql_path)
+
+ # Editable column: A column selected directly from a table, that is
+ # neither renamed nor is a duplicate of another selected column
+ _check_editable_columns(table_columns=table_columns,
+ results_columns=columns_info)
+
+ primary_keys, pk_names = \
+ _check_primary_keys(conn=conn,
+ columns_info=columns_info,
+ table_oid=table_oid,
+ sql_path=sql_path)
has_oids = _check_oids(conn=conn,
columns_info=columns_info,
table_oid=table_oid,
sql_path=sql_path)
- if has_oids or primary_keys is not None:
- return True, has_oids, primary_keys, pk_names, table_oid
+ is_resultset_updatable = has_oids or primary_keys is not None
+
+ if is_resultset_updatable:
+ column_types = get_columns_types(columns_info=columns_info,
+ table_oid=table_oid,
+ conn=conn,
+ has_oids=has_oids,
+ is_query_tool=True)
+ return True, has_oids, primary_keys, \
+ pk_names, table_oid, column_types
else:
+ _set_all_columns_not_editable(columns_info=columns_info)
return return_not_updatable()
else:
raise Exception(
@@ -66,20 +92,34 @@ def is_query_resultset_updatable(conn, sql_path):
def _check_single_table(columns_info):
- table_oid = columns_info[0]['table_oid']
+ table_oid = None
for column in columns_info:
- if column['table_oid'] != table_oid:
+ # Skip columns that are not directly from tables
+ if column['table_oid'] is None:
+ continue
+ # If we don't have a table_oid yet, store this one
+ if table_oid is None:
+ table_oid = column['table_oid']
+ # If we already have one, check that all the columns have the same one
+ elif column['table_oid'] != table_oid:
return None
return table_oid
-def _check_duplicate_columns(columns_info):
- column_numbers = \
- [col['table_column'] for col in columns_info]
- is_duplicate_columns = len(column_numbers) != len(set(column_numbers))
- if is_duplicate_columns:
- return False
- return True
+def _check_editable_columns(table_columns, results_columns):
+ table_columns_numbers = set()
+ for results_column in results_columns:
+ table_column_number = results_column['table_column']
+ if table_column_number is None: # Not a table column
+ results_column['is_editable'] = False
+ elif table_column_number in table_columns_numbers: # Duplicate
+ results_column['is_editable'] = False
+ elif results_column['display_name'] \
+ != table_columns[table_column_number]:
+ results_column['is_editable'] = False
+ else:
+ results_column['is_editable'] = True
+ table_columns_numbers.add(table_column_number)
def _check_oids(conn, sql_path, table_oid, columns_info):
@@ -111,26 +151,34 @@ def _check_primary_keys(conn, columns_info, sql_path, table_oid):
table_oid=table_oid,
sql_path=sql_path)
- if not _check_primary_keys_uniquely_exist(primary_keys_columns,
- columns_info):
+ if not _check_all_primary_keys_exist(primary_keys_columns,
+ columns_info):
primary_keys = None
pk_names = None
return primary_keys, pk_names
-def _check_primary_keys_uniquely_exist(primary_keys_columns, columns_info):
+def _check_all_primary_keys_exist(primary_keys_columns, columns_info):
+ """
+ Check that all primary keys exist.
+
+ If another column is selected with the same name as the primary key
+ before the primary key (e.g SELECT some_col as pk, pk from table) the
+ name of the actual primary key column gets changed to pk-2.
+ This is also reversed here.
+ """
for pk in primary_keys_columns:
pk_exists = False
for col in columns_info:
- if col['table_column'] == pk['column_number']:
+ if col['is_editable'] and \
+ col['table_column'] == pk['column_number']:
pk_exists = True
- # If the primary key column is renamed
- if col['display_name'] != pk['name']:
- return False
- # If a normal column is renamed to a primary key column name
- elif col['display_name'] == pk['name']:
- return False
-
+ # If the primary key is renamed, restore to its original name
+ if col['name'] != pk['name']:
+ col['name'], _ = col['name'].split('-')
+ # If another column is renamed to the primary key name, change it
+ elif col['name'] == pk['name']:
+ col['name'] += '-0'
if not pk_exists:
return False
return True
@@ -160,5 +208,26 @@ def _get_primary_keys(sql_path, table_oid, conn):
return primary_keys, primary_keys_columns, pk_names
+def _get_table_columns(sql_path, table_oid, conn):
+ query = render_template(
+ "/".join([sql_path, 'get_columns.sql']),
+ obj_id=table_oid
+ )
+ status, result = conn.execute_dict(query)
+ if not status:
+ raise Exception(result)
+
+ columns = {}
+ for row in result['rows']:
+ columns[row['attnum']] = row['attname']
+
+ return columns
+
+
+def _set_all_columns_not_editable(columns_info):
+ for col in columns_info:
+ col['is_editable'] = False
+
+
def return_not_updatable():
- return False, False, None, None, None
+ return False, False, None, None, None, None
diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_is_query_resultset_updatable.py b/web/pgadmin/tools/sqleditor/utils/tests/test_is_query_resultset_updatable.py
index 40ca4c8f7..8b451174a 100644
--- a/web/pgadmin/tools/sqleditor/utils/tests/test_is_query_resultset_updatable.py
+++ b/web/pgadmin/tools/sqleditor/utils/tests/test_is_query_resultset_updatable.py
@@ -25,67 +25,109 @@ class TestQueryUpdatableResultset(BaseTestGenerator):
scenarios = [
('When selecting all columns of the table', dict(
sql='SELECT * FROM %s;',
- primary_keys={
+ expected_primary_keys={
'pk_col1': 'int4',
'pk_col2': 'int4'
},
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[True, True, True, True]
)),
('When selecting all primary keys of the table', dict(
sql='SELECT pk_col1, pk_col2 FROM %s;',
- primary_keys={
+ expected_primary_keys={
'pk_col1': 'int4',
'pk_col2': 'int4'
},
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[True, True]
)),
('When selecting some of the primary keys of the table', dict(
sql='SELECT pk_col2 FROM %s;',
- primary_keys=None,
+ expected_primary_keys=None,
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[False]
)),
('When selecting none of the primary keys of the table', dict(
sql='SELECT normal_col1 FROM %s;',
- primary_keys=None,
+ expected_primary_keys=None,
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[False]
)),
('When renaming a primary key', dict(
sql='SELECT pk_col1 as some_col, pk_col2 FROM "%s";',
- primary_keys=None,
+ expected_primary_keys=None,
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[False, False]
)),
- ('When renaming a column to a primary key name', dict(
- sql='SELECT pk_col1, pk_col2, normal_col1 as pk_col1 FROM %s;',
- primary_keys=None,
+ ('When renaming a normal column', dict(
+ sql='SELECT pk_col1, pk_col2, normal_col1 as some_col FROM "%s";',
+ expected_primary_keys={
+ 'pk_col1': 'int4',
+ 'pk_col2': 'int4'
+ },
+ expected_has_oids=False,
+ table_has_oids=False,
+ expected_cols_is_editable=[True, True, False]
+ )),
+ ('When renaming a normal column to a primary key name', dict(
+ sql='SELECT normal_col1 as pk_col1, pk_col1, pk_col2 FROM %s;',
+ expected_primary_keys={
+ 'pk_col1': 'int4',
+ 'pk_col2': 'int4'
+ },
expected_has_oids=False,
- table_has_oids=False
+ table_has_oids=False,
+ expected_cols_is_editable=[False, True, True]
+ )),
+ ('When selecting a normal column twice', dict(
+ sql='SELECT pk_col1, pk_col2, normal_col1, normal_col1 FROM %s;',
+ expected_primary_keys={
+ 'pk_col1': 'int4',
+ 'pk_col2': 'int4'
+ },
+ expected_has_oids=False,
+ table_has_oids=False,
+ expected_cols_is_editable=[True, True, True, False]
+ )),
+ ('When selecting a non-table column', dict(
+ sql='SELECT pk_col1, pk_col2, normal_col1 || normal_col2 FROM %s;',
+ expected_primary_keys={
+ 'pk_col1': 'int4',
+ 'pk_col2': 'int4'
+ },
+ expected_has_oids=False,
+ table_has_oids=False,
+ expected_cols_is_editable=[True, True, False]
)),
('When selecting primary keys and oids (table with oids)', dict(
sql='SELECT *, oid FROM %s;',
- primary_keys={
+ expected_primary_keys={
'pk_col1': 'int4',
'pk_col2': 'int4'
},
expected_has_oids=True,
- table_has_oids=True
+ table_has_oids=True,
+ expected_cols_is_editable=[True, True, True, True, False]
)),
('When selecting oids without primary keys (table with oids)', dict(
sql='SELECT oid, normal_col1, normal_col2 FROM %s;',
- primary_keys=None,
+ expected_primary_keys=None,
expected_has_oids=True,
- table_has_oids=True
+ table_has_oids=True,
+ expected_cols_is_editable=[False, True, True]
)),
('When selecting none of the primary keys or oids (table with oids)',
dict(
sql='SELECT normal_col1, normal_col2 FROM %s;',
- primary_keys=None,
+ expected_primary_keys=None,
expected_has_oids=False,
- table_has_oids=True
+ table_has_oids=True,
+ expected_cols_is_editable=[False, False]
))
]
@@ -99,6 +141,7 @@ class TestQueryUpdatableResultset(BaseTestGenerator):
response_data = self._execute_select_sql()
self._check_primary_keys(response_data)
self._check_oids(response_data)
+ self._check_editable_columns(response_data)
def tearDown(self):
# Disconnect the database
@@ -116,12 +159,18 @@ class TestQueryUpdatableResultset(BaseTestGenerator):
def _check_primary_keys(self, response_data):
primary_keys = response_data['data']['primary_keys']
- self.assertEquals(primary_keys, self.primary_keys)
+ self.assertEquals(primary_keys, self.expected_primary_keys)
def _check_oids(self, response_data):
has_oids = response_data['data']['has_oids']
self.assertEquals(has_oids, self.expected_has_oids)
+ def _check_editable_columns(self, response_data):
+ columns_info = response_data['data']['colinfo']
+ for col, expected_is_editable in \
+ zip(columns_info, self.expected_cols_is_editable):
+ self.assertEquals(col['is_editable'], expected_is_editable)
+
def _initialize_database_connection(self):
database_info = parent_node_dict["database"][-1]
self.db_name = database_info["db_name"]
diff --git a/web/regression/feature_utils/locators.py b/web/regression/feature_utils/locators.py
index b2bcb6224..f37afddd5 100644
--- a/web/regression/feature_utils/locators.py
+++ b/web/regression/feature_utils/locators.py
@@ -219,3 +219,11 @@ class QueryToolLocators:
"//div[label[normalize-space(" \
"text())='Show queries generated internally by pgAdmin?']]" \
"//div[contains(@class,'toggle btn')]"
+
+ editable_column_icon_xpath = "//div[contains(@class," \
+ " 'editable-column-header-icon')]" \
+ "/i[contains(@class, 'fa-pencil')]"
+
+ read_only_column_icon_xpath = "//div[contains(@class," \
+ " 'editable-column-header-icon')]" \
+ "/i[contains(@class, 'fa-lock')]"