kopia lustrzana https://github.com/simonw/datasette
homepage_actions() plugin hook, closes #2298
rodzic
7b32d5f7d8
commit
daf5ca02ca
|
@ -155,6 +155,11 @@ def database_actions(datasette, actor, database, request):
|
||||||
"""Links for the database actions menu"""
|
"""Links for the database actions menu"""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def homepage_actions(datasette, actor, request):
|
||||||
|
"""Links for the homepage actions menu"""
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def skip_csrf(datasette, scope):
|
def skip_csrf(datasette, scope):
|
||||||
"""Mechanism for skipping CSRF checks for certain requests"""
|
"""Mechanism for skipping CSRF checks for certain requests"""
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{{ top_database() }}
|
{{ top_database() }}
|
||||||
|
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
||||||
|
|
|
@ -7,6 +7,37 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>
|
<h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>
|
||||||
|
|
||||||
|
{% set links = homepage_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">Homepage 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>Homepage actions</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<div class="hook"></div>
|
||||||
|
{% if links %}
|
||||||
|
<ul>
|
||||||
|
{% for link in links %}
|
||||||
|
<li><a href="{{ link.href }}">{{ link.label }}
|
||||||
|
{% if link.description %}
|
||||||
|
<p class="dropdown-description">{{ link.description }}</p>
|
||||||
|
{% endif %}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ top_homepage() }}
|
{{ top_homepage() }}
|
||||||
|
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import json
|
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.utils.asgi import Response
|
||||||
from datasette.version import __version__
|
from datasette.version import __version__
|
||||||
|
|
||||||
|
@ -131,6 +137,15 @@ class IndexView(BaseView):
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
else:
|
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(
|
return await self.render(
|
||||||
["index.html"],
|
["index.html"],
|
||||||
request=request,
|
request=request,
|
||||||
|
@ -144,5 +159,6 @@ class IndexView(BaseView):
|
||||||
"top_homepage": make_slot_function(
|
"top_homepage": make_slot_function(
|
||||||
"top_homepage", self.ds, request
|
"top_homepage", self.ds, request
|
||||||
),
|
),
|
||||||
|
"homepage_actions": homepage_actions,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -1629,6 +1629,39 @@ This example adds a new database action for creating a table, if the user has th
|
||||||
|
|
||||||
Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_, `datasette-edit-schema <https://datasette.io/plugins/datasette-edit-schema>`_
|
Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_, `datasette-edit-schema <https://datasette.io/plugins/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 <authentication_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:
|
.. _plugin_hook_skip_csrf:
|
||||||
|
|
||||||
skip_csrf(datasette, scope)
|
skip_csrf(datasette, scope)
|
||||||
|
|
|
@ -42,6 +42,7 @@ EXPECTED_PLUGINS = [
|
||||||
"extra_js_urls",
|
"extra_js_urls",
|
||||||
"extra_template_vars",
|
"extra_template_vars",
|
||||||
"forbidden",
|
"forbidden",
|
||||||
|
"homepage_actions",
|
||||||
"menu_links",
|
"menu_links",
|
||||||
"permission_allowed",
|
"permission_allowed",
|
||||||
"prepare_connection",
|
"prepare_connection",
|
||||||
|
|
|
@ -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
|
@hookimpl
|
||||||
def skip_csrf(scope):
|
def skip_csrf(scope):
|
||||||
return scope["path"] == "/skip-csrf"
|
return scope["path"] == "/skip-csrf"
|
||||||
|
|
|
@ -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 "<span>Homepage actions</span>" 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 "<span>Homepage actions</span>" 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):
|
def test_hook_skip_csrf(app_client):
|
||||||
cookie = app_client.actor_cookie({"id": "test"})
|
cookie = app_client.actor_cookie({"id": "test"})
|
||||||
csrf_response = app_client.post(
|
csrf_response = app_client.post(
|
||||||
|
|
Ładowanie…
Reference in New Issue