view_actions plugin hook, closes #2297

pull/2082/merge
Simon Willison 2024-03-12 14:25:07 -07:00
rodzic daf5ca02ca
commit 909c85cd2b
7 zmienionych plików z 85 dodań i 21 usunięć

Wyświetl plik

@ -145,6 +145,11 @@ def table_actions(datasette, actor, database, table, request):
"""Links for the table actions menu"""
@hookspec
def view_actions(datasette, actor, database, view, request):
"""Links for the view actions menu"""
@hookspec
def query_actions(datasette, actor, database, query_name, request, sql, params):
"""Links for the query and canned query actions menu"""

Wyświetl plik

@ -24,17 +24,17 @@
<div class="page-header" style="border-color: #{{ database_color }}">
<h1>{{ metadata.get("title") or table }}{% if is_view %} (view){% endif %}{% if private %} 🔒{% endif %}</h1>
</div>
{% set links = table_actions() %}{% if links %}
{% set links = actions() %}{% if links %}
<div class="page-action-menu">
<details class="actions-menu-links details-menu">
<summary>
<div class="icon-text">
<svg class="icon" aria-labelledby="actions-menu-links-title" role="img" style="color: #fff" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title id="actions-menu-links-title">Table actions</title>
<title id="actions-menu-links-title">{% if is_view %}View{% else %}Table{% endif %} actions</title>
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span>Table actions</span>
<span>{% if is_view %}View{% else %}Table{% endif %} actions</span>
</div>
</summary>
<div class="dropdown-menu">

Wyświetl plik

@ -1401,22 +1401,28 @@ async def table_view_data(
"Primary keys for this table"
return pks
async def extra_table_actions():
async def table_actions():
async def extra_actions():
async def actions():
links = []
for hook in pm.hook.table_actions(
datasette=datasette,
table=table_name,
database=database_name,
actor=request.actor,
request=request,
):
kwargs = {
"datasette": datasette,
"database": database_name,
"actor": request.actor,
"request": request,
}
if is_view:
kwargs["view"] = table_name
method = pm.hook.view_actions
else:
kwargs["table"] = table_name
method = pm.hook.table_actions
for hook in method(**kwargs):
extra_links = await await_me_maybe(hook)
if extra_links:
links.extend(extra_links)
return links
return table_actions
return actions
async def extra_is_view():
return is_view
@ -1606,7 +1612,7 @@ async def table_view_data(
"database",
"table",
"database_color",
"table_actions",
"actions",
"filters",
"renderers",
"custom_table_templates",
@ -1647,7 +1653,7 @@ async def table_view_data(
extra_database,
extra_table,
extra_database_color,
extra_table_actions,
extra_actions,
extra_filters,
extra_renderers,
extra_custom_table_templates,

Wyświetl plik

@ -1521,6 +1521,28 @@ This example adds a new table action if the signed in user is ``"root"``:
Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_
.. _plugin_hook_view_actions:
view_actions(datasette, actor, database, view, 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 <authentication_actor>`.
``database`` - string
The name of the database.
``view`` - string
The name of the SQL view.
``request`` - :ref:`internals_request` or None
The current HTTP request. This can be ``None`` if the request object is not available.
Like :ref:`plugin_hook_table_actions` but for SQL views.
.. _plugin_hook_query_actions:
query_actions(datasette, actor, database, query_name, request, sql, params)
@ -1657,7 +1679,9 @@ This example adds a link an imagined tool for editing the homepage, only for sig
if actor:
return [
{
"href": datasette.urls.path("/-/customize-homepage"),
"href": datasette.urls.path(
"/-/customize-homepage"
),
"label": "Customize homepage",
}
]

Wyświetl plik

@ -56,6 +56,7 @@ EXPECTED_PLUGINS = [
"skip_csrf",
"startup",
"table_actions",
"view_actions",
],
},
{

Wyświetl plik

@ -391,6 +391,18 @@ def table_actions(datasette, database, table, actor):
]
@hookimpl
def view_actions(datasette, database, view, actor):
if actor:
return [
{
"href": datasette.urls.instance(),
"label": f"Database: {database}",
},
{"href": datasette.urls.instance(), "label": f"View: {view}"},
]
@hookimpl
def query_actions(datasette, database, query_name, sql):
# Don't explain an explain

Wyświetl plik

@ -923,18 +923,34 @@ async def test_hook_menu_links(ds_client):
@pytest.mark.asyncio
@pytest.mark.parametrize("table_or_view", ["facetable", "simple_view"])
async def test_hook_table_actions(ds_client, table_or_view):
response = await ds_client.get(f"/fixtures/{table_or_view}")
async def test_hook_table_actions(ds_client):
response = await ds_client.get("/fixtures/facetable")
assert get_actions_links(response.text) == []
response_2 = await ds_client.get(f"/fixtures/{table_or_view}?_bot=1&_hello=BOB")
response_2 = await ds_client.get("/fixtures/facetable?_bot=1&_hello=BOB")
assert sorted(
get_actions_links(response_2.text), key=lambda link: link["label"]
) == [
{"label": "Database: fixtures", "href": "/", "description": None},
{"label": "From async BOB", "href": "/", "description": None},
{"label": f"Table: {table_or_view}", "href": "/", "description": None},
{"label": "Table: facetable", "href": "/", "description": None},
]
@pytest.mark.asyncio
async def test_hook_view_actions(ds_client):
response = await ds_client.get("/fixtures/simple_view")
assert get_actions_links(response.text) == []
response_2 = await ds_client.get(
"/fixtures/simple_view",
cookies={"ds_actor": ds_client.actor_cookie({"id": "bob"})},
)
assert sorted(
get_actions_links(response_2.text), key=lambda link: link["label"]
) == [
{"label": "Database: fixtures", "href": "/", "description": None},
{"label": "View: simple_view", "href": "/", "description": None},
]