render_cell(value) plugin hook, closes #352

New plugin hook for customizing the way cells values are rendered in HTML.

The first full example of this hook in use is https://github.com/simonw/datasette-json-html
pull/381/head
Simon Willison 2018-08-04 17:14:56 -07:00 zatwierdzone przez GitHub
rodzic 295d005ca4
commit 4ac9132240
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
10 zmienionych plików z 150 dodań i 30 usunięć

Wyświetl plik

@ -2,7 +2,6 @@ import asyncio
import click
import collections
import hashlib
import importlib
import itertools
import os
import sqlite3
@ -14,7 +13,6 @@ from concurrent import futures
from pathlib import Path
from markupsafe import Markup
import pluggy
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
from sanic import Sanic, response
from sanic.exceptions import InvalidUsage, NotFound
@ -28,7 +26,6 @@ from .views.index import IndexView
from .views.special import JsonDataView
from .views.table import RowView, TableView
from . import hookspecs
from .utils import (
InterruptedError,
Results,
@ -40,26 +37,13 @@ from .utils import (
to_css_class
)
from .inspect import inspect_hash, inspect_views, inspect_tables
from .plugins import pm
from .version import __version__
default_plugins = (
"datasette.publish.heroku",
"datasette.publish.now",
)
app_root = Path(__file__).parent.parent
connections = threading.local()
pm = pluggy.PluginManager("datasette")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("datasette")
# Load default plugins
for plugin in default_plugins:
mod = importlib.import_module(plugin)
pm.register(mod, plugin)
ConfigOption = collections.namedtuple(
"ConfigOption", ("name", "default", "help")

Wyświetl plik

@ -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"

Wyświetl plik

@ -0,0 +1,17 @@
import importlib
import pluggy
from . import hookspecs
default_plugins = (
"datasette.publish.heroku",
"datasette.publish.now",
)
pm = pluggy.PluginManager("datasette")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("datasette")
# Load default plugins
for plugin in default_plugins:
mod = importlib.import_module(plugin)
pm.register(mod, plugin)

Wyświetl plik

@ -16,7 +16,6 @@ import shutil
import urllib
import numbers
# From https://www.sqlite.org/lang_keywords.html
reserved_words = set((
'abort action add after all alter analyze and as asc attach autoincrement '

Wyświetl plik

@ -13,6 +13,7 @@ from sanic.exceptions import NotFound
from sanic.views import HTTPMethodView
from datasette import __version__
from datasette.plugins import pm
from datasette.utils import (
CustomJSONEncoder,
InterruptedError,
@ -493,14 +494,19 @@ 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(
'<a href="{url}">{url}</a>'.format(
url=jinja2.escape(value.strip())
# Let the plugins have a go
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("&nbsp;")
elif is_url(str(display_value).strip()):
display_value = jinja2.Markup(
'<a href="{url}">{url}</a>'.format(
url=jinja2.escape(value.strip())
)
)
)
display_row.append(display_value)
display_rows.append(display_row)
return {

Wyświetl plik

@ -5,6 +5,7 @@ import jinja2
from sanic.exceptions import NotFound
from sanic.request import RequestParameters
from datasette.plugins import pm
from datasette.utils import (
CustomRow,
Filters,
@ -22,7 +23,6 @@ from datasette.utils import (
urlsafe_components,
value_as_boolean,
)
from .base import BaseView, DatasetteError, ureg
LINK_WITH_LABEL = '<a href="/{database}/{table}/{link_id}">{label}</a>&nbsp;<em>{id}</em>'
@ -166,7 +166,11 @@ class RowTableShared(BaseView):
# already shown in the link column.
continue
if isinstance(value, dict):
# First let the plugins have a go
plugin_display_value = pm.hook.render_cell(value=value)
if plugin_display_value is not None:
display_value = plugin_display_value
elif isinstance(value, dict):
# It's an expanded foreign key - display link to other row
label = value["label"]
value = value["value"]

Wyświetl plik

@ -267,3 +267,56 @@ command. Datasette uses this hook internally to implement the default ``now``
and ``heroku`` subcommands, so you can read
`their source <https://github.com/simonw/datasette/tree/master/datasette/publish>`_
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
if not isinstance(value, str):
return None
stripped = value.strip()
if not stripped.startswith("{") and stripped.endswith("}"):
return None
try:
data = json.loads(value)
except ValueError:
return None
if not isinstance(data, dict):
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('<a href="{href}">{label}</a>'.format(
href=jinja2.escape(data["href"]),
label=jinja2.escape(data["label"] or "") or "&nbsp;"
))

Wyświetl plik

@ -208,6 +208,8 @@ def extra_js_urls():
PLUGIN2 = '''
from datasette import hookimpl
import jinja2
import json
@hookimpl
@ -216,6 +218,34 @@ 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
if not isinstance(value, str):
return None
stripped = value.strip()
if not stripped.startswith("{") and stripped.endswith("}"):
return None
try:
data = json.loads(value)
except ValueError:
return None
if not isinstance(data, dict):
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('<a href="{href}">{label}</a>'.format(
href=jinja2.escape(data["href"]),
label=jinja2.escape(data["label"] or "") or "&nbsp;"
))
'''
TABLES = '''
@ -363,9 +393,12 @@ INSERT INTO "searchable_fts" (rowid, text1, text2, [name with . and spaces])
CREATE TABLE [select] (
[group] text,
[having] text,
[and] text
[and] text,
[json] text
);
INSERT INTO [select] VALUES ('group', 'having', 'and',
'{"href": "http://example.com/", "label":"Example"}'
);
INSERT INTO [select] VALUES ('group', 'having', 'and');
CREATE TABLE infinity (
value REAL

Wyświetl plik

@ -231,7 +231,7 @@ def test_database_page(app_client):
],
},
}, {
'columns': ['group', 'having', 'and'],
'columns': ['group', 'having', 'and', 'json'],
'name': 'select',
'count': 1,
'hidden': False,
@ -599,6 +599,7 @@ def test_table_with_reserved_word_name(app_client):
'group': 'group',
'having': 'having',
'and': 'and',
'json': '{"href": "http://example.com/", "label":"Example"}'
}]

Wyświetl plik

@ -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"