Skip to content

Commit

Permalink
view_actions plugin hook, closes #2297
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Mar 12, 2024
1 parent daf5ca0 commit 909c85c
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 21 deletions.
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
6 changes: 3 additions & 3 deletions datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
30 changes: 18 additions & 12 deletions datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -1401,22 +1401,28 @@ async def extra_primary_keys():
"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
Expand Down Expand Up @@ -1606,7 +1612,7 @@ async def extra_facets_timed_out(extra_facet_results):
"database",
"table",
"database_color",
"table_actions",
"actions",
"filters",
"renderers",
"custom_table_templates",
Expand Down Expand Up @@ -1647,7 +1653,7 @@ async def extra_facets_timed_out(extra_facet_results):
extra_database,
extra_table,
extra_database_color,
extra_table_actions,
extra_actions,
extra_filters,
extra_renderers,
extra_custom_table_templates,
Expand Down
26 changes: 25 additions & 1 deletion docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
}
]
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"skip_csrf",
"startup",
"table_actions",
"view_actions",
],
},
{
Expand Down
12 changes: 12 additions & 0 deletions tests/plugins/my_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 21 additions & 5 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,18 +923,34 @@ def get_menu_links(html):


@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},
]


Expand Down

0 comments on commit 909c85c

Please sign in to comment.