?_col=/?_nocol= to show/hide columns on the table page

Closes #615

* Cog icon for hiding columns
* Show all columns cog menu item
* Do not allow hide column on primary keys
* Allow both ?_col= and ?_nocol=
* De-duplicate if ?_col= passed multiple times
* 400 error if user tries to ?_nocol= a primary key
* Documentation for ?_col= and ?_nocol=
pull/1348/head
Simon Willison 2021-05-26 21:17:43 -07:00 zatwierdzone przez GitHub
rodzic c0a748e5c3
commit f1c29fd6a1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 142 dodań i 15 usunięć

Wyświetl plik

@ -4,6 +4,8 @@ var DROPDOWN_HTML = `<div class="dropdown-menu">
<li><a class="dropdown-sort-asc" href="#">Sort ascending</a></li>
<li><a class="dropdown-sort-desc" href="#">Sort descending</a></li>
<li><a class="dropdown-facet" href="#">Facet by this</a></li>
<li><a class="dropdown-hide-column" href="#">Hide this column</a></li>
<li><a class="dropdown-show-all-columns" href="#">Show all columns</a></li>
<li><a class="dropdown-not-blank" href="#">Show not-blank rows</a></li>
</ul>
<p class="dropdown-column-type"></p>
@ -24,7 +26,7 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
}
function paramsToUrl(params) {
var s = params.toString();
return s ? "?" + s : "";
return s ? "?" + s : location.pathname;
}
function sortDescUrl(column) {
var params = getParams();
@ -45,6 +47,16 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
params.append("_facet", column);
return paramsToUrl(params);
}
function hideColumnUrl(column) {
var params = getParams();
params.append("_nocol", column);
return paramsToUrl(params);
}
function showAllColumnsUrl() {
var params = getParams();
params.delete("_nocol");
return paramsToUrl(params);
}
function notBlankUrl(column) {
var params = getParams();
params.set(`${column}__notblank`, "1");
@ -87,18 +99,33 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
var sortDesc = menu.querySelector("a.dropdown-sort-desc");
var facetItem = menu.querySelector("a.dropdown-facet");
var notBlank = menu.querySelector("a.dropdown-not-blank");
var hideColumn = menu.querySelector("a.dropdown-hide-column");
var showAllColumns = menu.querySelector("a.dropdown-show-all-columns");
if (params.get("_sort") == column) {
sort.style.display = "none";
sort.parentNode.style.display = "none";
} else {
sort.style.display = "block";
sort.parentNode.style.display = "block";
sort.setAttribute("href", sortAscUrl(column));
}
if (params.get("_sort_desc") == column) {
sortDesc.style.display = "none";
sortDesc.parentNode.style.display = "none";
} else {
sortDesc.style.display = "block";
sortDesc.parentNode.style.display = "block";
sortDesc.setAttribute("href", sortDescUrl(column));
}
/* Show hide columns options */
if (params.get("_nocol")) {
showAllColumns.parentNode.style.display = "block";
showAllColumns.setAttribute("href", showAllColumnsUrl());
} else {
showAllColumns.parentNode.style.display = "none";
}
if (th.getAttribute("data-is-pk") != "1") {
hideColumn.parentNode.style.display = "block";
hideColumn.setAttribute("href", hideColumnUrl(column));
} else {
hideColumn.parentNode.style.display = "none";
}
/* Only show facet if it's not the first column, not selected, not a single PK */
var isFirstColumn =
th.parentElement.querySelector("th:first-of-type") == th;
@ -110,9 +137,9 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
params.getAll("_facet").includes(column) ||
isSinglePk
) {
facetItem.style.display = "none";
facetItem.parentNode.style.display = "none";
} else {
facetItem.style.display = "block";
facetItem.parentNode.style.display = "block";
facetItem.setAttribute("href", facetUrl(column));
}
/* Show notBlank option if not selected AND at least one visible blank value */
@ -123,10 +150,10 @@ var DROPDOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" heig
params.get(`${column}__notblank`) != "1" &&
tdsForThisColumn.filter((el) => el.innerText.trim() == "").length
) {
notBlank.style.display = "block";
notBlank.parentNode.style.display = "block";
notBlank.setAttribute("href", notBlankUrl(column));
} else {
notBlank.style.display = "none";
notBlank.parentNode.style.display = "none";
}
var columnTypeP = menu.querySelector(".dropdown-column-type");
var columnType = th.dataset.columnType;

Wyświetl plik

@ -64,6 +64,41 @@ class Row:
class RowTableShared(DataView):
async def columns_to_select(self, db, table, request):
table_columns = await db.table_columns(table)
pks = await db.primary_keys(table)
columns = list(table_columns)
if "_col" in request.args:
columns = list(pks)
_cols = request.args.getlist("_col")
bad_columns = [column for column in _cols if column not in table_columns]
if bad_columns:
raise DatasetteError(
"_col={} - invalid columns".format(", ".join(bad_columns)),
status=400,
)
# De-duplicate maintaining order:
columns.extend(dict.fromkeys(_cols))
if "_nocol" in request.args:
# Return all columns EXCEPT these
bad_columns = [
column
for column in request.args.getlist("_nocol")
if (column not in table_columns) or (column in pks)
]
if bad_columns:
raise DatasetteError(
"_nocol={} - invalid columns".format(", ".join(bad_columns)),
status=400,
)
tmp_columns = [
column
for column in columns
if column not in request.args.getlist("_nocol")
]
columns = tmp_columns
return columns
async def sortable_columns_for_table(self, database, table, use_rowid):
db = self.ds.databases[database]
table_metadata = self.ds.table_metadata(database, table)
@ -323,18 +358,16 @@ class TableView(RowTableShared):
)
pks = await db.primary_keys(table)
table_column_details = await db.table_column_details(table)
table_columns = [column.name for column in table_column_details]
select_columns = ", ".join(escape_sqlite(t) for t in table_columns)
table_columns = await self.columns_to_select(db, table, request)
select_clause = ", ".join(escape_sqlite(t) for t in table_columns)
use_rowid = not pks and not is_view
if use_rowid:
select = f"rowid, {select_columns}"
select = f"rowid, {select_clause}"
order_by = "rowid"
order_by_pks = "rowid"
else:
select = select_columns
select = select_clause
order_by_pks = ", ".join([escape_sqlite(pk) for pk in pks])
order_by = order_by_pks
@ -717,6 +750,8 @@ class TableView(RowTableShared):
column = fk["column"]
if column not in columns_to_expand:
continue
if column not in columns:
continue
expanded_columns.append(column)
# Gather the values
column_index = columns.index(column)

Wyświetl plik

@ -296,6 +296,12 @@ You can filter the data returned by the table based on column values using a que
Special table arguments
~~~~~~~~~~~~~~~~~~~~~~~
``?_col=COLUMN1&_col=COLUMN2``
List specific columns to display. These will be shown along with any primary keys.
``?_nocol=COLUMN1&_nocol=COLUMN2``
List specific columns to hide - any column not listed will be displayed. Primary keys cannot be hidden.
``?_labels=on/off``
Expand foreign key references for every possible column. See below.

Wyświetl plik

@ -2009,3 +2009,62 @@ def test_http_options_request(app_client):
response = app_client.request("/fixtures", method="OPTIONS")
assert response.status == 200
assert response.text == "ok"
@pytest.mark.parametrize(
"path,expected_columns",
(
("/fixtures/facetable.json?_col=created", ["pk", "created"]),
(
"/fixtures/facetable.json?_nocol=created",
[
"pk",
"planet_int",
"on_earth",
"state",
"city_id",
"neighborhood",
"tags",
"complex_array",
"distinct_some_null",
],
),
(
"/fixtures/facetable.json?_col=state&_col=created",
["pk", "state", "created"],
),
(
"/fixtures/facetable.json?_col=state&_col=state",
["pk", "state"],
),
(
"/fixtures/facetable.json?_col=state&_col=created&_nocol=created",
["pk", "state"],
),
(
"/fixtures/simple_view.json?_nocol=content",
["upper_content"],
),
("/fixtures/simple_view.json?_col=content", ["content"]),
),
)
def test_col_nocol(app_client, path, expected_columns):
response = app_client.get(path)
assert response.status == 200
columns = response.json["columns"]
assert columns == expected_columns
@pytest.mark.parametrize(
"path,expected_error",
(
("/fixtures/facetable.json?_col=bad", "_col=bad - invalid columns"),
("/fixtures/facetable.json?_nocol=bad", "_nocol=bad - invalid columns"),
("/fixtures/facetable.json?_nocol=pk", "_nocol=pk - invalid columns"),
("/fixtures/simple_view.json?_col=bad", "_col=bad - invalid columns"),
),
)
def test_col_nocol_errors(app_client, path, expected_error):
response = app_client.get(path)
assert response.status == 400
assert response.json["error"] == expected_error