Add a construct for sortable columns

in-house-export-serialization
Kevin Chung 2020-04-21 15:39:14 -04:00
parent 24c3520685
commit 052911f930
12 changed files with 198 additions and 35 deletions

View File

@ -66,3 +66,11 @@ tbody tr:hover {
tr[data-href] {
cursor: pointer;
}
.sort-col {
cursor: pointer;
}
input[type="checkbox"] {
cursor: pointer;
}

View File

@ -1,6 +1,7 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { ezQuery } from "core/ezq";
const api_func = {
users: (x, y) => CTFd.api.patch_user_public({ userId: x }, y),
@ -37,6 +38,58 @@ function toggleAccount() {
});
}
function toggleSelectedAccounts(accountIDs, action) {
const params = {
hidden: action === "hide" ? true : false
};
const reqs = [];
for (var accId of accountIDs) {
reqs.push(api_func[CTFd.config.userMode](accId, params));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
function hideSelectedAccounts(event) {
let accountIDs = $("input[data-account-id]:checked").map(function() {
return $(this).data("account-id");
});
let target = accountIDs.length === 1 ? "account" : "accounts";
ezQuery({
title: "Hide Accounts",
body: `Are you sure you want to hide ${accountIDs.length} ${target}?`,
success: function() {
toggleSelectedAccounts(accountIDs, "hide");
}
});
}
function showSelectedAccounts(event) {
let accountIDs = $("input[data-account-id]:checked").map(function() {
return $(this).data("account-id");
});
let target = accountIDs.length === 1 ? "account" : "accounts";
ezQuery({
title: "Unhide Accounts",
body: `Are you sure you want to unhide ${accountIDs.length} ${target}?`,
success: function() {
toggleSelectedAccounts(accountIDs, "show");
}
});
}
function toggleScoreboardSelect(event) {
const checked = $(this).prop("checked");
$(this)
.closest("table")
.find("input[data-account-id]")
.prop("checked", checked);
}
$(() => {
$(".scoreboard-toggle").click(toggleAccount);
$("#scoreboard-bulk-select").click(toggleScoreboardSelect);
$("#scoreboard-hide-button").click(hideSelectedAccounts);
$("#scoreboard-show-button").click(showSelectedAccounts);
});

View File

@ -40,6 +40,37 @@ function deleteCorrectSubmission(event) {
});
}
function deleteSelectedSubmissions(event) {
let submissionIDs = $("input[data-submission-id]:checked").map(function() {
return $(this).data("submission-id");
});
let target = submissionIDs.length === 1 ? "submission" : "submissions";
ezQuery({
title: "Delete Submissions",
body: `Are you sure you want to delete ${submissionIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var subId of submissionIDs) {
reqs.push(CTFd.api.delete_submission({ submissionId: subId }));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function toggleSubmissionSelect(event) {
const checked = $(this).prop("checked");
$(this)
.closest("table")
.find("input[data-submission-id]")
.prop("checked", checked);
}
$(() => {
$(".delete-correct-submission").click(deleteCorrectSubmission);
$("#submissions-bulk-select").change(toggleSubmissionSelect);
$("#submission-delete-button").click(deleteSelectedSubmissions);
});

View File

@ -1,4 +1,5 @@
import "bootstrap/dist/js/bootstrap.bundle";
import { makeSortableTables } from "core/utils";
import $ from "jquery";
export default () => {
@ -52,6 +53,7 @@ export default () => {
}
});
makeSortableTables();
$('[data-toggle="tooltip"]').tooltip();
});
};

View File

@ -1,4 +1,4 @@
html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal !important;z-index:-20}
#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{height:400px;display:block}#categories-pie-graph{height:400px;display:block}#solve-percentages-graph{height:400px;display:block}.no-decoration{color:inherit !important;text-decoration:none !important}.no-decoration:hover{color:inherit !important;text-decoration:none !important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,0.1) !important}tr[data-href]{cursor:pointer}
#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{height:400px;display:block}#categories-pie-graph{height:400px;display:block}#solve-percentages-graph{height:400px;display:block}.no-decoration{color:inherit !important;text-decoration:none !important}.no-decoration:hover{color:inherit !important;text-decoration:none !important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,0.1) !important}tr[data-href]{cursor:pointer}th{cursor:pointer}input[type=checkbox]{cursor:pointer}

File diff suppressed because one or more lines are too long

View File

@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar api_func = {\n users: function users(x, y) {\n return _CTFd.default.api.patch_user_public({\n userId: x\n }, y);\n },\n teams: function teams(x, y) {\n return _CTFd.default.api.patch_team_public({\n teamId: x\n }, y);\n }\n};\n\nfunction toggleAccount() {\n var $btn = (0, _jquery.default)(this);\n var id = $btn.data(\"account-id\");\n var state = $btn.data(\"state\");\n var hidden = undefined;\n\n if (state === \"visible\") {\n hidden = true;\n } else if (state === \"hidden\") {\n hidden = false;\n }\n\n var params = {\n hidden: hidden\n };\n\n api_func[_CTFd.default.config.userMode](id, params).then(function (response) {\n if (response.success) {\n if (hidden) {\n $btn.data(\"state\", \"hidden\");\n $btn.addClass(\"btn-danger\").removeClass(\"btn-success\");\n $btn.text(\"Hidden\");\n } else {\n $btn.data(\"state\", \"visible\");\n $btn.addClass(\"btn-success\").removeClass(\"btn-danger\");\n $btn.text(\"Visible\");\n }\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".scoreboard-toggle\").click(toggleAccount);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/scoreboard.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar api_func = {\n users: function users(x, y) {\n return _CTFd.default.api.patch_user_public({\n userId: x\n }, y);\n },\n teams: function teams(x, y) {\n return _CTFd.default.api.patch_team_public({\n teamId: x\n }, y);\n }\n};\n\nfunction toggleAccount() {\n var $btn = (0, _jquery.default)(this);\n var id = $btn.data(\"account-id\");\n var state = $btn.data(\"state\");\n var hidden = undefined;\n\n if (state === \"visible\") {\n hidden = true;\n } else if (state === \"hidden\") {\n hidden = false;\n }\n\n var params = {\n hidden: hidden\n };\n\n api_func[_CTFd.default.config.userMode](id, params).then(function (response) {\n if (response.success) {\n if (hidden) {\n $btn.data(\"state\", \"hidden\");\n $btn.addClass(\"btn-danger\").removeClass(\"btn-success\");\n $btn.text(\"Hidden\");\n } else {\n $btn.data(\"state\", \"visible\");\n $btn.addClass(\"btn-success\").removeClass(\"btn-danger\");\n $btn.text(\"Visible\");\n }\n }\n });\n}\n\nfunction toggleSelectedAccounts(accountIDs, action) {\n var params = {\n hidden: action === \"hide\" ? true : false\n };\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = accountIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var accId = _step.value;\n reqs.push(api_func[_CTFd.default.config.userMode](accId, params));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n}\n\nfunction hideSelectedAccounts(event) {\n var accountIDs = (0, _jquery.default)(\"input[data-account-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"account-id\");\n });\n var target = accountIDs.length === 1 ? \"account\" : \"accounts\";\n (0, _ezq.ezQuery)({\n title: \"Hide Accounts\",\n body: \"Are you sure you want to hide \".concat(accountIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n toggleSelectedAccounts(accountIDs, \"hide\");\n }\n });\n}\n\nfunction showSelectedAccounts(event) {\n var accountIDs = (0, _jquery.default)(\"input[data-account-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"account-id\");\n });\n var target = accountIDs.length === 1 ? \"account\" : \"accounts\";\n (0, _ezq.ezQuery)({\n title: \"Unhide Accounts\",\n body: \"Are you sure you want to unhide \".concat(accountIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n toggleSelectedAccounts(accountIDs, \"show\");\n }\n });\n}\n\nfunction toggleScoreboardSelect(event) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n (0, _jquery.default)(this).closest(\"table\").find(\"input[data-account-id]\").prop(\"checked\", checked);\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".scoreboard-toggle\").click(toggleAccount);\n (0, _jquery.default)(\"#scoreboard-bulk-select\").click(toggleScoreboardSelect);\n (0, _jquery.default)(\"#scoreboard-hide-button\").click(hideSelectedAccounts);\n (0, _jquery.default)(\"#scoreboard-show-button\").click(showSelectedAccounts);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/scoreboard.js?");
/***/ })

View File

@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteCorrectSubmission(event) {\n var key_id = (0, _jquery.default)(this).data(\"submission-id\");\n var $elem = (0, _jquery.default)(this).parent().parent();\n var chal_name = $elem.find(\".chal\").text().trim();\n var team_name = $elem.find(\".team\").text().trim();\n var row = (0, _jquery.default)(this).parent().parent();\n (0, _ezq.ezQuery)({\n title: \"Delete Submission\",\n body: \"Are you sure you want to delete correct submission from {0} for challenge {1}\".format(\"<strong>\" + (0, _utils.htmlEntities)(team_name) + \"</strong>\", \"<strong>\" + (0, _utils.htmlEntities)(chal_name) + \"</strong>\"),\n success: function success() {\n _CTFd.default.api.delete_submission({\n submissionId: key_id\n }).then(function (response) {\n if (response.success) {\n row.remove();\n }\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".delete-correct-submission\").click(deleteCorrectSubmission);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/submissions.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteCorrectSubmission(event) {\n var key_id = (0, _jquery.default)(this).data(\"submission-id\");\n var $elem = (0, _jquery.default)(this).parent().parent();\n var chal_name = $elem.find(\".chal\").text().trim();\n var team_name = $elem.find(\".team\").text().trim();\n var row = (0, _jquery.default)(this).parent().parent();\n (0, _ezq.ezQuery)({\n title: \"Delete Submission\",\n body: \"Are you sure you want to delete correct submission from {0} for challenge {1}\".format(\"<strong>\" + (0, _utils.htmlEntities)(team_name) + \"</strong>\", \"<strong>\" + (0, _utils.htmlEntities)(chal_name) + \"</strong>\"),\n success: function success() {\n _CTFd.default.api.delete_submission({\n submissionId: key_id\n }).then(function (response) {\n if (response.success) {\n row.remove();\n }\n });\n }\n });\n}\n\nfunction deleteSelectedSubmissions(event) {\n var submissionIDs = (0, _jquery.default)(\"input[data-submission-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"submission-id\");\n });\n var target = submissionIDs.length === 1 ? \"submission\" : \"submissions\";\n (0, _ezq.ezQuery)({\n title: \"Delete Submissions\",\n body: \"Are you sure you want to delete \".concat(submissionIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = submissionIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var subId = _step.value;\n reqs.push(_CTFd.default.api.delete_submission({\n submissionId: subId\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction toggleSubmissionSelect(event) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n (0, _jquery.default)(this).closest(\"table\").find(\"input[data-submission-id]\").prop(\"checked\", checked);\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".delete-correct-submission\").click(deleteCorrectSubmission);\n (0, _jquery.default)(\"#submissions-bulk-select\").change(toggleSubmissionSelect);\n (0, _jquery.default)(\"#submission-delete-button\").click(deleteSelectedSubmissions);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/submissions.js?");
/***/ })

View File

@ -9,19 +9,43 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="scoreboard" class="table table-striped">
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" id="scoreboard-hide-button" data-toggle="tooltip" title="Hide Accounts">
<i class="btn-fa fas fa-eye-slash"></i>
</button>
<button type="button" class="btn btn-success" id="scoreboard-show-button" data-toggle="tooltip" title="Unhide Accounts">
<i class="btn-fa fas fa-eye"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="scoreboard" class="table table-striped border">
<thead>
<tr>
<td width="10px"><b>Place</b></td>
<td><b>Team</b></td>
<td><b>Score</b></td>
<td><b>Visibility</b></td>
<td class="d-block border-right border-bottom text-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="scoreboard-bulk-select">&nbsp;
</div>
</td>
<th class="sort-col text-center"><b>Place</b></th>
<th class="sort-col"><b>Team</b></th>
<th class="sort-col"><b>Score</b></th>
<th class="sort-col"><b>Visibility</b></th>
</tr>
</thead>
<tbody>
{% for standing in standings %}
<tr data-href="{{ generate_account_url(standing.account_id, admin=True) }}">
<td>{{ loop.index }}</td>
<td class="d-block border-right text-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" value="{{ standing.account_id }}" data-account-id="{{ standing.account_id }}">&nbsp;
</div>
</td>
<td class="text-center" width="10%">{{ loop.index }}</td>
<td>
<a href="{{ generate_account_url(standing.account_id, admin=True) }}">
{{ standing.name }}
@ -40,15 +64,11 @@
</td>
<td>{{ standing.score }}</td>
<td>
{% if standing.hidden %}
<button class="btn-sm btn-danger cursor-pointer scoreboard-toggle" type="submit"
data-account-id="{{ standing.account_id }}" data-state="hidden">Hidden
</button>
{% else %}
<button class="btn-sm btn-success cursor-pointer scoreboard-toggle" type="submit"
data-account-id="{{ standing.account_id }}" data-state="visible">Visible
</button>
{% endif %}
{% if standing.hidden %}
<span class="badge badge-danger">Hidden</span>
{% else %}
<span class="badge badge-success">Visible</span>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -14,21 +14,41 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="teamsboard" class=" table table-striped">
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-danger" id="submission-delete-button">
<i class="btn-fa fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="teamsboard" class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>ID</b></td>
<td><b>Team</b></td>
<td><b>Challenge</b></td>
<td><b>Type</b></td>
<td><b>Submission</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Delete</b></td>
<td class="d-block border-right border-bottom">
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" id="submissions-bulk-select">&nbsp;
</div>
</td>
<th class="text-center sort-col"><b>ID</b></th>
<th class="sort-col"><b>Team</b></th>
<th class="sort-col"><b>Challenge</b></th>
<th class="sort-col"><b>Type</b></th>
<th class="sort-col"><b>Submission</b></th>
<th class="text-center sort-col"><b>Date</b></th>
</tr>
</thead>
<tbody>
{% for sub in submissions %}
<tr>
<td class="d-block border-right">
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ sub.id }}" data-submission-id="{{ sub.id }}">&nbsp;
</div>
</td>
<td class="text-center" id="{{ sub.id }}">
{{ sub.id }}
</td>
@ -47,12 +67,6 @@
<td class="text-center solve-time">
<span data-time="{{ sub.date | isoformat }}"></span>
</td>
<td class="text-center">
<span class="delete-correct-submission" data-toggle="tooltip"
data-placement="top" title="Delete submission #{{ sub.id }}" data-submission-id="{{ sub.id }}">
<i class="fas fa-times"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>

View File

@ -256,3 +256,38 @@ export function copyToClipboard(event, selector) {
$(event.target).tooltip("hide");
}, 1500);
}
export function makeSortableTables() {
$("th.sort-col").append(` <i class="fas fa-sort"></i>`);
$("th.sort-col").click(function() {
var table = $(this)
.parents("table")
.eq(0);
var rows = table
.find("tr:gt(0)")
.toArray()
.sort(comparer($(this).index()));
this.asc = !this.asc;
if (!this.asc) {
rows = rows.reverse();
}
for (var i = 0; i < rows.length; i++) {
table.append(rows[i]);
}
});
function comparer(index) {
return function(a, b) {
var valA = getCellValue(a, index),
valB = getCellValue(b, index);
return $.isNumeric(valA) && $.isNumeric(valB)
? valA - valB
: valA.toString().localeCompare(valB);
};
}
function getCellValue(row, index) {
return $(row)
.children("td")
.eq(index)
.text();
}
}

File diff suppressed because one or more lines are too long