kopia lustrzana https://github.com/simonw/datasette
Improved UI for CSV/JSON export, closes #266
rodzic
fc3660cfad
commit
83f4ef7ec7
|
@ -118,6 +118,13 @@ form label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 15%;
|
width: 15%;
|
||||||
}
|
}
|
||||||
|
.advanced-export form label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.advanced-export input[type=submit] {
|
||||||
|
font-size: 0.6em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
label.sort_by_desc {
|
label.sort_by_desc {
|
||||||
width: auto;
|
width: auto;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
|
@ -272,3 +279,10 @@ a.not-underlined {
|
||||||
.facet-info a.cross:active {
|
.facet-info a.cross:active {
|
||||||
text-decoration: none;
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if rows %}
|
{% if rows %}
|
||||||
<p class="export-links">This data as <a href="{{ url_json }}">JSON</a>, <a href="{{ url_csv }}">CSV</a> (<a href="{{ url_csv_dl }}">download CSV</a>)</p>
|
<p class="export-links">This data as <a href="{{ url_json }}">JSON</a>, <a href="{{ url_csv }}">CSV</a> (<a href="#export">advanced</a>)</p>
|
||||||
<table class="rows-and-columns">
|
<table class="rows-and-columns">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
<p><a class="not-underlined" title="{{ query.sql }}" href="/{{ database }}-{{ database_hash }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
<p><a class="not-underlined" title="{{ query.sql }}" href="/{{ database }}-{{ database_hash }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="export-links">This data as <a href="{{ url_json }}">JSON</a>, <a href="{{ url_csv }}">CSV</a> (<a href="{{ url_csv_dl }}">download CSV</a>)</p>
|
<p class="export-links">This data as <a href="{{ url_json }}">JSON</a>{% if display_rows %}, <a href="{{ url_csv }}">CSV</a> (<a href="#export">advanced</a>){% endif %}</p>
|
||||||
|
|
||||||
{% if suggested_facets %}
|
{% if suggested_facets %}
|
||||||
<p class="suggested-facets">
|
<p class="suggested-facets">
|
||||||
|
@ -137,6 +137,27 @@
|
||||||
<p><a href="{{ next_url }}">Next page</a></p>
|
<p><a href="{{ next_url }}">Next page</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if display_rows %}
|
||||||
|
<div id="export" class="advanced-export">
|
||||||
|
<h3>Advanced export</h3>
|
||||||
|
<p>JSON shape: <a href="{{ url_json }}">default</a>, <a href="{{ append_querystring(url_json, '_shape=array') }}">array</a>{% if primary_keys %}, <a href="{{ append_querystring(url_json, '_shape=object') }}">object</a>{% endif %}</p>
|
||||||
|
<form action="{{ url_csv_path }}" method="get">
|
||||||
|
<p>
|
||||||
|
CSV options:
|
||||||
|
<label><input type="checkbox" name="_dl"> download file</label>
|
||||||
|
{% if expandable_columns %}<label><input type="checkbox" name="_labels"> expand labels</label>{% endif %}
|
||||||
|
{% if next_url %}<label><input type="checkbox" name="_stream"> stream all records</label>{% endif %}
|
||||||
|
<input type="submit" value="Export CSV">
|
||||||
|
{% for key, value in url_csv_args.items() %}
|
||||||
|
{% if key != "_labels" %}
|
||||||
|
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if table_definition %}
|
{% if table_definition %}
|
||||||
<pre>{{ table_definition }}</pre>
|
<pre>{{ table_definition }}</pre>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -170,6 +170,13 @@ def validate_sql_select(sql):
|
||||||
raise InvalidSql(msg)
|
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):
|
def path_with_added_args(request, args, path=None):
|
||||||
path = path or request.path
|
path = path or request.path
|
||||||
if isinstance(args, dict):
|
if isinstance(args, dict):
|
||||||
|
|
|
@ -382,6 +382,12 @@ class BaseView(RenderMixin):
|
||||||
url_labels_extra = {}
|
url_labels_extra = {}
|
||||||
if data.get("expandable_columns"):
|
if data.get("expandable_columns"):
|
||||||
url_labels_extra = {"_labels": "on"}
|
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 = {
|
context = {
|
||||||
**data,
|
**data,
|
||||||
**extras,
|
**extras,
|
||||||
|
@ -389,15 +395,9 @@ class BaseView(RenderMixin):
|
||||||
"url_json": path_with_format(request, "json", {
|
"url_json": path_with_format(request, "json", {
|
||||||
**url_labels_extra,
|
**url_labels_extra,
|
||||||
}),
|
}),
|
||||||
"url_csv": path_with_format(request, "csv", {
|
"url_csv": url_csv,
|
||||||
"_size": "max",
|
"url_csv_path": url_csv_path,
|
||||||
**url_labels_extra
|
"url_csv_args": url_csv_args,
|
||||||
}),
|
|
||||||
"url_csv_dl": path_with_format(request, "csv", {
|
|
||||||
"_dl": "1",
|
|
||||||
"_size": "max",
|
|
||||||
**url_labels_extra
|
|
||||||
}),
|
|
||||||
"extra_css_urls": self.ds.extra_css_urls(),
|
"extra_css_urls": self.ds.extra_css_urls(),
|
||||||
"extra_js_urls": self.ds.extra_js_urls(),
|
"extra_js_urls": self.ds.extra_js_urls(),
|
||||||
"datasette_version": __version__,
|
"datasette_version": __version__,
|
||||||
|
|
|
@ -10,6 +10,7 @@ from datasette.utils import (
|
||||||
CustomRow,
|
CustomRow,
|
||||||
Filters,
|
Filters,
|
||||||
InterruptedError,
|
InterruptedError,
|
||||||
|
append_querystring,
|
||||||
compound_keys_after_sql,
|
compound_keys_after_sql,
|
||||||
escape_sqlite,
|
escape_sqlite,
|
||||||
filters_should_redirect,
|
filters_should_redirect,
|
||||||
|
@ -748,6 +749,7 @@ class TableView(RowTableShared):
|
||||||
"is_sortable": any(c["sortable"] for c in display_columns),
|
"is_sortable": any(c["sortable"] for c in display_columns),
|
||||||
"path_with_replaced_args": path_with_replaced_args,
|
"path_with_replaced_args": path_with_replaced_args,
|
||||||
"path_with_removed_args": path_with_removed_args,
|
"path_with_removed_args": path_with_removed_args,
|
||||||
|
"append_querystring": append_querystring,
|
||||||
"request": request,
|
"request": request,
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
"sort_desc": sort_desc,
|
"sort_desc": sort_desc,
|
||||||
|
|
|
@ -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')]
|
] == [[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')
|
response = app_client.get('/fixtures/simple_primary_key')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
# The links at the top of the page
|
||||||
links = Soup(response.body, "html.parser").find("p", {
|
links = Soup(response.body, "html.parser").find("p", {
|
||||||
"class": "export-links"
|
"class": "export-links"
|
||||||
}).findAll("a")
|
}).findAll("a")
|
||||||
|
@ -284,9 +285,28 @@ def test_table_csv_json_export_links(app_client):
|
||||||
expected = [
|
expected = [
|
||||||
"simple_primary_key.json",
|
"simple_primary_key.json",
|
||||||
"simple_primary_key.csv?_size=max",
|
"simple_primary_key.csv?_size=max",
|
||||||
"simple_primary_key.csv?_dl=1&_size=max"
|
"#export"
|
||||||
]
|
]
|
||||||
assert expected == actual
|
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 [
|
||||||
|
'<input name="_dl" type="checkbox"/>',
|
||||||
|
'<input type="submit" value="Export CSV"/>',
|
||||||
|
'<input name="_size" type="hidden" value="max"/>'
|
||||||
|
] == inputs
|
||||||
|
|
||||||
|
|
||||||
def test_csv_json_export_links_include_labels_if_foreign_keys(app_client):
|
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 = [
|
expected = [
|
||||||
"facetable.json?_labels=on",
|
"facetable.json?_labels=on",
|
||||||
"facetable.csv?_labels=on&_size=max",
|
"facetable.csv?_labels=on&_size=max",
|
||||||
"facetable.csv?_dl=1&_labels=on&_size=max"
|
"#export"
|
||||||
]
|
]
|
||||||
assert expected == actual
|
assert expected == actual
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue