Skip to content

Commit

Permalink
Nav menu plus menu_links() hook, closes #1064
Browse files Browse the repository at this point in the history
Refs #690
  • Loading branch information
simonw committed Oct 30, 2020
1 parent 76879c5 commit 5f118b5
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 12 deletions.
12 changes: 12 additions & 0 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,11 +750,22 @@ async def render_template(
)
extra_template_vars.update(extra_vars)

async def menu_links():
links = []
for hook in pm.hook.menu_links(
datasette=self, actor=request.actor if request else None
):
extra_links = await await_me_maybe(hook)
if extra_links:
links.extend(extra_links)
return links

template_context = {
**context,
**{
"urls": self.urls,
"actor": request.actor if request else None,
"menu_links": menu_links,
"display_actor": display_actor,
"show_logout": request is not None and "ds_actor" in request.cookies,
"app_css_hash": self.app_css_hash(),
Expand Down Expand Up @@ -1161,6 +1172,7 @@ async def handle_500(self, request, send, exception):
info,
urls=self.ds.urls,
app_css_hash=self.ds.app_css_hash(),
menu_links=lambda: [],
)
),
status=status,
Expand Down
40 changes: 40 additions & 0 deletions datasette/default_menu_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from datasette import hookimpl


@hookimpl
def menu_links(datasette, actor):
if actor and actor.get("id") == "root":
return [
{"href": datasette.urls.path("/-/databases"), "label": "Databases"},
{
"href": datasette.urls.path("/-/plugins"),
"label": "Installed plugins",
},
{
"href": datasette.urls.path("/-/versions"),
"label": "Version info",
},
{
"href": datasette.urls.path("/-/metadata"),
"label": "Metadata",
},
{
"href": datasette.urls.path("/-/config"),
"label": "Config",
},
{
"href": datasette.urls.path("/-/permissions"),
"label": "Debug permissions",
},
{
"href": datasette.urls.path("/-/messages"),
"label": "Debug messages",
},
{
"href": datasette.urls.path("/-/allow-debug"),
"label": "Debug allow rules",
},
{"href": datasette.urls.path("/-/threads"), "label": "Debug threads"},
{"href": datasette.urls.path("/-/actor"), "label": "Debug actor"},
{"href": datasette.urls.path("/-/patterns"), "label": "Pattern portfolio"},
]
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,8 @@ def register_magic_parameters(datasette):
@hookspec
def forbidden(datasette, request, message):
"Custom response for a 403 forbidden error"


@hookspec
def menu_links(datasette, actor):
"Links for the navigation menu"
1 change: 1 addition & 0 deletions datasette/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"datasette.default_permissions",
"datasette.default_magic_parameters",
"datasette.blob_renderer",
"datasette.default_menu_links",
)

pm = pluggy.PluginManager("datasette")
Expand Down
22 changes: 12 additions & 10 deletions datasette/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</head>
<body class="{% block body_class %}{% endblock %}">
<header><nav>{% block nav %}
{% set links = menu_links() %}{% if links or show_logout %}
<details class="nav-menu">
<summary><svg aria-labelledby="nav-menu-svg-title" role="img"
fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
Expand All @@ -22,19 +23,20 @@
<path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path>
</svg></summary>
<div class="nav-menu-inner">
{% if links %}
<ul>
<li><a href="{{ urls.instance() }}">Home</a></li>
<li><a href="{{ urls.path('/-/plugins') }}">Installed plugins</a></li>
<li><a href="{{ urls.path('/-/versions') }}">Software versions</a></li>
<li><a href="{{ urls.path('/-/metadata') }}">Metadata</a></li>
{% if show_logout %}
<form action="{{ urls.logout() }}" method="post">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<button class="button-as-link">Log out</button>
</form>{% endif %}
{% for link in links %}
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if show_logout %}
<form action="{{ urls.logout() }}" method="post">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<button class="button-as-link">Log out</button>
</form>{% endif %}
</div>
</details>
</details>{% endif %}
{% if actor %}
<div class="actor">
<strong>{{ display_actor(actor) }}</strong>
Expand Down
32 changes: 32 additions & 0 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,35 @@ The function can alternatively return an awaitable function if it needs to make
return Response.html(await datasette.render_template("forbidden.html"))
return inner
.. _plugin_hook_menu_links:

menu_links(datasette, actor)
----------------------------

``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.

``request`` - object
The current HTTP :ref:`internals_request`.

This hook provides items to be included in the menu displayed by Datasette's top right menu icon.

The hook should return a list of ``{"href": "...", "label": "..."}`` menu items. These will be added to the menu.

It can alternatively return an ``async def`` awaitable function which returns a list of menu items.

This example adds a new menu item but only if the signed in user is ``"root"``:

.. code-block:: python
from datasette import hookimpl
@hookimpl
def menu_links(datasette, actor):
if actor and actor.get("id") == "root":
return [
{"href": datasette.urls.path("/-/edit-schema"), "label": "Edit schema"},
]
Using :ref:`internals_datasette_urls` here ensures that links in the menu will take the :ref:`config_base_url` setting into account.
2 changes: 2 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"extra_js_urls",
"extra_template_vars",
"forbidden",
"menu_links",
"permission_allowed",
"prepare_connection",
"prepare_jinja2_environment",
Expand All @@ -64,6 +65,7 @@
"canned_queries",
"extra_js_urls",
"extra_template_vars",
"menu_links",
"permission_allowed",
"render_cell",
"startup",
Expand Down
6 changes: 6 additions & 0 deletions tests/plugins/my_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,9 @@ def forbidden(datasette, request, message):
datasette._last_forbidden_message = message
if request.path == "/data2":
return Response.redirect("/login?message=" + message)


@hookimpl
def menu_links(datasette, actor):
if actor:
return [{"href": datasette.urls.instance(), "label": "Hello"}]
9 changes: 9 additions & 0 deletions tests/plugins/my_plugin_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,12 @@ async def inner():
}

return inner


@hookimpl(trylast=True)
def menu_links(datasette, actor):
async def inner():
if actor:
return [{"href": datasette.urls.instance(), "label": "Hello 2"}]

return inner
3 changes: 1 addition & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_logout_button_in_navigation(app_client, path):
)
anon_response = app_client.get(path)
for fragment in (
"<strong>test</strong> &middot;",
"<strong>test</strong>",
'<form action="/-/logout" method="post">',
):
assert fragment in response.text
Expand All @@ -112,5 +112,4 @@ def test_logout_button_in_navigation(app_client, path):
def test_no_logout_button_in_navigation_if_no_ds_actor_cookie(app_client, path):
response = app_client.get(path + "?_bot=1")
assert "<strong>bot</strong>" in response.text
assert "<strong>bot</strong> &middot;" not in response.text
assert '<form action="/-/logout" method="post">' not in response.text
17 changes: 17 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,3 +765,20 @@ def test_hook_forbidden(restore_working_directory):
assert 302 == response2.status
assert "/login?message=view-database" == response2.headers["Location"]
assert "view-database" == client.ds._last_forbidden_message


def test_hook_menu_links(app_client):
def get_menu_links(html):
soup = Soup(html, "html.parser")
return [
{"label": a.text, "href": a["href"]} for a in soup.find("nav").select("a")
]

response = app_client.get("/")
assert get_menu_links(response.text) == []

response_2 = app_client.get("/?_bot=1")
assert get_menu_links(response_2.text) == [
{"label": "Hello", "href": "/"},
{"label": "Hello 2", "href": "/"},
]

0 comments on commit 5f118b5

Please sign in to comment.