From 89d9fbb91bfc0dd9091b34dbf3cf540ab849cc44 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 27 Mar 2018 09:18:32 -0700 Subject: [PATCH] Database/Table views inherit source/license/source_url/license_url metadata If you set the source_url/license_url/source/license fields in your root metadata those values will now be inherited all the way down to the database and table templates. The title/description are NOT inherited. Also added unit tests for the HTML generated by the metadata. Refs #185 --- datasette/app.py | 22 ++++++++++++++----- docs/index.rst | 1 + tests/fixtures.py | 22 +++++++++++++++++++ tests/test_html.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index e07cb2ef..ce4e2e95 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -384,6 +384,7 @@ class DatabaseView(BaseView): return await self.custom_sql(request, name, hash, sql) info = self.ds.inspect()[name] metadata = self.ds.metadata.get('databases', {}).get(name, {}) + self.ds.update_with_inherited_metadata(metadata) tables = list(info['tables'].values()) tables.sort(key=lambda t: (t['hidden'], t['name'])) return { @@ -399,9 +400,7 @@ class DatabaseView(BaseView): 'database_hash': hash, 'show_hidden': request.args.get('_show_hidden'), 'editable': True, - 'metadata': self.ds.metadata.get( - 'databases', {} - ).get(name, {}), + 'metadata': metadata, }, ('database-{}.html'.format(to_css_class(name)), 'database.html') @@ -691,6 +690,10 @@ class TableView(RowTableShared): display_columns, display_rows = await self.display_columns_and_rows( name, table, description, rows, link_column=not is_view, expand_foreign_keys=True ) + metadata = self.ds.metadata.get( + 'databases', {} + ).get(name, {}).get('tables', {}).get(table, {}) + self.ds.update_with_inherited_metadata(metadata) return { 'database_hash': hash, 'human_filter_description': human_description, @@ -706,9 +709,7 @@ class TableView(RowTableShared): '_rows_and_columns-table-{}-{}.html'.format(to_css_class(name), to_css_class(table)), '_rows_and_columns.html', ], - 'metadata': self.ds.metadata.get( - 'databases', {} - ).get(name, {}).get('tables', {}).get(table, {}), + 'metadata': metadata, } return { @@ -892,6 +893,15 @@ class Datasette: def extra_js_urls(self): return self.asset_urls('extra_js_urls') + def update_with_inherited_metadata(self, metadata): + # Fills in source/license with defaults, if available + metadata.update({ + 'source': metadata.get('source') or self.metadata.get('source'), + 'source_url': metadata.get('source_url') or self.metadata.get('source_url'), + 'license': metadata.get('license') or self.metadata.get('license'), + 'license_url': metadata.get('license_url') or self.metadata.get('license_url'), + }) + def prepare_connection(self, conn): conn.row_factory = sqlite3.Row conn.text_factory = lambda x: str(x, 'utf-8', 'replace') diff --git a/docs/index.rst b/docs/index.rst index 68cc11b0..7f2b9269 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ Contents :maxdepth: 2 getting_started + json_api sql_queries metadata custom_templates diff --git a/tests/fixtures.py b/tests/fixtures.py index 92c38482..d5a53d8f 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -16,6 +16,7 @@ def app_client(): page_size=50, max_returned_rows=100, sql_time_limit_ms=20, + metadata=METADATA, ) ds.sqlite_functions.append( ('sleep', 1, lambda n: time.sleep(float(n))), @@ -23,6 +24,27 @@ def app_client(): yield ds.app().test_client +METADATA = { + 'title': 'Datasette Title', + 'description': 'Datasette Description', + 'license': 'License', + 'license_url': 'http://www.example.com/license', + 'source': 'Source', + 'source_url': 'http://www.example.com/source', + 'databases': { + 'test_tables': { + 'description': 'Test tables description', + 'tables': { + 'simple_primary_key': { + 'description_html': 'Simple primary key', + 'title': 'This HTML is escaped', + } + } + } + } +} + + TABLES = ''' CREATE TABLE simple_primary_key ( pk varchar(30) primary key, diff --git a/tests/test_html.py b/tests/test_html.py index 50d26703..c93fdcdc 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -280,3 +280,58 @@ def test_view_html(app_client): ] ] assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')] + + +def test_index_metadata(app_client): + response = app_client.get('/', gather_request=False) + soup = Soup(response.body, 'html.parser') + assert 'Datasette Title' == soup.find('h1').text + assert 'Datasette Description' == inner_html( + soup.find('div', {'class': 'metadata-description'}) + ) + assert_footer_links(soup) + + +def test_database_metadata(app_client): + response = app_client.get('/test_tables', gather_request=False) + soup = Soup(response.body, 'html.parser') + # Page title should be the default + assert 'test_tables' == soup.find('h1').text + # Description should be custom + assert 'Test tables description' == inner_html( + soup.find('div', {'class': 'metadata-description'}) + ) + # The source/license should be inherited + assert_footer_links(soup) + + +def test_table_metadata(app_client): + response = app_client.get('/test_tables/simple_primary_key', gather_request=False) + soup = Soup(response.body, 'html.parser') + # Page title should be custom and should be HTML escaped + assert 'This <em>HTML</em> is escaped' == inner_html(soup.find('h1')) + # Description should be custom and NOT escaped (we used description_html) + assert 'Simple primary key' == inner_html(soup.find( + 'div', {'class': 'metadata-description'}) + ) + # The source/license should be inherited + assert_footer_links(soup) + + +def assert_footer_links(soup): + footer_links = soup.find('div', {'class': 'ft'}).findAll('a') + assert 3 == len(footer_links) + datasette_link, license_link, source_link = footer_links + assert 'Datasette' == datasette_link.text.strip() + assert 'Source' == source_link.text.strip() + assert 'License' == license_link.text.strip() + assert 'https://github.com/simonw/datasette' == datasette_link['href'] + assert 'http://www.example.com/source' == source_link['href'] + assert 'http://www.example.com/license' == license_link['href'] + + +def inner_html(soup): + html = str(soup) + # This includes the parent tag - so remove that + inner_html = html.split('>', 1)[1].rsplit('<', 1)[0] + return inner_html.strip()