mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Column metadata, closes #942
This commit is contained in:
parent
b1fed48a95
commit
e837095ef3
8 changed files with 88 additions and 2 deletions
|
|
@ -784,9 +784,14 @@ svg.dropdown-menu-icon {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
|
||||||
padding: 4px 8px 4px 8px;
|
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 {
|
.dropdown-menu li {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
@ -836,6 +841,16 @@ svg.dropdown-menu-icon {
|
||||||
background-repeat: no-repeat;
|
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 {
|
.anim-scale-in {
|
||||||
animation-name: scale-in;
|
animation-name: scale-in;
|
||||||
animation-duration: 0.15s;
|
animation-duration: 0.15s;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ var DROPDOWN_HTML = `<div class="dropdown-menu">
|
||||||
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
|
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="dropdown-column-type"></p>
|
<p class="dropdown-column-type"></p>
|
||||||
|
<p class="dropdown-column-description"></p>
|
||||||
</div>`;
|
</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">
|
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 {
|
} else {
|
||||||
columnTypeP.style.display = "none";
|
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.position = "absolute";
|
||||||
menu.style.top = menuTop + 6 + "px";
|
menu.style.top = menuTop + 6 + "px";
|
||||||
menu.style.left = menuLeft + "px";
|
menu.style.left = menuLeft + "px";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in display_columns %}
|
{% 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 %}
|
{% if not column.sortable %}
|
||||||
{{ column.name }}
|
{{ column.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,14 @@
|
||||||
|
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% 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 %}
|
{% 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 %}
|
<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 %}
|
{% if human_description_en %}{{ human_description_en }}{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ class RowTableShared(DataView):
|
||||||
"""Returns columns, rows for specified table - including fancy foreign key treatment"""
|
"""Returns columns, rows for specified table - including fancy foreign key treatment"""
|
||||||
db = self.ds.databases[database]
|
db = self.ds.databases[database]
|
||||||
table_metadata = self.ds.table_metadata(database, table)
|
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)}
|
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)
|
sortable_columns = await self.sortable_columns_for_table(database, table, True)
|
||||||
pks = await db.primary_keys(table)
|
pks = await db.primary_keys(table)
|
||||||
|
|
@ -147,6 +148,7 @@ class RowTableShared(DataView):
|
||||||
"is_pk": r[0] in pks_for_display,
|
"is_pk": r[0] in pks_for_display,
|
||||||
"type": type_,
|
"type": type_,
|
||||||
"notnull": notnull,
|
"notnull": notnull,
|
||||||
|
"description": column_descriptions.get(r[0]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
Specifying units for a column
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,12 @@ METADATA = {
|
||||||
"fts_table": "searchable_fts",
|
"fts_table": "searchable_fts",
|
||||||
"fts_pk": "pk",
|
"fts_pk": "pk",
|
||||||
},
|
},
|
||||||
|
"roadside_attractions": {
|
||||||
|
"columns": {
|
||||||
|
"name": "The name of the attraction",
|
||||||
|
"address": "The street address for the attraction",
|
||||||
|
}
|
||||||
|
},
|
||||||
"attraction_characteristic": {"sort_desc": "pk"},
|
"attraction_characteristic": {"sort_desc": "pk"},
|
||||||
"facet_cities": {"sort": "name"},
|
"facet_cities": {"sort": "name"},
|
||||||
"paginated_view": {"size": 25},
|
"paginated_view": {"size": 25},
|
||||||
|
|
|
||||||
|
|
@ -1777,3 +1777,21 @@ def test_trace_correctly_escaped(app_client):
|
||||||
response = app_client.get("/fixtures?sql=select+'<h1>Hello'&_trace=1")
|
response = app_client.get("/fixtures?sql=select+'<h1>Hello'&_trace=1")
|
||||||
assert "select '<h1>Hello" not in response.text
|
assert "select '<h1>Hello" not in response.text
|
||||||
assert "select '<h1>Hello" in response.text
|
assert "select '<h1>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"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue