From 510e01f2240ef5ce6cf444ea8c295e996d57f61e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 30 Jul 2018 08:55:26 -0700 Subject: [PATCH] render_cell(value) plugin hook Still needs performance testing before I merge this into master --- datasette/hookspecs.py | 5 +++++ datasette/views/base.py | 20 +++++++++++------ docs/plugins.rst | 49 +++++++++++++++++++++++++++++++++++++++++ tests/fixtures.py | 26 ++++++++++++++++++++++ tests/test_plugins.py | 18 +++++++++++++++ 5 files changed, 111 insertions(+), 7 deletions(-) diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 9546eebf..233a9aa0 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -28,3 +28,8 @@ def extra_js_urls(): @hookspec def publish_subcommand(publish): "Subcommands for 'datasette publish'" + + +@hookspec(firstresult=True) +def render_cell(value): + "Customize rendering of HTML table cell values" diff --git a/datasette/views/base.py b/datasette/views/base.py index 45bc8183..fbb9b173 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -493,14 +493,20 @@ class BaseView(RenderMixin): display_row = [] for value in row: display_value = value - if value in ("", None): - display_value = jinja2.Markup(" ") - elif is_url(str(value).strip()): - display_value = jinja2.Markup( - '{url}'.format( - url=jinja2.escape(value.strip()) + # Let the plugins have a go + from datasette.app import pm + plugin_value = pm.hook.render_cell(value=value) + if plugin_value is not None: + display_value = plugin_value + else: + if value in ("", None): + display_value = jinja2.Markup(" ") + elif is_url(str(display_value).strip()): + display_value = jinja2.Markup( + '{url}'.format( + url=jinja2.escape(value.strip()) + ) ) - ) display_row.append(display_value) display_rows.append(display_row) return { diff --git a/docs/plugins.rst b/docs/plugins.rst index fc351bf6..add20669 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -267,3 +267,52 @@ command. Datasette uses this hook internally to implement the default ``now`` and ``heroku`` subcommands, so you can read `their source `_ to see examples of this hook in action. + +render_cell(value) +~~~~~~~~~~~~~~~~~~ + +Lets you customize the display of values within table cells in the HTML table view. + +``value`` is the value that was loaded from the database. + +If your hook returns ``None``, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value. + +If the hook returns a string, that string will be rendered in the table cell. + +If you want to return HTML markup you can do so by returning a ``jinja2.Markup`` object. + +Here is an example of a custom ``render_cell()`` plugin which looks for values that are a JSON string matching the following format:: + + {"href": "https://www.example.com/", "label": "Name"} + +If the value matches that pattern, the plugin returns an HTML link element: + +.. code-block:: python + + from datasette import hookimpl + import jinja2 + import json + + + @hookimpl + def render_cell(value): + # Render {"href": "...", "label": "..."} as link + stripped = value.strip() + if not stripped.startswith("{") and stripped.endswith("}"): + return None + try: + data = json.loads(value) + except ValueError: + return None + if set(data.keys()) != {"href", "label"}: + return None + href = data["href"] + if not ( + href.startswith("/") or href.startswith("http://") + or href.startswith("https://") + ): + return None + return jinja2.Markup('{label}'.format( + href=jinja2.escape(data["href"]), + label=jinja2.escape(data["label"] or "") or " " + )) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6335494a..955b155d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -208,6 +208,8 @@ def extra_js_urls(): PLUGIN2 = ''' from datasette import hookimpl +import jinja2 +import json @hookimpl @@ -216,6 +218,30 @@ def extra_js_urls(): 'url': 'https://example.com/jquery.js', 'sri': 'SRIHASH', }, 'https://example.com/plugin2.js'] + + +@hookimpl +def render_cell(value): + # Render {"href": "...", "label": "..."} as link + stripped = value.strip() + if not stripped.startswith("{") and stripped.endswith("}"): + return None + try: + data = json.loads(value) + except ValueError: + return None + if set(data.keys()) != {"href", "label"}: + return None + href = data["href"] + if not ( + href.startswith("/") or href.startswith("http://") + or href.startswith("https://") + ): + return None + return jinja2.Markup('{label}'.format( + href=jinja2.escape(data["href"]), + label=jinja2.escape(data["label"] or "") or " " + )) ''' TABLES = ''' diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 487aa10c..fee27c24 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -3,6 +3,7 @@ from .fixtures import ( # noqa app_client, ) import pytest +import urllib def test_plugins_dir_plugin(app_client): @@ -67,3 +68,20 @@ def test_plugins_with_duplicate_js_urls(app_client): ) < srcs.index( 'https://example.com/plugin2.js' ) + + +def test_plugins_render_cell(app_client): + sql = """ + select '{"href": "http://example.com/", "label":"Example"}' + """.strip() + path = "/fixtures?" + urllib.parse.urlencode({ + "sql": sql, + }) + response = app_client.get(path) + td = Soup( + response.body, "html.parser" + ).find("table").find("tbody").find("td") + a = td.find("a") + assert a is not None, str(a) + assert a.attrs["href"] == "http://example.com/" + assert a.text == "Example"