load_template() plugin hook

Closes #1042
This commit is contained in:
Simon Willison 2020-10-30 10:47:18 -07:00 committed by GitHub
commit 81dea4b07a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 12 deletions

View file

@ -21,7 +21,7 @@ from pathlib import Path
from markupsafe import Markup from markupsafe import Markup
from itsdangerous import URLSafeSerializer from itsdangerous import URLSafeSerializer
import jinja2 import jinja2
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader, escape from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
from jinja2.environment import Template from jinja2.environment import Template
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
import uvicorn import uvicorn
@ -713,12 +713,41 @@ class Datasette:
self, templates, context=None, request=None, view_name=None self, templates, context=None, request=None, view_name=None
): ):
context = context or {} context = context or {}
templates_considered = []
if isinstance(templates, Template): if isinstance(templates, Template):
template = templates template = templates
else: else:
if isinstance(templates, str): if isinstance(templates, str):
templates = [templates] templates = [templates]
template = self.jinja_env.select_template(templates)
# Give plugins first chance at loading the template
break_outer = False
plugin_template_source = None
plugin_template_name = None
template_name = None
for template_name in templates:
if break_outer:
break
plugin_template_source = pm.hook.load_template(
template=template_name,
request=request,
datasette=self,
)
plugin_template_source = await await_me_maybe(plugin_template_source)
if plugin_template_source:
break_outer = True
plugin_template_name = template_name
break
if plugin_template_source is not None:
template = self.jinja_env.from_string(plugin_template_source)
else:
template = self.jinja_env.select_template(templates)
for template_name in templates:
from_plugin = template_name == plugin_template_name
used = from_plugin or template_name == template.name
templates_considered.append(
{"name": template_name, "used": used, "from_plugin": from_plugin}
)
body_scripts = [] body_scripts = []
# pylint: disable=no-member # pylint: disable=no-member
for extra_script in pm.hook.extra_body_script( for extra_script in pm.hook.extra_body_script(
@ -783,6 +812,7 @@ class Datasette:
), ),
"base_url": self.config("base_url"), "base_url": self.config("base_url"),
"csrftoken": request.scope["csrftoken"] if request else lambda: "", "csrftoken": request.scope["csrftoken"] if request else lambda: "",
"templates_considered": templates_considered,
}, },
**extra_template_vars, **extra_template_vars,
} }

View file

@ -49,6 +49,11 @@ def extra_template_vars(
"Extra template variables to be made available to the template - can return dict or callable or awaitable" "Extra template variables to be made available to the template - can return dict or callable or awaitable"
@hookspec(firstresult=True)
def load_template(template, request, datasette):
"Load the specified template, returning the template code as a string"
@hookspec @hookspec
def publish_subcommand(publish): def publish_subcommand(publish):
"Subcommands for 'datasette publish'" "Subcommands for 'datasette publish'"

View file

@ -79,6 +79,10 @@ document.body.addEventListener('click', (ev) => {
<script>{{ body_script }}</script> <script>{{ body_script }}</script>
{% endfor %} {% endfor %}
{% if select_templates %}<!-- Templates considered: {{ select_templates|join(", ") }} -->{% endif %} {% if templates_considered %}
<!-- Templates considered:
{% for template in templates_considered %}- {{ template.name }}{% if template.used %} (used{% if template.from_plugin %}, from plugin{% endif %}){% endif %}
{% endfor %}-->
{% endif %}
</body> </body>
</html> </html>

View file

@ -8,7 +8,6 @@ import urllib
import pint import pint
from datasette import __version__ from datasette import __version__
from datasette.plugins import pm
from datasette.database import QueryInterrupted from datasette.database import QueryInterrupted
from datasette.utils import ( from datasette.utils import (
await_me_maybe, await_me_maybe,
@ -119,22 +118,15 @@ class BaseView:
async def render(self, templates, request, context=None): async def render(self, templates, request, context=None):
context = context or {} context = context or {}
template = self.ds.jinja_env.select_template(templates)
template_context = { template_context = {
**context, **context,
**{ **{
"database_color": self.database_color, "database_color": self.database_color,
"select_templates": [
"{}{}".format(
"*" if template_name == template.name else "", template_name
)
for template_name in templates
],
}, },
} }
return Response.html( return Response.html(
await self.ds.render_template( await self.ds.render_template(
template, template_context, request=request, view_name=self.name templates, template_context, request=request, view_name=self.name
) )
) )

View file

@ -271,6 +271,24 @@ You can also return an awaitable function that returns a string.
Example: `datasette-cluster-map <https://github.com/simonw/datasette-cluster-map>`_ Example: `datasette-cluster-map <https://github.com/simonw/datasette-cluster-map>`_
.. _plugin_hook_load_template:
load_template(template, request, datasette)
-------------------------------------------
``template`` - string
The template that is being rendered, e.g. ``database.html``
``request`` - object or None
The current HTTP :ref:`internals_request`. This can be ``None`` if the request object is not available.
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
Load the source code for a template from a custom location. Hooks should return a string, or ``None`` if the template is not found.
Datasette will fall back to serving templates from files on disk if the requested template cannot be loaded by any plugins.
.. _plugin_hook_publish_subcommand: .. _plugin_hook_publish_subcommand:
publish_subcommand(publish) publish_subcommand(publish)

View file

@ -43,6 +43,7 @@ EXPECTED_PLUGINS = [
"extra_js_urls", "extra_js_urls",
"extra_template_vars", "extra_template_vars",
"forbidden", "forbidden",
"load_template",
"menu_links", "menu_links",
"permission_allowed", "permission_allowed",
"prepare_connection", "prepare_connection",

View file

@ -308,3 +308,9 @@ def table_actions(datasette, database, table, actor):
}, },
{"href": datasette.urls.instance(), "label": "Table: {}".format(table)}, {"href": datasette.urls.instance(), "label": "Table: {}".format(table)},
] ]
@hookimpl
def load_template(template, request):
if template == "show_json.html" and request.args.get("_special"):
return "<h1>Special show_json: {{ filename }}</h1>"

View file

@ -801,3 +801,8 @@ def test_hook_table_actions(app_client):
{"label": "Database: fixtures", "href": "/"}, {"label": "Database: fixtures", "href": "/"},
{"label": "Table: facetable", "href": "/"}, {"label": "Table: facetable", "href": "/"},
] ]
def test_hook_load_template(app_client):
response = app_client.get("/-/databases?_special=1")
assert response.text == "<h1>Special show_json: databases.json</h1>"