Edit SQL button on canned queries, closes #1019

This commit is contained in:
Simon Willison 2020-10-13 20:44:18 -07:00
commit f3a087a578
4 changed files with 85 additions and 4 deletions

View file

@ -447,3 +447,9 @@ svg.dropdown-menu-icon {
border-right: 5px solid transparent; border-right: 5px solid transparent;
border-bottom: 5px solid #666; border-bottom: 5px solid #666;
} }
.canned-query-edit-sql {
padding-left: 0.5em;
position: relative;
top: 1px;
}

View file

@ -54,6 +54,7 @@
<button id="sql-format" type="button" hidden>Format SQL</button> <button id="sql-format" type="button" hidden>Format SQL</button>
{% if canned_write %}<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">{% endif %} {% if canned_write %}<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">{% endif %}
<input type="submit" value="Run SQL"> <input type="submit" value="Run SQL">
{% if canned_query and edit_sql_url %}<a href="{{ edit_sql_url }}" class="canned-query-edit-sql">Edit SQL</a>{% endif %}
</p> </p>
</form> </form>

View file

@ -2,7 +2,7 @@ import os
import itertools import itertools
import jinja2 import jinja2
import json import json
from urllib.parse import parse_qsl from urllib.parse import parse_qsl, urlencode
from datasette.utils import ( from datasette.utils import (
check_visibility, check_visibility,
@ -11,6 +11,7 @@ from datasette.utils import (
is_url, is_url,
path_with_added_args, path_with_added_args,
path_with_removed_args, path_with_removed_args,
InvalidSql,
) )
from datasette.utils.asgi import AsgiFileDownload, Response, Forbidden from datasette.utils.asgi import AsgiFileDownload, Response, Forbidden
from datasette.plugins import pm from datasette.plugins import pm
@ -301,6 +302,10 @@ class QueryView(DataView):
), ),
) )
allow_execute_sql = await self.ds.permission_allowed(
request.actor, "execute-sql", database, default=True
)
async def extra_template(): async def extra_template():
display_rows = [] display_rows = []
for row in results.rows: for row in results.rows:
@ -329,12 +334,38 @@ class QueryView(DataView):
) )
display_row.append(display_value) display_row.append(display_value)
display_rows.append(display_row) display_rows.append(display_row)
# Show 'Edit SQL' button only if:
# - User is allowed to execute SQL
# - SQL is an approved SELECT statement
# - No magic parameters, so no :_ in the SQL string
edit_sql_url = None
is_validated_sql = False
try:
validate_sql_select(sql)
is_validated_sql = True
except InvalidSql:
pass
if allow_execute_sql and is_validated_sql and ":_" not in sql:
edit_sql_url = (
self.database_url(database)
+ "?"
+ urlencode(
{
**{
"sql": sql,
},
**named_parameter_values,
}
)
)
return { return {
"display_rows": display_rows, "display_rows": display_rows,
"custom_sql": True, "custom_sql": True,
"named_parameter_values": named_parameter_values, "named_parameter_values": named_parameter_values,
"editable": editable, "editable": editable,
"canned_query": canned_query, "canned_query": canned_query,
"edit_sql_url": edit_sql_url,
"metadata": metadata, "metadata": metadata,
"config": self.ds.config_dict(), "config": self.ds.config_dict(),
"request": request, "request": request,
@ -352,9 +383,7 @@ class QueryView(DataView):
"columns": columns, "columns": columns,
"query": {"sql": sql, "params": params}, "query": {"sql": sql, "params": params},
"private": private, "private": private,
"allow_execute_sql": await self.ds.permission_allowed( "allow_execute_sql": allow_execute_sql,
request.actor, "execute-sql", database, default=True
),
}, },
extra_template, extra_template,
templates, templates,

View file

@ -1403,3 +1403,48 @@ def test_base_url_config(base_url, path):
"href_or_src": href, "href_or_src": href,
"element_parent": str(el.parent), "element_parent": str(el.parent),
} }
@pytest.mark.parametrize(
"path,expected",
[
(
"/fixtures/neighborhood_search",
"/fixtures?sql=%0Aselect+neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable.city_id+%3D+facet_cities.id%0Awhere+neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+neighborhood%3B%0A&amp;text=",
),
(
"/fixtures/neighborhood_search?text=ber",
"/fixtures?sql=%0Aselect+neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable.city_id+%3D+facet_cities.id%0Awhere+neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+neighborhood%3B%0A&amp;text=ber",
),
("/fixtures/pragma_cache_size", None),
(
"/fixtures/𝐜𝐢𝐭𝐢𝐞𝐬",
"/fixtures?sql=select+id%2C+name+from+facet_cities+order+by+id+limit+1%3B",
),
("/fixtures/magic_parameters", None),
],
)
def test_edit_sql_link_on_canned_queries(app_client, path, expected):
response = app_client.get(path)
expected_link = '<a href="{}" class="canned-query-edit-sql">Edit SQL</a>'.format(
expected
)
if expected:
assert expected_link in response.text
else:
assert "Edit SQL" not in response.text
@pytest.mark.parametrize("permission_allowed", [True, False])
def test_edit_sql_link_not_shown_if_user_lacks_permission(permission_allowed):
with make_app_client(
metadata={
"allow_sql": None if permission_allowed else {"id": "not-you"},
"databases": {"fixtures": {"queries": {"simple": "select 1 + 1"}}},
}
) as client:
response = client.get("/fixtures/simple")
if permission_allowed:
assert "Edit SQL" in response.text
else:
assert "Edit SQL" not in response.text