Skip to content

Commit c2b39db

Browse files
tsuyoshizawakravets-levko
authored andcommitted
Support download as TSV File (#4445)
1 parent f420e02 commit c2b39db

File tree

7 files changed

+70
-19
lines changed

7 files changed

+70
-19
lines changed

client/app/components/EditVisualizationButton/QueryControlDropdown.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function QueryControlDropdown(props) {
2727
)}
2828
<Menu.Item>
2929
<QueryResultsLink
30+
fileType="csv"
3031
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
3132
query={props.query}
3233
queryResult={props.queryResult}
@@ -35,6 +36,17 @@ export function QueryControlDropdown(props) {
3536
<Icon type="file" /> Download as CSV File
3637
</QueryResultsLink>
3738
</Menu.Item>
39+
<Menu.Item>
40+
<QueryResultsLink
41+
fileType="tsv"
42+
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
43+
query={props.query}
44+
queryResult={props.queryResult}
45+
embed={props.embed}
46+
apiKey={props.apiKey}>
47+
<Icon type="file" /> Download as TSV File
48+
</QueryResultsLink>
49+
</Menu.Item>
3850
<Menu.Item>
3951
<QueryResultsLink
4052
fileType="xlsx"

client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters
3737
"Download as CSV File"
3838
)}
3939
</Menu.Item>,
40+
<Menu.Item key="download_tsv" disabled={isQueryResultEmpty}>
41+
{!isQueryResultEmpty ? (
42+
<a href={downloadLink("tsv")} download={downloadName("tsv")} target="_self">
43+
Download as TSV File
44+
</a>
45+
) : (
46+
"Download as TSV File"
47+
)}
48+
</Menu.Item>,
4049
<Menu.Item key="download_excel" disabled={isQueryResultEmpty}>
4150
{!isQueryResultEmpty ? (
4251
<a href={downloadLink("xlsx")} download={downloadName("xlsx")} target="_self">

client/app/components/queries/VisualizationEmbed.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
5252
<Menu>
5353
<Menu.Item>
5454
<QueryResultsLink
55+
fileType="csv"
5556
query={query}
5657
queryResult={queryResults}
5758
apiKey={$routeParams.api_key}
@@ -60,6 +61,17 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
6061
<Icon type="file" /> Download as CSV File
6162
</QueryResultsLink>
6263
</Menu.Item>
64+
<Menu.Item>
65+
<QueryResultsLink
66+
fileType="tsv"
67+
query={query}
68+
queryResult={queryResults}
69+
apiKey={$routeParams.api_key}
70+
disabled={!queryResults || !queryResults.getData || !queryResults.getData()}
71+
embed>
72+
<Icon type="file" /> Download as TSV File
73+
</QueryResultsLink>
74+
</Menu.Item>
6375
<Menu.Item>
6476
<QueryResultsLink
6577
fileType="xlsx"

redash/handlers/query_results.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
)
3131
from redash.serializers import (
3232
serialize_query_result,
33-
serialize_query_result_to_csv,
33+
serialize_query_result_to_dsv,
3434
serialize_query_result_to_xlsx,
3535
)
3636

@@ -364,12 +364,13 @@ def get(self, query_id=None, query_result_id=None, filetype="json"):
364364

365365
self.record_event(event)
366366

367-
if filetype == "json":
368-
response = self.make_json_response(query_result)
369-
elif filetype == "xlsx":
370-
response = self.make_excel_response(query_result)
371-
else:
372-
response = self.make_csv_response(query_result)
367+
response_builders = {
368+
'json': self.make_json_response,
369+
'xlsx': self.make_excel_response,
370+
'csv': self.make_csv_response,
371+
'tsv': self.make_tsv_response
372+
}
373+
response = response_builders[filetype](query_result)
373374

374375
if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0:
375376
self.add_cors_headers(response.headers)
@@ -390,15 +391,21 @@ def get(self, query_id=None, query_result_id=None, filetype="json"):
390391
else:
391392
abort(404, message="No cached result found for this query.")
392393

393-
def make_json_response(self, query_result):
394+
@staticmethod
395+
def make_json_response(query_result):
394396
data = json_dumps({"query_result": query_result.to_dict()})
395397
headers = {"Content-Type": "application/json"}
396398
return make_response(data, 200, headers)
397399

398400
@staticmethod
399401
def make_csv_response(query_result):
400402
headers = {"Content-Type": "text/csv; charset=UTF-8"}
401-
return make_response(serialize_query_result_to_csv(query_result), 200, headers)
403+
return make_response(serialize_query_result_to_dsv(query_result, ","), 200, headers)
404+
405+
@staticmethod
406+
def make_tsv_response(query_result):
407+
headers = {"Content-Type": "text/tab-separated-values; charset=UTF-8"}
408+
return make_response(serialize_query_result_to_dsv(query_result, "\t"), 200, headers)
402409

403410
@staticmethod
404411
def make_excel_response(query_result):

redash/serializers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from .query_result import (
1616
serialize_query_result,
17-
serialize_query_result_to_csv,
17+
serialize_query_result_to_dsv,
1818
serialize_query_result_to_xlsx,
1919
)
2020

redash/serializers/query_result.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ def serialize_query_result(query_result, is_api_user):
7878
return query_result.to_dict()
7979

8080

81-
def serialize_query_result_to_csv(query_result):
81+
def serialize_query_result_to_dsv(query_result, delimiter):
8282
s = io.StringIO()
8383

8484
query_data = query_result.data
8585

8686
fieldnames, special_columns = _get_column_lists(query_data["columns"] or [])
8787

88-
writer = csv.DictWriter(s, extrasaction="ignore", fieldnames=fieldnames)
88+
writer = csv.DictWriter(s, extrasaction="ignore", fieldnames=fieldnames, delimiter=delimiter)
8989
writer.writeheader()
9090

9191
for row in query_data["rows"]:

tests/serializers/test_query_results.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from redash import models
88
from redash.utils import utcnow, json_dumps
9-
from redash.serializers import serialize_query_result, serialize_query_result_to_csv
9+
from redash.serializers import serialize_query_result, serialize_query_result_to_dsv
1010

1111

1212
data = {
@@ -37,14 +37,14 @@ def test_doesnt_serialize_sensitive_keys_for_unauthenticated_users(self):
3737
self.assertSetEqual(set(["data", "retrieved_at"]), set(serialized.keys()))
3838

3939

40-
class CsvSerializationTest(BaseTestCase):
41-
def get_csv_content(self):
40+
class DsvSerializationTest(BaseTestCase):
41+
def delimited_content(self, delimiter):
4242
query_result = self.factory.create_query_result(data=json_dumps(data))
43-
return serialize_query_result_to_csv(query_result)
43+
return serialize_query_result_to_dsv(query_result, delimiter)
4444

4545
def test_serializes_booleans_correctly(self):
4646
with self.app.test_request_context("/"):
47-
parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
47+
parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
4848
rows = list(parsed)
4949

5050
self.assertEqual(rows[0]["bool"], "true")
@@ -53,7 +53,7 @@ def test_serializes_booleans_correctly(self):
5353

5454
def test_serializes_datatime_with_correct_format(self):
5555
with self.app.test_request_context("/"):
56-
parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
56+
parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
5757
rows = list(parsed)
5858

5959
self.assertEqual(rows[0]["datetime"], "26/05/19 12:39")
@@ -65,8 +65,19 @@ def test_serializes_datatime_with_correct_format(self):
6565

6666
def test_serializes_datatime_as_is_in_case_of_error(self):
6767
with self.app.test_request_context("/"):
68-
parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
68+
parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
6969
rows = list(parsed)
7070

7171
self.assertEqual(rows[3]["datetime"], "459")
7272
self.assertEqual(rows[3]["date"], "123")
73+
74+
def test_serializes_tsv_format(self):
75+
delimiter = "\t"
76+
with self.app.test_request_context("/"):
77+
parsed = csv.DictReader(io.StringIO(self.delimited_content(delimiter)), delimiter=delimiter)
78+
rows = list(parsed)
79+
80+
self.assertEqual(rows[0]["datetime"], "26/05/19 12:39")
81+
self.assertEqual(rows[1]["bool"], "false")
82+
self.assertEqual(rows[2]["date"], "")
83+
self.assertEqual(rows[3]["datetime"], "459")

0 commit comments

Comments
 (0)