From 6e37f091edec35e2706197489f54fff5d890c63c Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 15 Jul 2018 19:33:30 -0700 Subject: [PATCH] Support title/description for canned queries, closes #342 Demo here: https://latest.datasette.io/fixtures/neighborhood_search --- datasette/app.py | 13 ++++++++++++- datasette/templates/database.html | 2 +- datasette/templates/query.html | 4 +++- datasette/views/base.py | 3 ++- datasette/views/database.py | 5 +---- datasette/views/table.py | 1 + docs/sql_queries.rst | 15 +++++++++++++-- tests/fixtures.py | 19 ++++++++++++------- tests/test_html.py | 17 +++++++++++++++++ 9 files changed, 62 insertions(+), 17 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 209cf44a..8fb772f3 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -174,6 +174,14 @@ class Datasette: ] return self._app_css_hash + def get_canned_queries(self, database_name): + names = self.metadata.get("databases", {}).get(database_name, {}).get( + "queries", {} + ).keys() + return [ + self.get_canned_query(database_name, name) for name in names + ] + def get_canned_query(self, database_name, query_name): query = self.metadata.get("databases", {}).get(database_name, {}).get( "queries", {} @@ -181,7 +189,10 @@ class Datasette: query_name ) if query: - return {"name": query_name, "sql": query} + if not isinstance(query, dict): + query = {"sql": query} + query["name"] = query_name + return query async def get_table_definition(self, database_name, table, type_="table"): table_definition_rows = list( diff --git a/datasette/templates/database.html b/datasette/templates/database.html index 7469fdb0..05acff84 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -51,7 +51,7 @@

Queries

{% endif %} diff --git a/datasette/templates/query.html b/datasette/templates/query.html index 35524b24..2fb461a1 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -21,7 +21,9 @@ {% block content %}
home / {{ database }}
-

{{ database }}

+

{{ metadata.title or database }}

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

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

diff --git a/datasette/views/base.py b/datasette/views/base.py index 4484ecf7..88f492f0 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -433,7 +433,7 @@ class BaseView(RenderMixin): async def custom_sql( self, request, name, hash, sql, editable=True, canned_query=None, - _size=None + metadata=None, _size=None ): params = request.raw_args if "sql" in params: @@ -483,6 +483,7 @@ class BaseView(RenderMixin): "named_parameter_values": named_parameter_values, "editable": editable, "canned_query": canned_query, + "metadata": metadata, "config": self.ds.config, }, templates diff --git a/datasette/views/database.py b/datasette/views/database.py index 6badc724..a25ea498 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -27,10 +27,7 @@ class DatabaseView(BaseView): "tables": tables, "hidden_count": len([t for t in tables if t["hidden"]]), "views": info["views"], - "queries": [ - {"name": query_name, "sql": query_sql} - for query_name, query_sql in (metadata.get("queries") or {}).items() - ], + "queries": self.ds.get_canned_queries(name), }, { "database_hash": hash, "show_hidden": request.args.get("_show_hidden"), diff --git a/datasette/views/table.py b/datasette/views/table.py index 281788e6..92c187ac 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -232,6 +232,7 @@ class TableView(RowTableShared): name, hash, canned_query["sql"], + metadata=canned_query, editable=False, canned_query=table, ) diff --git a/docs/sql_queries.rst b/docs/sql_queries.rst index 80d74574..4b5761e4 100644 --- a/docs/sql_queries.rst +++ b/docs/sql_queries.rst @@ -73,7 +73,9 @@ queries inside your ``metadata.json`` file. Here's an example:: "databases": { "sf-trees": { "queries": { - "just_species": "select qSpecies from Street_Tree_List" + "just_species": { + "sql": select qSpecies from Street_Tree_List" + } } } } @@ -92,6 +94,11 @@ For the above example, that URL would be:: /sf-trees/just_species +You can optionally include ``"title"`` and ``"description"`` keys to show a +title and description on the canned query page. As with regular table metadata +you can alternatively specify ``"description_html"`` to have your description +rendered as HTML (rather than having HTML special characters escaped). + Canned queries support named parameters, so if you include those in the SQL you will then be able to enter them using the form fields on the canned query page or by adding them to the URL. This means canned queries can be used to create @@ -111,7 +118,11 @@ In the canned query JSON it looks like this:: "databases": { "fixtures": { "queries": { - "neighborhood_search": "select neighborhood, facet_cities.name, state\nfrom facetable join facet_cities on facetable.city_id = facet_cities.id\nwhere neighborhood like '%' || :text || '%' order by neighborhood;" + "neighborhood_search": { + "sql": "select neighborhood, facet_cities.name, state\nfrom facetable join facet_cities on facetable.city_id = facet_cities.id\nwhere neighborhood like '%' || :text || '%' order by neighborhood;", + "title": "Search neighborhoods", + "description_html": "Demonstrating simple like search" + } } } } diff --git a/tests/fixtures.py b/tests/fixtures.py index cc1734f4..f2dc7502 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -161,13 +161,18 @@ METADATA = { }, 'queries': { 'pragma_cache_size': 'PRAGMA cache_size;', - 'neighborhood_search': ''' - select neighborhood, facet_cities.name, state - from facetable - join facet_cities on facetable.city_id = facet_cities.id - where neighborhood like '%' || :text || '%' - order by neighborhood; - ''' + 'neighborhood_search': { + 'sql': ''' + select neighborhood, facet_cities.name, state + from facetable + join facet_cities + on facetable.city_id = facet_cities.id + where neighborhood like '%' || :text || '%' + order by neighborhood; + ''', + 'title': 'Search neighborhoods', + 'description_html': 'Demonstrating simple like search', + }, } }, } diff --git a/tests/test_html.py b/tests/test_html.py index 12e70af9..0791ece5 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -754,3 +754,20 @@ def test_404_trailing_slash_redirect(app_client, path, expected_redirect): response = app_client.get(path, allow_redirects=False) assert 302 == response.status assert expected_redirect == response.headers["Location"] + + +def test_canned_query_with_custom_metadata(app_client): + response = app_client.get("/fixtures/neighborhood_search?text=town") + assert response.status == 200 + soup = Soup(response.body, "html.parser") + assert "Search neighborhoods" == soup.find("h1").text + assert ( + """ +""".strip() + == soup.find("div", {"class": "metadata-description"}).prettify().strip() + )