diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js index 358db8d..a29e0f5 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_actions.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -83,11 +83,11 @@ let queryToolActions = { } if (!sqlQuery) return; - - let filename = 'data-' + new Date().getTime() + '.csv'; + let extension = sqlEditorController.preferences.csv_field_separator === ',' ? '.csv': '.txt'; + let filename = 'data-' + new Date().getTime() + extension; if (!sqlEditorController.is_query_tool) { - filename = sqlEditorController.table_name + '.csv'; + filename = sqlEditorController.table_name + extension; } sqlEditorController.trigger_csv_download(sqlQuery, filename); diff --git a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js index 5539847..5a3d155 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_preferences.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_preferences.js @@ -114,9 +114,9 @@ function updateUIPreferences(sqlEditor) { $el.find('#btn-download') .attr('title', - shortcut_title(gettext('Download as CSV'),preferences.download_csv)) + shortcut_title(gettext('Download as CSV/TXT'),preferences.download_csv)) .attr('aria-label', - shortcut_title(gettext('Download as CSV'),preferences.download_csv)); + shortcut_title(gettext('Download as CSV/TXT'),preferences.download_csv)); $el.find('#btn-save-data') .attr('title', diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 386a655..c632a43 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -1347,14 +1347,18 @@ def start_query_download_tool(trans_id): field_separator=blueprint.csv_field_separator.get(), replace_nulls_with=blueprint.replace_nulls_with.get() ), - mimetype='text/csv' + mimetype='text/csv' if + blueprint.csv_field_separator.get() == ',' + else 'text/plain' ) if 'filename' in data and data['filename'] != "": filename = data['filename'] else: import time - filename = str(int(time.time())) + ".csv" + filename = '{0}.{1}'. \ + format(int(time.time()), 'csv' if blueprint. + csv_field_separator.get() == ',' else 'txt') # We will try to encode report file name with latin-1 # If it fails then we will fallback to default ascii file name diff --git a/web/pgadmin/tools/sqleditor/tests/test_download_csv_query_tool.py b/web/pgadmin/tools/sqleditor/tests/test_download_csv_query_tool.py index f4a72f0..98d0fae 100644 --- a/web/pgadmin/tools/sqleditor/tests/test_download_csv_query_tool.py +++ b/web/pgadmin/tools/sqleditor/tests/test_download_csv_query_tool.py @@ -7,6 +7,7 @@ # This software is released under the PostgreSQL Licence # ########################################################################## +from unittest.mock import patch from pgadmin.utils.route import BaseTestGenerator from pgadmin.browser.server_groups.servers.databases.tests import utils as \ @@ -32,7 +33,9 @@ class TestDownloadCSV(BaseTestGenerator): output_columns='"A","B","C"', output_values='1,2,3', is_valid_tx=True, - is_valid=True + is_valid=True, + download_as_txt=False, + filename='test.csv' ) ), ( @@ -44,7 +47,9 @@ class TestDownloadCSV(BaseTestGenerator): output_columns=None, output_values=None, is_valid_tx=False, - is_valid=False + is_valid=False, + download_as_txt=False, + filename='test.csv' ) ), ( @@ -56,7 +61,37 @@ class TestDownloadCSV(BaseTestGenerator): output_columns=None, output_values=None, is_valid_tx=True, - is_valid=False + is_valid=False, + download_as_txt=False, + filename='test.csv' + ) + ), + ( + 'Download as txt without filename parameter', + dict( + sql='SELECT 1 as "A",2 as "B",3 as "C"', + init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}', + donwload_url="/sqleditor/query_tool/download/{0}", + output_columns='"A";"B";"C"', + output_values='1;2;3', + is_valid_tx=True, + is_valid=True, + download_as_txt=True, + filename=None + ) + ), + ( + 'Download as csv without filename parameter', + dict( + sql='SELECT 1 as "A",2 as "B",3 as "C"', + init_url='/datagrid/initialize/query_tool/{0}/{1}/{2}/{3}', + donwload_url="/sqleditor/query_tool/download/{0}", + output_columns='"A","B","C"', + output_values='1,2,3', + is_valid_tx=True, + is_valid=True, + download_as_txt=False, + filename=None ) ), ] @@ -95,30 +130,62 @@ class TestDownloadCSV(BaseTestGenerator): url = self.donwload_url.format(self.trans_id) # Disable the console logging from Flask logger self.app.logger.disabled = True - response = self.tester.post( - url, - data={"query": self.sql, "filename": 'test.csv'} - ) - # Enable the console logging from Flask logger - self.app.logger.disabled = False - if self.is_valid: - # when valid query - self.assertEquals(response.status_code, 200) - csv_data = response.data.decode() - self.assertTrue(self.output_columns in csv_data) - self.assertTrue(self.output_values in csv_data) - elif not self.is_valid and self.is_valid_tx: - # When user enters wrong query - self.assertEquals(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.assertFalse(response_data['data']['status']) - self.assertTrue( - 'relation "this_table_does_not_exist" does not exist' in - response_data['data']['result'] - ) + if self.filename is None: + if self.download_as_txt: + with patch('pgadmin.tools.sqleditor.blueprint.' + 'csv_field_separator.get', return_value=';'), patch( + 'time.time', return_value=1587031962.3808076): + response = self.tester.post(url, data={"query": self.sql}) + headers = dict(response.headers) + # when valid query + self.assertEquals(response.status_code, 200) + csv_data = response.data.decode() + self.assertTrue(self.output_columns in csv_data) + self.assertTrue(self.output_values in csv_data) + self.assertIn('text/plain', headers['Content-Type']) + self.assertIn('1587031962.txt', + headers['Content-Disposition']) + else: + with patch('time.time', return_value=1587031962.3808076): + response = self.tester.post(url, data={"query": self.sql}) + headers = dict(response.headers) + # when valid query + self.assertEquals(response.status_code, 200) + csv_data = response.data.decode() + self.assertTrue(self.output_columns in csv_data) + self.assertTrue(self.output_values in csv_data) + self.assertIn('text/csv', headers['Content-Type']) + self.assertIn('1587031962.csv', + headers['Content-Disposition']) + else: - # when TX id is invalid - self.assertEquals(response.status_code, 500) + response = self.tester.post( + url, + data={"query": self.sql, "filename": self.filename} + ) + headers = dict(response.headers) + # Enable the console logging from Flask logger + self.app.logger.disabled = False + if self.is_valid: + # when valid query + self.assertEquals(response.status_code, 200) + csv_data = response.data.decode() + self.assertTrue(self.output_columns in csv_data) + self.assertTrue(self.output_values in csv_data) + self.assertIn('text/csv', headers['Content-Type']) + self.assertIn(self.filename, headers['Content-Disposition']) + elif not self.is_valid and self.is_valid_tx: + # When user enters wrong query + self.assertEquals(response.status_code, 200) + response_data = json.loads(response.data.decode('utf-8')) + self.assertFalse(response_data['data']['status']) + self.assertTrue( + 'relation "this_table_does_not_exist" does not exist' in + response_data['data']['result'] + ) + else: + # when TX id is invalid + self.assertEquals(response.status_code, 500) database_utils.disconnect_database(self, self._sid, self._did) diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index a4d74e8..d96a600 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -211,7 +211,7 @@ def RegisterQueryToolPreferences(self): self.csv_quoting = self.preference.register( 'CSV_output', 'csv_quoting', gettext("CSV quoting"), 'options', 'strings', - category_label=gettext('CSV Output'), + category_label=gettext('CSV/TXT Output'), options=[{'label': gettext('None'), 'value': 'none'}, {'label': gettext('All'), 'value': 'all'}, {'label': gettext('Strings'), 'value': 'strings'}], @@ -224,7 +224,7 @@ def RegisterQueryToolPreferences(self): self.csv_quote_char = self.preference.register( 'CSV_output', 'csv_quote_char', gettext("CSV quote character"), 'options', '"', - category_label=gettext('CSV Output'), + category_label=gettext('CSV/TXT Output'), options=[{'label': '"', 'value': '"'}, {'label': '\'', 'value': '\''}], select2={ @@ -236,7 +236,7 @@ def RegisterQueryToolPreferences(self): self.csv_field_separator = self.preference.register( 'CSV_output', 'csv_field_separator', gettext("CSV field separator"), 'options', ',', - category_label=gettext('CSV output'), + category_label=gettext('CSV/TXT output'), options=[{'label': ';', 'value': ';'}, {'label': ',', 'value': ','}, {'label': '|', 'value': '|'}, @@ -250,7 +250,7 @@ def RegisterQueryToolPreferences(self): self.replace_nulls_with = self.preference.register( 'CSV_output', 'csv_replace_nulls_with', gettext("Replace null values with"), 'text', 'NULL', - category_label=gettext('CSV output'), + category_label=gettext('CSV/TXT output'), help_str=gettext('Specifies the string that represents a null value ' 'while downloading query results as CSV. You can ' 'specify any arbitrary string to represent a ' diff --git a/web/regression/javascript/sqleditor/query_tool_actions_spec.js b/web/regression/javascript/sqleditor/query_tool_actions_spec.js index 9152b8e..b2be463 100644 --- a/web/regression/javascript/sqleditor/query_tool_actions_spec.js +++ b/web/regression/javascript/sqleditor/query_tool_actions_spec.js @@ -298,13 +298,22 @@ describe('queryToolActions', () => { })); }); - it('calls trigger_csv_download with the query and the filename', () => { + it('calls trigger_csv_download with the query and the filename with .csv extension', () => { let filename = 'data-' + time + '.csv'; queryToolActions.download(sqlEditorController); expect(sqlEditorController.trigger_csv_download).toHaveBeenCalledWith(selectedQueryString, filename); }); + + it('calls trigger_csv_download with the query and the filename with .txt extension', () => { + sqlEditorController.preferences.csv_field_separator = ';'; + let filename = 'data-' + time + '.txt'; + + queryToolActions.download(sqlEditorController); + + expect(sqlEditorController.trigger_csv_download).toHaveBeenCalledWith(selectedQueryString, filename); + }); }); describe('when there is no selection', () => { @@ -622,6 +631,9 @@ describe('queryToolActions', () => { table_name: 'iAmATable', is_query_tool: true, check_data_changes_to_execute_query: jasmine.createSpy('check_data_changes_to_execute_query'), + preferences: { + csv_field_separator: ',', + }, }; } });