Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple sort #1399

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions datasette/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ input[type="search"]::-webkit-search-results-decoration {
display: none;
}

form input[type=submit], form button[type=button] {
form input[type=submit], form button[type=button], .btn-primary, .btn-secondary {
font-weight: 400;
cursor: pointer;
text-align: center;
Expand All @@ -532,14 +532,14 @@ form input[type=submit], form button[type=button] {
border-radius: .25rem;
}

form input[type=submit] {
form input[type=submit], .btn-primary{
color: #fff;
background-color: #007bff;
border-color: #007bff;
-webkit-appearance: button;
}

form button[type=button] {
form button[type=button]{
color: #007bff;
background-color: #fff;
border-color: #007bff;
Expand Down
142 changes: 142 additions & 0 deletions datasette/static/sort_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
function sort() {
/*
Generate url with sorting parameters according to the
sorting queue
*/
let sq = window.sorting_queue;
if (!sq.length) {
if (!document.getElementById(`alert_cols`))
document
.getElementById("sort_utils")
.insertAdjacentHTML(
"afterend",
`<p id='alert_cols'>You need to select some columns to sort!</p>`
);
return;
}
var sort_url = new URLSearchParams(window.search);
for (let option of sq) {
sort_url.append(
`_sort${option.direction === "dsc" ? "_desc" : ""}`,
option.name
);
}
//Taken from table.js - line 37
window.location.href = sort_url ? "?" + sort_url : location.pathname;
}

function toggleSortMenu() {
/*
Function used to toggle the visibility of the sorting menu
*/
let menu = document.getElementById("sort_menu");
let btn = document.getElementById("toggle_sort_menu");
if (menu.style.display != "none") {
menu.style.dispaly = "none";
return;
}
menu.style.display = "inline-block";
menu.classList.add("anim-scale-in");
//Taken from table.js - lines 79-85
document.addEventListener("click", function (ev) {
var target = ev.target;
while (target && target != menu && target != btn) {
target = target.parentNode;
}
if (!target) {
menu.style.display = "none";
}
});
}

function populateSortMenu() {
window.sorting_queue = [];
var sort_url = new URLSearchParams(window.location.search);
/*
Clearing all of the checkboxes and selecting them according to url parameters.
*/
var all_checkboxes = document.querySelectorAll(`input[type=checkbox]`);
for (let checkbox of all_checkboxes) {
checkbox.checked = false;
}
params = [];
sort_url.forEach(function (value, key) {
params.push({
name: key,
value: value,
});
modifySortingQueue(value, key.includes("_desc") ? "dsc" : "asc");
});
if (!params.length) return;
for (let param of params) {
let chb = document.getElementsByName(param.value)[0];
chb.checked = true;
if (param.name.includes("_desc")) var rdb = `${param.value}_dsc`;
else var rdb = `${param.value}_asc`;
document.getElementById(rdb).checked = true;
}
}

function modifySortingQueue(column, type = undefined) {
/*
Function that runs every time a checkbox is clicked.
If it does not exist in the queue, it is added.
If it exists, it is removed from the queue.
*/
let sq = window.sorting_queue;
var s_option = sq.find((condition) => condition["name"] === column);
if (!s_option) {
var type =
document.querySelector(`input[name="${column}_direction"]:checked`)
.value || type;
if (!type) type = "asc";
sq.push({
name: column,
direction: type,
selected: true,
});
} else
sq.splice(
sq.findIndex(function (e) {
return e === s_option;
}),
1
);
displaySortingDescription();
}

function displaySortingDescription() {
/*
Function that generates a human description of sorting
based on selected options in the sorting queue.
*/
let sq = window.sorting_queue;
var s_option_p = document.getElementById("selected_options");
var hd = [];
for (let condition of sq) {
if (condition.selected)
hd.push(
`${condition.name}${condition.direction === "dsc" ? " descending" : ""}`
);
}
s_option_p.innerHTML = hd.join(", ");
}

function modifySortingDirection(column) {
/*
Every time a radio button is clicked,
the corresponding value in the sorting queue is modified.
*/
let sq = window.sorting_queue;
var s_option = sq.find((condition) => condition["name"] === column);
if (!s_option) return;
var type = document.querySelector(
`input[name="${column}_direction"]:checked`
).value;
sq[
sq.findIndex(function (e) {
return e === s_option;
})
].direction = type;
displaySortingDescription();
}
30 changes: 30 additions & 0 deletions datasette/templates/__sorting_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script src="{{ urls.static('sort_utils.js') }}"></script>
<div id="sort_utils">
<p style="display:inline-block;">Sorting options:</p>
<button type="button" class="btn-secondary" id="toggle_sort_menu" onclick="toggleSortMenu();" style="display:inline-block;">show</button>
</div>
<div style="display:block; position: absolute; z-index: 1;">
<div class="dropdown-menu" id="sort_menu" style="display:none;">
<div class="hook"></div>
<div class="row" name="sorting_options">
<div class="col">
<p style="margin:0; display:inline-block;">Selected options: <div style="display:inline-block;" id="selected_options"> </div>
<p style="margin:0; margin-top:1%;">Column | Type</p>
</div>
{% for column in display_columns if column.sortable %}
<div class="col">
<input type="checkbox" onclick="modifySortingQueue('{{column.name}}');" name="{{column.name}}">
{{column.name}}
<input type="radio" id="{{column.name}}_asc" name="{{column.name}}_direction" onclick="modifySortingDirection('{{column.name}}');" value="asc" checked>
<label for="{{column.name}}_asc">Ascending</label>
<input type="radio" id="{{column.name}}_dsc" name="{{column.name}}_direction" onclick="modifySortingDirection('{{column.name}}');" value="dsc">
<label for="{{column.name}}_dsc">Descending</label>
</div>
{% endfor %}
<input type="button" class="btn-primary" value="Sort" onclick="sort();" style="width:100%; border-top-right-radius: 0px; border-top-left-radius: 0px;">
</div>
</div>
<script type="text/javascript">
populateSortMenu();
</script>
</div>
5 changes: 3 additions & 2 deletions datasette/templates/_table.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% if display_rows %}
{% include "__sorting_example.html" %}
<div class="table-wrapper">
<table class="rows-and-columns">
<thead>
Expand All @@ -8,10 +9,10 @@
{% if not column.sortable %}
{{ column.name }}
{% else %}
{% if column.name == sort %}
{% if column.name in sort %}
<a href="{{ path_with_replaced_args(request, {'_sort_desc': column.name, '_sort': None, '_next': None}) }}" rel="nofollow">{{ column.name }}&nbsp;▼</a>
{% else %}
<a href="{{ path_with_replaced_args(request, {'_sort': column.name, '_sort_desc': None, '_next': None}) }}" rel="nofollow">{{ column.name }}{% if column.name == sort_desc %}&nbsp;▲{% endif %}</a>
<a href="{{ path_with_replaced_args(request, {'_sort': column.name, '_sort_desc': None, '_next': None}) }}" rel="nofollow">{{ column.name }}{% if column.name in sort_desc %}&nbsp;▲{% endif %}</a>
{% endif %}
{% endif %}
</th>
Expand Down
44 changes: 44 additions & 0 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ async def await_me_maybe(value):
return value


def check_nulls(list, values):
"""Takes a Python list, escapes its values and converts to '{column_name} is null'"""
value_list = ""
for item in list:
if not values[item]:
value_list += f"{escape_sqlite(item)} is null and"
return value_list[:-4]


def check_not_nulls(list, values):
"""Takes a Python list, escapes its values and converts to '{column_name} is not null'"""
value_list = ""
for item in list:
if not values[item]:
value_list += f"{escape_sqlite(item)} is not null and"
return value_list[:-4]


def urlsafe_components(token):
"""Splits token on commas and URL decodes each component"""
return [urllib.parse.unquote_plus(b) for b in token.split(",")]
Expand Down Expand Up @@ -116,6 +134,28 @@ def compound_keys_after_sql(pks, start_index=0):
return "({})".format("\n or\n".join(or_clauses))


def compound_sort_sql(sort, start_index=0):
#
# A modified version of compound_keys_after_sql supporting sorting by multiple columns in any direction
#
or_clauses = []
pks_left = sort[:]
while pks_left:
last = pks_left[-1]
rest = pks_left[:-1]
and_clauses = [
f"{escape_sqlite(pk.name)} = :p{i + start_index}"
for i, pk in enumerate(rest)
]
and_clauses.append(
f"{escape_sqlite(last.name)} {'>' if last.direction=='asc' else '<'} :p{len(rest) + start_index}"
)
or_clauses.append(f"({' and '.join(and_clauses)})")
pks_left.pop()
or_clauses.reverse()
return "({})".format("\n or\n".join(or_clauses))


class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, sqlite3.Row):
Expand Down Expand Up @@ -1076,3 +1116,7 @@ def method(self, *args, **kwargs):

class StartupError(Exception):
pass


SortingOrder = namedtuple("SortingOrder", ["name", "direction"])
"""An Object storing a particular sorting condition."""
Loading