From 5b252a2348aef3e70e6e436368a1a1df5ea03a4a Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 29 Apr 2020 00:47:12 -0700 Subject: [PATCH] First working version of writable canned queries, refs #698 --- datasette/static/app.css | 6 +++++ datasette/templates/query.html | 10 ++++++-- datasette/views/database.py | 46 ++++++++++++++++++++++++++++++---- datasette/views/table.py | 18 +++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/datasette/static/app.css b/datasette/static/app.css index 92f268ae6f..b733fc30c3 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -351,3 +351,9 @@ p.zero-results { .type-float, .type-int { color: #666; } + +.success { + padding: 1em; + border: 1px solid green; + background-color: #c7fbc7; +} diff --git a/datasette/templates/query.html b/datasette/templates/query.html index 2c8c05a02f..dca80ce37d 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -27,11 +27,12 @@ {% endblock %} {% block content %} +

{{ metadata.title or database }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} -
+

Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %} {% if hide_sql %}(show){% else %}(hide){% endif %}

{% if not hide_sql %} {% if editable and config.allow_sql %} @@ -74,7 +75,12 @@

Query parameters

{% else %} -

0 results

+ {% if success_message %} +

{{ success_message }}

+ {% endif %} + {% if not canned_write %} +

0 results

+ {% endif %} {% endif %} {% include "_codemirror_foot.html" %} diff --git a/datasette/views/database.py b/datasette/views/database.py index 15545fb85e..8753802393 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -106,6 +106,8 @@ async def data( canned_query=None, metadata=None, _size=None, + named_parameters=None, + write=False, ): params = {key: request.args.get(key) for key in request.args} if "sql" in params: @@ -113,7 +115,7 @@ async def data( if "_shape" in params: params.pop("_shape") # Extract any :named parameters - named_parameters = self.re_named_parameter.findall(sql) + named_parameters = named_parameters or self.re_named_parameter.findall(sql) named_parameter_values = { named_parameter: params.get(named_parameter) or "" for named_parameter in named_parameters @@ -129,12 +131,46 @@ async def data( extra_args["custom_time_limit"] = int(params["_timelimit"]) if _size: extra_args["page_size"] = _size - results = await self.ds.execute( - database, sql, params, truncate=True, **extra_args - ) - columns = [r[0] for r in results.description] templates = ["query-{}.html".format(to_css_class(database)), "query.html"] + + # Execute query - as write or as read + if write: + if request.method == "POST": + params = await request.post_vars() + write_ok = await self.ds.databases[database].execute_write( + sql, params, block=True + ) + return self.redirect(request, request.path + '?_success=Query+executed_successfully') + else: + async def extra_template(): + return { + "request": request, + "path_with_added_args": path_with_added_args, + "path_with_removed_args": path_with_removed_args, + "named_parameter_values": named_parameter_values, + "canned_query": canned_query, + "success_message": request.raw_args.get("_success") or "", + "canned_write": True, + } + + return ( + { + "database": database, + "rows": [], + "truncated": False, + "columns": [], + "query": {"sql": sql, "params": params}, + }, + extra_template, + templates, + ) + else: # Not a write + results = await self.ds.execute( + database, sql, params, truncate=True, **extra_args + ) + columns = [r[0] for r in results.description] + if canned_query: templates.insert( 0, diff --git a/datasette/views/table.py b/datasette/views/table.py index 2e9515c33d..79bf8b0852 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -221,6 +221,22 @@ async def display_columns_and_rows( class TableView(RowTableShared): name = "table" + async def post(self, request, db_name, table_and_format): + # Handle POST to a canned query + canned_query = self.ds.get_canned_query(db_name, table_and_format) + assert canned_query, "You may only POST to a canned query" + return await QueryView(self.ds).data( + request, + db_name, + None, + canned_query["sql"], + metadata=canned_query, + editable=False, + canned_query=table_and_format, + named_parameters=canned_query.get("params"), + write=bool(canned_query.get("write")), + ) + async def data( self, request, @@ -241,6 +257,8 @@ async def data( metadata=canned_query, editable=False, canned_query=table, + named_parameters=canned_query.get("params"), + write=bool(canned_query.get("write")), ) db = self.ds.databases[database]