From daf5ca02ca9df56de1f920a6bd20e81dbad2686c Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 12 Mar 2024 13:44:07 -0700 Subject: [PATCH] homepage_actions() plugin hook, closes #2298 --- datasette/hookspecs.py | 5 +++++ datasette/templates/database.html | 1 - datasette/templates/index.html | 31 +++++++++++++++++++++++++++++ datasette/views/index.py | 18 ++++++++++++++++- docs/plugin_hooks.rst | 33 +++++++++++++++++++++++++++++++ tests/fixtures.py | 1 + tests/plugins/my_plugin.py | 12 +++++++++++ tests/test_plugins.py | 19 ++++++++++++++++++ 8 files changed, 118 insertions(+), 2 deletions(-) diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 1141ca75..5a8439b4 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -155,6 +155,11 @@ def database_actions(datasette, actor, database, request): """Links for the database actions menu""" +@hookspec +def homepage_actions(datasette, actor, request): + """Links for the homepage actions menu""" + + @hookspec def skip_csrf(datasette, scope): """Mechanism for skipping CSRF checks for certain requests""" diff --git a/datasette/templates/database.html b/datasette/templates/database.html index 02e6fb3d..b5a0edc4 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -44,7 +44,6 @@ {% endif %} - {{ top_database() }} {% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} diff --git a/datasette/templates/index.html b/datasette/templates/index.html index 203abca8..d08cdb10 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -7,6 +7,37 @@ {% block content %}

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

+{% set links = homepage_actions %}{% if links %} +
+ +
+{% endif %} + {{ top_homepage() }} {% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %} diff --git a/datasette/views/index.py b/datasette/views/index.py index 2cb18b1c..6546b7ae 100644 --- a/datasette/views/index.py +++ b/datasette/views/index.py @@ -1,6 +1,12 @@ import json -from datasette.utils import add_cors_headers, make_slot_function, CustomJSONEncoder +from datasette.plugins import pm +from datasette.utils import ( + add_cors_headers, + await_me_maybe, + make_slot_function, + CustomJSONEncoder, +) from datasette.utils.asgi import Response from datasette.version import __version__ @@ -131,6 +137,15 @@ class IndexView(BaseView): headers=headers, ) else: + homepage_actions = [] + for hook in pm.hook.homepage_actions( + datasette=self.ds, + actor=request.actor, + request=request, + ): + extra_links = await await_me_maybe(hook) + if extra_links: + homepage_actions.extend(extra_links) return await self.render( ["index.html"], request=request, @@ -144,5 +159,6 @@ class IndexView(BaseView): "top_homepage": make_slot_function( "top_homepage", self.ds, request ), + "homepage_actions": homepage_actions, }, ) diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 91db80f8..4ac391ef 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -1629,6 +1629,39 @@ This example adds a new database action for creating a table, if the user has th Example: `datasette-graphql `_, `datasette-edit-schema `_ +.. _plugin_hook_homepage_actions: + +homepage_actions(datasette, actor, request) +------------------------------------------- + +``datasette`` - :ref:`internals_datasette` + You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries. + +``actor`` - dictionary or None + The currently authenticated :ref:`actor `. + +``request`` - :ref:`internals_request` + The current HTTP request. + +This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the index page of the Datasette instance. + +This example adds a link an imagined tool for editing the homepage, only for signed in users: + +.. code-block:: python + + from datasette import hookimpl + + + @hookimpl + def homepage_actions(datasette, actor): + if actor: + return [ + { + "href": datasette.urls.path("/-/customize-homepage"), + "label": "Customize homepage", + } + ] + .. _plugin_hook_skip_csrf: skip_csrf(datasette, scope) diff --git a/tests/fixtures.py b/tests/fixtures.py index c3c77fce..61d240da 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -42,6 +42,7 @@ EXPECTED_PLUGINS = [ "extra_js_urls", "extra_template_vars", "forbidden", + "homepage_actions", "menu_links", "permission_allowed", "prepare_connection", diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 5f000537..94267f04 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -425,6 +425,18 @@ def database_actions(datasette, database, actor, request): ] +@hookimpl +def homepage_actions(datasette, actor, request): + if actor: + label = f"Custom homepage for: {actor['id']}" + return [ + { + "href": datasette.urls.path("/-/custom-homepage"), + "label": label, + } + ] + + @hookimpl def skip_csrf(scope): return scope["path"] == "/skip-csrf" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 9f69e4fa..dcc5de20 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -995,6 +995,25 @@ async def test_hook_database_actions(ds_client): ] +@pytest.mark.asyncio +async def test_hook_homepage_actions(ds_client): + response = await ds_client.get("/") + # No button for anonymous users + assert "Homepage actions" not in response.text + # Signed in user gets an action + response2 = await ds_client.get( + "/", cookies={"ds_actor": ds_client.actor_cookie({"id": "troy"})} + ) + assert "Homepage actions" in response2.text + assert get_actions_links(response2.text) == [ + { + "label": "Custom homepage for: troy", + "href": "/-/custom-homepage", + "description": None, + }, + ] + + def test_hook_skip_csrf(app_client): cookie = app_client.actor_cookie({"id": "test"}) csrf_response = app_client.post(