kopia lustrzana https://github.com/simonw/datasette
?_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
rodzic
c0a748e5c3
commit
f1c29fd6a1
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue