diff --git a/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx b/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx
index fd36e2ead..fdbfa2afc 100644
--- a/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx
+++ b/client/app/components/EditVisualizationButton/QueryControlDropdown.jsx
@@ -27,6 +27,7 @@ export function QueryControlDropdown(props) {
)}
Download as CSV File
+
+
+ Download as TSV File
+
+
,
+
+ {!isQueryResultEmpty ? (
+
+ Download as TSV File
+
+ ) : (
+ "Download as TSV File"
+ )}
+ ,
{!isQueryResultEmpty ? (
diff --git a/client/app/components/queries/VisualizationEmbed.jsx b/client/app/components/queries/VisualizationEmbed.jsx
index 7ef854561..7a7cc1e9f 100644
--- a/client/app/components/queries/VisualizationEmbed.jsx
+++ b/client/app/components/queries/VisualizationEmbed.jsx
@@ -52,6 +52,7 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
+
+
+ Download as TSV File
+
+
0:
self.add_cors_headers(response.headers)
@@ -390,7 +391,8 @@ class QueryResultResource(BaseResource):
else:
abort(404, message="No cached result found for this query.")
- def make_json_response(self, query_result):
+ @staticmethod
+ def make_json_response(query_result):
data = json_dumps({"query_result": query_result.to_dict()})
headers = {"Content-Type": "application/json"}
return make_response(data, 200, headers)
@@ -398,7 +400,12 @@ class QueryResultResource(BaseResource):
@staticmethod
def make_csv_response(query_result):
headers = {"Content-Type": "text/csv; charset=UTF-8"}
- return make_response(serialize_query_result_to_csv(query_result), 200, headers)
+ return make_response(serialize_query_result_to_dsv(query_result, ","), 200, headers)
+
+ @staticmethod
+ def make_tsv_response(query_result):
+ headers = {"Content-Type": "text/tab-separated-values; charset=UTF-8"}
+ return make_response(serialize_query_result_to_dsv(query_result, "\t"), 200, headers)
@staticmethod
def make_excel_response(query_result):
diff --git a/redash/serializers/__init__.py b/redash/serializers/__init__.py
index 755f8fa01..b0c5fc820 100644
--- a/redash/serializers/__init__.py
+++ b/redash/serializers/__init__.py
@@ -14,7 +14,7 @@ from redash.models.parameterized_query import ParameterizedQuery
from .query_result import (
serialize_query_result,
- serialize_query_result_to_csv,
+ serialize_query_result_to_dsv,
serialize_query_result_to_xlsx,
)
diff --git a/redash/serializers/query_result.py b/redash/serializers/query_result.py
index 92fff2387..9470c1a48 100644
--- a/redash/serializers/query_result.py
+++ b/redash/serializers/query_result.py
@@ -78,14 +78,14 @@ def serialize_query_result(query_result, is_api_user):
return query_result.to_dict()
-def serialize_query_result_to_csv(query_result):
+def serialize_query_result_to_dsv(query_result, delimiter):
s = io.StringIO()
query_data = query_result.data
fieldnames, special_columns = _get_column_lists(query_data["columns"] or [])
- writer = csv.DictWriter(s, extrasaction="ignore", fieldnames=fieldnames)
+ writer = csv.DictWriter(s, extrasaction="ignore", fieldnames=fieldnames, delimiter=delimiter)
writer.writeheader()
for row in query_data["rows"]:
diff --git a/tests/serializers/test_query_results.py b/tests/serializers/test_query_results.py
index 49e02ce41..68649bf7f 100644
--- a/tests/serializers/test_query_results.py
+++ b/tests/serializers/test_query_results.py
@@ -6,7 +6,7 @@ from tests import BaseTestCase
from redash import models
from redash.utils import utcnow, json_dumps
-from redash.serializers import serialize_query_result, serialize_query_result_to_csv
+from redash.serializers import serialize_query_result, serialize_query_result_to_dsv
data = {
@@ -37,14 +37,14 @@ class QueryResultSerializationTest(BaseTestCase):
self.assertSetEqual(set(["data", "retrieved_at"]), set(serialized.keys()))
-class CsvSerializationTest(BaseTestCase):
- def get_csv_content(self):
+class DsvSerializationTest(BaseTestCase):
+ def delimited_content(self, delimiter):
query_result = self.factory.create_query_result(data=json_dumps(data))
- return serialize_query_result_to_csv(query_result)
+ return serialize_query_result_to_dsv(query_result, delimiter)
def test_serializes_booleans_correctly(self):
with self.app.test_request_context("/"):
- parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
+ parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
rows = list(parsed)
self.assertEqual(rows[0]["bool"], "true")
@@ -53,7 +53,7 @@ class CsvSerializationTest(BaseTestCase):
def test_serializes_datatime_with_correct_format(self):
with self.app.test_request_context("/"):
- parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
+ parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
rows = list(parsed)
self.assertEqual(rows[0]["datetime"], "26/05/19 12:39")
@@ -65,8 +65,19 @@ class CsvSerializationTest(BaseTestCase):
def test_serializes_datatime_as_is_in_case_of_error(self):
with self.app.test_request_context("/"):
- parsed = csv.DictReader(io.StringIO(self.get_csv_content()))
+ parsed = csv.DictReader(io.StringIO(self.delimited_content(",")))
rows = list(parsed)
self.assertEqual(rows[3]["datetime"], "459")
self.assertEqual(rows[3]["date"], "123")
+
+ def test_serializes_tsv_format(self):
+ delimiter = "\t"
+ with self.app.test_request_context("/"):
+ parsed = csv.DictReader(io.StringIO(self.delimited_content(delimiter)), delimiter=delimiter)
+ rows = list(parsed)
+
+ self.assertEqual(rows[0]["datetime"], "26/05/19 12:39")
+ self.assertEqual(rows[1]["bool"], "false")
+ self.assertEqual(rows[2]["date"], "")
+ self.assertEqual(rows[3]["datetime"], "459")