Skip to content

Commit

Permalink
Column metadata, closes #942
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Aug 12, 2021
1 parent b1fed48 commit e837095
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 2 deletions.
17 changes: 16 additions & 1 deletion datasette/static/app.css
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions datasette/static/table.js
Expand Up @@ -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">
Expand Down Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/_table.html
Expand Up @@ -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 %}
Expand Down
8 changes: 8 additions & 0 deletions datasette/templates/table.html
Expand Up @@ -51,6 +51,14 @@ <h1>{{ metadata.title or table }}{% if is_view %} (view){% endif %}{% if private

{% 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 %}
Expand Down
2 changes: 2 additions & 0 deletions datasette/views/table.py
Expand Up @@ -125,6 +125,7 @@ async def display_columns_and_rows(
"""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)
Expand All @@ -147,6 +148,7 @@ async def display_columns_and_rows(
"is_pk": r[0] in pks_for_display,
"type": type_,
"notnull": notnull,
"description": column_descriptions.get(r[0]),
}
)

Expand Down
28 changes: 28 additions & 0 deletions docs/metadata.rst
Expand Up @@ -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
-----------------------------

Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures.py
Expand Up @@ -336,6 +336,12 @@ def generate_sortable_rows(num):
"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},
Expand Down
18 changes: 18 additions & 0 deletions tests/test_html.py
Expand Up @@ -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"
)

0 comments on commit e837095

Please sign in to comment.