diff --git a/datasette/static/app.css b/datasette/static/app.css
index 9e95505f..2fc16940 100644
--- a/datasette/static/app.css
+++ b/datasette/static/app.css
@@ -118,6 +118,13 @@ form label {
display: inline-block;
width: 15%;
}
+.advanced-export form label {
+ width: auto;
+}
+.advanced-export input[type=submit] {
+ font-size: 0.6em;
+ margin-left: 1em;
+}
label.sort_by_desc {
width: auto;
padding-right: 1em;
@@ -272,3 +279,10 @@ a.not-underlined {
.facet-info a.cross:active {
text-decoration: none;
}
+.advanced-export {
+ margin-top: 1em;
+ padding: 0.01em 2em 0.01em 1em;
+ width: auto;
+ display: inline-block;
+ box-shadow: 1px 2px 8px 2px rgba(0,0,0,0.08);
+}
diff --git a/datasette/templates/query.html b/datasette/templates/query.html
index e04df160..8e2f9036 100644
--- a/datasette/templates/query.html
+++ b/datasette/templates/query.html
@@ -40,7 +40,7 @@
{% if rows %}
-
This data as JSON, CSV (download CSV)
+This data as JSON, CSV (advanced)
diff --git a/datasette/templates/table.html b/datasette/templates/table.html
index eda37bc7..bb2522d6 100644
--- a/datasette/templates/table.html
+++ b/datasette/templates/table.html
@@ -92,7 +92,7 @@
✎ View and edit SQL
{% endif %}
-This data as JSON, CSV (download CSV)
+This data as JSON{% if display_rows %}, CSV (advanced){% endif %}
{% if suggested_facets %}
@@ -137,6 +137,27 @@
Next page
{% endif %}
+{% if display_rows %}
+
+
Advanced export
+
JSON shape: default, array{% if primary_keys %}, object{% endif %}
+
+
+{% endif %}
+
{% if table_definition %}
{{ table_definition }}
{% endif %}
diff --git a/datasette/utils.py b/datasette/utils.py
index 005db87f..6253fb7a 100644
--- a/datasette/utils.py
+++ b/datasette/utils.py
@@ -170,6 +170,13 @@ def validate_sql_select(sql):
raise InvalidSql(msg)
+def append_querystring(url, querystring):
+ op = "&" if ("?" in url) else "?"
+ return "{}{}{}".format(
+ url, op, querystring
+ )
+
+
def path_with_added_args(request, args, path=None):
path = path or request.path
if isinstance(args, dict):
diff --git a/datasette/views/base.py b/datasette/views/base.py
index 0ca52e61..c3da3ab7 100644
--- a/datasette/views/base.py
+++ b/datasette/views/base.py
@@ -382,6 +382,12 @@ class BaseView(RenderMixin):
url_labels_extra = {}
if data.get("expandable_columns"):
url_labels_extra = {"_labels": "on"}
+ url_csv_args = {
+ "_size": "max",
+ **url_labels_extra
+ }
+ url_csv = path_with_format(request, "csv", url_csv_args)
+ url_csv_path = url_csv.split('?')[0]
context = {
**data,
**extras,
@@ -389,15 +395,9 @@ class BaseView(RenderMixin):
"url_json": path_with_format(request, "json", {
**url_labels_extra,
}),
- "url_csv": path_with_format(request, "csv", {
- "_size": "max",
- **url_labels_extra
- }),
- "url_csv_dl": path_with_format(request, "csv", {
- "_dl": "1",
- "_size": "max",
- **url_labels_extra
- }),
+ "url_csv": url_csv,
+ "url_csv_path": url_csv_path,
+ "url_csv_args": url_csv_args,
"extra_css_urls": self.ds.extra_css_urls(),
"extra_js_urls": self.ds.extra_js_urls(),
"datasette_version": __version__,
diff --git a/datasette/views/table.py b/datasette/views/table.py
index cb2c9ae5..89dec455 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -10,6 +10,7 @@ from datasette.utils import (
CustomRow,
Filters,
InterruptedError,
+ append_querystring,
compound_keys_after_sql,
escape_sqlite,
filters_should_redirect,
@@ -748,6 +749,7 @@ class TableView(RowTableShared):
"is_sortable": any(c["sortable"] for c in display_columns),
"path_with_replaced_args": path_with_replaced_args,
"path_with_removed_args": path_with_removed_args,
+ "append_querystring": append_querystring,
"request": request,
"sort": sort,
"sort_desc": sort_desc,
diff --git a/tests/test_html.py b/tests/test_html.py
index 98e54998..11c5fd43 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -274,9 +274,10 @@ def test_table_html_simple_primary_key(app_client):
] == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
-def test_table_csv_json_export_links(app_client):
+def test_table_csv_json_export_interface(app_client):
response = app_client.get('/fixtures/simple_primary_key')
assert response.status == 200
+ # The links at the top of the page
links = Soup(response.body, "html.parser").find("p", {
"class": "export-links"
}).findAll("a")
@@ -284,9 +285,28 @@ def test_table_csv_json_export_links(app_client):
expected = [
"simple_primary_key.json",
"simple_primary_key.csv?_size=max",
- "simple_primary_key.csv?_dl=1&_size=max"
+ "#export"
]
assert expected == actual
+ # And the advaced export box at the bottom:
+ div = Soup(response.body, "html.parser").find("div", {
+ "class": "advanced-export"
+ })
+ json_links = [a["href"].split("/")[-1] for a in div.find("p").findAll("a")]
+ assert [
+ "simple_primary_key.json",
+ "simple_primary_key.json?_shape=array",
+ "simple_primary_key.json?_shape=object"
+ ] == json_links
+ # And the CSV form
+ form = div.find("form")
+ assert form["action"].endswith("/simple_primary_key.csv")
+ inputs = [str(input) for input in form.findAll("input")]
+ assert [
+ '',
+ '',
+ ''
+ ] == inputs
def test_csv_json_export_links_include_labels_if_foreign_keys(app_client):
@@ -299,7 +319,7 @@ def test_csv_json_export_links_include_labels_if_foreign_keys(app_client):
expected = [
"facetable.json?_labels=on",
"facetable.csv?_labels=on&_size=max",
- "facetable.csv?_dl=1&_labels=on&_size=max"
+ "#export"
]
assert expected == actual