From 052911f9302d8512cc9126f9b5620f74bedeae4a Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 21 Apr 2020 15:39:14 -0400 Subject: [PATCH] Add a construct for sortable columns --- CTFd/themes/admin/assets/css/admin.scss | 8 +++ .../admin/assets/js/pages/scoreboard.js | 53 +++++++++++++++++++ .../admin/assets/js/pages/submissions.js | 31 +++++++++++ CTFd/themes/admin/assets/js/styles.js | 2 + CTFd/themes/admin/static/css/admin.dev.css | 2 +- CTFd/themes/admin/static/js/core.dev.js | 4 +- .../admin/static/js/pages/scoreboard.dev.js | 2 +- .../admin/static/js/pages/submissions.dev.js | 2 +- CTFd/themes/admin/templates/scoreboard.html | 50 +++++++++++------ CTFd/themes/admin/templates/submissions.html | 42 ++++++++++----- CTFd/themes/core/assets/js/utils.js | 35 ++++++++++++ CTFd/themes/core/static/js/core.dev.js | 2 +- 12 files changed, 198 insertions(+), 35 deletions(-) diff --git a/CTFd/themes/admin/assets/css/admin.scss b/CTFd/themes/admin/assets/css/admin.scss index 910bdb91..a30273e0 100644 --- a/CTFd/themes/admin/assets/css/admin.scss +++ b/CTFd/themes/admin/assets/css/admin.scss @@ -66,3 +66,11 @@ tbody tr:hover { tr[data-href] { cursor: pointer; } + +.sort-col { + cursor: pointer; +} + +input[type="checkbox"] { + cursor: pointer; +} diff --git a/CTFd/themes/admin/assets/js/pages/scoreboard.js b/CTFd/themes/admin/assets/js/pages/scoreboard.js index 3eab3284..9ab43414 100644 --- a/CTFd/themes/admin/assets/js/pages/scoreboard.js +++ b/CTFd/themes/admin/assets/js/pages/scoreboard.js @@ -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); }); diff --git a/CTFd/themes/admin/assets/js/pages/submissions.js b/CTFd/themes/admin/assets/js/pages/submissions.js index 3f34ddad..651cd824 100644 --- a/CTFd/themes/admin/assets/js/pages/submissions.js +++ b/CTFd/themes/admin/assets/js/pages/submissions.js @@ -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); }); diff --git a/CTFd/themes/admin/assets/js/styles.js b/CTFd/themes/admin/assets/js/styles.js index 7f5e3321..88dd2e16 100644 --- a/CTFd/themes/admin/assets/js/styles.js +++ b/CTFd/themes/admin/assets/js/styles.js @@ -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(); }); }; diff --git a/CTFd/themes/admin/static/css/admin.dev.css b/CTFd/themes/admin/static/css/admin.dev.css index b6e34eaf..9ad61a10 100644 --- a/CTFd/themes/admin/static/css/admin.dev.css +++ b/CTFd/themes/admin/static/css/admin.dev.css @@ -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} diff --git a/CTFd/themes/admin/static/js/core.dev.js b/CTFd/themes/admin/static/js/core.dev.js index 516e1cc2..9501d880 100644 --- a/CTFd/themes/admin/static/js/core.dev.js +++ b/CTFd/themes/admin/static/js/core.dev.js @@ -20,7 +20,7 @@ eval("\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd * /***/ (function(module, exports, __webpack_require__) { ; -eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\n__webpack_require__(/*! bootstrap/dist/js/bootstrap.bundle */ \"./node_modules/bootstrap/dist/js/bootstrap.bundle.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 _default = function _default() {\n // TODO: This is kind of a hack to mimic a React-like state construct.\n // It should be removed once we have a real front-end framework in place.\n (0, _jquery.default)(\":input\").each(function () {\n (0, _jquery.default)(this).data(\"initial\", (0, _jquery.default)(this).val());\n });\n (0, _jquery.default)(\".form-control\").bind({\n focus: function focus() {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n },\n blur: function blur() {\n if ((0, _jquery.default)(this).val() === \"\") {\n (0, _jquery.default)(this).removeClass(\"input-filled-valid\");\n }\n }\n });\n (0, _jquery.default)(\".modal\").on(\"show.bs.modal\", function (e) {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n });\n (0, _jquery.default)(function () {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n (0, _jquery.default)(\"tr[data-href]\").click(function () {\n var sel = getSelection().toString();\n\n if (!sel) {\n var href = (0, _jquery.default)(this).attr(\"data-href\");\n\n if (href) {\n window.location = href;\n }\n }\n\n return false;\n });\n (0, _jquery.default)(\"tr[data-href] a, tr[data-href] button\").click(function (e) {\n // TODO: This is a hack to allow modal close buttons to work\n if (!(0, _jquery.default)(this).attr(\"data-dismiss\")) {\n e.stopPropagation();\n }\n });\n (0, _jquery.default)('[data-toggle=\"tooltip\"]').tooltip();\n });\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/styles.js?"); +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\n__webpack_require__(/*! bootstrap/dist/js/bootstrap.bundle */ \"./node_modules/bootstrap/dist/js/bootstrap.bundle.js\");\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.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 _default = function _default() {\n // TODO: This is kind of a hack to mimic a React-like state construct.\n // It should be removed once we have a real front-end framework in place.\n (0, _jquery.default)(\":input\").each(function () {\n (0, _jquery.default)(this).data(\"initial\", (0, _jquery.default)(this).val());\n });\n (0, _jquery.default)(\".form-control\").bind({\n focus: function focus() {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n },\n blur: function blur() {\n if ((0, _jquery.default)(this).val() === \"\") {\n (0, _jquery.default)(this).removeClass(\"input-filled-valid\");\n }\n }\n });\n (0, _jquery.default)(\".modal\").on(\"show.bs.modal\", function (e) {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n });\n (0, _jquery.default)(function () {\n (0, _jquery.default)(\".form-control\").each(function () {\n if ((0, _jquery.default)(this).val()) {\n (0, _jquery.default)(this).addClass(\"input-filled-valid\");\n }\n });\n (0, _jquery.default)(\"tr[data-href]\").click(function () {\n var sel = getSelection().toString();\n\n if (!sel) {\n var href = (0, _jquery.default)(this).attr(\"data-href\");\n\n if (href) {\n window.location = href;\n }\n }\n\n return false;\n });\n (0, _jquery.default)(\"tr[data-href] a, tr[data-href] button\").click(function (e) {\n // TODO: This is a hack to allow modal close buttons to work\n if (!(0, _jquery.default)(this).attr(\"data-dismiss\")) {\n e.stopPropagation();\n }\n });\n (0, _utils.makeSortableTables)();\n (0, _jquery.default)('[data-toggle=\"tooltip\"]').tooltip();\n });\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/styles.js?"); /***/ }), @@ -128,7 +128,7 @@ eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n /***/ (function(module, exports, __webpack_require__) { ; -eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.WindowController = WindowController;\nexports.colorHash = colorHash;\nexports.htmlEntities = htmlEntities;\nexports.cumulativeSum = cumulativeSum;\nexports.init_notification_counter = init_notification_counter;\nexports.set_notification_counter = set_notification_counter;\nexports.inc_notification_counter = inc_notification_counter;\nexports.dec_notification_counter = dec_notification_counter;\nexports.clear_notification_counter = clear_notification_counter;\nexports.copyToClipboard = copyToClipboard;\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\n_jquery.default.fn.serializeJSON = function (omit_nulls) {\n var params = {};\n var form = (0, _jquery.default)(this);\n var values = form.serializeArray();\n values = values.concat(form.find(\"input[type=checkbox]:checked\").map(function () {\n return {\n name: this.name,\n value: true\n };\n }).get());\n values = values.concat(form.find(\"input[type=checkbox]:not(:checked)\").map(function () {\n return {\n name: this.name,\n value: false\n };\n }).get());\n values.map(function (x) {\n if (omit_nulls) {\n if (x.value !== null && x.value !== \"\") {\n params[x.name] = x.value;\n } else {\n var input = form.find(\":input[name=\".concat(x.name, \"]\"));\n\n if (input.data(\"initial\") !== input.val()) {\n params[x.name] = x.value;\n }\n }\n } else {\n params[x.name] = x.value;\n }\n });\n return params;\n}; //http://stackoverflow.com/a/2648463 - wizardry!\n\n\nString.prototype.format = String.prototype.f = function () {\n var s = this,\n i = arguments.length;\n\n while (i--) {\n s = s.replace(new RegExp(\"\\\\{\" + i + \"\\\\}\", \"gm\"), arguments[i]);\n }\n\n return s;\n}; //http://stackoverflow.com/a/7616484\n\n\nString.prototype.hashCode = function () {\n var hash = 0,\n i,\n chr,\n len;\n if (this.length == 0) return hash;\n\n for (i = 0, len = this.length; i < len; i++) {\n chr = this.charCodeAt(i);\n hash = (hash << 5) - hash + chr;\n hash |= 0; // Convert to 32bit integer\n }\n\n return hash;\n}; // https://gist.github.com/neilj/4146038\n// https://fastmail.blog/2012/11/26/inter-tab-communication-using-local-storage/\n\n\nfunction WindowController() {\n this.id = Math.random();\n this.isMaster = false;\n this.others = {};\n window.addEventListener(\"storage\", this, false);\n window.addEventListener(\"unload\", this, false);\n this.broadcast(\"hello\");\n var that = this;\n\n var check = function check() {\n that.check();\n that._checkTimeout = setTimeout(check, 9000);\n };\n\n var ping = function ping() {\n that.sendPing();\n that._pingTimeout = setTimeout(ping, 17000);\n };\n\n this._checkTimeout = setTimeout(check, 500);\n this._pingTimeout = setTimeout(ping, 17000);\n}\n\nWindowController.prototype.destroy = function () {\n clearTimeout(this._pingTimeout);\n clearTimeout(this._checkTimeout);\n window.removeEventListener(\"storage\", this, false);\n window.removeEventListener(\"unload\", this, false);\n this.broadcast(\"bye\");\n};\n\nWindowController.prototype.handleEvent = function (event) {\n if (event.type === \"unload\") {\n this.destroy();\n } else if (event.key === \"broadcast\") {\n try {\n var data = JSON.parse(event.newValue);\n\n if (data.id !== this.id) {\n this[data.type](data);\n }\n } catch (error) {}\n }\n};\n\nWindowController.prototype.sendPing = function () {\n this.broadcast(\"ping\");\n};\n\nWindowController.prototype.hello = function (event) {\n this.ping(event);\n\n if (event.id < this.id) {\n this.check();\n } else {\n this.sendPing();\n }\n};\n\nWindowController.prototype.ping = function (event) {\n this.others[event.id] = +new Date();\n};\n\nWindowController.prototype.bye = function (event) {\n delete this.others[event.id];\n this.check();\n};\n\nWindowController.prototype.check = function (event) {\n var now = +new Date(),\n takeMaster = true,\n id;\n\n for (id in this.others) {\n if (this.others[id] + 23000 < now) {\n delete this.others[id];\n } else if (id < this.id) {\n takeMaster = false;\n }\n }\n\n if (this.isMaster !== takeMaster) {\n this.isMaster = takeMaster;\n this.masterDidChange();\n }\n};\n\nWindowController.prototype.masterDidChange = function () {};\n\nWindowController.prototype.broadcast = function (type, data) {\n var event = {\n id: this.id,\n type: type\n };\n\n for (var x in data) {\n event[x] = data[x];\n }\n\n try {\n localStorage.setItem(\"broadcast\", JSON.stringify(event));\n } catch (error) {\n console.log(error);\n }\n};\n\nfunction colorHash(str) {\n var hash = 0;\n\n for (var i = 0; i < str.length; i++) {\n hash = str.charCodeAt(i) + ((hash << 5) - hash);\n }\n\n var colour = \"#\";\n\n for (var _i = 0; _i < 3; _i++) {\n var value = hash >> _i * 8 & 0xff;\n colour += (\"00\" + value.toString(16)).substr(-2);\n }\n\n return colour;\n}\n\nfunction htmlEntities(string) {\n return (0, _jquery.default)(\"
\").text(string).html();\n}\n\nfunction cumulativeSum(arr) {\n var result = arr.concat();\n\n for (var i = 0; i < arr.length; i++) {\n result[i] = arr.slice(0, i + 1).reduce(function (p, i) {\n return p + i;\n });\n }\n\n return result;\n}\n\nvar storage = window.localStorage;\nvar counter_key = \"unread_notifications\";\n\nfunction init_notification_counter() {\n var count = storage.getItem(counter_key);\n\n if (count === null) {\n storage.setItem(counter_key, 0);\n } else {\n if (count > 0) {\n (0, _jquery.default)(\".badge-notification\").text(count);\n }\n }\n}\n\nfunction set_notification_counter(count) {\n storage.setItem(counter_key, count);\n}\n\nfunction inc_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n storage.setItem(counter_key, ++count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n}\n\nfunction dec_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n\n if (count > 0) {\n storage.setItem(counter_key, --count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n } // Always clear if count is 0\n\n\n if (count == 0) {\n clear_notification_counter();\n }\n}\n\nfunction clear_notification_counter() {\n storage.setItem(counter_key, 0);\n (0, _jquery.default)(\".badge-notification\").empty();\n}\n\nfunction copyToClipboard(event, selector) {\n // Select element\n (0, _jquery.default)(selector).select(); // Copy to clipboard\n\n document.execCommand(\"copy\"); // Show tooltip to user\n\n (0, _jquery.default)(event.target).tooltip({\n title: \"Copied!\",\n trigger: \"manual\"\n });\n (0, _jquery.default)(event.target).tooltip(\"show\");\n setTimeout(function () {\n (0, _jquery.default)(event.target).tooltip(\"hide\");\n }, 1500);\n}\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/utils.js?"); +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.WindowController = WindowController;\nexports.colorHash = colorHash;\nexports.htmlEntities = htmlEntities;\nexports.cumulativeSum = cumulativeSum;\nexports.init_notification_counter = init_notification_counter;\nexports.set_notification_counter = set_notification_counter;\nexports.inc_notification_counter = inc_notification_counter;\nexports.dec_notification_counter = dec_notification_counter;\nexports.clear_notification_counter = clear_notification_counter;\nexports.copyToClipboard = copyToClipboard;\nexports.makeSortableTables = makeSortableTables;\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\n_jquery.default.fn.serializeJSON = function (omit_nulls) {\n var params = {};\n var form = (0, _jquery.default)(this);\n var values = form.serializeArray();\n values = values.concat(form.find(\"input[type=checkbox]:checked\").map(function () {\n return {\n name: this.name,\n value: true\n };\n }).get());\n values = values.concat(form.find(\"input[type=checkbox]:not(:checked)\").map(function () {\n return {\n name: this.name,\n value: false\n };\n }).get());\n values.map(function (x) {\n if (omit_nulls) {\n if (x.value !== null && x.value !== \"\") {\n params[x.name] = x.value;\n } else {\n var input = form.find(\":input[name=\".concat(x.name, \"]\"));\n\n if (input.data(\"initial\") !== input.val()) {\n params[x.name] = x.value;\n }\n }\n } else {\n params[x.name] = x.value;\n }\n });\n return params;\n}; //http://stackoverflow.com/a/2648463 - wizardry!\n\n\nString.prototype.format = String.prototype.f = function () {\n var s = this,\n i = arguments.length;\n\n while (i--) {\n s = s.replace(new RegExp(\"\\\\{\" + i + \"\\\\}\", \"gm\"), arguments[i]);\n }\n\n return s;\n}; //http://stackoverflow.com/a/7616484\n\n\nString.prototype.hashCode = function () {\n var hash = 0,\n i,\n chr,\n len;\n if (this.length == 0) return hash;\n\n for (i = 0, len = this.length; i < len; i++) {\n chr = this.charCodeAt(i);\n hash = (hash << 5) - hash + chr;\n hash |= 0; // Convert to 32bit integer\n }\n\n return hash;\n}; // https://gist.github.com/neilj/4146038\n// https://fastmail.blog/2012/11/26/inter-tab-communication-using-local-storage/\n\n\nfunction WindowController() {\n this.id = Math.random();\n this.isMaster = false;\n this.others = {};\n window.addEventListener(\"storage\", this, false);\n window.addEventListener(\"unload\", this, false);\n this.broadcast(\"hello\");\n var that = this;\n\n var check = function check() {\n that.check();\n that._checkTimeout = setTimeout(check, 9000);\n };\n\n var ping = function ping() {\n that.sendPing();\n that._pingTimeout = setTimeout(ping, 17000);\n };\n\n this._checkTimeout = setTimeout(check, 500);\n this._pingTimeout = setTimeout(ping, 17000);\n}\n\nWindowController.prototype.destroy = function () {\n clearTimeout(this._pingTimeout);\n clearTimeout(this._checkTimeout);\n window.removeEventListener(\"storage\", this, false);\n window.removeEventListener(\"unload\", this, false);\n this.broadcast(\"bye\");\n};\n\nWindowController.prototype.handleEvent = function (event) {\n if (event.type === \"unload\") {\n this.destroy();\n } else if (event.key === \"broadcast\") {\n try {\n var data = JSON.parse(event.newValue);\n\n if (data.id !== this.id) {\n this[data.type](data);\n }\n } catch (error) {}\n }\n};\n\nWindowController.prototype.sendPing = function () {\n this.broadcast(\"ping\");\n};\n\nWindowController.prototype.hello = function (event) {\n this.ping(event);\n\n if (event.id < this.id) {\n this.check();\n } else {\n this.sendPing();\n }\n};\n\nWindowController.prototype.ping = function (event) {\n this.others[event.id] = +new Date();\n};\n\nWindowController.prototype.bye = function (event) {\n delete this.others[event.id];\n this.check();\n};\n\nWindowController.prototype.check = function (event) {\n var now = +new Date(),\n takeMaster = true,\n id;\n\n for (id in this.others) {\n if (this.others[id] + 23000 < now) {\n delete this.others[id];\n } else if (id < this.id) {\n takeMaster = false;\n }\n }\n\n if (this.isMaster !== takeMaster) {\n this.isMaster = takeMaster;\n this.masterDidChange();\n }\n};\n\nWindowController.prototype.masterDidChange = function () {};\n\nWindowController.prototype.broadcast = function (type, data) {\n var event = {\n id: this.id,\n type: type\n };\n\n for (var x in data) {\n event[x] = data[x];\n }\n\n try {\n localStorage.setItem(\"broadcast\", JSON.stringify(event));\n } catch (error) {\n console.log(error);\n }\n};\n\nfunction colorHash(str) {\n var hash = 0;\n\n for (var i = 0; i < str.length; i++) {\n hash = str.charCodeAt(i) + ((hash << 5) - hash);\n }\n\n var colour = \"#\";\n\n for (var _i = 0; _i < 3; _i++) {\n var value = hash >> _i * 8 & 0xff;\n colour += (\"00\" + value.toString(16)).substr(-2);\n }\n\n return colour;\n}\n\nfunction htmlEntities(string) {\n return (0, _jquery.default)(\"
\").text(string).html();\n}\n\nfunction cumulativeSum(arr) {\n var result = arr.concat();\n\n for (var i = 0; i < arr.length; i++) {\n result[i] = arr.slice(0, i + 1).reduce(function (p, i) {\n return p + i;\n });\n }\n\n return result;\n}\n\nvar storage = window.localStorage;\nvar counter_key = \"unread_notifications\";\n\nfunction init_notification_counter() {\n var count = storage.getItem(counter_key);\n\n if (count === null) {\n storage.setItem(counter_key, 0);\n } else {\n if (count > 0) {\n (0, _jquery.default)(\".badge-notification\").text(count);\n }\n }\n}\n\nfunction set_notification_counter(count) {\n storage.setItem(counter_key, count);\n}\n\nfunction inc_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n storage.setItem(counter_key, ++count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n}\n\nfunction dec_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n\n if (count > 0) {\n storage.setItem(counter_key, --count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n } // Always clear if count is 0\n\n\n if (count == 0) {\n clear_notification_counter();\n }\n}\n\nfunction clear_notification_counter() {\n storage.setItem(counter_key, 0);\n (0, _jquery.default)(\".badge-notification\").empty();\n}\n\nfunction copyToClipboard(event, selector) {\n // Select element\n (0, _jquery.default)(selector).select(); // Copy to clipboard\n\n document.execCommand(\"copy\"); // Show tooltip to user\n\n (0, _jquery.default)(event.target).tooltip({\n title: \"Copied!\",\n trigger: \"manual\"\n });\n (0, _jquery.default)(event.target).tooltip(\"show\");\n setTimeout(function () {\n (0, _jquery.default)(event.target).tooltip(\"hide\");\n }, 1500);\n}\n\nfunction makeSortableTables() {\n (0, _jquery.default)(\"th.sort-col\").append(\" \");\n (0, _jquery.default)(\"th.sort-col\").click(function () {\n var table = (0, _jquery.default)(this).parents(\"table\").eq(0);\n var rows = table.find(\"tr:gt(0)\").toArray().sort(comparer((0, _jquery.default)(this).index()));\n this.asc = !this.asc;\n\n if (!this.asc) {\n rows = rows.reverse();\n }\n\n for (var i = 0; i < rows.length; i++) {\n table.append(rows[i]);\n }\n });\n\n function comparer(index) {\n return function (a, b) {\n var valA = getCellValue(a, index),\n valB = getCellValue(b, index);\n return _jquery.default.isNumeric(valA) && _jquery.default.isNumeric(valB) ? valA - valB : valA.toString().localeCompare(valB);\n };\n }\n\n function getCellValue(row, index) {\n return (0, _jquery.default)(row).children(\"td\").eq(index).text();\n }\n}\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/utils.js?"); /***/ }), diff --git a/CTFd/themes/admin/static/js/pages/scoreboard.dev.js b/CTFd/themes/admin/static/js/pages/scoreboard.dev.js index b6f11eeb..644965ce 100644 --- a/CTFd/themes/admin/static/js/pages/scoreboard.dev.js +++ b/CTFd/themes/admin/static/js/pages/scoreboard.dev.js @@ -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?"); /***/ }) diff --git a/CTFd/themes/admin/static/js/pages/submissions.dev.js b/CTFd/themes/admin/static/js/pages/submissions.dev.js index ab31c736..771da222 100644 --- a/CTFd/themes/admin/static/js/pages/submissions.dev.js +++ b/CTFd/themes/admin/static/js/pages/submissions.dev.js @@ -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(\"\" + (0, _utils.htmlEntities)(team_name) + \"\", \"\" + (0, _utils.htmlEntities)(chal_name) + \"\"),\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(\"\" + (0, _utils.htmlEntities)(team_name) + \"\", \"\" + (0, _utils.htmlEntities)(chal_name) + \"\"),\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?"); /***/ }) diff --git a/CTFd/themes/admin/templates/scoreboard.html b/CTFd/themes/admin/templates/scoreboard.html index aa855090..a7202afa 100644 --- a/CTFd/themes/admin/templates/scoreboard.html +++ b/CTFd/themes/admin/templates/scoreboard.html @@ -9,19 +9,43 @@
- +
+
+ + +
+
+ + +
+
+
- - - - + + + + + {% for standing in standings %} - + + {% endfor %} diff --git a/CTFd/themes/admin/templates/submissions.html b/CTFd/themes/admin/templates/submissions.html index 458cadef..87e60834 100644 --- a/CTFd/themes/admin/templates/submissions.html +++ b/CTFd/themes/admin/templates/submissions.html @@ -14,21 +14,41 @@
-
PlaceTeamScoreVisibility +
+   +
+
PlaceTeamScoreVisibility
{{ loop.index }} +
+   +
+
{{ loop.index }} {{ standing.name }} @@ -40,15 +64,11 @@ {{ standing.score }} - {% if standing.hidden %} - - {% else %} - - {% endif %} + {% if standing.hidden %} + Hidden + {% else %} + Visible + {% endif %}
+
+
+ +
+
+ + +
+
+
- - - - - - - + + + + + + + {% for sub in submissions %} + @@ -47,12 +67,6 @@ - {% endfor %} diff --git a/CTFd/themes/core/assets/js/utils.js b/CTFd/themes/core/assets/js/utils.js index 4980ffe0..dd778517 100644 --- a/CTFd/themes/core/assets/js/utils.js +++ b/CTFd/themes/core/assets/js/utils.js @@ -256,3 +256,38 @@ export function copyToClipboard(event, selector) { $(event.target).tooltip("hide"); }, 1500); } + +export function makeSortableTables() { + $("th.sort-col").append(` `); + $("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(); + } +} diff --git a/CTFd/themes/core/static/js/core.dev.js b/CTFd/themes/core/static/js/core.dev.js index 271a0942..0a781769 100644 --- a/CTFd/themes/core/static/js/core.dev.js +++ b/CTFd/themes/core/static/js/core.dev.js @@ -128,7 +128,7 @@ eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n /***/ (function(module, exports, __webpack_require__) { ; -eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.WindowController = WindowController;\nexports.colorHash = colorHash;\nexports.htmlEntities = htmlEntities;\nexports.cumulativeSum = cumulativeSum;\nexports.init_notification_counter = init_notification_counter;\nexports.set_notification_counter = set_notification_counter;\nexports.inc_notification_counter = inc_notification_counter;\nexports.dec_notification_counter = dec_notification_counter;\nexports.clear_notification_counter = clear_notification_counter;\nexports.copyToClipboard = copyToClipboard;\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\n_jquery.default.fn.serializeJSON = function (omit_nulls) {\n var params = {};\n var form = (0, _jquery.default)(this);\n var values = form.serializeArray();\n values = values.concat(form.find(\"input[type=checkbox]:checked\").map(function () {\n return {\n name: this.name,\n value: true\n };\n }).get());\n values = values.concat(form.find(\"input[type=checkbox]:not(:checked)\").map(function () {\n return {\n name: this.name,\n value: false\n };\n }).get());\n values.map(function (x) {\n if (omit_nulls) {\n if (x.value !== null && x.value !== \"\") {\n params[x.name] = x.value;\n } else {\n var input = form.find(\":input[name=\".concat(x.name, \"]\"));\n\n if (input.data(\"initial\") !== input.val()) {\n params[x.name] = x.value;\n }\n }\n } else {\n params[x.name] = x.value;\n }\n });\n return params;\n}; //http://stackoverflow.com/a/2648463 - wizardry!\n\n\nString.prototype.format = String.prototype.f = function () {\n var s = this,\n i = arguments.length;\n\n while (i--) {\n s = s.replace(new RegExp(\"\\\\{\" + i + \"\\\\}\", \"gm\"), arguments[i]);\n }\n\n return s;\n}; //http://stackoverflow.com/a/7616484\n\n\nString.prototype.hashCode = function () {\n var hash = 0,\n i,\n chr,\n len;\n if (this.length == 0) return hash;\n\n for (i = 0, len = this.length; i < len; i++) {\n chr = this.charCodeAt(i);\n hash = (hash << 5) - hash + chr;\n hash |= 0; // Convert to 32bit integer\n }\n\n return hash;\n}; // https://gist.github.com/neilj/4146038\n// https://fastmail.blog/2012/11/26/inter-tab-communication-using-local-storage/\n\n\nfunction WindowController() {\n this.id = Math.random();\n this.isMaster = false;\n this.others = {};\n window.addEventListener(\"storage\", this, false);\n window.addEventListener(\"unload\", this, false);\n this.broadcast(\"hello\");\n var that = this;\n\n var check = function check() {\n that.check();\n that._checkTimeout = setTimeout(check, 9000);\n };\n\n var ping = function ping() {\n that.sendPing();\n that._pingTimeout = setTimeout(ping, 17000);\n };\n\n this._checkTimeout = setTimeout(check, 500);\n this._pingTimeout = setTimeout(ping, 17000);\n}\n\nWindowController.prototype.destroy = function () {\n clearTimeout(this._pingTimeout);\n clearTimeout(this._checkTimeout);\n window.removeEventListener(\"storage\", this, false);\n window.removeEventListener(\"unload\", this, false);\n this.broadcast(\"bye\");\n};\n\nWindowController.prototype.handleEvent = function (event) {\n if (event.type === \"unload\") {\n this.destroy();\n } else if (event.key === \"broadcast\") {\n try {\n var data = JSON.parse(event.newValue);\n\n if (data.id !== this.id) {\n this[data.type](data);\n }\n } catch (error) {}\n }\n};\n\nWindowController.prototype.sendPing = function () {\n this.broadcast(\"ping\");\n};\n\nWindowController.prototype.hello = function (event) {\n this.ping(event);\n\n if (event.id < this.id) {\n this.check();\n } else {\n this.sendPing();\n }\n};\n\nWindowController.prototype.ping = function (event) {\n this.others[event.id] = +new Date();\n};\n\nWindowController.prototype.bye = function (event) {\n delete this.others[event.id];\n this.check();\n};\n\nWindowController.prototype.check = function (event) {\n var now = +new Date(),\n takeMaster = true,\n id;\n\n for (id in this.others) {\n if (this.others[id] + 23000 < now) {\n delete this.others[id];\n } else if (id < this.id) {\n takeMaster = false;\n }\n }\n\n if (this.isMaster !== takeMaster) {\n this.isMaster = takeMaster;\n this.masterDidChange();\n }\n};\n\nWindowController.prototype.masterDidChange = function () {};\n\nWindowController.prototype.broadcast = function (type, data) {\n var event = {\n id: this.id,\n type: type\n };\n\n for (var x in data) {\n event[x] = data[x];\n }\n\n try {\n localStorage.setItem(\"broadcast\", JSON.stringify(event));\n } catch (error) {\n console.log(error);\n }\n};\n\nfunction colorHash(str) {\n var hash = 0;\n\n for (var i = 0; i < str.length; i++) {\n hash = str.charCodeAt(i) + ((hash << 5) - hash);\n }\n\n var colour = \"#\";\n\n for (var _i = 0; _i < 3; _i++) {\n var value = hash >> _i * 8 & 0xff;\n colour += (\"00\" + value.toString(16)).substr(-2);\n }\n\n return colour;\n}\n\nfunction htmlEntities(string) {\n return (0, _jquery.default)(\"
\").text(string).html();\n}\n\nfunction cumulativeSum(arr) {\n var result = arr.concat();\n\n for (var i = 0; i < arr.length; i++) {\n result[i] = arr.slice(0, i + 1).reduce(function (p, i) {\n return p + i;\n });\n }\n\n return result;\n}\n\nvar storage = window.localStorage;\nvar counter_key = \"unread_notifications\";\n\nfunction init_notification_counter() {\n var count = storage.getItem(counter_key);\n\n if (count === null) {\n storage.setItem(counter_key, 0);\n } else {\n if (count > 0) {\n (0, _jquery.default)(\".badge-notification\").text(count);\n }\n }\n}\n\nfunction set_notification_counter(count) {\n storage.setItem(counter_key, count);\n}\n\nfunction inc_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n storage.setItem(counter_key, ++count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n}\n\nfunction dec_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n\n if (count > 0) {\n storage.setItem(counter_key, --count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n } // Always clear if count is 0\n\n\n if (count == 0) {\n clear_notification_counter();\n }\n}\n\nfunction clear_notification_counter() {\n storage.setItem(counter_key, 0);\n (0, _jquery.default)(\".badge-notification\").empty();\n}\n\nfunction copyToClipboard(event, selector) {\n // Select element\n (0, _jquery.default)(selector).select(); // Copy to clipboard\n\n document.execCommand(\"copy\"); // Show tooltip to user\n\n (0, _jquery.default)(event.target).tooltip({\n title: \"Copied!\",\n trigger: \"manual\"\n });\n (0, _jquery.default)(event.target).tooltip(\"show\");\n setTimeout(function () {\n (0, _jquery.default)(event.target).tooltip(\"hide\");\n }, 1500);\n}\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/utils.js?"); +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.WindowController = WindowController;\nexports.colorHash = colorHash;\nexports.htmlEntities = htmlEntities;\nexports.cumulativeSum = cumulativeSum;\nexports.init_notification_counter = init_notification_counter;\nexports.set_notification_counter = set_notification_counter;\nexports.inc_notification_counter = inc_notification_counter;\nexports.dec_notification_counter = dec_notification_counter;\nexports.clear_notification_counter = clear_notification_counter;\nexports.copyToClipboard = copyToClipboard;\nexports.makeSortableTables = makeSortableTables;\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\n_jquery.default.fn.serializeJSON = function (omit_nulls) {\n var params = {};\n var form = (0, _jquery.default)(this);\n var values = form.serializeArray();\n values = values.concat(form.find(\"input[type=checkbox]:checked\").map(function () {\n return {\n name: this.name,\n value: true\n };\n }).get());\n values = values.concat(form.find(\"input[type=checkbox]:not(:checked)\").map(function () {\n return {\n name: this.name,\n value: false\n };\n }).get());\n values.map(function (x) {\n if (omit_nulls) {\n if (x.value !== null && x.value !== \"\") {\n params[x.name] = x.value;\n } else {\n var input = form.find(\":input[name=\".concat(x.name, \"]\"));\n\n if (input.data(\"initial\") !== input.val()) {\n params[x.name] = x.value;\n }\n }\n } else {\n params[x.name] = x.value;\n }\n });\n return params;\n}; //http://stackoverflow.com/a/2648463 - wizardry!\n\n\nString.prototype.format = String.prototype.f = function () {\n var s = this,\n i = arguments.length;\n\n while (i--) {\n s = s.replace(new RegExp(\"\\\\{\" + i + \"\\\\}\", \"gm\"), arguments[i]);\n }\n\n return s;\n}; //http://stackoverflow.com/a/7616484\n\n\nString.prototype.hashCode = function () {\n var hash = 0,\n i,\n chr,\n len;\n if (this.length == 0) return hash;\n\n for (i = 0, len = this.length; i < len; i++) {\n chr = this.charCodeAt(i);\n hash = (hash << 5) - hash + chr;\n hash |= 0; // Convert to 32bit integer\n }\n\n return hash;\n}; // https://gist.github.com/neilj/4146038\n// https://fastmail.blog/2012/11/26/inter-tab-communication-using-local-storage/\n\n\nfunction WindowController() {\n this.id = Math.random();\n this.isMaster = false;\n this.others = {};\n window.addEventListener(\"storage\", this, false);\n window.addEventListener(\"unload\", this, false);\n this.broadcast(\"hello\");\n var that = this;\n\n var check = function check() {\n that.check();\n that._checkTimeout = setTimeout(check, 9000);\n };\n\n var ping = function ping() {\n that.sendPing();\n that._pingTimeout = setTimeout(ping, 17000);\n };\n\n this._checkTimeout = setTimeout(check, 500);\n this._pingTimeout = setTimeout(ping, 17000);\n}\n\nWindowController.prototype.destroy = function () {\n clearTimeout(this._pingTimeout);\n clearTimeout(this._checkTimeout);\n window.removeEventListener(\"storage\", this, false);\n window.removeEventListener(\"unload\", this, false);\n this.broadcast(\"bye\");\n};\n\nWindowController.prototype.handleEvent = function (event) {\n if (event.type === \"unload\") {\n this.destroy();\n } else if (event.key === \"broadcast\") {\n try {\n var data = JSON.parse(event.newValue);\n\n if (data.id !== this.id) {\n this[data.type](data);\n }\n } catch (error) {}\n }\n};\n\nWindowController.prototype.sendPing = function () {\n this.broadcast(\"ping\");\n};\n\nWindowController.prototype.hello = function (event) {\n this.ping(event);\n\n if (event.id < this.id) {\n this.check();\n } else {\n this.sendPing();\n }\n};\n\nWindowController.prototype.ping = function (event) {\n this.others[event.id] = +new Date();\n};\n\nWindowController.prototype.bye = function (event) {\n delete this.others[event.id];\n this.check();\n};\n\nWindowController.prototype.check = function (event) {\n var now = +new Date(),\n takeMaster = true,\n id;\n\n for (id in this.others) {\n if (this.others[id] + 23000 < now) {\n delete this.others[id];\n } else if (id < this.id) {\n takeMaster = false;\n }\n }\n\n if (this.isMaster !== takeMaster) {\n this.isMaster = takeMaster;\n this.masterDidChange();\n }\n};\n\nWindowController.prototype.masterDidChange = function () {};\n\nWindowController.prototype.broadcast = function (type, data) {\n var event = {\n id: this.id,\n type: type\n };\n\n for (var x in data) {\n event[x] = data[x];\n }\n\n try {\n localStorage.setItem(\"broadcast\", JSON.stringify(event));\n } catch (error) {\n console.log(error);\n }\n};\n\nfunction colorHash(str) {\n var hash = 0;\n\n for (var i = 0; i < str.length; i++) {\n hash = str.charCodeAt(i) + ((hash << 5) - hash);\n }\n\n var colour = \"#\";\n\n for (var _i = 0; _i < 3; _i++) {\n var value = hash >> _i * 8 & 0xff;\n colour += (\"00\" + value.toString(16)).substr(-2);\n }\n\n return colour;\n}\n\nfunction htmlEntities(string) {\n return (0, _jquery.default)(\"
\").text(string).html();\n}\n\nfunction cumulativeSum(arr) {\n var result = arr.concat();\n\n for (var i = 0; i < arr.length; i++) {\n result[i] = arr.slice(0, i + 1).reduce(function (p, i) {\n return p + i;\n });\n }\n\n return result;\n}\n\nvar storage = window.localStorage;\nvar counter_key = \"unread_notifications\";\n\nfunction init_notification_counter() {\n var count = storage.getItem(counter_key);\n\n if (count === null) {\n storage.setItem(counter_key, 0);\n } else {\n if (count > 0) {\n (0, _jquery.default)(\".badge-notification\").text(count);\n }\n }\n}\n\nfunction set_notification_counter(count) {\n storage.setItem(counter_key, count);\n}\n\nfunction inc_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n storage.setItem(counter_key, ++count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n}\n\nfunction dec_notification_counter() {\n var count = storage.getItem(counter_key) || 0;\n\n if (count > 0) {\n storage.setItem(counter_key, --count);\n (0, _jquery.default)(\".badge-notification\").text(count);\n } // Always clear if count is 0\n\n\n if (count == 0) {\n clear_notification_counter();\n }\n}\n\nfunction clear_notification_counter() {\n storage.setItem(counter_key, 0);\n (0, _jquery.default)(\".badge-notification\").empty();\n}\n\nfunction copyToClipboard(event, selector) {\n // Select element\n (0, _jquery.default)(selector).select(); // Copy to clipboard\n\n document.execCommand(\"copy\"); // Show tooltip to user\n\n (0, _jquery.default)(event.target).tooltip({\n title: \"Copied!\",\n trigger: \"manual\"\n });\n (0, _jquery.default)(event.target).tooltip(\"show\");\n setTimeout(function () {\n (0, _jquery.default)(event.target).tooltip(\"hide\");\n }, 1500);\n}\n\nfunction makeSortableTables() {\n (0, _jquery.default)(\"th.sort-col\").append(\" \");\n (0, _jquery.default)(\"th.sort-col\").click(function () {\n var table = (0, _jquery.default)(this).parents(\"table\").eq(0);\n var rows = table.find(\"tr:gt(0)\").toArray().sort(comparer((0, _jquery.default)(this).index()));\n this.asc = !this.asc;\n\n if (!this.asc) {\n rows = rows.reverse();\n }\n\n for (var i = 0; i < rows.length; i++) {\n table.append(rows[i]);\n }\n });\n\n function comparer(index) {\n return function (a, b) {\n var valA = getCellValue(a, index),\n valB = getCellValue(b, index);\n return _jquery.default.isNumeric(valA) && _jquery.default.isNumeric(valB) ? valA - valB : valA.toString().localeCompare(valB);\n };\n }\n\n function getCellValue(row, index) {\n return (0, _jquery.default)(row).children(\"td\").eq(index).text();\n }\n}\n\n//# sourceURL=webpack:///./CTFd/themes/core/assets/js/utils.js?"); /***/ }),
IDTeamChallengeTypeSubmissionDateDelete +
+   +
+
IDTeamChallengeTypeSubmissionDate
+
+   +
+
{{ sub.id }} - - - -