mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
parent
45b88f2056
commit
c7a4706bcc
8 changed files with 128 additions and 25 deletions
|
|
@ -420,21 +420,31 @@ class Datasette:
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.jinja_env = Environment(
|
environment = Environment(
|
||||||
loader=template_loader,
|
loader=template_loader,
|
||||||
autoescape=True,
|
autoescape=True,
|
||||||
enable_async=True,
|
enable_async=True,
|
||||||
# undefined=StrictUndefined,
|
# undefined=StrictUndefined,
|
||||||
)
|
)
|
||||||
self.jinja_env.filters["escape_css_string"] = escape_css_string
|
environment.filters["escape_css_string"] = escape_css_string
|
||||||
self.jinja_env.filters["quote_plus"] = urllib.parse.quote_plus
|
environment.filters["quote_plus"] = urllib.parse.quote_plus
|
||||||
self.jinja_env.filters["escape_sqlite"] = escape_sqlite
|
self._jinja_env = environment
|
||||||
self.jinja_env.filters["to_css_class"] = to_css_class
|
environment.filters["escape_sqlite"] = escape_sqlite
|
||||||
|
environment.filters["to_css_class"] = to_css_class
|
||||||
self._register_renderers()
|
self._register_renderers()
|
||||||
self._permission_checks = collections.deque(maxlen=200)
|
self._permission_checks = collections.deque(maxlen=200)
|
||||||
self._root_token = secrets.token_hex(32)
|
self._root_token = secrets.token_hex(32)
|
||||||
self.client = DatasetteClient(self)
|
self.client = DatasetteClient(self)
|
||||||
|
|
||||||
|
def get_jinja_environment(self, request: Request = None) -> Environment:
|
||||||
|
environment = self._jinja_env
|
||||||
|
if request:
|
||||||
|
for environment in pm.hook.jinja2_environment_from_request(
|
||||||
|
datasette=self, request=request, env=environment
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
return environment
|
||||||
|
|
||||||
def get_permission(self, name_or_abbr: str) -> "Permission":
|
def get_permission(self, name_or_abbr: str) -> "Permission":
|
||||||
"""
|
"""
|
||||||
Returns a Permission object for the given name or abbreviation. Raises KeyError if not found.
|
Returns a Permission object for the given name or abbreviation. Raises KeyError if not found.
|
||||||
|
|
@ -514,7 +524,7 @@ class Datasette:
|
||||||
abbrs[p.abbr] = p
|
abbrs[p.abbr] = p
|
||||||
self.permissions[p.name] = p
|
self.permissions[p.name] = p
|
||||||
for hook in pm.hook.prepare_jinja2_environment(
|
for hook in pm.hook.prepare_jinja2_environment(
|
||||||
env=self.jinja_env, datasette=self
|
env=self._jinja_env, datasette=self
|
||||||
):
|
):
|
||||||
await await_me_maybe(hook)
|
await await_me_maybe(hook)
|
||||||
for hook in pm.hook.startup(datasette=self):
|
for hook in pm.hook.startup(datasette=self):
|
||||||
|
|
@ -1218,7 +1228,7 @@ class Datasette:
|
||||||
else:
|
else:
|
||||||
if isinstance(templates, str):
|
if isinstance(templates, str):
|
||||||
templates = [templates]
|
templates = [templates]
|
||||||
template = self.jinja_env.select_template(templates)
|
template = self.get_jinja_environment(request).select_template(templates)
|
||||||
if dataclasses.is_dataclass(context):
|
if dataclasses.is_dataclass(context):
|
||||||
context = dataclasses.asdict(context)
|
context = dataclasses.asdict(context)
|
||||||
body_scripts = []
|
body_scripts = []
|
||||||
|
|
@ -1568,16 +1578,6 @@ class DatasetteRouter:
|
||||||
def __init__(self, datasette, routes):
|
def __init__(self, datasette, routes):
|
||||||
self.ds = datasette
|
self.ds = datasette
|
||||||
self.routes = routes or []
|
self.routes = routes or []
|
||||||
# Build a list of pages/blah/{name}.html matching expressions
|
|
||||||
pattern_templates = [
|
|
||||||
filepath
|
|
||||||
for filepath in self.ds.jinja_env.list_templates()
|
|
||||||
if "{" in filepath and filepath.startswith("pages/")
|
|
||||||
]
|
|
||||||
self.page_routes = [
|
|
||||||
(route_pattern_from_filepath(filepath[len("pages/") :]), filepath)
|
|
||||||
for filepath in pattern_templates
|
|
||||||
]
|
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, scope, receive, send):
|
||||||
# Because we care about "foo/bar" v.s. "foo%2Fbar" we decode raw_path ourselves
|
# Because we care about "foo/bar" v.s. "foo%2Fbar" we decode raw_path ourselves
|
||||||
|
|
@ -1677,13 +1677,24 @@ class DatasetteRouter:
|
||||||
route_path = request.scope.get("route_path", request.scope["path"])
|
route_path = request.scope.get("route_path", request.scope["path"])
|
||||||
# Jinja requires template names to use "/" even on Windows
|
# Jinja requires template names to use "/" even on Windows
|
||||||
template_name = "pages" + route_path + ".html"
|
template_name = "pages" + route_path + ".html"
|
||||||
|
# Build a list of pages/blah/{name}.html matching expressions
|
||||||
|
environment = self.ds.get_jinja_environment(request)
|
||||||
|
pattern_templates = [
|
||||||
|
filepath
|
||||||
|
for filepath in environment.list_templates()
|
||||||
|
if "{" in filepath and filepath.startswith("pages/")
|
||||||
|
]
|
||||||
|
page_routes = [
|
||||||
|
(route_pattern_from_filepath(filepath[len("pages/") :]), filepath)
|
||||||
|
for filepath in pattern_templates
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
template = self.ds.jinja_env.select_template([template_name])
|
template = environment.select_template([template_name])
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
template = None
|
template = None
|
||||||
if template is None:
|
if template is None:
|
||||||
# Try for a pages/blah/{name}.html template match
|
# Try for a pages/blah/{name}.html template match
|
||||||
for regex, wildcard_template in self.page_routes:
|
for regex, wildcard_template in page_routes:
|
||||||
match = regex.match(route_path)
|
match = regex.match(route_path)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
context.update(match.groupdict())
|
context.update(match.groupdict())
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ def handle_exception(datasette, request, exception):
|
||||||
if request.path.split("?")[0].endswith(".json"):
|
if request.path.split("?")[0].endswith(".json"):
|
||||||
return Response.json(info, status=status, headers=headers)
|
return Response.json(info, status=status, headers=headers)
|
||||||
else:
|
else:
|
||||||
template = datasette.jinja_env.select_template(templates)
|
environment = datasette.get_jinja_environment(request)
|
||||||
|
template = environment.select_template(templates)
|
||||||
return Response.html(
|
return Response.html(
|
||||||
await template.render_async(
|
await template.render_async(
|
||||||
dict(
|
dict(
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,11 @@ def actors_from_ids(datasette, actor_ids):
|
||||||
"""Returns a dictionary mapping those IDs to actor dictionaries"""
|
"""Returns a dictionary mapping those IDs to actor dictionaries"""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def jinja2_environment_from_request(datasette, request, env):
|
||||||
|
"""Return a Jinja2 environment based on the incoming request"""
|
||||||
|
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def filters_from_request(request, database, table, datasette):
|
def filters_from_request(request, database, table, datasette):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ 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)
|
environment = self.ds.get_jinja_environment(request)
|
||||||
|
template = environment.select_template(templates)
|
||||||
template_context = {
|
template_context = {
|
||||||
**context,
|
**context,
|
||||||
**{
|
**{
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ class DatabaseView(View):
|
||||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||||
)
|
)
|
||||||
templates = (f"database-{to_css_class(database)}.html", "database.html")
|
templates = (f"database-{to_css_class(database)}.html", "database.html")
|
||||||
template = datasette.jinja_env.select_template(templates)
|
environment = datasette.get_jinja_environment(request)
|
||||||
|
template = environment.select_template(templates)
|
||||||
context = {
|
context = {
|
||||||
**json_data,
|
**json_data,
|
||||||
"database_color": db.color,
|
"database_color": db.color,
|
||||||
|
|
@ -594,7 +595,8 @@ class QueryView(View):
|
||||||
f"query-{to_css_class(database)}-{to_css_class(canned_query['name'])}.html",
|
f"query-{to_css_class(database)}-{to_css_class(canned_query['name'])}.html",
|
||||||
)
|
)
|
||||||
|
|
||||||
template = datasette.jinja_env.select_template(templates)
|
environment = datasette.get_jinja_environment(request)
|
||||||
|
template = environment.select_template(templates)
|
||||||
alternate_url_json = datasette.absolute_url(
|
alternate_url_json = datasette.absolute_url(
|
||||||
request,
|
request,
|
||||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||||
|
|
|
||||||
|
|
@ -806,7 +806,8 @@ async def table_view_traced(datasette, request):
|
||||||
f"table-{to_css_class(resolved.db.name)}-{to_css_class(resolved.table)}.html",
|
f"table-{to_css_class(resolved.db.name)}-{to_css_class(resolved.table)}.html",
|
||||||
"table.html",
|
"table.html",
|
||||||
]
|
]
|
||||||
template = datasette.jinja_env.select_template(templates)
|
environment = datasette.get_jinja_environment(request)
|
||||||
|
template = environment.select_template(templates)
|
||||||
alternate_url_json = datasette.absolute_url(
|
alternate_url_json = datasette.absolute_url(
|
||||||
request,
|
request,
|
||||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||||
|
|
|
||||||
|
|
@ -1128,6 +1128,48 @@ These IDs could be integers or strings, depending on how the actors used by the
|
||||||
|
|
||||||
Example: `datasette-remote-actors <https://github.com/datasette/datasette-remote-actors>`_
|
Example: `datasette-remote-actors <https://github.com/datasette/datasette-remote-actors>`_
|
||||||
|
|
||||||
|
.. _plugin_hook_jinja2_environment_from_request:
|
||||||
|
|
||||||
|
jinja2_environment_from_request(datasette, request, env)
|
||||||
|
--------------------------------------------------------
|
||||||
|
|
||||||
|
``datasette`` - :ref:`internals_datasette`
|
||||||
|
A Datasette instance.
|
||||||
|
|
||||||
|
``request`` - :ref:`internals_request` or ``None``
|
||||||
|
The current HTTP request, if one is available.
|
||||||
|
|
||||||
|
``env`` - ``Environment``
|
||||||
|
The Jinja2 environment that will be used to render the current page.
|
||||||
|
|
||||||
|
This hook can be used to return a customized `Jinja environment <https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment>`__ based on the incoming request.
|
||||||
|
|
||||||
|
If you want to run a single Datasette instance that serves different content for different domains, you can do so like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from datasette import hookimpl
|
||||||
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def jinja2_environment_from_request(request, env):
|
||||||
|
if request and request.host == "www.niche-museums.com":
|
||||||
|
return env.overlay(
|
||||||
|
loader=ChoiceLoader(
|
||||||
|
[
|
||||||
|
FileSystemLoader(
|
||||||
|
"/mnt/niche-museums/templates"
|
||||||
|
),
|
||||||
|
env.loader,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
enable_async=True,
|
||||||
|
)
|
||||||
|
return env
|
||||||
|
|
||||||
|
This uses the Jinja `overlay() method <https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment.overlay>`__ to create a new environment identical to the default environment except for having a different template loader, which first looks in the ``/mnt/niche-museums/templates`` directory before falling back on the default loader.
|
||||||
|
|
||||||
.. _plugin_hook_filters_from_request:
|
.. _plugin_hook_filters_from_request:
|
||||||
|
|
||||||
filters_from_request(request, database, table, datasette)
|
filters_from_request(request, database, table, datasette)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
|
||||||
from datasette.utils.sqlite import sqlite3
|
from datasette.utils.sqlite import sqlite3
|
||||||
from datasette.utils import CustomRow, StartupError
|
from datasette.utils import CustomRow, StartupError
|
||||||
from jinja2.environment import Template
|
from jinja2.environment import Template
|
||||||
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||||
import base64
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -563,7 +564,8 @@ async def test_hook_register_output_renderer_can_render(ds_client):
|
||||||
async def test_hook_prepare_jinja2_environment(ds_client):
|
async def test_hook_prepare_jinja2_environment(ds_client):
|
||||||
ds_client.ds._HELLO = "HI"
|
ds_client.ds._HELLO = "HI"
|
||||||
await ds_client.ds.invoke_startup()
|
await ds_client.ds.invoke_startup()
|
||||||
template = ds_client.ds.jinja_env.from_string(
|
environment = ds_client.ds.get_jinja_environment(None)
|
||||||
|
template = environment.from_string(
|
||||||
"Hello there, {{ a|format_numeric }}, {{ a|to_hello }}, {{ b|select_times_three }}",
|
"Hello there, {{ a|format_numeric }}, {{ a|to_hello }}, {{ b|select_times_three }}",
|
||||||
{"a": 3412341, "b": 5},
|
{"a": 3412341, "b": 5},
|
||||||
)
|
)
|
||||||
|
|
@ -1294,3 +1296,41 @@ async def test_plugin_is_installed():
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
pm.unregister(name="DummyPlugin")
|
pm.unregister(name="DummyPlugin")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hook_jinja2_environment_from_request(tmpdir):
|
||||||
|
templates = pathlib.Path(tmpdir / "templates")
|
||||||
|
templates.mkdir()
|
||||||
|
(templates / "index.html").write_text("Hello museums!", "utf-8")
|
||||||
|
|
||||||
|
class EnvironmentPlugin:
|
||||||
|
@hookimpl
|
||||||
|
def jinja2_environment_from_request(self, request, env):
|
||||||
|
if request and request.host == "www.niche-museums.com":
|
||||||
|
return env.overlay(
|
||||||
|
loader=ChoiceLoader(
|
||||||
|
[
|
||||||
|
FileSystemLoader(str(templates)),
|
||||||
|
env.loader,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
enable_async=True,
|
||||||
|
)
|
||||||
|
return env
|
||||||
|
|
||||||
|
datasette = Datasette(memory=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pm.register(EnvironmentPlugin(), name="EnvironmentPlugin")
|
||||||
|
response = await datasette.client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Hello museums!" not in response.text
|
||||||
|
# Try again with the hostname
|
||||||
|
response2 = await datasette.client.get(
|
||||||
|
"/", headers={"host": "www.niche-museums.com"}
|
||||||
|
)
|
||||||
|
assert response2.status_code == 200
|
||||||
|
assert "Hello museums!" in response2.text
|
||||||
|
finally:
|
||||||
|
pm.unregister(name="EnvironmentPlugin")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue