extra_template_vars plugin hook (#542)

* extra_template_vars plugin hook

Closes #541

* Workaround for cwd bug

Based on https://github.com/pytest-dev/pytest/issues/1235#issuecomment-175295691
pull/546/head
Simon Willison 2019-07-05 17:05:56 -07:00 zatwierdzone przez GitHub
rodzic a18e0964ec
commit fcfcae21e6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
9 zmienionych plików z 186 dodań i 19 usunięć

Wyświetl plik

@ -35,6 +35,11 @@ def extra_body_script(template, database, table, view_name, datasette):
"Extra JavaScript code to be included in <script> at bottom of body" "Extra JavaScript code to be included in <script> at bottom of body"
@hookspec
def extra_template_vars(template, database, table, view_name, request, datasette):
"Extra template variables to be made available to the template - can return dict or callable or awaitable"
@hookspec @hookspec
def publish_subcommand(publish): def publish_subcommand(publish):
"Subcommands for 'datasette publish'" "Subcommands for 'datasette publish'"

Wyświetl plik

@ -102,7 +102,7 @@ class BaseView(AsgiView):
def database_color(self, database): def database_color(self, database):
return "ff0000" return "ff0000"
def render(self, templates, **context): async def render(self, templates, request, context):
template = self.ds.jinja_env.select_template(templates) template = self.ds.jinja_env.select_template(templates)
select_templates = [ select_templates = [
"{}{}".format("*" if template_name == template.name else "", template_name) "{}{}".format("*" if template_name == template.name else "", template_name)
@ -118,6 +118,26 @@ class BaseView(AsgiView):
datasette=self.ds, datasette=self.ds,
): ):
body_scripts.append(jinja2.Markup(script)) body_scripts.append(jinja2.Markup(script))
extra_template_vars = {}
# pylint: disable=no-member
for extra_vars in pm.hook.extra_template_vars(
template=template.name,
database=context.get("database"),
table=context.get("table"),
view_name=self.name,
request=request,
datasette=self.ds,
):
if callable(extra_vars):
extra_vars = extra_vars()
if asyncio.iscoroutine(extra_vars):
extra_vars = await extra_vars
assert isinstance(extra_vars, dict), "extra_vars is of type {}".format(
type(extra_vars)
)
extra_template_vars.update(extra_vars)
return Response.html( return Response.html(
template.render( template.render(
{ {
@ -137,6 +157,7 @@ class BaseView(AsgiView):
"database_url": self.database_url, "database_url": self.database_url,
"database_color": self.database_color, "database_color": self.database_color,
}, },
**extra_template_vars,
} }
) )
) )
@ -471,7 +492,7 @@ class DataView(BaseView):
} }
if "metadata" not in context: if "metadata" not in context:
context["metadata"] = self.ds.metadata context["metadata"] = self.ds.metadata
r = self.render(templates, **context) r = await self.render(templates, request=request, context=context)
r.status = status_code r.status = status_code
ttl = request.args.get("_ttl", None) ttl = request.args.get("_ttl", None)

Wyświetl plik

@ -109,9 +109,12 @@ class IndexView(BaseView):
headers=headers, headers=headers,
) )
else: else:
return self.render( return await self.render(
["index.html"], ["index.html"],
databases=databases, request=request,
metadata=self.ds.metadata(), context={
datasette_version=__version__, "databases": databases,
"metadata": self.ds.metadata(),
"datasette_version": __version__,
},
) )

Wyświetl plik

@ -24,4 +24,8 @@ class JsonDataView(BaseView):
) )
else: else:
return self.render(["show_json.html"], filename=self.filename, data=data) return await self.render(
["show_json.html"],
request=request,
context={"filename": self.filename, "data": data},
)

Wyświetl plik

@ -562,6 +562,8 @@ If the value matches that pattern, the plugin returns an HTML link element:
extra_body_script(template, database, table, view_name, datasette) extra_body_script(template, database, table, view_name, datasette)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Extra JavaScript to be added to a ``<script>`` block at the end of the ``<body>`` element on the page.
``template`` - string ``template`` - string
The template that is being rendered, e.g. ``database.html`` The template that is being rendered, e.g. ``database.html``
@ -577,14 +579,74 @@ extra_body_script(template, database, table, view_name, datasette)
``datasette`` - Datasette instance ``datasette`` - Datasette instance
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
Extra JavaScript to be added to a ``<script>`` block at the end of the ``<body>`` element on the page.
The ``template``, ``database`` and ``table`` options can be used to return different code depending on which template is being rendered and which database or table are being processed. The ``template``, ``database`` and ``table`` options can be used to return different code depending on which template is being rendered and which database or table are being processed.
The ``datasette`` instance is provided primarily so that you can consult any plugin configuration options that may have been set, using the ``datasette.plugin_config(plugin_name)`` method documented above. The ``datasette`` instance is provided primarily so that you can consult any plugin configuration options that may have been set, using the ``datasette.plugin_config(plugin_name)`` method documented above.
The string that you return from this function will be treated as "safe" for inclusion in a ``<script>`` block directly in the page, so it is up to you to apply any necessary escaping. The string that you return from this function will be treated as "safe" for inclusion in a ``<script>`` block directly in the page, so it is up to you to apply any necessary escaping.
.. _plugin_hook_extra_template_vars:
extra_template_vars(template, database, table, view_name, request, datasette)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Extra template variables that should be made available in the rendered template context.
``template`` - string
The template that is being rendered, e.g. ``database.html``
``database`` - string or None
The name of the database, or ``None`` if the page does not correspond to a database (e.g. the root page)
``table`` - string or None
The name of the table, or ``None`` if the page does not correct to a table
``view_name`` - string
The name of the view being displayed. (`database`, `table`, and `row` are the most important ones.)
``request`` - object
The current HTTP request object. ``request.scope`` provides access to the ASGI scope.
``datasette`` - Datasette instance
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
This hook can return one of three different types:
Dictionary
If you return a dictionary its keys and values will be merged into the template context.
Function that returns a dictionary
If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context.
Function that returns an awaitable function that returns a dictionary
You can also return a function which returns an awaitable function which returns a dictionary. This means you can execute additional SQL queries using ``datasette.execute()``.
Here's an example plugin that returns an authentication object from the ASGI scope:
.. code-block:: python
@hookimpl
def extra_template_vars(request):
return {
"auth": request.scope.get("auth")
}
And here's an example which returns the current version of SQLite:
.. code-block:: python
@hookimpl
def extra_template_vars(datasette):
async def inner():
first_db = list(datasette.databases.keys())[0]
return {
"sqlite_version": (
await datasette.execute(first_db, "select sqlite_version()")
).rows[0][0]
}
return inner
.. _plugin_register_output_renderer: .. _plugin_register_output_renderer:
register_output_renderer(datasette) register_output_renderer(datasette)
@ -597,12 +659,12 @@ Allows the plugin to register a new output renderer, to output data in a custom
.. code-block:: python .. code-block:: python
@hookimpl @hookimpl
def register_output_renderer(datasette): def register_output_renderer(datasette):
return { return {
'extension': 'test', 'extension': 'test',
'callback': render_test 'callback': render_test
} }
This will register `render_test` to be called when paths with the extension `.test` (for example `/database.test`, `/database/table.test`, or `/database/table/row.test`) are requested. When a request is received, the callback function is called with three positional arguments: This will register `render_test` to be called when paths with the extension `.test` (for example `/database.test`, `/database/table.test`, or `/database/table/row.test`) are requested. When a request is received, the callback function is called with three positional arguments:
@ -630,10 +692,10 @@ A simple example of an output renderer callback function:
.. code-block:: python .. code-block:: python
def render_test(args, data, view_name): def render_test(args, data, view_name):
return { return {
'body': 'Hello World' 'body': 'Hello World'
} }
.. _plugin_register_facet_classes: .. _plugin_register_facet_classes:

Wyświetl plik

@ -1,3 +1,7 @@
import os
import pytest
def pytest_configure(config): def pytest_configure(config):
import sys import sys
@ -22,3 +26,14 @@ def move_to_front(items, test_name):
test = [fn for fn in items if fn.name == test_name] test = [fn for fn in items if fn.name == test_name]
if test: if test:
items.insert(0, items.pop(items.index(test[0]))) items.insert(0, items.pop(items.index(test[0])))
@pytest.fixture
def restore_working_directory(tmpdir, request):
previous_cwd = os.getcwd()
tmpdir.chdir()
def return_to_previous():
os.chdir(previous_cwd)
request.addfinalizer(return_to_previous)

Wyświetl plik

@ -376,6 +376,16 @@ def render_cell(value, column, table, database, datasette):
table=table, table=table,
) )
}) })
@hookimpl
def extra_template_vars(template, database, table, view_name, request, datasette):
return {
"extra_template_vars": json.dumps({
"template": template,
"scope_path": request.scope["path"]
}, default=lambda b: b.decode("utf8"))
}
""" """
PLUGIN2 = """ PLUGIN2 = """
@ -424,6 +434,19 @@ def render_cell(value, database):
) )
@hookimpl
def extra_template_vars(template, database, table, view_name, request, datasette):
async def inner():
return {
"extra_template_vars_from_awaitable": json.dumps({
"template": template,
"scope_path": request.scope["path"],
"awaitable": True,
}, default=lambda b: b.decode("utf8"))
}
return inner
@hookimpl @hookimpl
def asgi_wrapper(datasette): def asgi_wrapper(datasette):
def wrap_with_databases_header(app): def wrap_with_databases_header(app):

Wyświetl plik

@ -3,6 +3,7 @@ from .fixtures import app_client, make_app_client, TEMP_PLUGIN_SECRET_FILE # no
import base64 import base64
import json import json
import os import os
import pathlib
import re import re
import pytest import pytest
import urllib import urllib
@ -188,3 +189,28 @@ def test_plugins_extra_body_script(app_client, path, expected_extra_body_script)
def test_plugins_asgi_wrapper(app_client): def test_plugins_asgi_wrapper(app_client):
response = app_client.get("/fixtures") response = app_client.get("/fixtures")
assert "fixtures" == response.headers["x-databases"] assert "fixtures" == response.headers["x-databases"]
def test_plugins_extra_template_vars(restore_working_directory):
for client in make_app_client(
template_dir=str(pathlib.Path(__file__).parent / "test_templates")
):
response = client.get("/-/metadata")
assert response.status == 200
extra_template_vars = json.loads(
Soup(response.body, "html.parser").select("pre.extra_template_vars")[0].text
)
assert {
"template": "show_json.html",
"scope_path": "/-/metadata",
} == extra_template_vars
extra_template_vars_from_awaitable = json.loads(
Soup(response.body, "html.parser")
.select("pre.extra_template_vars_from_awaitable")[0]
.text
)
assert {
"template": "show_json.html",
"awaitable": True,
"scope_path": "/-/metadata",
} == extra_template_vars_from_awaitable

Wyświetl plik

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
{{ super() }}
Test data for extra_template_vars:
<pre class="extra_template_vars">{{ extra_template_vars|safe }}</pre>
<pre class="extra_template_vars_from_awaitable">{{ extra_template_vars_from_awaitable|safe }}</pre>
{% endblock %}