Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Template slot family of plugin hooks - top_homepage() and others #2238

Merged
merged 13 commits into from
Jan 31, 2024
30 changes: 30 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,33 @@ def skip_csrf(datasette, scope):
@hookspec
def handle_exception(datasette, request, exception):
"""Handle an uncaught exception. Can return a Response or None."""


@hookspec
def top_homepage(datasette, request):
"""HTML to include at the top of the homepage"""


@hookspec
def top_database(datasette, request, database):
"""HTML to include at the top of the database page"""


@hookspec
def top_table(datasette, request, database, table):
"""HTML to include at the top of the table page"""


@hookspec
def top_row(datasette, request, database, table, row):
"""HTML to include at the top of the row page"""


@hookspec
def top_query(datasette, request, database, sql):
"""HTML to include at the top of the query results page"""


@hookspec
def top_canned_query(datasette, request, database, query_name):
"""HTML to include at the top of the canned query page"""
2 changes: 2 additions & 0 deletions datasette/templates/database.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ <h1>{{ metadata.title or database }}{% if private %} 🔒{% endif %}</h1>
</details>{% endif %}
</div>

{{ top_database() }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% if allow_execute_sql %}
Expand Down
2 changes: 2 additions & 0 deletions datasette/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
{% block content %}
<h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>

{{ top_homepage() }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% for database in databases %}
Expand Down
2 changes: 2 additions & 0 deletions datasette/templates/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if private %} 🔒{% endif %}</h1>

{% if canned_query %}{{ top_canned_query() }}{% else %}{{ top_query() }}{% endif %}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

<form class="sql" action="{{ urls.database(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_query_write %}post{% else %}get{% endif %}">
Expand Down
2 changes: 2 additions & 0 deletions datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
{% block content %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}</h1>

{{ top_row() }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

<p>This data as {% for name, url in renderers.items() %}<a href="{{ url }}">{{ name }}</a>{{ ", " if not loop.last }}{% endfor %}</p>
Expand Down
2 changes: 2 additions & 0 deletions datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ <h1>{{ metadata.get("title") or table }}{% if is_view %} (view){% endif %}{% if
</details>{% endif %}
</div>

{{ top_table() }}

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% if metadata.get("columns") %}
Expand Down
17 changes: 17 additions & 0 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,3 +1283,20 @@ def fail_if_plugins_in_metadata(metadata: dict, filename=None):
f'Datasette no longer accepts plugin configuration in --metadata. Move your "plugins" configuration blocks to a separate file - we suggest calling that datasette.{suggested_extension} - and start Datasette with datasette -c datasette.{suggested_extension}. See https://docs.datasette.io/en/latest/configuration.html for more details.'
)
return metadata


def make_slot_function(name, datasette, request, **kwargs):
from datasette.plugins import pm

method = getattr(pm.hook, name, None)
assert method is not None, "No hook found for {}".format(name)

async def inner():
html_bits = []
for hook in method(datasette=datasette, request=request, **kwargs):
html = await await_me_maybe(hook)
if html is not None:
html_bits.append(html)
return markupsafe.Markup("".join(html_bits))

return inner
21 changes: 20 additions & 1 deletion datasette/views/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass, field
from typing import Callable
from urllib.parse import parse_qsl, urlencode
import asyncio
import hashlib
Expand All @@ -18,6 +17,7 @@
call_with_supported_arguments,
derive_named_parameters,
format_bytes,
make_slot_function,
tilde_decode,
to_css_class,
validate_sql_select,
Expand Down Expand Up @@ -161,6 +161,9 @@ async def database_actions():
f"{'*' if template_name == template.name else ''}{template_name}"
for template_name in templates
],
"top_database": make_slot_function(
"top_database", datasette, request, database=database
),
}
return Response.html(
await datasette.render_template(
Expand Down Expand Up @@ -246,6 +249,12 @@ class QueryContext:
"help": "List of templates that were considered for rendering this page"
}
)
top_query: callable = field(
metadata={"help": "Callable to render the top_query slot"}
)
top_canned_query: callable = field(
metadata={"help": "Callable to render the top_canned_query slot"}
)


async def get_tables(datasette, request, db):
Expand Down Expand Up @@ -727,6 +736,16 @@ async def fetch_data_for_csv(request, _next=None):
f"{'*' if template_name == template.name else ''}{template_name}"
for template_name in templates
],
top_query=make_slot_function(
"top_query", datasette, request, database=database, sql=sql
),
top_canned_query=make_slot_function(
"top_canned_query",
datasette,
request,
database=database,
query_name=canned_query["name"] if canned_query else None,
),
),
request=request,
view_name="database",
Expand Down
9 changes: 7 additions & 2 deletions datasette/views/index.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import hashlib
import json

from datasette.utils import add_cors_headers, CustomJSONEncoder
from datasette.plugins import pm
from datasette.utils import add_cors_headers, make_slot_function, CustomJSONEncoder
from datasette.utils.asgi import Response
from datasette.version import __version__

from markupsafe import Markup

from .base import BaseView


Expand Down Expand Up @@ -142,5 +144,8 @@ async def get(self, request):
"private": not await self.ds.permission_allowed(
None, "view-instance"
),
"top_homepage": make_slot_function(
"top_homepage", self.ds, request
),
},
)
12 changes: 9 additions & 3 deletions datasette/views/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
from datasette.database import QueryInterrupted
from .base import DataView, BaseView, _error
from datasette.utils import (
tilde_decode,
urlsafe_components,
make_slot_function,
to_css_class,
escape_sqlite,
row_sql_params_pks,
)
import json
import sqlite_utils
Expand Down Expand Up @@ -73,6 +71,14 @@ async def template_data():
.get(database, {})
.get("tables", {})
.get(table, {}),
"top_row": make_slot_function(
"top_row",
self.ds,
request,
database=resolved.db.name,
table=resolved.table,
row=rows[0],
),
}

data = {
Expand Down
8 changes: 8 additions & 0 deletions datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
append_querystring,
compound_keys_after_sql,
format_bytes,
make_slot_function,
tilde_encode,
escape_sqlite,
filters_should_redirect,
Expand Down Expand Up @@ -842,6 +843,13 @@ async def fetch_data(request, _next=None):
f"{'*' if template_name == template.name else ''}{template_name}"
for template_name in templates
],
top_table=make_slot_function(
"top_table",
datasette,
request,
database=resolved.db.name,
table=resolved.table,
),
),
request=request,
view_name="table",
Expand Down
119 changes: 119 additions & 0 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1641,3 +1641,122 @@ This hook is responsible for returning a dictionary corresponding to Datasette :
return metadata

Example: `datasette-remote-metadata plugin <https://datasette.io/plugins/datasette-remote-metadata>`__

.. _plugin_hook_slots:

Template slots
--------------

The following set of plugin hooks can be used to return extra HTML content that will be inserted into the corresponding page, directly below the ``<h1>`` heading.

Multiple plugins can contribute content here. The order in which it is displayed can be controlled using Pluggy's `call time order options <https://pluggy.readthedocs.io/en/stable/#call-time-order>`__.

Each of these plugin hooks can return either a string or an awaitable function that returns a string.

.. _plugin_hook_top_homepage:

top_homepage(datasette, request)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

Returns HTML to be displayed at the top of the Datasette homepage.

.. _plugin_hook_top_database:

top_database(datasette, request, database)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

``database`` - string
The name of the database.

Returns HTML to be displayed at the top of the database page.

.. _plugin_hook_top_table:

top_table(datasette, request, database, table)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

``database`` - string
The name of the database.

``table`` - string
The name of the table.

Returns HTML to be displayed at the top of the table page.

.. _plugin_hook_top_row:

top_row(datasette, request, database, table, row)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

``database`` - string
The name of the database.

``table`` - string
The name of the table.

``row`` - ``sqlite.Row``
The SQLite row object being displayed.

Returns HTML to be displayed at the top of the row page.

.. _plugin_hook_top_query:

top_query(datasette, request, database, sql)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

``database`` - string
The name of the database.

``sql`` - string
The SQL query.

Returns HTML to be displayed at the top of the query results page.

.. _plugin_hook_top_canned_query:

top_canned_query(datasette, request, database, query_name)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.

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

``database`` - string
The name of the database.

``query_name`` - string
The name of the canned query.

Returns HTML to be displayed at the top of the canned query page.
4 changes: 3 additions & 1 deletion tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def plugin_hooks_content():
"plugin", [name for name in dir(app.pm.hook) if not name.startswith("_")]
)
def test_plugin_hooks_are_documented(plugin, plugin_hooks_content):
headings = get_headings(plugin_hooks_content, "-")
headings = set()
headings.update(get_headings(plugin_hooks_content, "-"))
headings.update(get_headings(plugin_hooks_content, "~"))
assert plugin in headings
hook_caller = getattr(app.pm.hook, plugin)
arg_names = [a for a in hook_caller.spec.argnames if a != "__multicall__"]
Expand Down
Loading
Loading