Skip to content

Commit

Permalink
jinja2_environment_from_request() plugin hook
Browse files Browse the repository at this point in the history
With stub tests and documentation so far.

Refs #2225
  • Loading branch information
simonw committed Jan 5, 2024
1 parent 45b88f2 commit b43d5c3
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 25 deletions.
49 changes: 30 additions & 19 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,21 +420,31 @@ def __init__(
),
]
)
self.jinja_env = Environment(
environment = Environment(
loader=template_loader,
autoescape=True,
enable_async=True,
# undefined=StrictUndefined,
)
self.jinja_env.filters["escape_css_string"] = escape_css_string
self.jinja_env.filters["quote_plus"] = urllib.parse.quote_plus
self.jinja_env.filters["escape_sqlite"] = escape_sqlite
self.jinja_env.filters["to_css_class"] = to_css_class
environment.filters["escape_css_string"] = escape_css_string
environment.filters["quote_plus"] = urllib.parse.quote_plus
self._jinja_env = environment
environment.filters["escape_sqlite"] = escape_sqlite
environment.filters["to_css_class"] = to_css_class
self._register_renderers()
self._permission_checks = collections.deque(maxlen=200)
self._root_token = secrets.token_hex(32)
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

Check warning on line 445 in datasette/app.py

View check run for this annotation

Codecov / codecov/patch

datasette/app.py#L445

Added line #L445 was not covered by tests
return environment

def get_permission(self, name_or_abbr: str) -> "Permission":
"""
Returns a Permission object for the given name or abbreviation. Raises KeyError if not found.
Expand Down Expand Up @@ -514,7 +524,7 @@ async def invoke_startup(self):
abbrs[p.abbr] = p
self.permissions[p.name] = p
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)
for hook in pm.hook.startup(datasette=self):
Expand Down Expand Up @@ -1218,7 +1228,7 @@ async def render_template(
else:
if isinstance(templates, str):
templates = [templates]
template = self.jinja_env.select_template(templates)
template = self.get_jinja_environment(request).select_template(templates)
if dataclasses.is_dataclass(context):
context = dataclasses.asdict(context)
body_scripts = []
Expand Down Expand Up @@ -1568,16 +1578,6 @@ class DatasetteRouter:
def __init__(self, datasette, routes):
self.ds = datasette
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):
# Because we care about "foo/bar" v.s. "foo%2Fbar" we decode raw_path ourselves
Expand Down Expand Up @@ -1677,13 +1677,24 @@ async def handle_404(self, request, send, exception=None):
route_path = request.scope.get("route_path", request.scope["path"])
# Jinja requires template names to use "/" even on Windows
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:
template = self.ds.jinja_env.select_template([template_name])
template = environment.select_template([template_name])
except TemplateNotFound:
template = None
if template is None:
# 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)
if match is not None:
context.update(match.groupdict())
Expand Down
3 changes: 2 additions & 1 deletion datasette/handle_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ async def inner():
if request.path.split("?")[0].endswith(".json"):
return Response.json(info, status=status, headers=headers)
else:
template = datasette.jinja_env.select_template(templates)
environment = datasette.get_jinja_environment(request)
template = environment.select_template(templates)
return Response.html(
await template.render_async(
dict(
Expand Down
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ def actors_from_ids(datasette, actor_ids):
"""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
def filters_from_request(request, database, table, datasette):
"""
Expand Down
3 changes: 2 additions & 1 deletion datasette/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ async def dispatch_request(self, request):

async def render(self, templates, request, context=None):
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 = {
**context,
**{
Expand Down
6 changes: 4 additions & 2 deletions datasette/views/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ async def database_actions():
datasette.urls.path(path_with_format(request=request, format="json")),
)
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 = {
**json_data,
"database_color": db.color,
Expand Down Expand Up @@ -594,7 +595,8 @@ async def fetch_data_for_csv(request, _next=None):
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(
request,
datasette.urls.path(path_with_format(request=request, format="json")),
Expand Down
3 changes: 2 additions & 1 deletion datasette/views/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,8 @@ async def fetch_data(request, _next=None):
f"table-{to_css_class(resolved.db.name)}-{to_css_class(resolved.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(
request,
datasette.urls.path(path_with_format(request=request, format="json")),
Expand Down
16 changes: 16 additions & 0 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,22 @@ 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>`_

.. _plugin_hook_jinja2_environment_from_request:

jinja2_environment_from_request(datasette, request, env)
--------------------------------------------------------

``datasette`` - :ref:`internals_datasette`
A Datasette instance.

``request`` - :ref:`internals_request`
The current HTTP request.

``env`` - `Environment`
The Jinja2 environment that will be used to render the current page.

... use overlays here

.. _plugin_hook_filters_from_request:

filters_from_request(request, database, table, datasette)
Expand Down
8 changes: 7 additions & 1 deletion tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,8 @@ async def test_hook_register_output_renderer_can_render(ds_client):
async def test_hook_prepare_jinja2_environment(ds_client):
ds_client.ds._HELLO = "HI"
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 }}",
{"a": 3412341, "b": 5},
)
Expand Down Expand Up @@ -1294,3 +1295,8 @@ def actors_from_ids(self, datasette, actor_ids):

finally:
pm.unregister(name="DummyPlugin")


@pytest.mark.asyncio
async def test_hook_jinja2_environment_from_request():
pass

0 comments on commit b43d5c3

Please sign in to comment.