Merge pull request #1373 from CTFd/bulk-table-profile-actions

* Convert Admin Panel User/Team submission actions to be bulk actions
* Add "Mark Missing" feature for Teams
2.4.0-dev
Kevin Chung 2020-05-03 01:10:35 -04:00 committed by GitHub
commit f559c7d8fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 636 additions and 427 deletions

View File

@ -220,14 +220,4 @@ $(() => {
}
});
});
if (window.location.hash) {
let hash = window.location.hash.replace("<>[]'\"", "");
$('nav a[href="' + hash + '"]').tab("show");
}
$(".nav-tabs a").click(function(event) {
$(this).tab("show");
window.location.hash = this.hash;
});
});

View File

@ -447,16 +447,6 @@ $(() => {
$("#challenge-create-options form").submit(handleChallengeOptions);
$(".nav-tabs a").click(function(e) {
$(this).tab("show");
window.location.hash = this.hash;
});
if (window.location.hash) {
let hash = window.location.hash.replace("<>[]'\"", "");
$('nav a[href="' + hash + '"]').tab("show");
}
$("#tags-add-input").keyup(addTag);
$(".delete-tag").click(deleteTag);

View File

@ -220,10 +220,6 @@ function exportConfig(event) {
window.location.href = $(this).attr("href");
}
function showTab(event) {
window.location.hash = this.hash;
}
function insertTimezones(target) {
let current = $("<option>").text(moment.tz.guess());
$(target).append(current);
@ -258,7 +254,6 @@ $(() => {
$("#remove-logo").click(removeLogo);
$("#export-button").click(exportConfig);
$("#import-button").click(importConfig);
$(".nav-pills a").click(showTab);
$("#config-color-update").click(function() {
const hex_code = $("#config-color-picker").val();
const user_css = $("#theme-header").val();
@ -287,12 +282,6 @@ $(() => {
loadDateValues("freeze");
});
let hash = window.location.hash;
if (hash) {
hash = hash.replace("<>[]'\"", "");
$('ul.nav a[href="' + hash + '"]').tab("show");
}
const start = $("#start").val();
const end = $("#end").val();
const freeze = $("#freeze").val();

View File

@ -2,7 +2,7 @@ import "./main";
import $ from "jquery";
import CTFd from "core/CTFd";
import { htmlEntities } from "core/utils";
import { ezQuery, ezBadge } from "core/ezq";
import { ezAlert, ezQuery, ezBadge } from "core/ezq";
import { createGraph, updateGraph } from "core/graphs";
function createTeam(event) {
@ -80,6 +80,132 @@ function updateTeam(event) {
});
}
function deleteSelectedSubmissions(event, target) {
let submissions;
let type;
let title;
switch (target) {
case "solves":
submissions = $("input[data-submission-type=correct]:checked");
type = "solve";
title = "Solves";
break;
case "fails":
submissions = $("input[data-submission-type=incorrect]:checked");
type = "fail";
title = "Fails";
break;
default:
break;
}
let submissionIDs = submissions.map(function() {
return $(this).data("submission-id");
});
let target_string = submissionIDs.length === 1 ? type : type + "s";
ezQuery({
title: `Delete ${title}`,
body: `Are you sure you want to delete ${
submissionIDs.length
} ${target_string}?`,
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 deleteSelectedAwards(event) {
let awardIDs = $("input[data-award-id]:checked").map(function() {
return $(this).data("award-id");
});
let target = awardIDs.length === 1 ? "award" : "awards";
ezQuery({
title: `Delete Awards`,
body: `Are you sure you want to delete ${awardIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var awardID of awardIDs) {
let req = CTFd.fetch("/api/v1/awards/" + awardID, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function solveSelectedMissingChallenges(event) {
event.preventDefault();
let challengeIDs = $("input[data-missing-challenge-id]:checked").map(
function() {
return $(this).data("missing-challenge-id");
}
);
let target = challengeIDs.length === 1 ? "challenge" : "challenges";
ezQuery({
title: `Mark Correct`,
body: `Are you sure you want to mark ${
challengeIDs.length
} challenges correct for ${htmlEntities(TEAM_NAME)}?`,
success: function() {
ezAlert({
title: `User Attribution`,
body: `
Which user on ${htmlEntities(TEAM_NAME)} solved these challenges?
<div class="pb-3" id="query-team-member-solve">
${$("#team-member-select").html()}
</div>
`,
button: "Mark Correct",
success: function() {
const USER_ID = $("#query-team-member-solve > select").val();
const reqs = [];
for (var challengeID of challengeIDs) {
let params = {
provided: "MARKED AS SOLVED BY ADMIN",
user_id: USER_ID,
team_id: TEAM_ID,
challenge_id: challengeID,
type: "correct"
};
let req = CTFd.fetch("/api/v1/submissions", {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
});
}
const api_funcs = {
team: [
x => CTFd.api.get_team_solves({ teamId: x }),
@ -335,82 +461,20 @@ $(() => {
});
});
$(".delete-submission").click(function(e) {
e.preventDefault();
const submission_id = $(this).attr("submission-id");
const submission_type = $(this).attr("submission-type");
const submission_challenge = $(this).attr("submission-challenge");
const body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
htmlEntities(submission_type),
htmlEntities(TEAM_NAME),
htmlEntities(submission_challenge)
);
const row = $(this)
.parent()
.parent();
ezQuery({
title: "Delete Submission",
body: body,
success: function() {
CTFd.fetch("/api/v1/submissions/" + submission_id, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
row.remove();
}
});
}
});
$("#solves-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "solves");
});
$(".delete-award").click(function(e) {
e.preventDefault();
const award_id = $(this).attr("award-id");
const award_name = $(this).attr("award-name");
const body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
htmlEntities(award_name),
htmlEntities(TEAM_NAME)
);
const row = $(this)
.parent()
.parent();
ezQuery({
title: "Delete Award",
body: body,
success: function() {
CTFd.fetch("/api/v1/awards/" + award_id, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
row.remove();
}
$("#fails-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "fails");
});
}
$("#awards-delete-button").click(function(e) {
deleteSelectedAwards(e);
});
$("#missing-solve-button").click(function(e) {
solveSelectedMissingChallenges(e);
});
$("#team-info-create-form").submit(createTeam);

View File

@ -189,110 +189,102 @@ function emailUser(event) {
});
}
function deleteUserSubmission(event) {
event.preventDefault();
const submission_id = $(this).attr("submission-id");
const submission_type = $(this).attr("submission-type");
const submission_challenge = $(this).attr("submission-challenge");
function deleteSelectedSubmissions(event, target) {
let submissions;
let type;
let title;
switch (target) {
case "solves":
submissions = $("input[data-submission-type=correct]:checked");
type = "solve";
title = "Solves";
break;
case "fails":
submissions = $("input[data-submission-type=incorrect]:checked");
type = "fail";
title = "Fails";
break;
default:
break;
}
const body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
htmlEntities(submission_type),
htmlEntities(USER_NAME),
htmlEntities(submission_challenge)
);
const row = $(this)
.parent()
.parent();
let submissionIDs = submissions.map(function() {
return $(this).data("submission-id");
});
let target_string = submissionIDs.length === 1 ? type : type + "s";
ezQuery({
title: "Delete Submission",
body: body,
title: `Delete ${title}`,
body: `Are you sure you want to delete ${
submissionIDs.length
} ${target_string}?`,
success: function() {
CTFd.fetch("/api/v1/submissions/" + submission_id, {
const reqs = [];
for (var subId of submissionIDs) {
reqs.push(CTFd.api.delete_submission({ submissionId: subId }));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function deleteSelectedAwards(event) {
let awardIDs = $("input[data-award-id]:checked").map(function() {
return $(this).data("award-id");
});
let target = awardIDs.length === 1 ? "award" : "awards";
ezQuery({
title: `Delete Awards`,
body: `Are you sure you want to delete ${awardIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var awardID of awardIDs) {
let req = CTFd.fetch("/api/v1/awards/" + awardID, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
row.remove();
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function deleteUserAward(event) {
function solveSelectedMissingChallenges(event) {
event.preventDefault();
const award_id = $(this).attr("award-id");
const award_name = $(this).attr("award-name");
const body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
htmlEntities(award_name),
htmlEntities(USER_NAME)
let challengeIDs = $("input[data-missing-challenge-id]:checked").map(
function() {
return $(this).data("missing-challenge-id");
}
);
const row = $(this)
.parent()
.parent();
let target = challengeIDs.length === 1 ? "challenge" : "challenges";
ezQuery({
title: "Delete Award",
body: body,
title: `Mark Correct`,
body: `Are you sure you want to mark ${
challengeIDs.length
} correct for ${htmlEntities(USER_NAME)}?`,
success: function() {
CTFd.fetch("/api/v1/awards/" + award_id, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
row.remove();
}
});
}
});
}
function correctUserSubmission(event) {
event.preventDefault();
const challenge_id = $(this).attr("challenge-id");
const challenge_name = $(this).attr("challenge-name");
const row = $(this)
.parent()
.parent();
const body = "<span>Are you sure you want to mark <strong>{0}</strong> solved for from <strong>{1}</strong>?".format(
htmlEntities(challenge_name),
htmlEntities(USER_NAME)
);
const params = {
const reqs = [];
for (var challengeID of challengeIDs) {
let params = {
provided: "MARKED AS SOLVED BY ADMIN",
user_id: USER_ID,
team_id: TEAM_ID,
challenge_id: challenge_id,
challenge_id: challengeID,
type: "correct"
};
ezQuery({
title: "Mark Correct",
body: body,
success: function() {
CTFd.fetch("/api/v1/submissions", {
let req = CTFd.fetch("/api/v1/submissions", {
method: "POST",
credentials: "same-origin",
headers: {
@ -300,16 +292,11 @@ function correctUserSubmission(event) {
"Content-Type": "application/json"
},
body: JSON.stringify(params)
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
// TODO: Refresh missing and solves instead of reloading
row.remove();
window.location.reload();
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
@ -425,9 +412,21 @@ $(() => {
$("#user-mail-form").submit(emailUser);
$(".delete-submission").click(deleteUserSubmission);
$(".delete-award").click(deleteUserAward);
$(".correct-submission").click(correctUserSubmission);
$("#solves-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "solves");
});
$("#fails-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "fails");
});
$("#awards-delete-button").click(function(e) {
deleteSelectedAwards(e);
});
$("#missing-solve-button").click(function(e) {
solveSelectedMissingChallenges(e);
});
$("#user-info-create-form").submit(createUser);

View File

@ -80,6 +80,22 @@ export default () => {
window.location.href = url.toString();
});
$('a[data-toggle="tab"]').on("shown.bs.tab", function(e) {
sessionStorage.setItem("activeTab", $(e.target).attr("href"));
});
let activeTab = sessionStorage.getItem("activeTab");
if (activeTab) {
let target = $(
`.nav-tabs a[href="${activeTab}"], .nav-pills a[href="${activeTab}"]`
);
if (target.length) {
target.tab("show");
} else {
sessionStorage.removeItem("activeTab");
}
}
makeSortableTables();
$('[data-toggle="tooltip"]').tooltip();
});

View File

@ -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 _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)(\"[data-checkbox]\").click(function (e) {\n if ((0, _jquery.default)(e.target).is(\"input[type=checkbox]\")) {\n e.stopImmediatePropagation();\n return;\n }\n\n var checkbox = (0, _jquery.default)(this).find(\"input[type=checkbox]\"); // Doing it this way with an event allows data-checkbox-all to work\n\n checkbox.click();\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"[data-checkbox-all]\").on(\"click change\", function (e) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n var idx = (0, _jquery.default)(this).index() + 1;\n (0, _jquery.default)(this).closest(\"table\").find(\"tr td:nth-child(\".concat(idx, \") input[type=checkbox]\")).prop(\"checked\", checked);\n e.stopImmediatePropagation();\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)(\".page-select\").change(function () {\n var url = new URL(window.location);\n url.searchParams.set(\"page\", this.value);\n window.location.href = url.toString();\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?");
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)(\"[data-checkbox]\").click(function (e) {\n if ((0, _jquery.default)(e.target).is(\"input[type=checkbox]\")) {\n e.stopImmediatePropagation();\n return;\n }\n\n var checkbox = (0, _jquery.default)(this).find(\"input[type=checkbox]\"); // Doing it this way with an event allows data-checkbox-all to work\n\n checkbox.click();\n e.stopImmediatePropagation();\n });\n (0, _jquery.default)(\"[data-checkbox-all]\").on(\"click change\", function (e) {\n var checked = (0, _jquery.default)(this).prop(\"checked\");\n var idx = (0, _jquery.default)(this).index() + 1;\n (0, _jquery.default)(this).closest(\"table\").find(\"tr td:nth-child(\".concat(idx, \") input[type=checkbox]\")).prop(\"checked\", checked);\n e.stopImmediatePropagation();\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)(\".page-select\").change(function () {\n var url = new URL(window.location);\n url.searchParams.set(\"page\", this.value);\n window.location.href = url.toString();\n });\n (0, _jquery.default)('a[data-toggle=\"tab\"]').on(\"shown.bs.tab\", function (e) {\n sessionStorage.setItem(\"activeTab\", (0, _jquery.default)(e.target).attr(\"href\"));\n });\n var activeTab = sessionStorage.getItem(\"activeTab\");\n\n if (activeTab) {\n var target = (0, _jquery.default)(\".nav-tabs a[href=\\\"\".concat(activeTab, \"\\\"], .nav-pills a[href=\\\"\").concat(activeTab, \"\\\"]\"));\n\n if (target.length) {\n target.tab(\"show\");\n } else {\n sessionStorage.removeItem(\"activeTab\");\n }\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?");
/***/ }),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -61,6 +61,15 @@
</div>
</div>
<template id="team-member-select">
<select class="form-control custom-select">
<option value=""> -- </option>
{% for member in members %}
<option value="{{ member.id }}">{{ member.name }}</option>
{% endfor %}
</select>
</template>
<div id="team-addresses-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
@ -235,28 +244,53 @@
<a class="nav-item nav-link" id="nav-awards-tab" data-toggle="tab" href="#nav-awards" role="tab"
aria-controls="nav-awards" aria-selected="false">Awards</a>
<a class="nav-item nav-link" id="nav-missing-tab" data-toggle="tab" href="#nav-missing" role="tab"
aria-controls="nav-missing" aria-selected="false">Missing</a>
</nav>
<div class="tab-content min-vh-25" id="nav-tabContent">
<div class="tab-content min-vh-50" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-solves" role="tabpanel" aria-labelledby="nav-solves-tab">
<h3 class="text-center pt-5 d-block">Solves</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Solves</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="solves-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>User</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>User</b></th>
<th class="sort-col text-center"><b>Submitted</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Value</b></th>
<th class="sort-col text-center"><b>Time</b></th>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ solve.id }}" data-submission-id="{{ solve.id }}"
data-submission-type="{{ solve.type }}"
data-submission-challenge="{{ solve.challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ solve.challenge_id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
{{ solve.challenge.name }}
@ -273,14 +307,6 @@
<td class="text-center solve-time">
<span data-time="{{ solve.date | isoformat }}"></span>
</td>
<td class="text-center">
<span class="delete-submission" submission-id="{{ solve.id }}"
submission-type="{{ solve.type }}"
submission-challenge="{{ solve.challenge.name }}" data-toggle="tooltip"
data-placement="top" title="Delete solve #{{ solve.id }}">
<i class="fas fa-times"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@ -290,22 +316,44 @@
</div>
<div class="tab-pane fade" id="nav-wrong" role="tabpanel" aria-labelledby="nav-wrong-tab">
<h3 class="text-center pt-5 d-block">Fails</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Fails</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="fails-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>User</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>User</b></th>
<th class="sort-col text-center"><b>Submitted</b></th>
<th class="sort-col text-center"><b>Time</b></th>
</tr>
</thead>
<tbody>
{% for fail in fails %}
<tr class="chal-wrong" data-href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ fail.id }}" data-submission-id="{{ fail.id }}"
data-submission-type="{{ fail.type }}"
data-submission-challenge="{{ fail.challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ fail.challenge_id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
{{ fail.challenge.name }}
@ -320,14 +368,6 @@
<td class="text-center solve-time">
<span data-time="{{ fail.date | isoformat }}"></span>
</td>
<td class="text-center">
<span class="delete-submission" submission-id="{{ fail.id }}"
submission-type="{{ fail.type }}"
submission-challenge="{{ fail.challenge.name }}" data-toggle="tooltip"
data-placement="top" title="Delete fail #{{ fail.id }}">
<i class="fas fa-times"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@ -337,25 +377,45 @@
</div>
<div class="tab-pane fade" id="nav-awards" role="tabpanel" aria-labelledby="nav-awards-tab">
<h3 class="text-center pt-5 d-block">Awards</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Awards</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="awards-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Name</b></td>
<td class="text-center"><b>User</b></td>
<td class="text-center"><b>Description</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Icon</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Name</b></th>
<th class="sort-col text-center"><b>User</b></th>
<th class="sort-col text-center"><b>Description</b></th>
<th class="sort-col text-center"><b>Date</b></th>
<th class="sort-col text-center"><b>Value</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Icon</b></th>
</tr>
</thead>
<tbody id="awards-body">
{% for award in awards %}
<tr class="award-row">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ award.id }}" data-award-id="{{ award.id }}" data-award-name="{{ award.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ award.id }}">{{ award.name }}</td>
<td class="text-center">
<a href="{{ url_for("admin.users_detail", user_id=award.user_id) }}">
@ -368,14 +428,59 @@
</td>
<td class="text-center">{{ award.value }}</td>
<td class="text-center">{{ award.category }}</td>
<td class="text-center">{{ award.icon }}</td>
<td class="text-center"><i class="award-icon award-{{ award.icon }}"></i> {{ award.icon }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<td class="text-center">
<span class="delete-award" award-id="{{ award.id }}" award-name="{{ award.name }}"
data-toggle="tooltip" data-placement="top" title="Delete award #{{ award.id }}">
<i class="fas fa-times"></i>
</span>
<div class="tab-pane fade" id="nav-missing" role="tabpanel" aria-labelledby="nav-missing-tab">
<h3 class="text-center pt-5 d-block">Missing</h3>
<div class="row">
<div class="col-md-12">
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-success" id="missing-solve-button">
<i class="btn-fa fas fa-check"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped border">
<thead>
<tr>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Value</b></th>
</tr>
</thead>
<tbody>
{% for challenge in missing %}
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ challenge.id }}" data-missing-challenge-id="{{ challenge.id }}"
data-missing-challenge-name="{{ challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ challenge.id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
{{ challenge.name }}
</a>
</td>
<td class="text-center">{{ challenge.category }}</td>
<td class="text-center">{{ challenge.value }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -195,25 +195,47 @@
aria-controls="nav-missing" aria-selected="false">Missing</a>
</nav>
<div class="tab-content min-vh-25" id="nav-tabContent">
<div class="tab-content min-vh-50" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-solves" role="tabpanel" aria-labelledby="nav-solves-tab">
<h3 class="text-center pt-5 d-block">Solves</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Solves</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="solves-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>Submitted</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Value</b></th>
<th class="sort-col text-center"><b>Time</b></th>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ solve.id }}" data-submission-id="{{ solve.id }}"
data-submission-type="{{ solve.type }}"
data-submission-challenge="{{ solve.challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ solve.challenge_id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=solve.challenge_id) }}">
{{ solve.challenge.name }}
@ -225,13 +247,6 @@
<td class="text-center solve-time">
<span data-time="{{ solve.date | isoformat }}"></span>
</td>
<td class="text-center">
<span class="delete-submission" submission-id="{{ solve.id }}"
submission-type="{{ solve.type }}" submission-challenge="{{ solve.challenge.name }}" data-toggle="tooltip"
data-placement="top" title="Delete solve #{{ solve.id }}">
<i class="btn-fa fas fa-times"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@ -241,21 +256,43 @@
</div>
<div class="tab-pane fade" id="nav-wrong" role="tabpanel" aria-labelledby="nav-wrong-tab">
<h3 class="text-center pt-5 d-block">Fails</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Fails</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="fails-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>Submitted</b></th>
<th class="sort-col text-center"><b>Time</b></th>
</tr>
</thead>
<tbody>
{% for fail in fails %}
<tr class="chal-wrong" data-href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ fail.id }}" data-submission-id="{{ fail.id }}"
data-submission-type="{{ fail.type }}"
data-submission-challenge="{{ fail.challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ fail.challenge_id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=fail.challenge_id) }}">
{{ fail.challenge.name }}
@ -267,13 +304,6 @@
<td class="text-center solve-time">
<span data-time="{{ fail.date | isoformat }}"></span>
</td>
<td class="text-center">
<a class="delete-submission" submission-id="{{ fail.id }}"
submission-type="{{ fail.type }}" submission-challenge="{{ fail.challenge.name }}" data-toggle="tooltip"
data-placement="top" title="Delete fail #{{ fail.id }}">
<i class="fas fa-times"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
@ -283,24 +313,44 @@
</div>
<div class="tab-pane fade" id="nav-awards" role="tabpanel" aria-labelledby="nav-awards-tab">
<h3 class="text-center pt-5 d-block">Awards</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Awards</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" id="awards-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 class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Name</b></td>
<td class="text-center"><b>Description</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Icon</b></td>
<td class="text-center"><b>Delete</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Name</b></th>
<th class="sort-col text-center"><b>Description</b></th>
<th class="sort-col text-center"><b>Date</b></th>
<th class="sort-col text-center"><b>Value</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Icon</b></th>
</tr>
</thead>
<tbody id="awards-body">
{% for award in awards %}
<tr class="award-row">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ award.id }}" data-award-id="{{ award.id }}" data-award-name="{{ award.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ award.id }}">{{ award.name }}</td>
<td class=""><pre>{{ award.description }}</pre></td>
<td class="text-center solve-time">
@ -308,15 +358,7 @@
</td>
<td class="text-center">{{ award.value }}</td>
<td class="text-center">{{ award.category }}</td>
<td class="text-center">{{ award.icon }}</td>
<td class="text-center">
<span class="delete-award" data-toggle="tooltip"
data-placement="top" award-id="{{ award.id }}" award-name="{{ award.name }}"
title="Delete award #{{ award.id }}">
<i class="fas fa-times"></i>
</span>
</td>
<td class="text-center"> <i class="award-icon award-{{ award.icon }}"></i> {{ award.icon }}</td>
</tr>
{% endfor %}
</tbody>
@ -326,21 +368,42 @@
</div>
<div class="tab-pane fade" id="nav-missing" role="tabpanel" aria-labelledby="nav-missing-tab">
<h3 class="text-center pt-5 d-block">Missing</h3>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Missing</h3>
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-success" id="missing-solve-button">
<i class="btn-fa fas fa-check"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped border">
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Mark Solved</b></td>
<th class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</th>
<th class="sort-col text-center"><b>Challenge</b></th>
<th class="sort-col text-center"><b>Category</b></th>
<th class="sort-col text-center"><b>Value</b></th>
</tr>
</thead>
<tbody>
{% for challenge in missing %}
<tr class="chal-solve" data-href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
<td class="border-right" data-checkbox>
<div class="form-check text-center">
<input type="checkbox" class="form-check-input" value="{{ challenge.id }}" data-missing-challenge-id="{{ challenge.id }}"
data-missing-challenge-name="{{ challenge.name }}">&nbsp;
</div>
</td>
<td class="text-center chal" id="{{ challenge.id }}">
<a href="{{ url_for("admin.challenges_detail", challenge_id=challenge.id) }}">
{{ challenge.name }}
@ -348,13 +411,6 @@
</td>
<td class="text-center">{{ challenge.category }}</td>
<td class="text-center">{{ challenge.value }}</td>
<td class="text-center">
<a class="correct-submission" data-toggle="tooltip" challenge-id="{{ challenge.id }}"
challenge-name="{{ challenge.name }}"
data-placement="top" title="Mark {{ challenge.name }} correct for {{ user.name }}">
<i class="fas fa-check"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>