Skip to content

Commit

Permalink
Template slot family of plugin hooks - top_homepage() and others
Browse files Browse the repository at this point in the history
New plugin hooks:

top_homepage
top_database
top_table
top_row
top_query
top_canned_query

New datasette.utils.make_slot_function()

Closes #1191
  • Loading branch information
simonw authored Jan 31, 2024
1 parent 7a5adb5 commit c3caf36
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 7 deletions.
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

0 comments on commit c3caf36

Please sign in to comment.