Skip to content

Commit

Permalink
extra_template_vars plugin hook (#542)
Browse files Browse the repository at this point in the history
* extra_template_vars plugin hook

Closes #541

* Workaround for cwd bug

Based on pytest-dev/pytest#1235 (comment)
  • Loading branch information
simonw committed Nov 11, 2019
1 parent 859c79f commit 42d6877
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 19 deletions.
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


@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
def publish_subcommand(publish):
"Subcommands for 'datasette publish'"
Expand Down
25 changes: 23 additions & 2 deletions datasette/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def database_url(self, database):
def database_color(self, database):
return "ff0000"

def render(self, templates, **context):
async def render(self, templates, request, context):
template = self.ds.jinja_env.select_template(templates)
select_templates = [
"{}{}".format("*" if template_name == template.name else "", template_name)
Expand All @@ -118,6 +118,26 @@ def render(self, templates, **context):
datasette=self.ds,
):
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(
template.render(
{
Expand All @@ -137,6 +157,7 @@ def render(self, templates, **context):
"database_url": self.database_url,
"database_color": self.database_color,
},
**extra_template_vars,
}
)
)
Expand Down Expand Up @@ -471,7 +492,7 @@ async def view_get(self, request, database, hash, correct_hash_provided, **kwarg
}
if "metadata" not in context:
context["metadata"] = self.ds.metadata
r = self.render(templates, **context)
r = await self.render(templates, request=request, context=context)
r.status = status_code

ttl = request.args.get("_ttl", None)
Expand Down
11 changes: 7 additions & 4 deletions datasette/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ async def get(self, request, as_format):
headers=headers,
)
else:
return self.render(
return await self.render(
["index.html"],
databases=databases,
metadata=self.ds.metadata(),
datasette_version=__version__,
request=request,
context={
"databases": databases,
"metadata": self.ds.metadata(),
"datasette_version": __version__,
},
)
6 changes: 5 additions & 1 deletion datasette/views/special.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ async def get(self, request, as_format):
)

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},
)
86 changes: 74 additions & 12 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 JavaScript to be added to a ``<script>`` block at the end of the ``<body>`` element on the page.

``template`` - string
The template that is being rendered, e.g. ``database.html``

Expand All @@ -577,14 +579,74 @@ extra_body_script(template, database, table, view_name, datasette)
``datasette`` - Datasette instance
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 ``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.


.. _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:

register_output_renderer(datasette)
Expand All @@ -597,12 +659,12 @@ Allows the plugin to register a new output renderer, to output data in a custom

.. code-block:: python
@hookimpl
def register_output_renderer(datasette):
return {
'extension': 'test',
'callback': render_test
}
@hookimpl
def register_output_renderer(datasette):
return {
'extension': '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:

Expand Down Expand Up @@ -630,10 +692,10 @@ A simple example of an output renderer callback function:

.. code-block:: python
def render_test(args, data, view_name):
return {
'body': 'Hello World'
}
def render_test(args, data, view_name):
return {
'body': 'Hello World'
}
.. _plugin_register_facet_classes:

Expand Down
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import os
import pytest


def pytest_configure(config):
import sys

Expand All @@ -22,3 +26,14 @@ def move_to_front(items, test_name):
test = [fn for fn in items if fn.name == test_name]
if test:
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)
23 changes: 23 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,16 @@ def render_cell(value, column, table, database, datasette):
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 = """
Expand Down Expand Up @@ -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
def asgi_wrapper(datasette):
def wrap_with_databases_header(app):
Expand Down
26 changes: 26 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import json
import os
import pathlib
import re
import pytest
import urllib
Expand Down Expand Up @@ -188,3 +189,28 @@ def test_plugins_extra_body_script(app_client, path, expected_extra_body_script)
def test_plugins_asgi_wrapper(app_client):
response = app_client.get("/fixtures")
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
8 changes: 8 additions & 0 deletions tests/test_templates/show_json.html
Original file line number Diff line number Diff line change
@@ -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 %}

0 comments on commit 42d6877

Please sign in to comment.