From 02a1534abc0d19ed09f9f1787482a52a9a6dd4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Wale=C5=84?= Date: Wed, 10 Dec 2025 18:40:36 +0100 Subject: [PATCH 1/8] =?UTF-8?q?Revert=20"replace=20external=20MathJax=20CD?= =?UTF-8?q?N=20with=20local=20offline-ready=20version=20(fixes=20=E2=80=A6?= =?UTF-8?q?"=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0c1da0e35d77ab15ce275897249f365a33819729. --- oioioi/base/templates/base.html | 17 +- oioioi/default_settings.py | 5 +- package-lock.json | 270 +++++++++----------------------- package.json | 2 - webpack.config.js | 12 -- 5 files changed, 75 insertions(+), 231 deletions(-) diff --git a/oioioi/base/templates/base.html b/oioioi/base/templates/base.html index 368e1c188..50dcf7825 100644 --- a/oioioi/base/templates/base.html +++ b/oioioi/base/templates/base.html @@ -116,20 +116,7 @@ - - - {% endblock %} + + {% endblock %} diff --git a/oioioi/default_settings.py b/oioioi/default_settings.py index 227c1980b..cf5eb407b 100755 --- a/oioioi/default_settings.py +++ b/oioioi/default_settings.py @@ -836,8 +836,9 @@ # for new messages to notify about MAILNOTIFYD_INTERVAL = 60 -# Serve MathJax library from local static files -MATHJAX_LOCATION = '/static/mathjax/tex-chtml.js' +# If your contest has no access to the internet and you need MathJax typesetting, +# either whitelist this link or download your own copy of MathJax and link it here. +MATHJAX_LOCATION = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/" # Django message framework CSS classes # https://docs.djangoproject.com/en/1.9/ref/contrib/messages/#message-tags diff --git a/package-lock.json b/package-lock.json index 9952f59ea..ffca33933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,11 @@ "jquery": "^1.12.4", "js-cookie": "^3.0.5", "marked": "^15.0.7", - "mathjax": "^4.0.0-beta.7", "select2": "^4.1.0-rc.0" }, "devDependencies": { "@eslint/js": "^9.23.0", "concurrently": "^9.1.2", - "copy-webpack-plugin": "^13.0.1", "eslint": "^9.23.0", "expose-loader": "^5.0.1", "globals": "^16.0.0", @@ -61,9 +59,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "dev": true, "license": "MIT", "dependencies": { @@ -79,6 +77,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -90,13 +101,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -105,22 +116,19 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", + "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -192,22 +200,19 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -215,13 +220,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -646,14 +651,6 @@ } } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -669,9 +666,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -808,9 +805,9 @@ "license": "Apache-2.0" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1035,30 +1032,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/copy-webpack-plugin": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", - "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1192,32 +1165,33 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", - "@eslint/plugin-kit": "^0.4.1", + "@eslint/js": "9.23.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1266,13 +1240,13 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1296,9 +1270,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1312,19 +1286,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -1408,29 +1369,16 @@ } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "eslint-visitor-keys": "^4.2.0" }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1579,24 +1527,6 @@ "node": ">= 4.9.1" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1747,9 +1677,9 @@ } }, "node_modules/highcharts": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-4.2.7.tgz", - "integrity": "sha512-uZiKTHQxukKpSyzIQwzmusxyyp2TB7bRLQMgaZrNJILWsESDSRb3Xc5py2myJsBYIxSYdWwRth0oV8BSe9uPvA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-4.1.10.tgz", + "integrity": "sha512-9H6bihy5JA+iyrUxxw1XJmZwIRJnvZ2AlbxdPGq1pnHkCVLpF83pNmausHNTaK5EezKcs6Luy6PbqRHfHFQePg==", "license": "https://www.highcharts.com/license" }, "node_modules/highlight.js": { @@ -2084,21 +2014,6 @@ "node": ">= 18" } }, - "node_modules/mathjax": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-4.0.0-beta.7.tgz", - "integrity": "sha512-Tk07n0f4fedb/YNXJ5bgHUIW+3ONyLkfUdoZ3wsUeW7yOt6SeAkGAjegICbHuyV3k7bcddWLzDetTyb5LLEwaA==", - "dependencies": { - "@xmldom/xmldom": "^0.8.10", - "mathjax-modern-font": "^4.0.0-beta.7", - "wicked-good-xpath": "^1.3.0" - } - }, - "node_modules/mathjax-modern-font": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/mathjax-modern-font/-/mathjax-modern-font-4.0.0-beta.7.tgz", - "integrity": "sha512-mGrlxuFPRoHpGYyRaXJpfyM9k77XCpH11lIVLO7+VNTFJXIeqnMXv+AGFiYL/GftQpdhPlCg4zqd/JXlse9S0A==" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -2170,16 +2085,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2284,19 +2189,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -2706,23 +2598,6 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "license": "MIT" }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2954,11 +2829,6 @@ "node": ">= 8" } }, - "node_modules/wicked-good-xpath": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", - "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index 2d442ac2c..63ebbadbf 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "devDependencies": { "@eslint/js": "^9.23.0", "concurrently": "^9.1.2", - "copy-webpack-plugin": "^13.0.1", "eslint": "^9.23.0", "expose-loader": "^5.0.1", "globals": "^16.0.0", @@ -30,7 +29,6 @@ "jquery": "^1.12.4", "js-cookie": "^3.0.5", "marked": "^15.0.7", - "mathjax": "^4.0.0-beta.7", "select2": "^4.1.0-rc.0" } } diff --git a/webpack.config.js b/webpack.config.js index 4e754c051..cb3700d2b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ const path = require('path'); -const CopyPlugin = require('copy-webpack-plugin'); module.exports = { entry: { @@ -15,17 +14,6 @@ module.exports = { path: path.resolve(__dirname, 'dist_webpack'), asyncChunks: false, }, - plugins: [ - new CopyPlugin({ - patterns: [ - { - from: 'node_modules/mathjax', - to: 'mathjax', - info: { minimized: true }, - }, - ], - }), - ], externals: { jquery: 'jQuery', }, From 89fca42212932e015adc6ee127e95154b5307f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Wed, 26 Nov 2025 17:51:28 +0100 Subject: [PATCH 2/8] Add contest creation to mass_create_tool command This commit allows for creating and populating (with users & problems) contests, using the mass_create_tool management command. Two new arguments are added: --createcontest (-cc) to enable contest creation, and --contestname (-cn) to specify the contest's name. --- .../management/commands/mass_create_tool.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index 305e731fa..beb536cf9 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -6,6 +6,8 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError +from oioioi.contests.models import Contest, ProblemInstance +from oioioi.participants.models import Participant from oioioi.problems.models import ( AlgorithmTag, AlgorithmTagProposal, @@ -50,6 +52,9 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--wipe", "-w", action="store_true", help="Remove all previously generated mock data before creating new data") + # FIXME: not implemented yet + parser.add_argument("--createcontest", "-cc", action="store_true", help="Adds created problems and users to a new contest") + parser.add_argument("--contestname", "-cn", type=str, help="Name of the contest to create (default: random ID)") parser.add_argument("--problems", "-p", type=unsigned_int, default=0, metavar="N", help="Number of problems to create (default: 0)") parser.add_argument("--users", "-u", type=unsigned_int, default=0, metavar="N", help="Number of users to create (default: 0)") parser.add_argument("--algotags", "-at", type=unsigned_int, default=0, metavar="N", help="Number of algorithm tags to create (default: 0)") @@ -149,6 +154,38 @@ def create_problems(self, count, verbosity): ) return created_problems + def create_and_populate_contest(self, problems, users, verbose_name, verbosity, contest_name=None): + """ + Creates a Contest and adds all provided problems and users to it. + Returns the created Contest object. + """ + if contest_name is None: + candidate_prefix, random_length = "contest_", 10 + else: + candidate_prefix, random_length = contest_name, 0 + + contests = self.create_unique_objects( + count=1, + candidate_prefix=candidate_prefix, + random_length=random_length, + uniqueness_fn=lambda s: not Contest.objects.filter(id=s).exists(), + create_instance_fn=lambda candidate: Contest(id=candidate, name=contest_name), + verbose_name="Contest", + verbosity=verbosity, + ) + contest = contests[0] + + for problem in problems: + ProblemInstance.objects.create(contest=contest, problem=problem, short_name=problem.short_name) + + for user in users: + Participant.objects.create(contest=contest, user=user) + + if verbosity >= 2: + self.stdout.write(f"Created {verbose_name}: {contest.name} (ID: {contest.id}) with {len(problems)} problems and {len(users)} users") + + return contest + def create_through_records(self, count, problems, tags, through_model, verbose_name, verbosity): """ Creates exactly `count` distinct through-records connecting problems and tags. @@ -231,6 +268,11 @@ def remove_all_generated_data(self): user_qs.delete() self.stdout.write(self.style.SUCCESS(f"Deleted {user_count} Users")) + contest_qs = Contest.objects.filter(name__startswith=self.auto_prefix) + contest_count = contest_qs.count() + contest_qs.delete() + self.stdout.write(self.style.SUCCESS(f"Deleted {contest_count} Contests")) + algo_tag_qs = AlgorithmTag.objects.filter(name__startswith=self.auto_prefix) algo_tag_count = algo_tag_qs.count() algo_tag_qs.delete() @@ -251,6 +293,8 @@ def handle(self, *args, **options): self.auto_prefix = "_auto_" wipe = options["wipe"] + create_contest = options["createcontest"] + contest_name = options["contestname"] num_problems = options["problems"] num_users = options["users"] num_algotags = options["algotags"] @@ -263,7 +307,7 @@ def handle(self, *args, **options): verbosity = int(options.get("verbosity", 1)) total_objects_to_create = ( - num_problems + num_users + num_algotags + num_difftags + num_algothrough + num_diffthrough + num_algoproposals + num_diffproposals + int(bool(create_contest is not None)) + num_problems + num_users + num_algotags + num_difftags + num_algothrough + num_diffthrough + num_algoproposals + num_diffproposals ) max_algothrough = num_problems * num_algotags max_diffthrough = num_problems @@ -321,6 +365,15 @@ def handle(self, *args, **options): verbosity=verbosity, ) + if create_contest: + created_contest = self.create_and_populate_contest( + contest_name=contest_name, + problems=created_problems, + users=created_users, + verbose_name="Contest", + verbosity=verbosity, + ) + created_algotags = self.create_unique_objects( count=num_algotags, candidate_prefix="algo_", From 890d3cb54d391f52c757f26e9a0c14c719d34495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Wed, 3 Dec 2025 12:45:19 +0100 Subject: [PATCH 3/8] Add problem package upload option to mass_create_tool Previously, the mass_create_tool only created empty problems. This commit changes that by adding a list of problem_packages (-pp) as an option to the tool. The tool searches the oioioi/sinolpack/files/ directory for the specified problem packages and uploads them to the created contest. Example command enabled by this commit: python3 manage.py mass_create_tool -cn=quite_useful_contest -cc -pp=test_full_package.tgz -u=10 --wipe ^ (This will create a contest named 'quite_useful_contest' with 10 users, and upload 'test_full_package.tgz' to a new round in that contest.) --- oioioi/evalmgr/admin.py | 1 + .../management/commands/mass_create_tool.py | 117 +++++++++++++++--- 2 files changed, 104 insertions(+), 14 deletions(-) diff --git a/oioioi/evalmgr/admin.py b/oioioi/evalmgr/admin.py index 4b01a6161..b987a3541 100644 --- a/oioioi/evalmgr/admin.py +++ b/oioioi/evalmgr/admin.py @@ -111,6 +111,7 @@ def _get_contest_id(self, instance): def has_add_permission(self, request): return False + @_require_submission def submit_id(self, instance): res = instance.submission.id return self._get_link( diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index beb536cf9..e6b178d5b 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -1,12 +1,17 @@ import random import string import sys +import os +from io import StringIO +from django.utils.module_loading import import_string from django.conf import settings from django.contrib.auth import get_user_model +from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError +from django.db import transaction -from oioioi.contests.models import Contest, ProblemInstance +from oioioi.contests.models import Contest, ProblemInstance, RegistrationAvailabilityConfig from oioioi.participants.models import Participant from oioioi.problems.models import ( AlgorithmTag, @@ -19,6 +24,7 @@ ProblemName, ProblemSite, ) +from oioioi.problems.utils import get_new_problem_instance User = get_user_model() @@ -52,10 +58,15 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("--wipe", "-w", action="store_true", help="Remove all previously generated mock data before creating new data") - # FIXME: not implemented yet parser.add_argument("--createcontest", "-cc", action="store_true", help="Adds created problems and users to a new contest") parser.add_argument("--contestname", "-cn", type=str, help="Name of the contest to create (default: random ID)") parser.add_argument("--problems", "-p", type=unsigned_int, default=0, metavar="N", help="Number of problems to create (default: 0)") + parser.add_argument( + '--problempackages', '-pp', + nargs='+', + type=str, + help='List of problem package files to upload to contest' + ) parser.add_argument("--users", "-u", type=unsigned_int, default=0, metavar="N", help="Number of users to create (default: 0)") parser.add_argument("--algotags", "-at", type=unsigned_int, default=0, metavar="N", help="Number of algorithm tags to create (default: 0)") parser.add_argument("--difftags", "-dt", type=unsigned_int, default=0, metavar="N", help="Number of difficulty tags to create (default: 0)") @@ -137,7 +148,10 @@ def create_problems(self, count, verbosity): candidate_prefix="prob_", random_length=10, uniqueness_fn=lambda s: not Problem.objects.filter(short_name=s).exists(), - create_instance_fn=lambda candidate: Problem.create(short_name=candidate), + create_instance_fn=lambda candidate: Problem.create( + short_name=candidate, + controller_name='oioioi.problems.controllers.ProblemController' + ), verbose_name="Problem", verbosity=verbosity, ) @@ -153,30 +167,98 @@ def create_problems(self, count, verbosity): url_key=f"{problem.short_name}_site", ) return created_problems + + def create_problems_from_package(self, package_file_names, verbosity): + """ + Creates problems from package files. + Ensures that the controller_name is properly set after unpacking. + """ + if not package_file_names: + return [] + + TEST_FILES_DIR = os.path.join(settings.BASE_DIR, 'oioioi', 'sinolpack', 'files') + created_problems = [] + + for package_file_name in package_file_names: + package_path = os.path.join(TEST_FILES_DIR, package_file_name) + + out = StringIO() + try: + call_command('addproblem', package_path, stdout=out) + problem_id = int(out.getvalue().strip()) + if verbosity >= 2: + self.stdout.write(f"Unpacked package {package_file_name} to create problem with ID {problem_id}") + problem = Problem.objects.get(id=problem_id) + + if not problem.controller_name: + problem.controller_name = 'oioioi.problems.controllers.ProblemController' + problem.save() + if verbosity >= 2: + self.stdout.write(self.style.WARNING(f"Warning: Set default controller_name for problem {problem.short_name}")) + + created_problems.append(problem) + + if verbosity >= 2: + self.stdout.write(f"Created problem: {problem.short_name} (ID: {problem.id}, controller: {problem.controller_name}) from package {package_file_name}") + except Exception as e: + self.stderr.write(self.style.ERROR(f"Failed to unpack package {package_file_name} located at {package_path}. Error: {e}")) + + return created_problems + + def add_problems_to_new_round(self, problems, contest, verbosity): + """ + Adds all provided problems to a new round in the given contest. + Returns the created Round object. + """ + with transaction.atomic(): + round_number = contest.round_set.count() + 1 + new_round = contest.round_set.create(name=f"Round {round_number}") + + for problem in problems: + pi = get_new_problem_instance(problem, contest) + pi.short_name = problem.short_name + pi.round = new_round + pi.save() + + if verbosity >= 2: + self.stdout.write(f"Added {len(problems)} problems to new round '{new_round.name}' in contest '{contest.name}' (ID: {contest.id})") + + return new_round def create_and_populate_contest(self, problems, users, verbose_name, verbosity, contest_name=None): """ - Creates a Contest and adds all provided problems and users to it. + Creates an OI like Contest with a given (or generated) name and an auto-preixed ID. + Then adds all provided problems and users to it. + Returns the created Contest object. """ if contest_name is None: candidate_prefix, random_length = "contest_", 10 else: candidate_prefix, random_length = contest_name, 0 - - contests = self.create_unique_objects( + + created_contests = self.create_unique_objects( count=1, candidate_prefix=candidate_prefix, random_length=random_length, uniqueness_fn=lambda s: not Contest.objects.filter(id=s).exists(), - create_instance_fn=lambda candidate: Contest(id=candidate, name=contest_name), - verbose_name="Contest", + create_instance_fn=lambda candidate: Contest( + id=candidate, + name=contest_name or candidate, + controller_name='oioioi.programs.controllers.ProgrammingContestController' + ), + verbose_name=verbose_name, verbosity=verbosity, ) - contest = contests[0] - - for problem in problems: - ProblemInstance.objects.create(contest=contest, problem=problem, short_name=problem.short_name) + + contest = created_contests[0] + + RegistrationAvailabilityConfig.objects.create( + contest=contest, + enabled='YES' + ) + + self.add_problems_to_new_round(problems, contest, verbosity) for user in users: Participant.objects.create(contest=contest, user=user) @@ -268,7 +350,8 @@ def remove_all_generated_data(self): user_qs.delete() self.stdout.write(self.style.SUCCESS(f"Deleted {user_count} Users")) - contest_qs = Contest.objects.filter(name__startswith=self.auto_prefix) + contest_qs = Contest.objects.filter(id__startswith=self.auto_prefix) + self.stdout.write(self.style.WARNING(f"Deleting the following contests: {contest_qs.values_list('id', flat=True)}")) contest_count = contest_qs.count() contest_qs.delete() self.stdout.write(self.style.SUCCESS(f"Deleted {contest_count} Contests")) @@ -296,6 +379,7 @@ def handle(self, *args, **options): create_contest = options["createcontest"] contest_name = options["contestname"] num_problems = options["problems"] + package_file_names = options["problempackages"] num_users = options["users"] num_algotags = options["algotags"] num_difftags = options["difftags"] @@ -355,6 +439,11 @@ def handle(self, *args, **options): verbosity=verbosity, ) + created_problems += self.create_problems_from_package( + package_file_names=package_file_names, + verbosity=verbosity, + ) + created_users = self.create_unique_objects( count=num_users, candidate_prefix="user_", @@ -464,4 +553,4 @@ def handle(self, *args, **options): self.write_summary(len(created_algothrough), options["algothrough"], "Algorithm Tag Through Records") self.write_summary(len(created_diffthrough), options["diffthrough"], "Difficulty Tag Through Records") self.write_summary(len(created_algoproposals), options["algoproposals"], "Algorithm Tag Proposals") - self.write_summary(len(created_diffproposals), options["diffproposals"], "Difficulty Tag Proposals") + self.write_summary(len(created_diffproposals), options["diffproposals"], "Difficulty Tag Proposals") \ No newline at end of file From bc27f287da8a233afc170405157765faad7a7e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Tue, 9 Dec 2025 16:29:40 +0100 Subject: [PATCH 4/8] Add code submissions to mass_create_tool. Previously the mass_create_tool could only be used for creating uers, problems and contests. This commit makes it possible to automatically generate code submissions on behalf of users. --- .../management/commands/mass_create_tool.py | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index e6b178d5b..bb77e73f1 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -3,15 +3,19 @@ import sys import os from io import StringIO +from types import SimpleNamespace + from django.utils.module_loading import import_string +from django.core.files.base import ContentFile +from django.utils import timezone from django.conf import settings from django.contrib.auth import get_user_model from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError from django.db import transaction -from oioioi.contests.models import Contest, ProblemInstance, RegistrationAvailabilityConfig +from oioioi.contests.models import Contest, ProblemInstance, RegistrationAvailabilityConfig, Submission from oioioi.participants.models import Participant from oioioi.problems.models import ( AlgorithmTag, @@ -68,6 +72,8 @@ def add_arguments(self, parser): help='List of problem package files to upload to contest' ) parser.add_argument("--users", "-u", type=unsigned_int, default=0, metavar="N", help="Number of users to create (default: 0)") + parser.add_argument("--submission_files", "-sf", nargs='+', type=str, help="List of source code files to use for submissions") + parser.add_argument("--submissions_per_user", "-spu", type=unsigned_int, default=0, metavar="N", help="Number of submissions per user to create (default: 0)") parser.add_argument("--algotags", "-at", type=unsigned_int, default=0, metavar="N", help="Number of algorithm tags to create (default: 0)") parser.add_argument("--difftags", "-dt", type=unsigned_int, default=0, metavar="N", help="Number of difficulty tags to create (default: 0)") parser.add_argument( @@ -224,11 +230,60 @@ def add_problems_to_new_round(self, problems, contest, verbosity): self.stdout.write(f"Added {len(problems)} problems to new round '{new_round.name}' in contest '{contest.name}' (ID: {contest.id})") return new_round + + def fetch_submission_files(self, submission_file_names): + """ + Fetches submission files from the specified file names. + Returns a list of tuples (file_name, source_code). + """ + if not submission_file_names: + return [] + TEST_FILES_DIR = os.path.join(settings.BASE_DIR, 'oioioi', 'sinolpack', 'files') + submission_files = [] + for file_name in submission_file_names: + file_path = os.path.join(TEST_FILES_DIR, file_name) + try: + with open(file_path, 'r') as f: + source_code = f.read() + submission_files.append((file_name, source_code)) + if verbosity >= 2: + self.stdout.write(f"Loaded submission file: {file_name}") + except Exception as e: + self.stderr.write(self.style.ERROR(f"Failed to read submission file {file_name} located at {file_path}. Error: {e}")) + return submission_files + + def submit_by_user(self, user, problem_instance, source_code, source_code_name, verbosity=0): + """ + Submits source code to a problem instance on behalf of a user. + """ + # Mock request object with necessary attributes + request = SimpleNamespace() + request.user = user + request.timestamp = timezone.now() + request.contest = problem_instance.contest + request.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'en'} + request._cache = {} + + if verbosity >= 3: + self.stdout.write(self.style.SUCCESS(f"User {user.username} is submitting to problem {problem_instance.problem.short_name} in contest {problem_instance.contest.id}")) + with transaction.atomic(): + submission = problem_instance.controller.create_submission( + request=request, + problem_instance=problem_instance, + form_data={ + 'user': user, + 'file': ContentFile(source_code, source_code_name), + 'kind': 'NORMAL' + }, + judge_after_create=False + ) + submission.refresh_from_db() + problem_instance.controller.judge(submission) - def create_and_populate_contest(self, problems, users, verbose_name, verbosity, contest_name=None): + def create_and_populate_contest(self, problems, users, verbose_name, verbosity, contest_name=None, submission_file_names=None, num_submissions_per_user=0): """ - Creates an OI like Contest with a given (or generated) name and an auto-preixed ID. - Then adds all provided problems and users to it. + Creates a Contest with a given (or generated) name and an auto-preixed ID. + Then adds all provided problems, users and submissions to it. Returns the created Contest object. """ @@ -259,10 +314,20 @@ def create_and_populate_contest(self, problems, users, verbose_name, verbosity, ) self.add_problems_to_new_round(problems, contest, verbosity) + submission_files = self.fetch_submission_files(submission_file_names) for user in users: Participant.objects.create(contest=contest, user=user) + for submission_no in range(num_submissions_per_user): + file_name, source_code = random.choice(submission_files) + for problem in problems: + try: + pi = ProblemInstance.objects.get(problem=problem, contest=contest) + self.submit_by_user(user, pi, source_code, file_name, verbosity=verbosity) + except ProblemInstance.DoesNotExist: + self.stderr.write(self.style.ERROR(f"ProblemInstance does not exist for problem {problem.short_name} in contest {contest.id}. Skipping submission.")) + if verbosity >= 2: self.stdout.write(f"Created {verbose_name}: {contest.name} (ID: {contest.id}) with {len(problems)} problems and {len(users)} users") @@ -340,6 +405,13 @@ def remove_all_generated_data(self): Removes all mass-generated mock data created using this tool. """ + submission_qs = Submission.objects.filter(user__username__startswith=self.auto_prefix) + submission_count = submission_qs.count() + if submission_count: + self.stdout.write(self.style.WARNING(f"Deleting {submission_count} Submissions for generated users")) + submission_qs.delete() + self.stdout.write(self.style.SUCCESS(f"Deleted {submission_count} Submissions")) + prob_qs = Problem.objects.filter(short_name__startswith=self.auto_prefix) prob_count = prob_qs.count() prob_qs.delete() @@ -381,6 +453,8 @@ def handle(self, *args, **options): num_problems = options["problems"] package_file_names = options["problempackages"] num_users = options["users"] + submission_file_names = options["submission_files"] + num_submissions_per_user = options["submissions_per_user"] num_algotags = options["algotags"] num_difftags = options["difftags"] num_algothrough = options["algothrough"] @@ -459,6 +533,8 @@ def handle(self, *args, **options): contest_name=contest_name, problems=created_problems, users=created_users, + submission_file_names=submission_file_names, + num_submissions_per_user=num_submissions_per_user, verbose_name="Contest", verbosity=verbosity, ) From 2ebee0cbdff0db660e0917e77287f9f5eb6e99d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Wed, 10 Dec 2025 17:26:43 +0100 Subject: [PATCH 5/8] Clean up code & test --- .../management/commands/mass_create_tool.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index bb77e73f1..bffaba17a 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -58,6 +58,7 @@ class Command(BaseCommand): "Allows the creation of mock data for testing purposes. " "Creates Problems, Users, Algorithm Tags, Difficulty Tags, Algorithm Tag Proposals, and " "Algorithm Tag Through and Difficulty Tag Through records to assign tags to problems. Use with caution in production environments. " + "Additionally, can create a Contest and add created Problems and Users to it, along with Submissions. " ) def add_arguments(self, parser): @@ -178,6 +179,9 @@ def create_problems_from_package(self, package_file_names, verbosity): """ Creates problems from package files. Ensures that the controller_name is properly set after unpacking. + Returns a list of created Problem objects. + - package_file_names: List of package file names to unpack. + - verbosity: The current verbosity level. """ if not package_file_names: return [] @@ -215,6 +219,9 @@ def add_problems_to_new_round(self, problems, contest, verbosity): """ Adds all provided problems to a new round in the given contest. Returns the created Round object. + - problems: List of Problem objects to add. + - contest: Contest object to which the round will be added. + - verbosity: The current verbosity level. """ with transaction.atomic(): round_number = contest.round_set.count() + 1 @@ -231,10 +238,12 @@ def add_problems_to_new_round(self, problems, contest, verbosity): return new_round - def fetch_submission_files(self, submission_file_names): + def fetch_submission_files(self, submission_file_names, verbosity): """ Fetches submission files from the specified file names. Returns a list of tuples (file_name, source_code). + - submission_file_names: List of submission file names to read. + - verbosity: The current verbosity level. """ if not submission_file_names: return [] @@ -252,9 +261,14 @@ def fetch_submission_files(self, submission_file_names): self.stderr.write(self.style.ERROR(f"Failed to read submission file {file_name} located at {file_path}. Error: {e}")) return submission_files - def submit_by_user(self, user, problem_instance, source_code, source_code_name, verbosity=0): + def submit_by_user(self, user, problem_instance, source_code, source_code_name, verbosity): """ Submits source code to a problem instance on behalf of a user. + - user: User object submitting the code. + - problem_instance: ProblemInstance object to which the code is submitted. + - source_code: The source code string to submit. + - source_code_name: The filename to use for the submitted source code. + - verbosity: The current verbosity level. """ # Mock request object with necessary attributes request = SimpleNamespace() @@ -284,6 +298,14 @@ def create_and_populate_contest(self, problems, users, verbose_name, verbosity, """ Creates a Contest with a given (or generated) name and an auto-preixed ID. Then adds all provided problems, users and submissions to it. + - problems: List of Problem objects to add. + - users: List of User objects to add. + - verbose_name: A description used in output. + - verbosity: The current verbosity level. + - contest_name: Optional name for the contest. If None, a random ID is used. + - submission_file_names: List of submission file names to use for submissions + (could be None if num_submissions_per_user = 0). + - num_submissions_per_user: Number of submissions to create per user. Returns the created Contest object. """ From f937a71292086939cf12de3dc34fe8ad4e194bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Wed, 17 Dec 2025 15:03:43 +0100 Subject: [PATCH 6/8] Apply fixes from ruff --- .../management/commands/mass_create_tool.py | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index bffaba17a..cbae6ee3c 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -1,21 +1,24 @@ +import os import random import string import sys -import os from io import StringIO from types import SimpleNamespace -from django.utils.module_loading import import_string - -from django.core.files.base import ContentFile -from django.utils import timezone from django.conf import settings from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError from django.db import transaction +from django.utils import timezone -from oioioi.contests.models import Contest, ProblemInstance, RegistrationAvailabilityConfig, Submission +from oioioi.contests.models import ( + Contest, + ProblemInstance, + RegistrationAvailabilityConfig, + Submission, +) from oioioi.participants.models import Participant from oioioi.problems.models import ( AlgorithmTag, @@ -72,9 +75,18 @@ def add_arguments(self, parser): type=str, help='List of problem package files to upload to contest' ) - parser.add_argument("--users", "-u", type=unsigned_int, default=0, metavar="N", help="Number of users to create (default: 0)") - parser.add_argument("--submission_files", "-sf", nargs='+', type=str, help="List of source code files to use for submissions") - parser.add_argument("--submissions_per_user", "-spu", type=unsigned_int, default=0, metavar="N", help="Number of submissions per user to create (default: 0)") + parser.add_argument( + "--users", "-u", type=unsigned_int, default=0, metavar="N", + help="Number of users to create (default: 0)" + ) + parser.add_argument( + "--submission_files", "-sf", nargs='+', type=str, + help="List of source code files to use for submissions" + ) + parser.add_argument( + "--submissions_per_user", "-spu", type=unsigned_int, default=0, metavar="N", + help="Number of submissions per user to create (default: 0)" + ) parser.add_argument("--algotags", "-at", type=unsigned_int, default=0, metavar="N", help="Number of algorithm tags to create (default: 0)") parser.add_argument("--difftags", "-dt", type=unsigned_int, default=0, metavar="N", help="Number of difficulty tags to create (default: 0)") parser.add_argument( @@ -174,7 +186,7 @@ def create_problems(self, count, verbosity): url_key=f"{problem.short_name}_site", ) return created_problems - + def create_problems_from_package(self, package_file_names, verbosity): """ Creates problems from package files. @@ -185,7 +197,7 @@ def create_problems_from_package(self, package_file_names, verbosity): """ if not package_file_names: return [] - + TEST_FILES_DIR = os.path.join(settings.BASE_DIR, 'oioioi', 'sinolpack', 'files') created_problems = [] @@ -205,16 +217,16 @@ def create_problems_from_package(self, package_file_names, verbosity): problem.save() if verbosity >= 2: self.stdout.write(self.style.WARNING(f"Warning: Set default controller_name for problem {problem.short_name}")) - + created_problems.append(problem) if verbosity >= 2: - self.stdout.write(f"Created problem: {problem.short_name} (ID: {problem.id}, controller: {problem.controller_name}) from package {package_file_name}") + self.stdout.write(f"Created problem: {problem.short_name}, {problem.id}, {problem.controller_name}) from package {package_file_name}") except Exception as e: self.stderr.write(self.style.ERROR(f"Failed to unpack package {package_file_name} located at {package_path}. Error: {e}")) - + return created_problems - + def add_problems_to_new_round(self, problems, contest, verbosity): """ Adds all provided problems to a new round in the given contest. @@ -237,7 +249,7 @@ def add_problems_to_new_round(self, problems, contest, verbosity): self.stdout.write(f"Added {len(problems)} problems to new round '{new_round.name}' in contest '{contest.name}' (ID: {contest.id})") return new_round - + def fetch_submission_files(self, submission_file_names, verbosity): """ Fetches submission files from the specified file names. @@ -252,7 +264,7 @@ def fetch_submission_files(self, submission_file_names, verbosity): for file_name in submission_file_names: file_path = os.path.join(TEST_FILES_DIR, file_name) try: - with open(file_path, 'r') as f: + with open(file_path) as f: source_code = f.read() submission_files.append((file_name, source_code)) if verbosity >= 2: @@ -260,7 +272,7 @@ def fetch_submission_files(self, submission_file_names, verbosity): except Exception as e: self.stderr.write(self.style.ERROR(f"Failed to read submission file {file_name} located at {file_path}. Error: {e}")) return submission_files - + def submit_by_user(self, user, problem_instance, source_code, source_code_name, verbosity): """ Submits source code to a problem instance on behalf of a user. @@ -279,7 +291,11 @@ def submit_by_user(self, user, problem_instance, source_code, source_code_name, request._cache = {} if verbosity >= 3: - self.stdout.write(self.style.SUCCESS(f"User {user.username} is submitting to problem {problem_instance.problem.short_name} in contest {problem_instance.contest.id}")) + msg = ( + f"User {user.username} is submitting to problem " + f"{problem_instance.problem.short_name} in contest {problem_instance.contest.id}" + ) + self.stdout.write(self.style.SUCCESS(msg)) with transaction.atomic(): submission = problem_instance.controller.create_submission( request=request, @@ -306,7 +322,7 @@ def create_and_populate_contest(self, problems, users, verbose_name, verbosity, - submission_file_names: List of submission file names to use for submissions (could be None if num_submissions_per_user = 0). - num_submissions_per_user: Number of submissions to create per user. - + Returns the created Contest object. """ if contest_name is None: @@ -327,28 +343,32 @@ def create_and_populate_contest(self, problems, users, verbose_name, verbosity, verbose_name=verbose_name, verbosity=verbosity, ) - + contest = created_contests[0] - + RegistrationAvailabilityConfig.objects.create( contest=contest, enabled='YES' ) - + self.add_problems_to_new_round(problems, contest, verbosity) submission_files = self.fetch_submission_files(submission_file_names) for user in users: Participant.objects.create(contest=contest, user=user) - for submission_no in range(num_submissions_per_user): + for _submission_no in range(num_submissions_per_user): file_name, source_code = random.choice(submission_files) for problem in problems: try: pi = ProblemInstance.objects.get(problem=problem, contest=contest) self.submit_by_user(user, pi, source_code, file_name, verbosity=verbosity) except ProblemInstance.DoesNotExist: - self.stderr.write(self.style.ERROR(f"ProblemInstance does not exist for problem {problem.short_name} in contest {contest.id}. Skipping submission.")) + err_msg = ( + f"ProblemInstance does not exist for problem {problem.short_name} " + f"in contest {contest.id}. Skipping submission." + ) + self.stderr.write(self.style.ERROR(err_msg)) if verbosity >= 2: self.stdout.write(f"Created {verbose_name}: {contest.name} (ID: {contest.id}) with {len(problems)} problems and {len(users)} users") @@ -487,7 +507,8 @@ def handle(self, *args, **options): verbosity = int(options.get("verbosity", 1)) total_objects_to_create = ( - int(bool(create_contest is not None)) + num_problems + num_users + num_algotags + num_difftags + num_algothrough + num_diffthrough + num_algoproposals + num_diffproposals + int(bool(create_contest is not None)) + num_problems + num_users + num_algotags + + num_difftags + num_algothrough + num_diffthrough + num_algoproposals + num_diffproposals ) max_algothrough = num_problems * num_algotags max_diffthrough = num_problems @@ -560,6 +581,10 @@ def handle(self, *args, **options): verbose_name="Contest", verbosity=verbosity, ) + if (verbosity >= 1) and created_contest: + self.stdout.write( + self.style.SUCCESS(f"Created Contest: {created_contest.name} (ID: {created_contest.id})") + ) created_algotags = self.create_unique_objects( count=num_algotags, @@ -651,4 +676,4 @@ def handle(self, *args, **options): self.write_summary(len(created_algothrough), options["algothrough"], "Algorithm Tag Through Records") self.write_summary(len(created_diffthrough), options["diffthrough"], "Difficulty Tag Through Records") self.write_summary(len(created_algoproposals), options["algoproposals"], "Algorithm Tag Proposals") - self.write_summary(len(created_diffproposals), options["diffproposals"], "Difficulty Tag Proposals") \ No newline at end of file + self.write_summary(len(created_diffproposals), options["diffproposals"], "Difficulty Tag Proposals") From 72ea15f9dd9410ab3015fabd6867c78199f5c8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kwieci=C5=84ski?= Date: Tue, 6 Jan 2026 18:07:04 +0100 Subject: [PATCH 7/8] Add code coverage for tests This commit also removes the unnecessary line change in admin.py - it was experimental. --- oioioi/evalmgr/admin.py | 1 - .../management/commands/mass_create_tool.py | 2 +- oioioi/problems/tests/test_commands.py | 66 +++++++++++++++++-- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/oioioi/evalmgr/admin.py b/oioioi/evalmgr/admin.py index b987a3541..4b01a6161 100644 --- a/oioioi/evalmgr/admin.py +++ b/oioioi/evalmgr/admin.py @@ -111,7 +111,6 @@ def _get_contest_id(self, instance): def has_add_permission(self, request): return False - @_require_submission def submit_id(self, instance): res = instance.submission.id return self._get_link( diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index cbae6ee3c..0387dede8 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -352,7 +352,7 @@ def create_and_populate_contest(self, problems, users, verbose_name, verbosity, ) self.add_problems_to_new_round(problems, contest, verbosity) - submission_files = self.fetch_submission_files(submission_file_names) + submission_files = self.fetch_submission_files(submission_file_names, verbosity=verbosity) for user in users: Participant.objects.create(contest=contest, user=user) diff --git a/oioioi/problems/tests/test_commands.py b/oioioi/problems/tests/test_commands.py index f2729a5f7..d14b5fa78 100644 --- a/oioioi/problems/tests/test_commands.py +++ b/oioioi/problems/tests/test_commands.py @@ -5,6 +5,7 @@ from django.core.management import CommandError, call_command from django.test import TestCase, override_settings +from oioioi.contests.models import Contest, Submission from oioioi.problems.models import ( AlgorithmTag, AlgorithmTagProposal, @@ -30,19 +31,28 @@ class TestMassCreateTool(TestCase): "diffthrough": DifficultyTagThrough, "algoproposals": AlgorithmTagProposal, "diffproposals": DifficultyTagProposal, + "contests": Contest, + "submissions": Submission, } - def _assert_model_counts(self, expected_counts): + def _assert_model_counts(self, expected_counts, check_problem_names=True): + auto_prefix = "_auto_" for name, model in self.name_to_model.items(): - count = model.objects.count() + # For submissions, only count those from auto-generated users + # (the packages create submissions with model solutions). + if name == "submissions": + count = model.objects.filter(user__username__startswith=auto_prefix).count() + else: + count = model.objects.count() expected_count = expected_counts.get(name, 0) assert count == expected_count, f"Expected {expected_count} {name}, got {count}" - # Validation for i18n problem names - problem_amount: int = expected_counts.get("problems", Problem.objects.count()) - expected_probname_count = len(settings.LANGUAGES) * problem_amount - probname_count = ProblemName.objects.count() - assert probname_count == expected_probname_count, f"Expected {expected_probname_count} probnames, got {probname_count}" + # Validation for i18n problem names, can be ommited (e.g. for problems for packages). + if check_problem_names: + problem_amount: int = expected_counts.get("problems", Problem.objects.count()) + expected_probname_count = len(settings.LANGUAGES) * problem_amount + probname_count = ProblemName.objects.count() + assert probname_count == expected_probname_count, f"Expected {expected_probname_count} probnames, got {probname_count}" def test_long_flags(self): out = StringIO() @@ -115,6 +125,48 @@ def test_long_flags(self): call_command("mass_create_tool", "--wipe") self._assert_model_counts({}) + def test_long_flags_contest(self): + out = StringIO() + call_command( + "mass_create_tool", + "--contestname", + "demo", + "--createcontest", + "--problempackages", + "test_full_package.tgz", + "--users", + "10", + "--submission_files", + "sum-correct.cpp", + "sum-various-results.cpp", + "--submissions_per_user", + "3", + "--wipe", + stdout=out, + ) + self._assert_model_counts({"users": 10, "problems": 1, "contests": 1, "submissions": 30}, check_problem_names=False) + + def test_short_flags_contest(self): + out = StringIO() + call_command( + "mass_create_tool", + "-cn", + "demo", + "-cc", + "-pp", + "test_full_package.tgz", + "-u", + "10", + "-sf", + "sum-correct.cpp", + "sum-various-results.cpp", + "-spu", + "3", + "-w", + stdout=out, + ) + self._assert_model_counts({"users": 10, "problems": 1, "contests": 1, "submissions": 30}, check_problem_names=False) + def test_short_flags(self): out = StringIO() call_command( From 72f07d6f272dea467a3b41645442c385b05293bc Mon Sep 17 00:00:00 2001 From: Jankwi <153385549+Jankwi@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:20:52 +0100 Subject: [PATCH 8/8] Apply suggestions from copilot code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../management/commands/mass_create_tool.py | 17 +++++++++++++---- oioioi/problems/tests/test_commands.py | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/oioioi/problems/management/commands/mass_create_tool.py b/oioioi/problems/management/commands/mass_create_tool.py index 0387dede8..575795aa8 100644 --- a/oioioi/problems/management/commands/mass_create_tool.py +++ b/oioioi/problems/management/commands/mass_create_tool.py @@ -73,7 +73,7 @@ def add_arguments(self, parser): '--problempackages', '-pp', nargs='+', type=str, - help='List of problem package files to upload to contest' + help='List of problem package files to create problems from (optionally added to a new contest when --createcontest is used)' ) parser.add_argument( "--users", "-u", type=unsigned_int, default=0, metavar="N", @@ -221,7 +221,7 @@ def create_problems_from_package(self, package_file_names, verbosity): created_problems.append(problem) if verbosity >= 2: - self.stdout.write(f"Created problem: {problem.short_name}, {problem.id}, {problem.controller_name}) from package {package_file_name}") + self.stdout.write(f"Created problem: {problem.short_name}, {problem.id}, {problem.controller_name} from package {package_file_name}") except Exception as e: self.stderr.write(self.style.ERROR(f"Failed to unpack package {package_file_name} located at {package_path}. Error: {e}")) @@ -271,6 +271,10 @@ def fetch_submission_files(self, submission_file_names, verbosity): self.stdout.write(f"Loaded submission file: {file_name}") except Exception as e: self.stderr.write(self.style.ERROR(f"Failed to read submission file {file_name} located at {file_path}. Error: {e}")) + if submission_file_names and not submission_files: + raise CommandError( + "No submission files could be loaded. Please verify the file names and paths." + ) return submission_files def submit_by_user(self, user, problem_instance, source_code, source_code_name, verbosity): @@ -312,7 +316,7 @@ def submit_by_user(self, user, problem_instance, source_code, source_code_name, def create_and_populate_contest(self, problems, users, verbose_name, verbosity, contest_name=None, submission_file_names=None, num_submissions_per_user=0): """ - Creates a Contest with a given (or generated) name and an auto-preixed ID. + Creates a Contest with a given (or generated) name and an auto-prefixed ID. Then adds all provided problems, users and submissions to it. - problems: List of Problem objects to add. - users: List of User objects to add. @@ -507,7 +511,7 @@ def handle(self, *args, **options): verbosity = int(options.get("verbosity", 1)) total_objects_to_create = ( - int(bool(create_contest is not None)) + num_problems + num_users + num_algotags + + int(bool(create_contest)) + num_problems + num_users + num_algotags + num_difftags + num_algothrough + num_diffthrough + num_algoproposals + num_diffproposals ) max_algothrough = num_problems * num_algotags @@ -571,6 +575,11 @@ def handle(self, *args, **options): verbosity=verbosity, ) + if create_contest and num_submissions_per_user > 0 and not submission_file_names: + raise CommandError( + "When creating a contest with submissions, --submission-file-names must be provided " + "and contain at least one file." + ) if create_contest: created_contest = self.create_and_populate_contest( contest_name=contest_name, diff --git a/oioioi/problems/tests/test_commands.py b/oioioi/problems/tests/test_commands.py index d14b5fa78..81448c858 100644 --- a/oioioi/problems/tests/test_commands.py +++ b/oioioi/problems/tests/test_commands.py @@ -47,7 +47,7 @@ def _assert_model_counts(self, expected_counts, check_problem_names=True): expected_count = expected_counts.get(name, 0) assert count == expected_count, f"Expected {expected_count} {name}, got {count}" - # Validation for i18n problem names, can be ommited (e.g. for problems for packages). + # Validation for i18n problem names, can be omitted (e.g. for problems for packages). if check_problem_names: problem_amount: int = expected_counts.get("problems", Problem.objects.count()) expected_probname_count = len(settings.LANGUAGES) * problem_amount