Skip to content

Commit 770e770

Browse files
gregoreesmaalw
authored andcommitted
Client side table sorting for AWS contest ranking
1 parent 9f87384 commit 770e770

3 files changed

Lines changed: 140 additions & 8 deletions

File tree

cms/server/admin/static/aws_style.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,18 @@ table td.wrapping-options label {
390390
margin-right: 15px;
391391
}
392392

393+
table td.partial::after {
394+
content: "*";
395+
}
396+
397+
table.sortable th {
398+
white-space: nowrap;
399+
}
400+
401+
th .column-sort {
402+
text-decoration: none;
403+
margin-left: 8px;
404+
}
393405

394406
/* TODO */
395407

cms/server/admin/static/aws_utils.js

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright © 2012-2014 Luca Wehrstedt <luca.wehrstedt@gmail.com>
44
* Copyright © 2013 Fabian Gundlach <320pointsguy@gmail.com>
55
* Copyright © 2014 Artem Iglikov <artem.iglikov@gmail.com>
6+
* Copyright © 2018 Gregor Eesmaa <gregoreesmaa1@gmail.com>
67
*
78
* This program is free software: you can redistribute it and/or modify
89
* it under the terms of the GNU Affero General Public License as
@@ -52,7 +53,7 @@ CMS.AWSUtils = function(url_root, timestamp,
5253
CMS.AWSUtils.create_url_builder = function(url_root) {
5354
return function() {
5455
var url = url_root;
55-
for (let component of arguments) {
56+
for (var component in arguments) {
5657
if (url.substr(-1) != "/") {
5758
url += "/";
5859
}
@@ -311,6 +312,121 @@ CMS.AWSUtils.prototype.close_notification = function(item) {
311312
};
312313

313314

315+
/**
316+
* Provides table row comparator for specified column and order.
317+
*/
318+
function get_table_row_comparator(column_idx, numeric, ascending) {
319+
return function(a, b) {
320+
var valA = $(a).children("td").eq(column_idx).text();
321+
var valB = $(b).children("td").eq(column_idx).text();
322+
var result = numeric
323+
? Number(valA) - Number(valB)
324+
: valA.localeCompare(valB);
325+
return ascending ? -result : result;
326+
}
327+
}
328+
329+
330+
/**
331+
* Sorts specified table by specified column in specified order.
332+
*/
333+
CMS.AWSUtils.sort_table = function(table, column_idx, ascending) {
334+
var initial_column_idx = table.data("initial_sort_column_idx");
335+
var ranks_column = table.data("ranks_column");
336+
column_idx += ranks_column ? 1 : 0;
337+
var table_rows = table
338+
.children("tbody")
339+
.children("tr");
340+
var column_header = table
341+
.children("thead")
342+
.children("tr")
343+
.children("th")
344+
.eq(column_idx);
345+
var settings = (column_header.attr("data-sort-settings") || "").split(" ");
346+
347+
var numeric = settings.indexOf("numeric") >= 0;
348+
349+
// If specified, flip column's natural order, e.g. due to meaning of values.
350+
if (settings.indexOf("reversed") >= 0) {
351+
ascending = !ascending;
352+
}
353+
354+
// Normalize column index, converting negative to positive from the end.
355+
column_idx = column_header.index();
356+
357+
// Reassign arrows to headers
358+
table.find(".column-sort").html("&varr;");
359+
column_header.find(".column-sort").html(ascending ? "&uarr;" : "&darr;");
360+
361+
// Do the sorting, by initial column and then by selected column.
362+
table_rows
363+
.sort(get_table_row_comparator(initial_column_idx, numeric, ascending))
364+
.sort(get_table_row_comparator(column_idx, numeric, ascending))
365+
.each(function(idx, row) {
366+
table.children("tbody").append(row)
367+
});
368+
369+
if (ranks_column) {
370+
table_rows.each(function(idx, row) {
371+
$(row).children("td").first().text(idx + 1)
372+
});
373+
}
374+
};
375+
376+
377+
/**
378+
* Makes table sortable, adding ranks column and sorting buttons in header.
379+
*/
380+
CMS.AWSUtils.init_table_sort = function(table, ranks_column,
381+
initial_column_idx,
382+
initial_ascending) {
383+
table.addClass("sortable");
384+
var table_column_headers = table
385+
.children("thead")
386+
.children("tr");
387+
var table_rows = table
388+
.children("tbody")
389+
.children("tr");
390+
391+
// Normalize column index, converting negative to positive from the end.
392+
initial_column_idx = table_column_headers
393+
.children("th")
394+
.eq(initial_column_idx)
395+
.index();
396+
397+
table.data("ranks_column", ranks_column);
398+
table.data("initial_sort_column_idx", initial_column_idx);
399+
400+
// Declaring sort settings.
401+
var previous_column_idx = initial_column_idx;
402+
var ascending = initial_ascending;
403+
404+
// Add sorting indicators to column headers
405+
table_column_headers
406+
.children("th")
407+
.each(function(column_idx, header) {
408+
$("<a/>", {
409+
href: "#",
410+
class: "column-sort",
411+
click: function() {
412+
ascending = !ascending && previous_column_idx == column_idx;
413+
previous_column_idx = column_idx;
414+
CMS.AWSUtils.sort_table(table, column_idx, ascending);
415+
}
416+
}).appendTo(header);
417+
});
418+
419+
// Add ranks column
420+
if (ranks_column) {
421+
table_column_headers.prepend("<th>#</th>");
422+
table_rows.prepend("<td></td>");
423+
}
424+
425+
// Do initial sorting
426+
CMS.AWSUtils.sort_table(table, initial_column_idx, initial_ascending);
427+
};
428+
429+
314430
/**
315431
* Return a string representation of the number with two digits.
316432
*

cms/server/admin/templates/ranking.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<h1>Ranking</h1>
66
</div>
77
Download as <a href="{{ url("contest", contest.id, "ranking", "csv") }}">csv</a>, <a href="{{ url("contest", contest.id, "ranking", "txt") }}">text</a>.
8-
<table class="bordered">
8+
<table id="ranking-table" class="bordered">
99
<thead>
1010
<tr>
1111
<th>Username</th>
@@ -14,17 +14,16 @@ <h1>Ranking</h1>
1414
<th>Team</th>
1515
{% endif %}
1616
{% for task in contest.tasks %}
17-
<th><a href="{{ url("task", task.id) }}">{{ task.name }}</a></th>
17+
<th data-sort-settings="numeric reversed"><a href="{{ url("task", task.id) }}">{{ task.name }}</a></th>
1818
{% endfor %}
19-
<th>Global</th>
19+
<th data-sort-settings="numeric reversed">Global</th>
2020
</tr>
2121
</thead>
2222
<tbody>
23-
{# TODO: Consider client-side sorting #}
2423
{# This template assumes participations have two fields in addition to those in the DB: #}
2524
{# - scores: a list of pairs (score, is score partial) for each task; #}
2625
{# - total_score: the total score for the contest. #}
27-
{% for p in contest.participations|sort(attribute="total_score")|reverse %}
26+
{% for p in contest.participations %}
2827
{% if not p.hidden %}
2928
<tr>
3029
<td><a href="{{ url("contest", contest.id, "user", p.user.id, "edit") }}">{{ p.user.username }}</a></td>
@@ -37,13 +36,18 @@ <h1>Ranking</h1>
3736
{% endif %}
3837
{% endif %}
3938
{% for t_score, t_partial in p.scores %}
40-
<td>{{ t_score }}{% if t_partial %}*{% endif %}</td>
39+
<td {%- if t_partial %} class="partial" {%- endif -%}>{{ t_score }}</td>
4140
{% endfor %}
4241
{% set total_score, partial = p.total_score %}
43-
<td>{{ total_score }}{% if partial %}*{% endif %}</td>
42+
<td {%- if partial %} class="partial" {%- endif -%}>{{ total_score }}</td>
4443
</tr>
4544
{% endif %}
4645
{% endfor %}
4746
</tbody>
4847
</table>
48+
<script>
49+
$(document).ready(function() {
50+
CMS.AWSUtils.init_table_sort($("#ranking-table"), true, -1);
51+
})
52+
</script>
4953
{% endblock core %}

0 commit comments

Comments
 (0)