Skip to content

Commit c2eca90

Browse files
authored
Add API interface to mark submissions as correct (CTFd#2350)
* Add the `discard` type for submissions * Add `PATCH /api/v1/submissions/[submission_id]` to mark submissions as correct * Closes CTFd#181
1 parent deae9e1 commit c2eca90

File tree

16 files changed

+298
-13
lines changed

16 files changed

+298
-13
lines changed

CTFd/api/v1/statistics/challenges.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ class ChallengeSolvePercentages(Resource):
8080
@admins_only
8181
def get(self):
8282
challenges = (
83-
Challenges.query.add_columns("id", "name", "state", "max_attempts")
83+
Challenges.query.add_columns(
84+
Challenges.id,
85+
Challenges.name,
86+
Challenges.state,
87+
Challenges.max_attempts,
88+
)
8489
.order_by(Challenges.value)
8590
.all()
8691
)

CTFd/api/v1/submissions.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import List
22

3+
from flask import request
34
from flask_restx import Namespace, Resource
45

56
from CTFd.api.v1.helpers.request import validate_args
@@ -10,7 +11,7 @@
1011
)
1112
from CTFd.cache import clear_challenges, clear_standings
1213
from CTFd.constants import RawEnum
13-
from CTFd.models import Submissions, db
14+
from CTFd.models import Solves, Submissions, db
1415
from CTFd.schemas.submissions import SubmissionSchema
1516
from CTFd.utils.decorators import admins_only
1617
from CTFd.utils.helpers.models import build_model_filters
@@ -152,7 +153,7 @@ def post(self, json_args):
152153
class Submission(Resource):
153154
@admins_only
154155
@submissions_namespace.doc(
155-
description="Endpoint to get submission objects in bulk",
156+
description="Endpoint to get a submission object",
156157
responses={
157158
200: ("Success", "SubmissionDetailedSuccessResponse"),
158159
400: (
@@ -173,7 +174,51 @@ def get(self, submission_id):
173174

174175
@admins_only
175176
@submissions_namespace.doc(
176-
description="Endpoint to get submission objects in bulk",
177+
description="Endpoint to edit a submission object",
178+
responses={
179+
200: ("Success", "SubmissionDetailedSuccessResponse"),
180+
400: (
181+
"An error occured processing the provided or stored data",
182+
"APISimpleErrorResponse",
183+
),
184+
},
185+
)
186+
def patch(self, submission_id):
187+
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
188+
189+
req = request.get_json()
190+
submission_type = req.get("type")
191+
192+
if submission_type == "correct":
193+
solve = Solves(
194+
user_id=submission.user_id,
195+
challenge_id=submission.challenge_id,
196+
team_id=submission.team_id,
197+
ip=submission.ip,
198+
provided=submission.provided,
199+
date=submission.date,
200+
)
201+
db.session.add(solve)
202+
submission.type = "discard"
203+
db.session.commit()
204+
205+
# Delete standings cache
206+
clear_standings()
207+
clear_challenges()
208+
209+
submission = solve
210+
211+
schema = SubmissionSchema()
212+
response = schema.dump(submission)
213+
214+
if response.errors:
215+
return {"success": False, "errors": response.errors}, 400
216+
217+
return {"success": True, "data": response.data}
218+
219+
@admins_only
220+
@submissions_namespace.doc(
221+
description="Endpoint to delete a submission object",
177222
responses={
178223
200: ("Success", "APISimpleSuccessResponse"),
179224
400: (

CTFd/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,10 @@ class Fails(Submissions):
849849
__mapper_args__ = {"polymorphic_identity": "incorrect"}
850850

851851

852+
class Discards(Submissions):
853+
__mapper_args__ = {"polymorphic_identity": "discard"}
854+
855+
852856
class Unlocks(db.Model):
853857
__tablename__ = "unlocks"
854858
id = db.Column(db.Integer, primary_key=True)

CTFd/themes/admin/assets/js/pages/submissions.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,38 @@ function deleteSelectedSubmissions(_event) {
6161
});
6262
}
6363

64+
function correctSubmissions(_event) {
65+
let submissionIDs = $("input[data-submission-id]:checked").map(function() {
66+
return $(this).data("submission-id");
67+
});
68+
let target = submissionIDs.length === 1 ? "submission" : "submissions";
69+
70+
ezQuery({
71+
title: "Correct Submissions",
72+
body: `Are you sure you want to mark ${
73+
submissionIDs.length
74+
} ${target} correct?`,
75+
success: function() {
76+
const reqs = [];
77+
for (var subId of submissionIDs) {
78+
let req = CTFd.fetch(`/api/v1/submissions/${subId}`, {
79+
method: "PATCH",
80+
credentials: "same-origin",
81+
headers: {
82+
Accept: "application/json",
83+
"Content-Type": "application/json"
84+
},
85+
body: JSON.stringify({ type: "correct" })
86+
});
87+
reqs.push(req);
88+
}
89+
Promise.all(reqs).then(_responses => {
90+
window.location.reload();
91+
});
92+
}
93+
});
94+
}
95+
6496
function showFlagsToggle(_event) {
6597
const urlParams = new URLSearchParams(window.location.search);
6698
if (urlParams.has("full")) {
@@ -110,6 +142,7 @@ $(() => {
110142
$("#show-short-flags-button").click(showFlagsToggle);
111143
$(".show-flag").click(showFlag);
112144
$(".copy-flag").click(copyFlag);
145+
$("#correct-flags-button").click(correctSubmissions);
113146
$(".delete-correct-submission").click(deleteCorrectSubmission);
114147
$("#submission-delete-button").click(deleteSelectedSubmissions);
115148
});

CTFd/themes/admin/assets/js/pages/team.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,39 @@ function updateTeam(event) {
112112
});
113113
}
114114

115+
function correctSubmissions(_event) {
116+
let submissions = $("input[data-submission-type=incorrect]:checked");
117+
let submissionIDs = submissions.map(function() {
118+
return $(this).data("submission-id");
119+
});
120+
let target = submissionIDs.length === 1 ? "submission" : "submissions";
121+
122+
ezQuery({
123+
title: "Correct Submissions",
124+
body: `Are you sure you want to mark ${
125+
submissionIDs.length
126+
} ${target} correct?`,
127+
success: function() {
128+
const reqs = [];
129+
for (var subId of submissionIDs) {
130+
let req = CTFd.fetch(`/api/v1/submissions/${subId}`, {
131+
method: "PATCH",
132+
credentials: "same-origin",
133+
headers: {
134+
Accept: "application/json",
135+
"Content-Type": "application/json"
136+
},
137+
body: JSON.stringify({ type: "correct" })
138+
});
139+
reqs.push(req);
140+
}
141+
Promise.all(reqs).then(_responses => {
142+
window.location.reload();
143+
});
144+
}
145+
});
146+
}
147+
115148
function deleteSelectedSubmissions(event, target) {
116149
let submissions;
117150
let type;
@@ -529,6 +562,8 @@ $(() => {
529562
deleteSelectedSubmissions(e, "solves");
530563
});
531564

565+
$("#correct-fail-button").click(correctSubmissions);
566+
532567
$("#fails-delete-button").click(function(e) {
533568
deleteSelectedSubmissions(e, "fails");
534569
});

CTFd/themes/admin/assets/js/pages/user.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,39 @@ function emailUser(event) {
225225
});
226226
}
227227

228+
function correctSubmissions(_event) {
229+
let submissions = $("input[data-submission-type=incorrect]:checked");
230+
let submissionIDs = submissions.map(function() {
231+
return $(this).data("submission-id");
232+
});
233+
let target = submissionIDs.length === 1 ? "submission" : "submissions";
234+
235+
ezQuery({
236+
title: "Correct Submissions",
237+
body: `Are you sure you want to mark ${
238+
submissionIDs.length
239+
} ${target} correct?`,
240+
success: function() {
241+
const reqs = [];
242+
for (var subId of submissionIDs) {
243+
let req = CTFd.fetch(`/api/v1/submissions/${subId}`, {
244+
method: "PATCH",
245+
credentials: "same-origin",
246+
headers: {
247+
Accept: "application/json",
248+
"Content-Type": "application/json"
249+
},
250+
body: JSON.stringify({ type: "correct" })
251+
});
252+
reqs.push(req);
253+
}
254+
Promise.all(reqs).then(_responses => {
255+
window.location.reload();
256+
});
257+
}
258+
});
259+
}
260+
228261
function deleteSelectedSubmissions(event, target) {
229262
let submissions;
230263
let type;
@@ -452,6 +485,8 @@ $(() => {
452485
deleteSelectedSubmissions(e, "solves");
453486
});
454487

488+
$("#correct-fail-button").click(correctSubmissions);
489+
455490
$("#fails-delete-button").click(function(e) {
456491
deleteSelectedSubmissions(e, "fails");
457492
});

0 commit comments

Comments
 (0)