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()