Add next recommended challenge after solve (#2081)

* Add a next challenge recommendation to challenges
* Closes #1668
This commit is contained in:
Kevin Chung
2022-04-08 23:02:56 -04:00
committed by GitHub
parent c95591aa16
commit afc55bff75
14 changed files with 271 additions and 11 deletions

View File

@@ -86,6 +86,7 @@ class Challenges(db.Model):
name = db.Column(db.String(80))
description = db.Column(db.Text)
connection_info = db.Column(db.Text)
next_id = db.Column(db.Integer, db.ForeignKey("challenges.id", ondelete="SET NULL"))
max_attempts = db.Column(db.Integer, default=0)
value = db.Column(db.Integer)
category = db.Column(db.String(80))

View File

@@ -54,6 +54,7 @@ class BaseChallenge(object):
"value": challenge.value,
"description": challenge.description,
"connection_info": challenge.connection_info,
"next_id": challenge.next_id,
"category": challenge.category,
"state": challenge.state,
"max_attempts": challenge.max_attempts,

View File

@@ -0,0 +1,127 @@
<template>
<div>
<form @submit.prevent="updateNext">
<div class="form-group">
<label>
Next Challenge
<br />
<small class="text-muted"
>Challenge to recommend after solving this challenge</small
>
</label>
<select class="form-control custom-select" v-model="selected_id">
<option value="null"> -- </option>
<option
v-for="challenge in otherChallenges"
:value="challenge.id"
:key="challenge.id"
>{{ challenge.name }}</option
>
</select>
</div>
<div class="form-group">
<button
class="btn btn-success float-right"
:disabled="!updateAvailable"
>
Save
</button>
</div>
</form>
</div>
</template>
<script>
import CTFd from "core/CTFd";
export default {
props: {
challenge_id: Number
},
data: function() {
return {
challenge: null,
challenges: [],
selected_id: null
};
},
computed: {
updateAvailable: function() {
return this.selected_id != this.challenge.next_id;
},
// Get all challenges besides the current one and current next
otherChallenges: function() {
return this.challenges.filter(challenge => {
return challenge.id !== this.$props.challenge_id;
});
}
},
methods: {
loadData: function() {
CTFd.fetch(`/api/v1/challenges/${this.$props.challenge_id}`, {
method: "GET",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(response => {
return response.json();
})
.then(response => {
if (response.success) {
this.challenge = response.data;
this.selected_id = response.data.next_id;
}
});
},
loadChallenges: function() {
CTFd.fetch("/api/v1/challenges?view=admin", {
method: "GET",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
})
.then(response => {
return response.json();
})
.then(response => {
if (response.success) {
this.challenges = response.data;
}
});
},
updateNext: function() {
CTFd.fetch(`/api/v1/challenges/${this.$props.challenge_id}`, {
method: "PATCH",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
next_id: this.selected_id != "null" ? this.selected_id : null
})
})
.then(response => {
return response.json();
})
.then(data => {
if (data.success) {
this.loadData();
this.loadChallenges();
}
});
}
},
created() {
this.loadData();
this.loadChallenges();
}
};
</script>
<style scoped></style>

View File

@@ -15,6 +15,7 @@ import TopicsList from "../components/topics/TopicsList.vue";
import TagsList from "../components/tags/TagsList.vue";
import ChallengeFilesList from "../components/files/ChallengeFilesList.vue";
import HintsList from "../components/hints/HintsList.vue";
import NextChallenge from "../components/next/NextChallenge.vue";
import hljs from "highlight.js";
const displayHint = data => {
@@ -493,6 +494,16 @@ $(() => {
}).$mount(vueContainer);
}
// Load Next component
if (document.querySelector("#next-add-form")) {
const nextChallenge = Vue.extend(NextChallenge);
let vueContainer = document.createElement("div");
document.querySelector("#next-add-form").appendChild(vueContainer);
new nextChallenge({
propsData: { challenge_id: window.CHALLENGE_ID }
}).$mount(vueContainer);
}
// Because this JS is shared by a few pages,
// we should only insert the CommentBox if it's actually in use
if (document.querySelector("#comment-box")) {

View File

@@ -408,6 +408,42 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _nod
/***/ }),
/***/ "./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue":
/*!***********************************************************************!*\
!*** ./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue ***!
\***********************************************************************/
/*! no static exports found */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true& */ \"./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true&\");\n/* harmony import */ var _NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./NextChallenge.vue?vue&type=script&lang=js& */ \"./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js&\");\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n \"4099eef0\",\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"CTFd/themes/admin/assets/js/components/next/NextChallenge.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?");
/***/ }),
/***/ "./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js&":
/*!************************************************************************************************!*\
!*** ./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js& ***!
\************************************************************************************************/
/*! no static exports found */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/babel-loader/lib??ref--0!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./NextChallenge.vue?vue&type=script&lang=js& */ \"./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?");
/***/ }),
/***/ "./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true&":
/*!******************************************************************************************************************!*\
!*** ./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true& ***!
\******************************************************************************************************************/
/*! exports provided: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_NextChallenge_vue_vue_type_template_id_4099eef0_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?");
/***/ }),
/***/ "./CTFd/themes/admin/assets/js/components/notifications/Notification.vue":
/*!*******************************************************************************!*\
!*** ./CTFd/themes/admin/assets/js/components/notifications/Notification.vue ***!
@@ -732,6 +768,18 @@ eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n
/***/ }),
/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js&":
/*!******************************************************************************************************************************************************************************************!*\
!*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=script&lang=js& ***!
\******************************************************************************************************************************************************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n props: {\n challenge_id: Number\n },\n data: function data() {\n return {\n challenge: null,\n challenges: [],\n selected_id: null\n };\n },\n computed: {\n updateAvailable: function updateAvailable() {\n return this.selected_id != this.challenge.next_id;\n },\n // Get all challenges besides the current one and current next\n otherChallenges: function otherChallenges() {\n var _this = this;\n\n return this.challenges.filter(function (challenge) {\n return challenge.id !== _this.$props.challenge_id;\n });\n }\n },\n methods: {\n loadData: function loadData() {\n var _this2 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this2.challenge = response.data;\n _this2.selected_id = response.data.next_id;\n }\n });\n },\n loadChallenges: function loadChallenges() {\n var _this3 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges?view=admin\", {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this3.challenges = response.data;\n }\n });\n },\n updateNext: function updateNext() {\n var _this4 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n next_id: this.selected_id != \"null\" ? this.selected_id : null\n })\n }).then(function (response) {\n return response.json();\n }).then(function (data) {\n if (data.success) {\n _this4.loadData();\n\n _this4.loadChallenges();\n }\n });\n }\n },\n created: function created() {\n this.loadData();\n this.loadChallenges();\n }\n};\nexports[\"default\"] = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),
/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/notifications/Notification.vue?vue&type=script&lang=js&":
/*!**************************************************************************************************************************************************************************************************!*\
!*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/notifications/Notification.vue?vue&type=script&lang=js& ***!
@@ -946,6 +994,18 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
/***/ }),
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true&":
/*!************************************************************************************************************************************************************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?vue&type=template&id=4099eef0&scoped=true& ***!
\************************************************************************************************************************************************************************************************************************************************/
/*! exports provided: render, staticRenderFns */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
;
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", [\n _c(\n \"form\",\n {\n on: {\n submit: function($event) {\n $event.preventDefault()\n return _vm.updateNext($event)\n }\n }\n },\n [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _vm._m(0),\n _vm._v(\" \"),\n _c(\n \"select\",\n {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model\",\n value: _vm.selected_id,\n expression: \"selected_id\"\n }\n ],\n staticClass: \"form-control custom-select\",\n on: {\n change: function($event) {\n var $$selectedVal = Array.prototype.filter\n .call($event.target.options, function(o) {\n return o.selected\n })\n .map(function(o) {\n var val = \"_value\" in o ? o._value : o.value\n return val\n })\n _vm.selected_id = $event.target.multiple\n ? $$selectedVal\n : $$selectedVal[0]\n }\n }\n },\n [\n _c(\"option\", { attrs: { value: \"null\" } }, [_vm._v(\" -- \")]),\n _vm._v(\" \"),\n _vm._l(_vm.otherChallenges, function(challenge) {\n return _c(\n \"option\",\n { key: challenge.id, domProps: { value: challenge.id } },\n [_vm._v(_vm._s(challenge.name))]\n )\n })\n ],\n 2\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-success float-right\",\n attrs: { disabled: !_vm.updateAvailable }\n },\n [_vm._v(\"\\n Save\\n \")]\n )\n ])\n ]\n )\n ])\n}\nvar staticRenderFns = [\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"label\", [\n _vm._v(\"\\n Next Challenge\\n \"),\n _c(\"br\"),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"text-muted\" }, [\n _vm._v(\"Challenge to recommend after solving this challenge\")\n ])\n ])\n }\n]\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/next/NextChallenge.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
/***/ }),
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/notifications/Notification.vue?vue&type=template&id=e5ef5b64&":
/*!********************************************************************************************************************************************************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/notifications/Notification.vue?vue&type=template&id=e5ef5b64& ***!

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

@@ -66,12 +66,13 @@
<div class="row">
<div class="col-md-6">
<nav class="nav nav-tabs nav-fill" id="challenge-properties" role="tablist">
<a class="nav-item nav-link active" data-toggle="tab" href="#files" role="tab">Files</a>
<a class="nav-item nav-link" data-toggle="tab" href="#flags" role="tab">Flags</a>
<a class="nav-item nav-link" data-toggle="tab" href="#topics" role="tab">Topics</a>
<a class="nav-item nav-link" data-toggle="tab" href="#tags" role="tab">Tags</a>
<a class="nav-item nav-link" data-toggle="tab" href="#hints" role="tab">Hints</a>
<a class="nav-item nav-link" data-toggle="tab" href="#requirements" role="tab">Requirements</a>
<a class="nav-item nav-link small active" data-toggle="tab" href="#files" role="tab">Files</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#flags" role="tab">Flags</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#topics" role="tab">Topics</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#tags" role="tab">Tags</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#hints" role="tab">Hints</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#requirements" role="tab">Requirements</a>
<a class="nav-item nav-link small" data-toggle="tab" href="#next" role="tab">Next</a>
</nav>
<div class="tab-content" id="nav-tabContent">
@@ -123,6 +124,14 @@
</div>
</div>
</div>
<div class="tab-pane fade" id="next" role="tabpanel">
<div class="row">
<div class="col-md-12">
<h3 class="text-center py-3 d-block">Next Challenge</h3>
{% include "admin/modals/challenges/next.html" %}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<div id="next-add-form">
</div>

View File

@@ -146,6 +146,15 @@ function renderSubmissionResponse(response) {
result_notification.removeClass();
result_message.text(result.message);
const next_btn = $(
`<div class='col-md-12 pb-3'><button class='btn btn-info w-100'>Next Challenge</button></div>`
).click(function() {
$("#challenge-window").modal("toggle");
setTimeout(function() {
loadChal(CTFd._internal.challenge.data.next_id);
}, 500);
});
if (result.status === "authentication_required") {
window.location =
CTFd.config.urlRoot +
@@ -193,6 +202,10 @@ function renderSubmissionResponse(response) {
answer_input.val("");
answer_input.removeClass("wrong");
answer_input.addClass("correct");
if (CTFd._internal.challenge.data.next_id) {
$(".submit-row").html(next_btn);
}
} else if (result.status === "already_solved") {
// Challenge already solved
result_notification.addClass(
@@ -201,6 +214,10 @@ function renderSubmissionResponse(response) {
result_notification.slideDown();
answer_input.addClass("correct");
if (CTFd._internal.challenge.data.next_id) {
$(".submit-row").html(next_btn);
}
} else if (result.status === "paused") {
// CTF is paused
result_notification.addClass(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
"""Add next_id to Challenges table
Revision ID: 4d3c1b59d011
Revises: 6012fe8de495
Create Date: 2022-04-07 03:53:27.554190
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4d3c1b59d011"
down_revision = "6012fe8de495"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("challenges", sa.Column("next_id", sa.Integer(), nullable=True))
op.create_foreign_key(
None, "challenges", "challenges", ["next_id"], ["id"], ondelete="SET NULL"
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "challenges", type_="foreignkey")
op.drop_column("challenges", "next_id")
# ### end Alembic commands ###