kopia lustrzana https://github.com/simonw/datasette
render_cell(value, column, table, database, datasette)
The render_cell plugin hook previously was only passed value. It is now passed (value, column, table, database, datasette).pull/363/head
rodzic
9e1fca4b01
commit
2e836f72d9
|
@ -36,5 +36,5 @@ def publish_subcommand(publish):
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def render_cell(value):
|
def render_cell(value, column, table, database, datasette):
|
||||||
"Customize rendering of HTML table cell values"
|
"Customize rendering of HTML table cell values"
|
||||||
|
|
|
@ -54,6 +54,10 @@ class Results:
|
||||||
self.truncated = truncated
|
self.truncated = truncated
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def columns(self):
|
||||||
|
return [d[0] for d in self.description]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.rows)
|
return iter(self.rows)
|
||||||
|
|
||||||
|
|
|
@ -501,10 +501,16 @@ class BaseView(RenderMixin):
|
||||||
display_rows = []
|
display_rows = []
|
||||||
for row in results.rows:
|
for row in results.rows:
|
||||||
display_row = []
|
display_row = []
|
||||||
for value in row:
|
for column, value in zip(results.columns, row):
|
||||||
display_value = value
|
display_value = value
|
||||||
# Let the plugins have a go
|
# Let the plugins have a go
|
||||||
plugin_value = pm.hook.render_cell(value=value)
|
plugin_value = pm.hook.render_cell(
|
||||||
|
value=value,
|
||||||
|
column=column,
|
||||||
|
table=None,
|
||||||
|
database=name,
|
||||||
|
datasette=self.ds,
|
||||||
|
)
|
||||||
if plugin_value is not None:
|
if plugin_value is not None:
|
||||||
display_value = plugin_value
|
display_value = plugin_value
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -167,7 +167,13 @@ class RowTableShared(BaseView):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# First let the plugins have a go
|
# First let the plugins have a go
|
||||||
plugin_display_value = pm.hook.render_cell(value=value)
|
plugin_display_value = pm.hook.render_cell(
|
||||||
|
value=value,
|
||||||
|
column=column,
|
||||||
|
table=table,
|
||||||
|
database=database,
|
||||||
|
datasette=self.ds,
|
||||||
|
)
|
||||||
if plugin_display_value is not None:
|
if plugin_display_value is not None:
|
||||||
display_value = plugin_display_value
|
display_value = plugin_display_value
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
|
|
|
@ -217,9 +217,16 @@ Now that ``datasette-cluster-map`` plugin configuration will apply to every tabl
|
||||||
Plugin hooks
|
Plugin hooks
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Datasette will eventually have many more plugin hooks. You can track and
|
When you implement a plugin hook you can accept any or all of the parameters that are documented as being passed to that hook. For example, you can implement a ``render_cell`` plugin hook like this even though the hook definition defines more parameters than just ``value`` and ``column``:
|
||||||
contribute to their development in `issue #14
|
|
||||||
<https://github.com/simonw/datasette/issues/14>`_.
|
.. code-block:: python
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def render_cell(value, column):
|
||||||
|
if column == "stars":
|
||||||
|
return "*" * int(value)
|
||||||
|
|
||||||
|
The full list of available plugin hooks is as follows.
|
||||||
|
|
||||||
prepare_connection(conn)
|
prepare_connection(conn)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -333,12 +340,25 @@ and ``heroku`` subcommands, so you can read
|
||||||
`their source <https://github.com/simonw/datasette/tree/master/datasette/publish>`_
|
`their source <https://github.com/simonw/datasette/tree/master/datasette/publish>`_
|
||||||
to see examples of this hook in action.
|
to see examples of this hook in action.
|
||||||
|
|
||||||
render_cell(value)
|
render_cell(value, column, table, database, datasette)
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Lets you customize the display of values within table cells in the HTML table view.
|
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.
|
``value`` - string, integer or None
|
||||||
|
The value that was loaded from the database
|
||||||
|
|
||||||
|
``column`` - string
|
||||||
|
The name of the column being rendered
|
||||||
|
|
||||||
|
``table`` - string
|
||||||
|
The name of the table
|
||||||
|
|
||||||
|
``database`` - string
|
||||||
|
The name of the database
|
||||||
|
|
||||||
|
``datasette`` - Datasette instance
|
||||||
|
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``
|
||||||
|
|
||||||
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 your hook returns ``None``, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value.
|
||||||
|
|
||||||
|
@ -346,6 +366,8 @@ 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.
|
If you want to return HTML markup you can do so by returning a ``jinja2.Markup`` object.
|
||||||
|
|
||||||
|
Datasette will loop through all available ``render_cell`` hooks and display the value returned by the first one that does not return ``None``.
|
||||||
|
|
||||||
Here is an example of a custom ``render_cell()`` plugin which looks for values that are a JSON string matching the following format::
|
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"}
|
{"href": "https://www.example.com/", "label": "Name"}
|
||||||
|
|
|
@ -146,6 +146,12 @@ METADATA = {
|
||||||
'simple_primary_key': {
|
'simple_primary_key': {
|
||||||
'description_html': 'Simple <em>primary</em> key',
|
'description_html': 'Simple <em>primary</em> key',
|
||||||
'title': 'This <em>HTML</em> is escaped',
|
'title': 'This <em>HTML</em> is escaped',
|
||||||
|
"plugins": {
|
||||||
|
"name-of-plugin": {
|
||||||
|
"depth": "table",
|
||||||
|
"special": "this-is-simple_primary_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'sortable': {
|
'sortable': {
|
||||||
'sortable_columns': [
|
'sortable_columns': [
|
||||||
|
@ -199,6 +205,7 @@ METADATA = {
|
||||||
PLUGIN1 = '''
|
PLUGIN1 = '''
|
||||||
from datasette import hookimpl
|
from datasette import hookimpl
|
||||||
import pint
|
import pint
|
||||||
|
import json
|
||||||
|
|
||||||
ureg = pint.UnitRegistry()
|
ureg = pint.UnitRegistry()
|
||||||
|
|
||||||
|
@ -226,7 +233,6 @@ def extra_js_urls():
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def extra_body_script(template, database, table, datasette):
|
def extra_body_script(template, database, table, datasette):
|
||||||
import json
|
|
||||||
return 'var extra_body_script = {};'.format(
|
return 'var extra_body_script = {};'.format(
|
||||||
json.dumps({
|
json.dumps({
|
||||||
"template": template,
|
"template": template,
|
||||||
|
@ -239,6 +245,23 @@ def extra_body_script(template, database, table, datasette):
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def render_cell(value, column, table, database, datasette):
|
||||||
|
# Render some debug output in cell with value RENDER_CELL_DEMO
|
||||||
|
if value != "RENDER_CELL_DEMO":
|
||||||
|
return None
|
||||||
|
return json.dumps({
|
||||||
|
"column": column,
|
||||||
|
"table": table,
|
||||||
|
"database": database,
|
||||||
|
"config": datasette.plugin_config(
|
||||||
|
"name-of-plugin",
|
||||||
|
database=database,
|
||||||
|
table=table,
|
||||||
|
)
|
||||||
|
})
|
||||||
'''
|
'''
|
||||||
|
|
||||||
PLUGIN2 = '''
|
PLUGIN2 = '''
|
||||||
|
@ -256,7 +279,7 @@ def extra_js_urls():
|
||||||
|
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def render_cell(value):
|
def render_cell(value, database):
|
||||||
# Render {"href": "...", "label": "..."} as link
|
# Render {"href": "...", "label": "..."} as link
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return None
|
return None
|
||||||
|
@ -277,10 +300,13 @@ def render_cell(value):
|
||||||
or href.startswith("https://")
|
or href.startswith("https://")
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
return jinja2.Markup('<a href="{href}">{label}</a>'.format(
|
return jinja2.Markup(
|
||||||
href=jinja2.escape(data["href"]),
|
'<a data-database="{database}" href="{href}">{label}</a>'.format(
|
||||||
label=jinja2.escape(data["label"] or "") or " "
|
database=database,
|
||||||
))
|
href=jinja2.escape(data["href"]),
|
||||||
|
label=jinja2.escape(data["label"] or "") or " "
|
||||||
|
)
|
||||||
|
)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
TABLES = '''
|
TABLES = '''
|
||||||
|
@ -487,6 +513,7 @@ VALUES
|
||||||
INSERT INTO simple_primary_key VALUES (1, 'hello');
|
INSERT INTO simple_primary_key VALUES (1, 'hello');
|
||||||
INSERT INTO simple_primary_key VALUES (2, 'world');
|
INSERT INTO simple_primary_key VALUES (2, 'world');
|
||||||
INSERT INTO simple_primary_key VALUES (3, '');
|
INSERT INTO simple_primary_key VALUES (3, '');
|
||||||
|
INSERT INTO simple_primary_key VALUES (4, 'RENDER_CELL_DEMO');
|
||||||
|
|
||||||
INSERT INTO primary_key_multiple_columns VALUES (1, 'hey', 'world');
|
INSERT INTO primary_key_multiple_columns VALUES (1, 'hey', 'world');
|
||||||
INSERT INTO primary_key_multiple_columns_explicit_label VALUES (1, 'hey', 'world2');
|
INSERT INTO primary_key_multiple_columns_explicit_label VALUES (1, 'hey', 'world2');
|
||||||
|
|
|
@ -242,7 +242,7 @@ def test_database_page(app_client):
|
||||||
}, {
|
}, {
|
||||||
'columns': ['id', 'content'],
|
'columns': ['id', 'content'],
|
||||||
'name': 'simple_primary_key',
|
'name': 'simple_primary_key',
|
||||||
'count': 3,
|
'count': 4,
|
||||||
'hidden': False,
|
'hidden': False,
|
||||||
'foreign_keys': {
|
'foreign_keys': {
|
||||||
'incoming': [{
|
'incoming': [{
|
||||||
|
@ -383,7 +383,8 @@ def test_custom_sql(app_client):
|
||||||
assert [
|
assert [
|
||||||
{'content': 'hello'},
|
{'content': 'hello'},
|
||||||
{'content': 'world'},
|
{'content': 'world'},
|
||||||
{'content': ''}
|
{'content': ''},
|
||||||
|
{'content': 'RENDER_CELL_DEMO'}
|
||||||
] == data['rows']
|
] == data['rows']
|
||||||
assert ['content'] == data['columns']
|
assert ['content'] == data['columns']
|
||||||
assert 'fixtures' == data['database']
|
assert 'fixtures' == data['database']
|
||||||
|
@ -457,6 +458,9 @@ def test_table_json(app_client):
|
||||||
}, {
|
}, {
|
||||||
'id': '3',
|
'id': '3',
|
||||||
'content': '',
|
'content': '',
|
||||||
|
}, {
|
||||||
|
'id': '4',
|
||||||
|
'content': 'RENDER_CELL_DEMO',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
@ -490,6 +494,7 @@ def test_table_shape_arrays(app_client):
|
||||||
['1', 'hello'],
|
['1', 'hello'],
|
||||||
['2', 'world'],
|
['2', 'world'],
|
||||||
['3', ''],
|
['3', ''],
|
||||||
|
['4', 'RENDER_CELL_DEMO'],
|
||||||
] == response.json['rows']
|
] == response.json['rows']
|
||||||
|
|
||||||
|
|
||||||
|
@ -500,7 +505,7 @@ def test_table_shape_arrayfirst(app_client):
|
||||||
'_shape': 'arrayfirst'
|
'_shape': 'arrayfirst'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
assert ['hello', 'world', ''] == response.json
|
assert ['hello', 'world', '', 'RENDER_CELL_DEMO'] == response.json
|
||||||
|
|
||||||
|
|
||||||
def test_table_shape_objects(app_client):
|
def test_table_shape_objects(app_client):
|
||||||
|
@ -516,6 +521,9 @@ def test_table_shape_objects(app_client):
|
||||||
}, {
|
}, {
|
||||||
'id': '3',
|
'id': '3',
|
||||||
'content': '',
|
'content': '',
|
||||||
|
}, {
|
||||||
|
'id': '4',
|
||||||
|
'content': 'RENDER_CELL_DEMO',
|
||||||
}] == response.json['rows']
|
}] == response.json['rows']
|
||||||
|
|
||||||
|
|
||||||
|
@ -532,6 +540,9 @@ def test_table_shape_array(app_client):
|
||||||
}, {
|
}, {
|
||||||
'id': '3',
|
'id': '3',
|
||||||
'content': '',
|
'content': '',
|
||||||
|
}, {
|
||||||
|
'id': '4',
|
||||||
|
'content': 'RENDER_CELL_DEMO',
|
||||||
}] == response.json
|
}] == response.json
|
||||||
|
|
||||||
|
|
||||||
|
@ -563,6 +574,10 @@ def test_table_shape_object(app_client):
|
||||||
'3': {
|
'3': {
|
||||||
'id': '3',
|
'id': '3',
|
||||||
'content': '',
|
'content': '',
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
'id': '4',
|
||||||
|
'content': 'RENDER_CELL_DEMO',
|
||||||
}
|
}
|
||||||
} == response.json
|
} == response.json
|
||||||
|
|
||||||
|
@ -826,6 +841,7 @@ def test_searchable_invalid_column(app_client):
|
||||||
('/fixtures/simple_primary_key.json?content__contains=o', [
|
('/fixtures/simple_primary_key.json?content__contains=o', [
|
||||||
['1', 'hello'],
|
['1', 'hello'],
|
||||||
['2', 'world'],
|
['2', 'world'],
|
||||||
|
['4', 'RENDER_CELL_DEMO'],
|
||||||
]),
|
]),
|
||||||
('/fixtures/simple_primary_key.json?content__exact=', [
|
('/fixtures/simple_primary_key.json?content__exact=', [
|
||||||
['3', ''],
|
['3', ''],
|
||||||
|
@ -833,6 +849,7 @@ def test_searchable_invalid_column(app_client):
|
||||||
('/fixtures/simple_primary_key.json?content__not=world', [
|
('/fixtures/simple_primary_key.json?content__not=world', [
|
||||||
['1', 'hello'],
|
['1', 'hello'],
|
||||||
['3', ''],
|
['3', ''],
|
||||||
|
['4', 'RENDER_CELL_DEMO'],
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
def test_table_filter_queries(app_client, path, expected_rows):
|
def test_table_filter_queries(app_client, path, expected_rows):
|
||||||
|
@ -866,6 +883,9 @@ def test_view(app_client):
|
||||||
}, {
|
}, {
|
||||||
'upper_content': '',
|
'upper_content': '',
|
||||||
'content': '',
|
'content': '',
|
||||||
|
}, {
|
||||||
|
'upper_content': 'RENDER_CELL_DEMO',
|
||||||
|
'content': 'RENDER_CELL_DEMO',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ EXPECTED_TABLE_CSV = '''id,content
|
||||||
1,hello
|
1,hello
|
||||||
2,world
|
2,world
|
||||||
3,
|
3,
|
||||||
|
4,RENDER_CELL_DEMO
|
||||||
'''.replace('\n', '\r\n')
|
'''.replace('\n', '\r\n')
|
||||||
|
|
||||||
EXPECTED_CUSTOM_CSV = '''content
|
EXPECTED_CUSTOM_CSV = '''content
|
||||||
|
|
|
@ -372,7 +372,7 @@ def test_css_classes_on_body(app_client, path, expected_classes):
|
||||||
|
|
||||||
|
|
||||||
def test_table_html_simple_primary_key(app_client):
|
def test_table_html_simple_primary_key(app_client):
|
||||||
response = app_client.get('/fixtures/simple_primary_key')
|
response = app_client.get('/fixtures/simple_primary_key?_size=3')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
table = Soup(response.body, 'html.parser').find('table')
|
table = Soup(response.body, 'html.parser').find('table')
|
||||||
assert table['class'] == ['rows-and-columns']
|
assert table['class'] == ['rows-and-columns']
|
||||||
|
@ -381,7 +381,7 @@ def test_table_html_simple_primary_key(app_client):
|
||||||
for expected_col, th in zip(('content',), ths[1:]):
|
for expected_col, th in zip(('content',), ths[1:]):
|
||||||
a = th.find('a')
|
a = th.find('a')
|
||||||
assert expected_col == a.string
|
assert expected_col == a.string
|
||||||
assert a['href'].endswith('/simple_primary_key?_sort={}'.format(
|
assert a['href'].endswith('/simple_primary_key?_size=3&_sort={}'.format(
|
||||||
expected_col
|
expected_col
|
||||||
))
|
))
|
||||||
assert ['nofollow'] == a['rel']
|
assert ['nofollow'] == a['rel']
|
||||||
|
@ -613,13 +613,13 @@ def test_compound_primary_key_with_foreign_key_references(app_client):
|
||||||
|
|
||||||
|
|
||||||
def test_view_html(app_client):
|
def test_view_html(app_client):
|
||||||
response = app_client.get("/fixtures/simple_view")
|
response = app_client.get("/fixtures/simple_view?_size=3")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
table = Soup(response.body, "html.parser").find("table")
|
table = Soup(response.body, "html.parser").find("table")
|
||||||
ths = table.select("thead th")
|
ths = table.select("thead th")
|
||||||
assert 2 == len(ths)
|
assert 2 == len(ths)
|
||||||
assert ths[0].find("a") is not None
|
assert ths[0].find("a") is not None
|
||||||
assert ths[0].find("a")["href"].endswith("/simple_view?_sort=content")
|
assert ths[0].find("a")["href"].endswith("/simple_view?_size=3&_sort=content")
|
||||||
assert ths[0].find("a").string.strip() == "content"
|
assert ths[0].find("a").string.strip() == "content"
|
||||||
assert ths[1].find("a") is None
|
assert ths[1].find("a") is None
|
||||||
assert ths[1].string.strip() == "upper_content"
|
assert ths[1].string.strip() == "upper_content"
|
||||||
|
|
|
@ -72,7 +72,7 @@ def test_plugins_with_duplicate_js_urls(app_client):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_plugins_render_cell(app_client):
|
def test_plugins_render_cell_link_from_json(app_client):
|
||||||
sql = """
|
sql = """
|
||||||
select '{"href": "http://example.com/", "label":"Example"}'
|
select '{"href": "http://example.com/", "label":"Example"}'
|
||||||
""".strip()
|
""".strip()
|
||||||
|
@ -86,9 +86,25 @@ def test_plugins_render_cell(app_client):
|
||||||
a = td.find("a")
|
a = td.find("a")
|
||||||
assert a is not None, str(a)
|
assert a is not None, str(a)
|
||||||
assert a.attrs["href"] == "http://example.com/"
|
assert a.attrs["href"] == "http://example.com/"
|
||||||
|
assert a.attrs["data-database"] == "fixtures"
|
||||||
assert a.text == "Example"
|
assert a.text == "Example"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugins_render_cell_demo(app_client):
|
||||||
|
response = app_client.get("/fixtures/simple_primary_key?id=4")
|
||||||
|
soup = Soup(response.body, "html.parser")
|
||||||
|
td = soup.find("td", {"class": "col-content"})
|
||||||
|
assert {
|
||||||
|
"column": "content",
|
||||||
|
"table": "simple_primary_key",
|
||||||
|
"database": "fixtures",
|
||||||
|
"config": {
|
||||||
|
"depth": "table",
|
||||||
|
"special": "this-is-simple_primary_key"
|
||||||
|
}
|
||||||
|
} == json.loads(td.string)
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_config(app_client):
|
def test_plugin_config(app_client):
|
||||||
assert {"depth": "table"} == app_client.ds.plugin_config(
|
assert {"depth": "table"} == app_client.ds.plugin_config(
|
||||||
"name-of-plugin", database="fixtures", table="sortable"
|
"name-of-plugin", database="fixtures", table="sortable"
|
||||||
|
@ -138,7 +154,7 @@ def test_plugin_config(app_client):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_extra_body_script(app_client, path, expected_extra_body_script):
|
def test_plugins_extra_body_script(app_client, path, expected_extra_body_script):
|
||||||
r = re.compile(r"<script>var extra_body_script = (.*?);</script>")
|
r = re.compile(r"<script>var extra_body_script = (.*?);</script>")
|
||||||
json_data = r.search(app_client.get(path).body.decode("utf8")).group(1)
|
json_data = r.search(app_client.get(path).body.decode("utf8")).group(1)
|
||||||
actual_data = json.loads(json_data)
|
actual_data = json.loads(json_data)
|
||||||
|
|
Ładowanie…
Reference in New Issue