From 0c814ceb34841ec71f264cff1ca20d8ab767cddd Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 30 Jan 2024 15:55:41 -0800 Subject: [PATCH] top_homepage() plugin hook, refs #1191 --- datasette/hookspecs.py | 25 +++++++ datasette/templates/index.html | 2 + datasette/views/index.py | 25 ++++++- docs/plugin_hooks.rst | 115 +++++++++++++++++++++++++++++++++ tests/test_plugins.py | 19 ++++++ 5 files changed, 184 insertions(+), 2 deletions(-) diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index b6975dce7a..deea6cb9bc 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -158,3 +158,28 @@ def skip_csrf(datasette, scope): @hookspec def handle_exception(datasette, request, exception): """Handle an uncaught exception. Can return a Response or None.""" + + +@hookspec +def top_homepage(datasette, request): + """HTML to include at the top of the homepage""" + + +# @hookspec +# def top_database(datasette, request, database): +# """HTML to include at the top of the database page""" + + +# @hookspec +# def top_table(datasette, request, database, table): +# """HTML to include at the top of the table page""" + + +# @hookspec +# def top_row(datasette, request, database, table, row): +# """HTML to include at the top of the row page""" + + +# @hookspec +# def top_query(datasette, request, database, query): +# """HTML to include at the top of the query page""" diff --git a/datasette/templates/index.html b/datasette/templates/index.html index 06e0963512..203abca8e7 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -7,6 +7,8 @@ {% block content %}

{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}

+{{ top_homepage() }} + {% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} {% for database in databases %} diff --git a/datasette/views/index.py b/datasette/views/index.py index 95b2930250..b95daaae38 100644 --- a/datasette/views/index.py +++ b/datasette/views/index.py @@ -1,10 +1,12 @@ -import hashlib import json -from datasette.utils import add_cors_headers, CustomJSONEncoder +from datasette.plugins import pm +from datasette.utils import add_cors_headers, await_me_maybe, CustomJSONEncoder from datasette.utils.asgi import Response from datasette.version import __version__ +from markupsafe import Markup + from .base import BaseView @@ -142,5 +144,24 @@ async def get(self, request): "private": not await self.ds.permission_allowed( None, "view-instance" ), + "top_homepage": include_block_function( + "top_homepage", self.ds, request + ), }, ) + + +def include_block_function(name, datasette, request, **kwargs): + method = getattr(pm.hook, name, None) + if method is None: + raise Exception("No hook found for {}".format(name)) + + async def inner(): + html_bits = [] + for hook in method(datasette=datasette, request=request, **kwargs): + html = await await_me_maybe(hook) + if html is not None: + html_bits.append(html) + return Markup("".join(html_bits)) + + return inner diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 9115c3dfa6..7e053d05b8 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -1641,3 +1641,118 @@ This hook is responsible for returning a dictionary corresponding to Datasette : return metadata Example: `datasette-remote-metadata plugin `__ + + +@hookimpl +def top_homepage(datasette, request): + """HTML to include at the top of the homepage""" + + +@hookimpl +def top_database(datasette, request, database): + """HTML to include at the top of the database page""" + + +@hookimpl +def top_table(datasette, request, database, table): + """HTML to include at the top of the table page""" + + +@hookimpl +def top_row(datasette, request, database, table, row): + """HTML to include at the top of the row page""" + + +@hookimpl +def top_query(datasette, request, database, query): + """HTML to include at the top of the query page""" + + +.. _plugin_hook_top_homepage: + +top_homepage(datasette, request) +-------------------------------- + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. + +``request`` - :ref:`internals_request` + The current HTTP request. + +Returns HTML to be displayed at the top of the Datasette homepage. + +.. _plugin_hook_top_database: + +top_database(datasette, request, database) +------------------------------------------ + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. + +``request`` - :ref:`internals_request` + The current HTTP request. + +``database`` - string + The name of the database. + +Returns HTML to be displayed at the top of the database page. + +.. _plugin_hook_top_table: + +top_table(datasette, request, database, table) +--------------------------------------------- + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. + +``request`` - :ref:`internals_request` + The current HTTP request. + +``database`` - string + The name of the database. + +``table`` - string + The name of the table. + +Returns HTML to be displayed at the top of the table page. + +.. _plugin_hook_top_row: + +top_row(datasette, request, database, table, row) +------------------------------------------------ + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. + +``request`` - :ref:`internals_request` + The current HTTP request. + +``database`` - string + The name of the database. + +``table`` - string + The name of the table. + +``row`` - ``sqlite.Row`` + The SQLite row object being displayed. + +Returns HTML to be displayed at the top of the row page. + +.. _plugin_hook_top_query: + +top_query(datasette, request, database, query) +--------------------------------------------- + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. + +``request`` - :ref:`internals_request` + The current HTTP request. + +``database`` - string + The name of the database. + +``query`` - string + The name of the canned query. + +Returns HTML to be displayed at the top of the canned query page. diff --git a/tests/test_plugins.py b/tests/test_plugins.py index bdd4ba4943..346c55eb9b 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1334,3 +1334,22 @@ def jinja2_environment_from_request(self, request, env): assert "Hello museums!" in response2.text finally: pm.unregister(name="EnvironmentPlugin") + + +@pytest.mark.asyncio +async def test_hook_top_homepage(): + class HookPlugin: + __name__ = "HookPlugin" + + @hookimpl + def top_homepage(self, request): + return "XXX-YYY: " + request.args["z"] + + try: + pm.register(HookPlugin(), name="HookPlugin") + datasette = Datasette(memory=True) + response = await datasette.client.get("/?z=foo") + assert response.status_code == 200 + assert "XXX-YYY: foo" in response.text + finally: + pm.unregister(name="HookPlugin")