From 6204ebb7d507b08b81f13cc5b9c176f62b499634 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 17 Jun 2018 23:03:22 -0700 Subject: [PATCH] Improved UI for CSV/JSON export, closes #266 --- datasette/static/app.css | 14 ++++++++++++++ datasette/templates/query.html | 2 +- datasette/templates/table.html | 23 ++++++++++++++++++++++- datasette/utils.py | 7 +++++++ datasette/views/base.py | 18 +++++++++--------- datasette/views/table.py | 2 ++ tests/test_html.py | 26 +++++++++++++++++++++++--- 7 files changed, 78 insertions(+), 14 deletions(-) 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 %} - + 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 %} - + {% if suggested_facets %}

@@ -137,6 +137,27 @@

Next page

{% endif %} +{% if display_rows %} +
+

Advanced export

+

JSON shape: default, array{% if primary_keys %}, object{% endif %}

+
+

+ CSV options: + + {% if expandable_columns %}{% endif %} + {% if next_url %}{% endif %} + + {% for key, value in url_csv_args.items() %} + {% if key != "_labels" %} + + {% endif %} + {% endfor %} +

+ +
+{% 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