diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 26a04e3..42cc9da 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -111,7 +111,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -436,7 +435,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -459,7 +457,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } @@ -516,7 +513,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -557,7 +553,6 @@ "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1288,7 +1283,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6", "@mui/core-downloads-tracker": "^7.2.0", @@ -1889,7 +1883,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2007,7 +2002,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -2026,7 +2020,6 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2036,7 +2029,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2106,7 +2098,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", @@ -2458,7 +2449,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2505,6 +2495,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -2618,7 +2609,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2961,7 +2951,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -3069,7 +3060,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3898,7 +3888,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -4087,6 +4076,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4988,6 +4978,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5002,6 +4993,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5013,7 +5005,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/prismjs": { "version": "1.30.0", @@ -5081,7 +5074,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5090,7 +5082,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5822,7 +5813,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -5958,7 +5948,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6159,7 +6148,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6270,7 +6258,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index b141bc4..1150bba 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -51,6 +51,25 @@ export async function fetchCodes( return r.data; } +export async function onDeleteSubmission( + submissionId?: string | number, +): Promise { + if (!submissionId) { + throw new Error("No submission ID provided for deletion"); + } + + const res = await fetch(`/api/submission/${submissionId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to delete submission: ${text}`); + } +} + export async function fetchAllNews(): Promise { const res = await fetch("/api/news"); if (!res.ok) { diff --git a/frontend/src/pages/leaderboard/components/CodeDialog.tsx b/frontend/src/pages/leaderboard/components/CodeDialog.tsx index 23510d3..a80c33a 100644 --- a/frontend/src/pages/leaderboard/components/CodeDialog.tsx +++ b/frontend/src/pages/leaderboard/components/CodeDialog.tsx @@ -1,16 +1,38 @@ -import { Box, Button, Dialog, DialogContent, DialogTitle } from "@mui/material"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Stack, + Typography, +} from "@mui/material"; import { useState } from "react"; import CodeBlock from "../../../components/codeblock/CodeBlock"; +import { onDeleteSubmission } from "../../../api/api"; export function CodeDialog({ code, + submissionId = undefined, fileName = "file", + title = "Submission", }: { code: any; + title?: string; + submissionId?: number; fileName?: string; }) { const [open, setOpen] = useState(false); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + + const handleConfirmDelete = () => { + onDeleteSubmission(submissionId); + setConfirmDeleteOpen(false); + }; + if (!code) return {fileName}; return ( @@ -29,13 +51,78 @@ export function CodeDialog({ maxWidth="md" fullWidth > - Report JSON + {title} + + + Information + + + + + File Name + + + {fileName} + + + + + Submission ID + + + {submissionId} + + + + + + Admin Operations + + + Delete submission: + + + + + + {/* Confirm delete dialog */} + setConfirmDeleteOpen(false)} + > + Delete submission? + + + Are you sure to delete submission {submissionId}, file {fileName}? + This action cannot be undone. The submission and all related data + will be permanently removed. + + + + + + + + ); } diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx index ac2fa20..7b9cf6e 100644 --- a/frontend/src/pages/leaderboard/components/RankingLists.tsx +++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx @@ -180,6 +180,7 @@ export default function RankingsList({ diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index 2cb6a20..6933290 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -5,7 +5,6 @@ from kernelboard.lib.status_code import http_error, http_success from http import HTTPStatus - leaderboard_bp = Blueprint("leaderboard_bp", __name__, url_prefix="/leaderboard") diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 1f73f39..de9044b 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -120,6 +120,44 @@ def submission(): status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR, ) +@submission_bp.route("/submission", methods=["DELETE"]) +@login_required +def delete_submission(): + """ + DELETE /submission + Body example: + { + "submission_id": 123, + "leaderboard_id": 123, + } + this will soft-delete the submission, and can only be done by the admins of the leaderboard + """ + logger.info("[delete_submission] delete submission request is received") + + user_id, username = get_id_and_username_from_session() + submission_id = request.args.get("submission_id", type=int) + leaderboard_id = request.args.get("leaderboard_id", type=int) + + if submission_id is None or user_id is None or leaderboard_id is None: + return http_error( + message="submission_id, leaderboard_id and user_id are required (int) for delete submission", + code=10000 + http.HTTPStatus.BAD_REQUEST.value, + status_code=http.HTTPStatus.BAD_REQUEST, + ) + + # check if user able to delete the submission + whitelist = get_whitelist(leaderboard_id) + if user_id not in whitelist: + logger.info("[delete_submission] user is not admin, skip the request") + return http_success(message="skip since user is not admin", data={}) + else: + logger.info("[delete_submission] user is admin, continue the request") + + # try to soft-delete the submission + try: + # update submission status to "inactive" + + @submission_bp.route("/codes", methods=["POST"]) def list_codes():