Column metadata, closes #942

This commit is contained in:
Simon Willison 2021-08-12 16:53:23 -07:00 committed by GitHub
commit e837095ef3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 2 deletions

View file

@ -784,9 +784,14 @@ svg.dropdown-menu-icon {
font-size: 0.7em;
color: #666;
margin: 0;
padding: 0;
padding: 4px 8px 4px 8px;
}
.dropdown-menu .dropdown-column-description {
margin: 0;
color: #666;
padding: 4px 8px 4px 8px;
max-width: 20em;
}
.dropdown-menu li {
border-bottom: 1px solid #ccc;
}
@ -836,6 +841,16 @@ svg.dropdown-menu-icon {
background-repeat: no-repeat;
}
dl.column-descriptions dt {
font-weight: bold;
}
dl.column-descriptions dd {
padding-left: 1.5em;
white-space: pre-wrap;
line-height: 1.1em;
color: #666;
}
.anim-scale-in {
animation-name: scale-in;
animation-duration: 0.15s;

View file

@ -9,6 +9,7 @@ var DROPDOWN_HTML = `<div class="dropdown-menu">
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
</ul>
<p class="dropdown-column-type"></p>
<p class="dropdown-column-description"></p>
</div>`;
var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -166,6 +167,14 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
} else {
columnTypeP.style.display = "none";
}
var columnDescriptionP = menu.querySelector(".dropdown-column-description");
if (th.dataset.columnDescription) {
columnDescriptionP.innerText = th.dataset.columnDescription;
columnDescriptionP.style.display = "block";
} else {
columnDescriptionP.style.display = "none";
}
menu.style.position = "absolute";
menu.style.top = menuTop + 6 + "px";
menu.style.left = menuLeft + "px";

View file

@ -4,7 +4,7 @@
<thead>
<tr>
{% for column in display_columns %}
<th class="col-{{ column.name|to_css_class }}" scope="col" data-column="{{ column.name }}" data-column-type="{{ column.type }}" data-column-not-null="{{ column.notnull }}" data-is-pk="{% if column.is_pk %}1{% else %}0{% endif %}">
<th {% if column.description %}data-column-description="{{ column.description }}" {% endif %}class="col-{{ column.name|to_css_class }}" scope="col" data-column="{{ column.name }}" data-column-type="{{ column.type }}" data-column-not-null="{{ column.notnull }}" data-is-pk="{% if column.is_pk %}1{% else %}0{% endif %}">
{% if not column.sortable %}
{{ column.name }}
{% else %}

View file

@ -51,6 +51,14 @@
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
{% if metadata.columns %}
<dl class="column-descriptions">
{% for column_name, column_description in metadata.columns.items() %}
<dt>{{ column_name }}</dt><dd>{{ column_description }}</dd>
{% endfor %}
</dl>
{% endif %}
{% if filtered_table_rows_count or human_description_en %}
<h3>{% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}".format(filtered_table_rows_count) }} row{% if filtered_table_rows_count == 1 %}{% else %}s{% endif %}{% endif %}
{% if human_description_en %}{{ human_description_en }}{% endif %}

View file

@ -125,6 +125,7 @@ class RowTableShared(DataView):
"""Returns columns, rows for specified table - including fancy foreign key treatment"""
db = self.ds.databases[database]
table_metadata = self.ds.table_metadata(database, table)
column_descriptions = table_metadata.get("columns") or {}
column_details = {col.name: col for col in await db.table_column_details(table)}
sortable_columns = await self.sortable_columns_for_table(database, table, True)
pks = await db.primary_keys(table)
@ -147,6 +148,7 @@ class RowTableShared(DataView):
"is_pk": r[0] in pks_for_display,
"type": type_,
"notnull": notnull,
"description": column_descriptions.get(r[0]),
}
)

View file

@ -78,6 +78,34 @@ The three visible metadata fields you can apply to everything, specific database
For each of these you can provide just the ``*_url`` field and Datasette will treat that as the default link label text and display the URL directly on the page.
.. _metadata_column_descriptions:
Column descriptions
-------------------
You can include descriptions for your columns by adding a ``"columns": {"name-of-column": "description-of-column"}`` block to your table metadata:
.. code-block:: json
{
"databases": {
"database1": {
"tables": {
"example_table": {
"columns": {
"column1": "Description of column 1",
"column2": "Description of column 2"
}
}
}
}
}
}
These will be displayed at the top of the table page, and will also show in the cog menu for each column.
You can see an example of how these look at `latest.datasette.io/fixtures/roadside_attractions <https://latest.datasette.io/fixtures/roadside_attractions>`__.
Specifying units for a column
-----------------------------

View file

@ -336,6 +336,12 @@ METADATA = {
"fts_table": "searchable_fts",
"fts_pk": "pk",
},
"roadside_attractions": {
"columns": {
"name": "The name of the attraction",
"address": "The street address for the attraction",
}
},
"attraction_characteristic": {"sort_desc": "pk"},
"facet_cities": {"sort": "name"},
"paginated_view": {"size": 25},

View file

@ -1777,3 +1777,21 @@ def test_trace_correctly_escaped(app_client):
response = app_client.get("/fixtures?sql=select+'<h1>Hello'&_trace=1")
assert "select '<h1>Hello" not in response.text
assert "select &#39;&lt;h1&gt;Hello" in response.text
def test_column_metadata(app_client):
response = app_client.get("/fixtures/roadside_attractions")
soup = Soup(response.body, "html.parser")
dl = soup.find("dl")
assert [(dt.text, dt.nextSibling.text) for dt in dl.findAll("dt")] == [
("name", "The name of the attraction"),
("address", "The street address for the attraction"),
]
assert (
soup.select("th[data-column=name]")[0]["data-column-description"]
== "The name of the attraction"
)
assert (
soup.select("th[data-column=address]")[0]["data-column-description"]
== "The street address for the attraction"
)