Render templates using Jinja async mode

Closes #628
pull/579/head
Simon Willison 2019-11-14 15:14:22 -08:00 zatwierdzone przez GitHub
rodzic b51f258d00
commit 8c642f04e0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
6 zmienionych plików z 43 dodań i 15 usunięć

Wyświetl plik

@ -583,7 +583,9 @@ class Datasette:
),
]
)
self.jinja_env = Environment(loader=template_loader, autoescape=True)
self.jinja_env = Environment(
loader=template_loader, autoescape=True, enable_async=True
)
self.jinja_env.filters["escape_css_string"] = escape_css_string
self.jinja_env.filters["quote_plus"] = lambda u: urllib.parse.quote_plus(u)
self.jinja_env.filters["escape_sqlite"] = escape_sqlite
@ -730,5 +732,5 @@ class DatasetteRouter(AsgiRouter):
else:
template = self.ds.jinja_env.select_template(templates)
await asgi_send_html(
send, template.render(info), status=status, headers=headers
send, await template.render_async(info), status=status, headers=headers
)

Wyświetl plik

@ -139,7 +139,7 @@ class BaseView(AsgiView):
extra_template_vars.update(extra_vars)
return Response.html(
template.render(
await template.render_async(
{
**context,
**{

Wyświetl plik

@ -629,7 +629,9 @@ 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()``.
You can also return a function which returns an awaitable function which returns a dictionary.
Datasette runs Jinja2 in `async mode <https://jinja.palletsprojects.com/en/2.10.x/api/#async-support>`__, which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template.
Here's an example plugin that returns an authentication object from the ASGI scope:
@ -641,20 +643,19 @@ Here's an example plugin that returns an authentication object from the ASGI sco
"auth": request.scope.get("auth")
}
And here's an example which returns the current version of SQLite:
And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results:
.. 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
def extra_template_vars(datasette, database):
async def sql_first(sql, dbname=None):
dbname = dbname or database or next(iter(datasette.databases.keys()))
return (await datasette.execute(dbname, sql)).rows[0][0]
You can then use the new function in a template like so::
SQLite version: {{ sql_first("select sqlite_version()") }}
.. _plugin_register_output_renderer:

Wyświetl plik

@ -446,13 +446,19 @@ def render_cell(value, database):
@hookimpl
def extra_template_vars(template, database, table, view_name, request, datasette):
async def query_database(sql):
first_db = list(datasette.databases.keys())[0]
return (
await datasette.execute(first_db, sql)
).rows[0][0]
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"))
}, default=lambda b: b.decode("utf8")),
"query_database": query_database,
}
return inner

Wyświetl plik

@ -1,5 +1,6 @@
from bs4 import BeautifulSoup as Soup
from .fixtures import app_client, make_app_client, TEMP_PLUGIN_SECRET_FILE # noqa
from datasette.utils import sqlite3
import base64
import json
import os
@ -214,3 +215,20 @@ def test_plugins_extra_template_vars(restore_working_directory):
"awaitable": True,
"scope_path": "/-/metadata",
} == extra_template_vars_from_awaitable
def test_plugins_async_template_function(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_from_awaitable_function = (
Soup(response.body, "html.parser")
.select("pre.extra_from_awaitable_function")[0]
.text
)
expected = (
sqlite3.connect(":memory:").execute("select sqlite_version()").fetchone()[0]
)
assert expected == extra_from_awaitable_function

Wyświetl plik

@ -5,4 +5,5 @@
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>
<pre class="extra_from_awaitable_function">{{ query_database("select sqlite_version();") }}</pre>
{% endblock %}