diff --git a/datasette/views/database.py b/datasette/views/database.py index d9fe2b49..f835dfac 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -361,6 +361,7 @@ class QueryView(DataView): database=database, datasette=self.ds, ) + plugin_value = await await_me_maybe(plugin_value) if plugin_value is not None: display_value = plugin_value else: diff --git a/datasette/views/table.py b/datasette/views/table.py index 876a0c81..3d25a1a5 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -198,6 +198,7 @@ class RowTableShared(DataView): database=database, datasette=self.ds, ) + plugin_display_value = await await_me_maybe(plugin_display_value) if plugin_display_value is not None: display_value = plugin_display_value elif isinstance(value, bytes): diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 64c56309..5cdb1623 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -370,7 +370,7 @@ Lets you customize the display of values within table cells in the HTML table vi The name of the database ``datasette`` - :ref:`internals_datasette` - You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries. If your hook returns ``None``, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value. @@ -378,6 +378,8 @@ If the hook returns a string, that string will be rendered in the table cell. If you want to return HTML markup you can do so by returning a ``jinja2.Markup`` object. +You can also return an awaitable function which returns a value. + Datasette will loop through all available ``render_cell`` hooks and display the value returned by the first one that does not return ``None``. Here is an example of a custom ``render_cell()`` plugin which looks for values that are a JSON string matching the following format:: diff --git a/tests/fixtures.py b/tests/fixtures.py index 873f9d55..880e4347 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -644,6 +644,7 @@ INSERT INTO simple_primary_key VALUES (1, 'hello'); INSERT INTO simple_primary_key VALUES (2, 'world'); INSERT INTO simple_primary_key VALUES (3, ''); INSERT INTO simple_primary_key VALUES (4, 'RENDER_CELL_DEMO'); +INSERT INTO simple_primary_key VALUES (5, 'RENDER_CELL_ASYNC'); INSERT INTO primary_key_multiple_columns VALUES (1, 'hey', 'world'); INSERT INTO primary_key_multiple_columns_explicit_label VALUES (1, 'hey', 'world2'); diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 59ac8add..75c76ea8 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -97,21 +97,29 @@ def extra_body_script( @hookimpl def render_cell(value, column, table, database, datasette): - # Render some debug output in cell with value RENDER_CELL_DEMO - if value != "RENDER_CELL_DEMO": - return None - return json.dumps( - { - "column": column, - "table": table, - "database": database, - "config": datasette.plugin_config( - "name-of-plugin", - database=database, - table=table, - ), - } - ) + async def inner(): + # Render some debug output in cell with value RENDER_CELL_DEMO + if value == "RENDER_CELL_DEMO": + return json.dumps( + { + "column": column, + "table": table, + "database": database, + "config": datasette.plugin_config( + "name-of-plugin", + database=database, + table=table, + ), + } + ) + elif value == "RENDER_CELL_ASYNC": + return ( + await datasette.get_database(database).execute( + "select 'RENDER_CELL_ASYNC_RESULT'" + ) + ).single_value() + + return inner @hookimpl diff --git a/tests/test_api.py b/tests/test_api.py index 0049d76d..83cca521 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -415,7 +415,7 @@ def test_database_page(app_client): "name": "simple_primary_key", "columns": ["id", "content"], "primary_keys": ["id"], - "count": 4, + "count": 5, "hidden": False, "fts_table": None, "foreign_keys": { @@ -652,6 +652,7 @@ def test_custom_sql(app_client): {"content": "world"}, {"content": ""}, {"content": "RENDER_CELL_DEMO"}, + {"content": "RENDER_CELL_ASYNC"}, ] == data["rows"] assert ["content"] == data["columns"] assert "fixtures" == data["database"] @@ -693,6 +694,7 @@ def test_table_json(app_client): {"id": "2", "content": "world"}, {"id": "3", "content": ""}, {"id": "4", "content": "RENDER_CELL_DEMO"}, + {"id": "5", "content": "RENDER_CELL_ASYNC"}, ] @@ -723,6 +725,7 @@ def test_table_shape_arrays(app_client): ["2", "world"], ["3", ""], ["4", "RENDER_CELL_DEMO"], + ["5", "RENDER_CELL_ASYNC"], ] == response.json["rows"] @@ -736,7 +739,13 @@ def test_table_shape_arrayfirst(app_client): } ) ) - assert ["hello", "world", "", "RENDER_CELL_DEMO"] == response.json + assert [ + "hello", + "world", + "", + "RENDER_CELL_DEMO", + "RENDER_CELL_ASYNC", + ] == response.json def test_table_shape_objects(app_client): @@ -746,6 +755,7 @@ def test_table_shape_objects(app_client): {"id": "2", "content": "world"}, {"id": "3", "content": ""}, {"id": "4", "content": "RENDER_CELL_DEMO"}, + {"id": "5", "content": "RENDER_CELL_ASYNC"}, ] == response.json["rows"] @@ -756,6 +766,7 @@ def test_table_shape_array(app_client): {"id": "2", "content": "world"}, {"id": "3", "content": ""}, {"id": "4", "content": "RENDER_CELL_DEMO"}, + {"id": "5", "content": "RENDER_CELL_ASYNC"}, ] == response.json @@ -768,6 +779,7 @@ def test_table_shape_array_nl(app_client): {"id": "2", "content": "world"}, {"id": "3", "content": ""}, {"id": "4", "content": "RENDER_CELL_DEMO"}, + {"id": "5", "content": "RENDER_CELL_ASYNC"}, ] == results @@ -788,6 +800,7 @@ def test_table_shape_object(app_client): "2": {"id": "2", "content": "world"}, "3": {"id": "3", "content": ""}, "4": {"id": "4", "content": "RENDER_CELL_DEMO"}, + "5": {"id": "5", "content": "RENDER_CELL_ASYNC"}, } == response.json @@ -1145,12 +1158,21 @@ def test_searchable_invalid_column(app_client): ("/fixtures/simple_primary_key.json?content=hello", [["1", "hello"]]), ( "/fixtures/simple_primary_key.json?content__contains=o", - [["1", "hello"], ["2", "world"], ["4", "RENDER_CELL_DEMO"]], + [ + ["1", "hello"], + ["2", "world"], + ["4", "RENDER_CELL_DEMO"], + ], ), ("/fixtures/simple_primary_key.json?content__exact=", [["3", ""]]), ( "/fixtures/simple_primary_key.json?content__not=world", - [["1", "hello"], ["3", ""], ["4", "RENDER_CELL_DEMO"]], + [ + ["1", "hello"], + ["3", ""], + ["4", "RENDER_CELL_DEMO"], + ["5", "RENDER_CELL_ASYNC"], + ], ), ], ) @@ -1163,7 +1185,11 @@ def test_table_filter_queries_multiple_of_same_type(app_client): response = app_client.get( "/fixtures/simple_primary_key.json?content__not=world&content__not=hello" ) - assert [["3", ""], ["4", "RENDER_CELL_DEMO"]] == response.json["rows"] + assert [ + ["3", ""], + ["4", "RENDER_CELL_DEMO"], + ["5", "RENDER_CELL_ASYNC"], + ] == response.json["rows"] @pytest.mark.skipif(not detect_json1(), reason="Requires the SQLite json1 module") @@ -1293,6 +1319,7 @@ def test_view(app_client): {"upper_content": "WORLD", "content": "world"}, {"upper_content": "", "content": ""}, {"upper_content": "RENDER_CELL_DEMO", "content": "RENDER_CELL_DEMO"}, + {"upper_content": "RENDER_CELL_ASYNC", "content": "RENDER_CELL_ASYNC"}, ] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 0c01b7ae..9bda7420 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -185,6 +185,11 @@ def test_hook_render_cell_demo(app_client): } == json.loads(td.string) +def test_hook_render_cell_async(app_client): + response = app_client.get("/fixtures?sql=select+'RENDER_CELL_ASYNC'") + assert b"RENDER_CELL_ASYNC_RESULT" in response.body + + def test_plugin_config(app_client): assert {"depth": "table"} == app_client.ds.plugin_config( "name-of-plugin", database="fixtures", table="sortable"