mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
Add next recommended challenge after solve (#2081)
* Add a next challenge recommendation to challenges * Closes #1668
This commit is contained in:
@@ -86,6 +86,7 @@ class Challenges(db.Model):
|
|||||||
name = db.Column(db.String(80))
|
name = db.Column(db.String(80))
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
connection_info = 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)
|
max_attempts = db.Column(db.Integer, default=0)
|
||||||
value = db.Column(db.Integer)
|
value = db.Column(db.Integer)
|
||||||
category = db.Column(db.String(80))
|
category = db.Column(db.String(80))
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class BaseChallenge(object):
|
|||||||
"value": challenge.value,
|
"value": challenge.value,
|
||||||
"description": challenge.description,
|
"description": challenge.description,
|
||||||
"connection_info": challenge.connection_info,
|
"connection_info": challenge.connection_info,
|
||||||
|
"next_id": challenge.next_id,
|
||||||
"category": challenge.category,
|
"category": challenge.category,
|
||||||
"state": challenge.state,
|
"state": challenge.state,
|
||||||
"max_attempts": challenge.max_attempts,
|
"max_attempts": challenge.max_attempts,
|
||||||
|
|||||||
127
CTFd/themes/admin/assets/js/components/next/NextChallenge.vue
Normal file
127
CTFd/themes/admin/assets/js/components/next/NextChallenge.vue
Normal 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>
|
||||||
@@ -15,6 +15,7 @@ import TopicsList from "../components/topics/TopicsList.vue";
|
|||||||
import TagsList from "../components/tags/TagsList.vue";
|
import TagsList from "../components/tags/TagsList.vue";
|
||||||
import ChallengeFilesList from "../components/files/ChallengeFilesList.vue";
|
import ChallengeFilesList from "../components/files/ChallengeFilesList.vue";
|
||||||
import HintsList from "../components/hints/HintsList.vue";
|
import HintsList from "../components/hints/HintsList.vue";
|
||||||
|
import NextChallenge from "../components/next/NextChallenge.vue";
|
||||||
import hljs from "highlight.js";
|
import hljs from "highlight.js";
|
||||||
|
|
||||||
const displayHint = data => {
|
const displayHint = data => {
|
||||||
@@ -493,6 +494,16 @@ $(() => {
|
|||||||
}).$mount(vueContainer);
|
}).$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,
|
// Because this JS is shared by a few pages,
|
||||||
// we should only insert the CommentBox if it's actually in use
|
// we should only insert the CommentBox if it's actually in use
|
||||||
if (document.querySelector("#comment-box")) {
|
if (document.querySelector("#comment-box")) {
|
||||||
|
|||||||
@@ -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":
|
||||||
/*!*******************************************************************************!*\
|
/*!*******************************************************************************!*\
|
||||||
!*** ./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/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& ***!
|
!*** ./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?!./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& ***!
|
!*** ./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
@@ -66,12 +66,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<nav class="nav nav-tabs nav-fill" id="challenge-properties" role="tablist">
|
<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 small 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 small" 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 small" 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 small" 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 small" 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" 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>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
@@ -123,6 +124,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
CTFd/themes/admin/templates/modals/challenges/next.html
Normal file
2
CTFd/themes/admin/templates/modals/challenges/next.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<div id="next-add-form">
|
||||||
|
</div>
|
||||||
@@ -146,6 +146,15 @@ function renderSubmissionResponse(response) {
|
|||||||
result_notification.removeClass();
|
result_notification.removeClass();
|
||||||
result_message.text(result.message);
|
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") {
|
if (result.status === "authentication_required") {
|
||||||
window.location =
|
window.location =
|
||||||
CTFd.config.urlRoot +
|
CTFd.config.urlRoot +
|
||||||
@@ -193,6 +202,10 @@ function renderSubmissionResponse(response) {
|
|||||||
answer_input.val("");
|
answer_input.val("");
|
||||||
answer_input.removeClass("wrong");
|
answer_input.removeClass("wrong");
|
||||||
answer_input.addClass("correct");
|
answer_input.addClass("correct");
|
||||||
|
|
||||||
|
if (CTFd._internal.challenge.data.next_id) {
|
||||||
|
$(".submit-row").html(next_btn);
|
||||||
|
}
|
||||||
} else if (result.status === "already_solved") {
|
} else if (result.status === "already_solved") {
|
||||||
// Challenge already solved
|
// Challenge already solved
|
||||||
result_notification.addClass(
|
result_notification.addClass(
|
||||||
@@ -201,6 +214,10 @@ function renderSubmissionResponse(response) {
|
|||||||
result_notification.slideDown();
|
result_notification.slideDown();
|
||||||
|
|
||||||
answer_input.addClass("correct");
|
answer_input.addClass("correct");
|
||||||
|
|
||||||
|
if (CTFd._internal.challenge.data.next_id) {
|
||||||
|
$(".submit-row").html(next_btn);
|
||||||
|
}
|
||||||
} else if (result.status === "paused") {
|
} else if (result.status === "paused") {
|
||||||
// CTF is paused
|
// CTF is paused
|
||||||
result_notification.addClass(
|
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
@@ -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 ###
|
||||||
Reference in New Issue
Block a user