From 2b847240bb37cf21dc0e8d87f206e1c089670d72 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 25 Jun 2019 05:02:42 -0700 Subject: [PATCH 1/5] New experimental Row() for templates, refs #521 --- datasette/views/table.py | 43 +++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/datasette/views/table.py b/datasette/views/table.py index 06be5671..f2f5fda0 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -33,6 +33,35 @@ LINK_WITH_LABEL = ( LINK_WITH_VALUE = '{id}' +class Row: + def __init__(self, cells): + self.cells = cells + + def __iter__(self): + return iter(self.cells) + + def __getitem__(self, key): + for cell in self.cells: + if cell["column"] == key: + return cell["value"] + raise KeyError + + def raw(self, key): + for cell in self.cells: + if cell["column"] == key: + return cell["raw"] + return None + + def __str__(self): + d = { + key: self[key] + for key in [ + c["column"] for c in self.cells if not c.get("is_special_link_column") + ] + } + return json.dumps(d, default=repr, indent=2) + + class RowTableShared(DataView): async def sortable_columns_for_table(self, database, table, use_rowid): db = self.ds.databases[database] @@ -76,18 +105,18 @@ class RowTableShared(DataView): # Unless we are a view, the first column is a link - either to the rowid # or to the simple or compound primary key if link_column: + is_special_link_column = len(pks) != 1 + pk_path = path_from_row_pks(row, pks, not pks, False) cells.append( { "column": pks[0] if len(pks) == 1 else "Link", + "is_special_link_column": is_special_link_column, + "raw": pk_path, "value": jinja2.Markup( '{flat_pks}'.format( database=database, table=urllib.parse.quote_plus(table), - flat_pks=str( - jinja2.escape( - path_from_row_pks(row, pks, not pks, False) - ) - ), + flat_pks=str(jinja2.escape(pk_path)), flat_pks_quoted=path_from_row_pks(row, pks, not pks), ) ), @@ -159,8 +188,8 @@ class RowTableShared(DataView): if truncate_cells and len(display_value) > truncate_cells: display_value = display_value[:truncate_cells] + u"\u2026" - cells.append({"column": column, "value": display_value}) - cell_rows.append(cells) + cells.append({"column": column, "value": display_value, "raw": value}) + cell_rows.append(Row(cells)) if link_column: # Add the link column header. From 43a5567be8bb8f963b2ef1507fa4ed26209840c2 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 25 Jun 2019 05:21:10 -0700 Subject: [PATCH 2/5] Default to raw value, use Row.display(key) for display, refs #521 --- datasette/views/table.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datasette/views/table.py b/datasette/views/table.py index f2f5fda0..c41bc305 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -43,13 +43,13 @@ class Row: def __getitem__(self, key): for cell in self.cells: if cell["column"] == key: - return cell["value"] + return cell["raw"] raise KeyError - def raw(self, key): + def display(self, key): for cell in self.cells: if cell["column"] == key: - return cell["raw"] + return cell["value"] return None def __str__(self): From 7d6b0d6762707550eb58004842a13f5e37da8ee3 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 2 Jul 2019 17:50:45 -0700 Subject: [PATCH 3/5] Rename _rows_and_columns.html to _table.html, refs #521 --- .../{_rows_and_columns.html => _table.html} | 0 datasette/templates/row.html | 2 +- datasette/templates/table.html | 2 +- datasette/views/table.py | 16 +++++++-------- docs/custom_templates.rst | 20 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) rename datasette/templates/{_rows_and_columns.html => _table.html} (100%) diff --git a/datasette/templates/_rows_and_columns.html b/datasette/templates/_table.html similarity index 100% rename from datasette/templates/_rows_and_columns.html rename to datasette/templates/_table.html diff --git a/datasette/templates/row.html b/datasette/templates/row.html index baffaf96..bda1e4e2 100644 --- a/datasette/templates/row.html +++ b/datasette/templates/row.html @@ -24,7 +24,7 @@

This data as {% for name, url in renderers.items() %}{{ name }}{{ ", " if not loop.last }}{% endfor %}

-{% include custom_rows_and_columns_templates %} +{% include custom_table_templates %} {% if foreign_key_tables %}

Links from other tables

diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 5ba3ff6d..2287e901 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -145,7 +145,7 @@ {% endif %} -{% include custom_rows_and_columns_templates %} +{% include custom_table_templates %} {% if next_url %}

Next page

diff --git a/datasette/views/table.py b/datasette/views/table.py index c41bc305..8ba3abe4 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -744,14 +744,14 @@ class TableView(RowTableShared): "sort": sort, "sort_desc": sort_desc, "disable_sort": is_view, - "custom_rows_and_columns_templates": [ - "_rows_and_columns-{}-{}.html".format( + "custom_table_templates": [ + "_table-{}-{}.html".format( to_css_class(database), to_css_class(table) ), - "_rows_and_columns-table-{}-{}.html".format( + "_table-table-{}-{}.html".format( to_css_class(database), to_css_class(table) ), - "_rows_and_columns.html", + "_table.html", ], "metadata": metadata, "view_definition": await db.get_view_definition(table), @@ -828,14 +828,14 @@ class RowView(RowTableShared): ), "display_columns": display_columns, "display_rows": display_rows, - "custom_rows_and_columns_templates": [ - "_rows_and_columns-{}-{}.html".format( + "custom_table_templates": [ + "_table-{}-{}.html".format( to_css_class(database), to_css_class(table) ), - "_rows_and_columns-row-{}-{}.html".format( + "_table-row-{}-{}.html".format( to_css_class(database), to_css_class(table) ), - "_rows_and_columns.html", + "_table.html", ], "metadata": (self.ds.metadata("databases") or {}) .get(database, {}) diff --git a/docs/custom_templates.rst b/docs/custom_templates.rst index b0863381..1dfaf892 100644 --- a/docs/custom_templates.rst +++ b/docs/custom_templates.rst @@ -145,14 +145,14 @@ The lookup rules Datasette uses are as follows:: row.html Rows and columns include on table page: - _rows_and_columns-table-mydatabase-mytable.html - _rows_and_columns-mydatabase-mytable.html - _rows_and_columns.html + _table-table-mydatabase-mytable.html + _table-mydatabase-mytable.html + _table.html Rows and columns include on row page: - _rows_and_columns-row-mydatabase-mytable.html - _rows_and_columns-mydatabase-mytable.html - _rows_and_columns.html + _table-row-mydatabase-mytable.html + _table-mydatabase-mytable.html + _table.html If a table name has spaces or other unexpected characters in it, the template filename will follow the same rules as our custom ```` CSS classes - for @@ -189,16 +189,16 @@ content you can do so by creating a ``row.html`` template like this:: Note the ``default:row.html`` template name, which ensures Jinja will inherit from the default template. -The ``_rows_and_columns.html`` template is included on both the row and the table -page, and displays the content of the row. The default ``_rows_and_columns.html`` template -`can be seen here `_. +The ``_table.html`` template is included on both the row and the table +page, and displays the content of the row. The default ``_table.html`` template +`can be seen here `_. You can provide a custom template that applies to all of your databases and tables, or you can provide custom templates for specific tables using the template naming scheme described above. Say for example you want to output a certain column as unescaped HTML. You could -provide a custom ``_rows_and_columns.html`` template like this:: +provide a custom ``_table.html`` template like this:: From 94d856c9a10057f5109643249540413a784e639f Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 2 Jul 2019 20:06:22 -0700 Subject: [PATCH 4/5] Unit test for _table custom template, refs #521 --- tests/fixtures.py | 2 ++ tests/test_html.py | 13 +++++++++++++ tests/test_templates/_table.html | 3 +++ 3 files changed, 18 insertions(+) create mode 100644 tests/test_templates/_table.html diff --git a/tests/fixtures.py b/tests/fixtures.py index 00140f50..0330c8ed 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -101,6 +101,7 @@ def make_app_client( extra_databases=None, inspect_data=None, static_mounts=None, + template_dir=None, ): with tempfile.TemporaryDirectory() as tmpdir: filepath = os.path.join(tmpdir, filename) @@ -143,6 +144,7 @@ def make_app_client( config=config, inspect_data=inspect_data, static_mounts=static_mounts, + template_dir=template_dir, ) ds.sqlite_functions.append(("sleep", 1, lambda n: time.sleep(float(n)))) client = TestClient(ds.app()) diff --git a/tests/test_html.py b/tests/test_html.py index 32fa2fe3..f76f98b9 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -964,3 +964,16 @@ def test_metadata_json_html(app_client): assert response.status == 200 pre = Soup(response.body, "html.parser").find("pre") assert METADATA == json.loads(pre.text) + + +def test_custom_table_include(): + for client in make_app_client( + template_dir=str(pathlib.Path(__file__).parent / "test_templates") + ): + response = client.get("/fixtures/complex_foreign_keys") + assert response.status == 200 + assert ( + '
' + '1 - 2 - hello 1' + "
" + ) == str(Soup(response.text, "html.parser").select_one("div.custom-table-row")) diff --git a/tests/test_templates/_table.html b/tests/test_templates/_table.html new file mode 100644 index 00000000..14f635a8 --- /dev/null +++ b/tests/test_templates/_table.html @@ -0,0 +1,3 @@ +{% for row in display_rows %} +
{{ row["f1"] }} - {{ row["f2"] }} - {{ row.display("f3") }}
+{% endfor %} From 1add905532b7bc4f681318b8f22b9b74cca2b2a0 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 2 Jul 2019 20:13:34 -0700 Subject: [PATCH 5/5] Updated custom template docs, refs #521 --- docs/custom_templates.rst | 52 ++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/docs/custom_templates.rst b/docs/custom_templates.rst index 1dfaf892..47271542 100644 --- a/docs/custom_templates.rst +++ b/docs/custom_templates.rst @@ -144,12 +144,12 @@ The lookup rules Datasette uses are as follows:: row-mydatabase-mytable.html row.html - Rows and columns include on table page: + Table of rows and columns include on table page: _table-table-mydatabase-mytable.html _table-mydatabase-mytable.html _table.html - Rows and columns include on row page: + Table of rows and columns include on row page: _table-row-mydatabase-mytable.html _table-mydatabase-mytable.html _table.html @@ -189,38 +189,28 @@ content you can do so by creating a ``row.html`` template like this:: Note the ``default:row.html`` template name, which ensures Jinja will inherit from the default template. -The ``_table.html`` template is included on both the row and the table -page, and displays the content of the row. The default ``_table.html`` template -`can be seen here `_. +The ``_table.html`` template is included by both the row and the table pages, +and a list of rows. The default ``_table.html`` template renders them as an +HTML template and `can be seen here `_. You can provide a custom template that applies to all of your databases and tables, or you can provide custom templates for specific tables using the template naming scheme described above. -Say for example you want to output a certain column as unescaped HTML. You could -provide a custom ``_table.html`` template like this:: +If you want to present your data in a format other than an HTML table, you +can do so by looping through ``display_rows`` in your own ``_table.html`` +template. You can use ``{{ row["column_name"] }}`` to output the raw value +of a specific column. -
- - - {% for column in display_columns %} - - {% endfor %} - - - - {% for row in display_rows %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} - -
{{ column }}
- {% if cell.column == 'description' %} - {{ cell.value|safe }} - {% else %} - {{ cell.value }} - {% endif %} -
+If you want to output the rendered HTML version of a column, including any +links to foreign keys, you can use ``{{ row.display("column_name") }}``. + +Here is an example of a custom ``_table.html`` template:: + + {% for row in display_rows %} +
+

{{ row["title"] }}

+

{{ row["description"] }} +

Category: {{ row.display("category_id") }}

+
+ {% endfor %}