Skip to content

Commit e06e083

Browse files
committed
extra_template_vars plugin hook
Refs #541
1 parent a18e096 commit e06e083

File tree

8 files changed

+171
-19
lines changed

8 files changed

+171
-19
lines changed

datasette/hookspecs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ def extra_body_script(template, database, table, view_name, datasette):
3535
"Extra JavaScript code to be included in <script> at bottom of body"
3636

3737

38+
@hookspec
39+
def extra_template_vars(template, database, table, view_name, request, datasette):
40+
"Extra template variables to be made available to the template - can return dict or callable or awaitable"
41+
42+
3843
@hookspec
3944
def publish_subcommand(publish):
4045
"Subcommands for 'datasette publish'"

datasette/views/base.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def database_url(self, database):
102102
def database_color(self, database):
103103
return "ff0000"
104104

105-
def render(self, templates, **context):
105+
async def render(self, templates, request, context):
106106
template = self.ds.jinja_env.select_template(templates)
107107
select_templates = [
108108
"{}{}".format("*" if template_name == template.name else "", template_name)
@@ -118,6 +118,26 @@ def render(self, templates, **context):
118118
datasette=self.ds,
119119
):
120120
body_scripts.append(jinja2.Markup(script))
121+
122+
extra_template_vars = {}
123+
# pylint: disable=no-member
124+
for extra_vars in pm.hook.extra_template_vars(
125+
template=template.name,
126+
database=context.get("database"),
127+
table=context.get("table"),
128+
view_name=self.name,
129+
request=request,
130+
datasette=self.ds,
131+
):
132+
if callable(extra_vars):
133+
extra_vars = extra_vars()
134+
if asyncio.iscoroutine(extra_vars):
135+
extra_vars = await extra_vars
136+
assert isinstance(extra_vars, dict), "extra_vars is of type {}".format(
137+
type(extra_vars)
138+
)
139+
extra_template_vars.update(extra_vars)
140+
121141
return Response.html(
122142
template.render(
123143
{
@@ -137,6 +157,7 @@ def render(self, templates, **context):
137157
"database_url": self.database_url,
138158
"database_color": self.database_color,
139159
},
160+
**extra_template_vars,
140161
}
141162
)
142163
)
@@ -471,7 +492,7 @@ async def view_get(self, request, database, hash, correct_hash_provided, **kwarg
471492
}
472493
if "metadata" not in context:
473494
context["metadata"] = self.ds.metadata
474-
r = self.render(templates, **context)
495+
r = await self.render(templates, request=request, context=context)
475496
r.status = status_code
476497

477498
ttl = request.args.get("_ttl", None)

datasette/views/index.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ async def get(self, request, as_format):
109109
headers=headers,
110110
)
111111
else:
112-
return self.render(
112+
return await self.render(
113113
["index.html"],
114-
databases=databases,
115-
metadata=self.ds.metadata(),
116-
datasette_version=__version__,
114+
request=request,
115+
context=dict(
116+
databases=databases,
117+
metadata=self.ds.metadata(),
118+
datasette_version=__version__,
119+
),
117120
)

datasette/views/special.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@ async def get(self, request, as_format):
2424
)
2525

2626
else:
27-
return self.render(["show_json.html"], filename=self.filename, data=data)
27+
return await self.render(
28+
["show_json.html"],
29+
request=request,
30+
context={"filename": self.filename, "data": data},
31+
)

docs/plugins.rst

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ If the value matches that pattern, the plugin returns an HTML link element:
562562
extra_body_script(template, database, table, view_name, datasette)
563563
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
564564

565+
Extra JavaScript to be added to a ``<script>`` block at the end of the ``<body>`` element on the page.
566+
565567
``template`` - string
566568
The template that is being rendered, e.g. ``database.html``
567569

@@ -577,14 +579,74 @@ extra_body_script(template, database, table, view_name, datasette)
577579
``datasette`` - Datasette instance
578580
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
579581

580-
Extra JavaScript to be added to a ``<script>`` block at the end of the ``<body>`` element on the page.
581-
582582
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.
583583

584584
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.
585585

586586
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.
587587

588+
589+
.. _plugin_hook_extra_template_vars:
590+
591+
extra_template_vars(template, database, table, view_name, request, datasette)
592+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
593+
594+
Extra template variables that should be made available in the rendered template context.
595+
596+
``template`` - string
597+
The template that is being rendered, e.g. ``database.html``
598+
599+
``database`` - string or None
600+
The name of the database, or ``None`` if the page does not correspond to a database (e.g. the root page)
601+
602+
``table`` - string or None
603+
The name of the table, or ``None`` if the page does not correct to a table
604+
605+
``view_name`` - string
606+
The name of the view being displayed. (`database`, `table`, and `row` are the most important ones.)
607+
608+
``request`` - object
609+
The current HTTP request object. ``request.scope`` provides access to the ASGI scope.
610+
611+
``datasette`` - Datasette instance
612+
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
613+
614+
This hook can return one of three different types:
615+
616+
Dictionary
617+
If you return a dictionary its keys and values will be merged into the template context.
618+
619+
Function that returns a dictionary
620+
If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context.
621+
622+
Function that returns an awaitable function that returns a dictionary
623+
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()``.
624+
625+
Here's an example plugin that returns an authentication object from the ASGI scope:
626+
627+
.. code-block:: python
628+
629+
@hookimpl
630+
def extra_template_vars(request):
631+
return {
632+
"auth": request.scope.get("auth")
633+
}
634+
635+
And here's an example which returns the current version of SQLite:
636+
637+
.. code-block:: python
638+
639+
@hookimpl
640+
def extra_template_vars(datasette):
641+
async def inner():
642+
first_db = list(datasette.databases.keys())[0]
643+
return {
644+
"sqlite_version": (
645+
await datasette.execute(first_db, "select sqlite_version()")
646+
).rows[0][0]
647+
}
648+
return inner
649+
588650
.. _plugin_register_output_renderer:
589651

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

598660
.. code-block:: python
599661
600-
@hookimpl
601-
def register_output_renderer(datasette):
602-
return {
603-
'extension': 'test',
604-
'callback': render_test
605-
}
662+
@hookimpl
663+
def register_output_renderer(datasette):
664+
return {
665+
'extension': 'test',
666+
'callback': render_test
667+
}
606668
607669
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:
608670

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

631693
.. code-block:: python
632694
633-
def render_test(args, data, view_name):
634-
return {
635-
'body': 'Hello World'
636-
}
695+
def render_test(args, data, view_name):
696+
return {
697+
'body': 'Hello World'
698+
}
637699
638700
.. _plugin_register_facet_classes:
639701

tests/fixtures.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,16 @@ def render_cell(value, column, table, database, datasette):
376376
table=table,
377377
)
378378
})
379+
380+
381+
@hookimpl
382+
def extra_template_vars(template, database, table, view_name, request, datasette):
383+
return {
384+
"extra_template_vars": json.dumps({
385+
"template": template,
386+
"scope_path": request.scope["path"]
387+
}, default=lambda b: b.decode("utf8"))
388+
}
379389
"""
380390

381391
PLUGIN2 = """
@@ -424,6 +434,19 @@ def render_cell(value, database):
424434
)
425435
426436
437+
@hookimpl
438+
def extra_template_vars(template, database, table, view_name, request, datasette):
439+
async def inner():
440+
return {
441+
"extra_template_vars_from_awaitable": json.dumps({
442+
"template": template,
443+
"scope_path": request.scope["path"],
444+
"awaitable": True,
445+
}, default=lambda b: b.decode("utf8"))
446+
}
447+
return inner
448+
449+
427450
@hookimpl
428451
def asgi_wrapper(datasette):
429452
def wrap_with_databases_header(app):

tests/test_plugins.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import base64
44
import json
55
import os
6+
import pathlib
67
import re
78
import pytest
89
import urllib
@@ -188,3 +189,28 @@ def test_plugins_extra_body_script(app_client, path, expected_extra_body_script)
188189
def test_plugins_asgi_wrapper(app_client):
189190
response = app_client.get("/fixtures")
190191
assert "fixtures" == response.headers["x-databases"]
192+
193+
194+
def test_plugins_extra_template_vars():
195+
for client in make_app_client(
196+
template_dir=str(pathlib.Path(__file__).parent / "test_templates")
197+
):
198+
response = client.get("/-/metadata")
199+
assert response.status == 200
200+
extra_template_vars = json.loads(
201+
Soup(response.body, "html.parser").select("pre.extra_template_vars")[0].text
202+
)
203+
assert {
204+
"template": "show_json.html",
205+
"scope_path": "/-/metadata",
206+
} == extra_template_vars
207+
extra_template_vars_from_awaitable = json.loads(
208+
Soup(response.body, "html.parser")
209+
.select("pre.extra_template_vars_from_awaitable")[0]
210+
.text
211+
)
212+
assert {
213+
"template": "show_json.html",
214+
"awaitable": True,
215+
"scope_path": "/-/metadata",
216+
} == extra_template_vars_from_awaitable

tests/test_templates/show_json.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
3+
{% block content %}
4+
{{ super() }}
5+
Test data for extra_template_vars:
6+
<pre class="extra_template_vars">{{ extra_template_vars|safe }}</pre>
7+
<pre class="extra_template_vars_from_awaitable">{{ extra_template_vars_from_awaitable|safe }}</pre>
8+
{% endblock %}

0 commit comments

Comments
 (0)