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;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-----------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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 '<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