diff --git a/package.json b/package.json
index d00626301..8c569b6de 100644
--- a/package.json
+++ b/package.json
@@ -18,51 +18,53 @@
"benchmark": "cross-env BENCHMARK=true mocha",
"lint": "eslint packages --ext ts --fix",
"lint:ci": "eslint packages --ext ts",
- "lint:ui": "yarn workspace @hydrooj/ui-default eslint --ext .js,.ts,.jsx,.tsx . --fix",
- "lint:ui:ci": "yarn workspace @hydrooj/ui-default eslint --ext .js,.ts,.jsx,.tsx .",
+ "lint:ui": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --fix",
+ "lint:ui:ci": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx .",
"typedoc": "cross-env NODE_OPTIONS=--max_old_space_size=8192 typedoc",
"debug:all": "node --trace-warnings --async-stack-traces --trace-deprecation build/start --debug --template",
"debug": "node --trace-warnings --async-stack-traces --trace-deprecation node_modules/hydrooj/bin/hydrooj --debug --template",
"start:all": "node build/start",
"start": "node_modules/.bin/hydrooj",
- "postinstall": "node build/hack && node build/prepare.js && rm -rf node_modules/esbuild-loader/node_modules/esbuild"
+ "postinstall": "node build/hack && node build/prepare.js"
},
"version": "1.0.0",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/autocannon": "^7.6.1",
"@types/cross-spawn": "^6.0.2",
- "@types/mocha": "^9.1.0",
- "@types/node": "^17.0.23",
+ "@types/mocha": "^9.1.1",
+ "@types/node": "^17.0.32",
"@types/semver": "^7.3.9",
"@types/supertest": "^2.0.12",
- "@typescript-eslint/eslint-plugin": "5.14.0",
- "@typescript-eslint/parser": "5.14.0",
- "autocannon": "^7.8.1",
+ "@typescript-eslint/eslint-plugin": "^5.23.0",
+ "@typescript-eslint/parser": "^5.23.0",
+ "autocannon": "^7.9.0",
"cac": "^6.7.12",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3",
"esbuild": "0.14.3",
- "eslint": "^8.12.0",
+ "eslint": "^8.15.0",
"eslint-config-airbnb": "^19.0.4",
- "eslint-config-airbnb-typescript": "16.1.1",
- "eslint-import-resolver-typescript": "2.5.0",
+ "eslint-config-airbnb-typescript": "^17.0.0",
+ "eslint-import-resolver-typescript": "^2.7.1",
"eslint-import-resolver-webpack": "^0.13.2",
- "eslint-plugin-import": "2.25.4",
+ "eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-simple-import-sort": "7.0.0",
- "fs-extra": "^10.0.1",
+ "fs-extra": "^10.1.0",
"globby": "11.1.0",
"latest-version": "^6.0.0",
- "mocha": "^9.2.2",
- "mongo-mock": "^4.1.0",
+ "mocha": "^10.0.0",
"mongodb": "^3.7.3",
- "mongodb-memory-server": "^8.4.2",
+ "mongodb-memory-server": "^8.5.2",
"ora": "5.4.1",
- "semver": "^7.3.5",
- "supertest": "^6.2.2",
- "typedoc": "^0.22.13",
- "typescript": "4.6.2"
+ "semver": "^7.3.7",
+ "supertest": "^6.2.3",
+ "typedoc": "^0.22.15",
+ "typescript": "^4.6.4"
+ },
+ "resolutions": {
+ "esbuild": "0.14.3"
}
}
diff --git a/packages/center/package.json b/packages/center/package.json
index f259298d9..c4a01711f 100644
--- a/packages/center/package.json
+++ b/packages/center/package.json
@@ -8,7 +8,7 @@
"preferUnplugged": true,
"dependencies": {
"js-yaml": "^4.1.0",
- "superagent": "^7.1.2"
+ "superagent": "^7.1.3"
},
"devDependencies": {
"@types/js-yaml": "^4.0.5"
diff --git a/packages/elastic/package.json b/packages/elastic/package.json
index 3ecf7f1e1..6c6507332 100644
--- a/packages/elastic/package.json
+++ b/packages/elastic/package.json
@@ -1,11 +1,11 @@
{
"name": "@hydrooj/elastic-search",
- "version": "1.0.2",
+ "version": "1.0.5",
"main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro",
"author": "undefined ",
"license": "AGPL-3.0-or-later",
"dependencies": {
- "@elastic/elasticsearch": "^8.1.0"
+ "@elastic/elasticsearch": "^8.2.0-patch.1"
}
}
diff --git a/packages/elastic/script.ts b/packages/elastic/script.ts
index 0e269e476..0c4412913 100644
--- a/packages/elastic/script.ts
+++ b/packages/elastic/script.ts
@@ -32,8 +32,9 @@ bus.on('problem/del', async (domainId, docId) => {
});
global.Hydro.lib.problemSearch = async (domainId, q, opts) => {
+ const allowedSize = system.get('elasic-search.indexSize') || 10000;
const size = opts?.limit || system.get('pagination.problem');
- const from = opts?.skip || 0;
+ const from = Math.min(allowedSize - size, opts?.skip || 0);
const union = await DomainModel.getUnion(domainId);
const domainIds = [domainId, ...(union?.union || [])];
const res = await client.search({
diff --git a/packages/elastic/setting.yaml b/packages/elastic/setting.yaml
index aea785885..f40c9cc15 100644
--- a/packages/elastic/setting.yaml
+++ b/packages/elastic/setting.yaml
@@ -1,2 +1,4 @@
url:
value: http://127.0.0.1:9200
+indexSize:
+ value: 10000
\ No newline at end of file
diff --git a/packages/fps-importer/handler.ts b/packages/fps-importer/handler.ts
index 3278192de..f6569802e 100644
--- a/packages/fps-importer/handler.ts
+++ b/packages/fps-importer/handler.ts
@@ -104,15 +104,15 @@ class FpsProblemImportHandler extends Handler {
if (!this.request.files.file) throw new ValidationError('file');
const tasks = [];
try {
- const file = await fs.stat(this.request.files.file.path);
+ const file = await fs.stat(this.request.files.file.filepath);
if (file.size > 64 * 1024 * 1024) throw new FileTooLargeError('64m');
- const content = fs.readFileSync(this.request.files.file.path, 'utf-8');
+ const content = fs.readFileSync(this.request.files.file.filepath, 'utf-8');
const result = await xml2js.parseStringPromise(content);
tasks.push(result);
} catch (e) {
if (e instanceof FileTooLargeError) throw e;
console.log(e);
- const zip = new AdmZip(this.request.files.file.path);
+ const zip = new AdmZip(this.request.files.file.filepath);
for (const entry of zip.getEntries()) {
try {
const buf = entry.getData();
diff --git a/packages/fps-importer/package.json b/packages/fps-importer/package.json
index c6cd48c0c..96cf8473d 100644
--- a/packages/fps-importer/package.json
+++ b/packages/fps-importer/package.json
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/fps-importer",
- "version": "1.3.3",
+ "version": "1.3.4",
"description": "Import FPS problems",
"main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git",
@@ -10,12 +10,12 @@
"dependencies": {
"adm-zip": "0.5.5",
"decode-html": "^2.0.0",
- "fs-extra": "^10.0.1",
+ "fs-extra": "^10.1.0",
"mongodb": "^3.7.3",
"xml2js": "^0.4.23"
},
"devDependencies": {
- "@types/xml2js": "^0.4.9"
+ "@types/xml2js": "^0.4.11"
},
"peerDependencies": {
"hydrooj": "*"
diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json
index 6e7a8550f..ce698f16e 100644
--- a/packages/hydrojudge/package.json
+++ b/packages/hydrojudge/package.json
@@ -1,29 +1,29 @@
{
"name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js",
- "version": "2.18.1",
+ "version": "2.20.4",
"main": "package.json",
"author": "undefined ",
"repository": "https://github.com/hydro-dev/Hydro.git",
"dependencies": {
"@hydrooj/utils": "workspace:*",
- "axios": "^0.26.1",
+ "axios": "^0.27.2",
"cac": "^6.7.12",
"esbuild": "0.14.3",
- "fs-extra": "^10.0.1",
+ "fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"mongodb": "^3.7.3",
"p-queue": "^7.2.0",
"shell-quote": "^1.7.3",
- "ws": "^8.5.0"
+ "ws": "^8.6.0"
},
"preferUnplugged": true,
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/js-yaml": "^4.0.5",
- "@types/lodash": "^4.14.181",
+ "@types/lodash": "^4.14.182",
"@types/shell-quote": "^1.7.1",
"@types/ws": "^8.5.3"
},
diff --git a/packages/hydrojudge/setting.yaml b/packages/hydrojudge/setting.yaml
index d9533b83a..a3af6198d 100644
--- a/packages/hydrojudge/setting.yaml
+++ b/packages/hydrojudge/setting.yaml
@@ -37,11 +37,6 @@ memoryMax:
type: text
name: memoryMax
default: 512m
-retry_delay_sec:
- type: number
- name: retry_delay_sec
- desc: Retry delay sec
- default: 15
cache_dir:
type: text
name: cache_dir
@@ -52,6 +47,10 @@ parallelism:
name: parallelism
desc: Runner parallelism
default: 2
+singleTaskParallelism:
+ type: number
+ name: singleTaskParallelism
+ default: 2
rate:
type: float
name: rate
diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts
index 9c71ed897..baf38cd5d 100644
--- a/packages/hydrojudge/src/cases.ts
+++ b/packages/hydrojudge/src/cases.ts
@@ -1,233 +1,104 @@
+/* eslint-disable no-await-in-loop */
import path from 'path';
import fs from 'fs-extra';
import yaml from 'js-yaml';
import { max, sum } from 'lodash';
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
+import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
import { changeErrorType } from '@hydrooj/utils/lib/utils';
import { getConfig } from './config';
import { FormatError, SystemError } from './error';
-import { ensureFile, parseMemoryMB, parseTimeMS } from './utils';
-
-interface Re0 {
- reg: RegExp,
- output: ((a: RegExpExecArray) => string)[],
- id: (a: RegExpExecArray) => number,
-}
-
-interface Re1 extends Re0 {
- subtask: (a: RegExpExecArray) => number,
-}
-
-const RE0: Re0[] = [
- {
- reg: /^([^\d]*)(\d+).(in|txt)$/,
- output: [
- (a) => `${a[1] + a[2]}.out`,
- (a) => `${a[1] + a[2]}.ans`,
- (a) => `${a[1] + a[2]}.out`.replace(/input/g, 'output'),
- (a) => (a[1].includes('input') ? `${a[1] + a[2]}.txt`.replace(/input/g, 'output') : null),
- ],
- id: (a) => +a[2],
- },
- {
- reg: /^([^\d]*)\.in(\d+)$/,
- output: [
- (a) => `${a[1]}.ou${a[2]}`,
- (a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
- ],
- id: (a) => +a[2],
- },
-];
-const RE1: Re1[] = [
- {
- reg: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
- output: [(a) => `${a[1] + a[2]}${a[3]}${a[4]}.out`],
- subtask: (a) => +a[2],
- id: (a) => +a[4],
- },
-];
-
-async function read0(folder: string, files: string[], checkFile, cfg) {
- const cases = [];
- for (const file of files) {
- for (const REG of RE0) {
- if (REG.reg.test(file)) {
- const data = REG.reg.exec(file);
- const c = { input: file, output: '', id: REG.id(data) };
- for (const func of REG.output) {
- if (cfg.noOutputFile) c.output = '/dev/null';
- else c.output = func(data);
- if (c.output && (c.output === '/dev/null' || fs.existsSync(path.resolve(folder, c.output)))) {
- cases.push(c);
- break;
- }
- }
- }
- }
- }
- cases.sort((a, b) => (a.id - b.id));
- const extra = cases.length - (100 % cases.length);
- const config = {
- count: 0,
- subtasks: [{
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'sum',
- cases: [],
- score: Math.floor(100 / cases.length),
- }],
- };
- for (let i = 0; i < extra; i++) {
- config.count++;
- config.subtasks[0].cases.push({
- id: config.count,
- input: checkFile(cases[i].input),
- output: checkFile(cases[i].output),
- });
- }
- if (extra < cases.length) {
- config.subtasks.push({
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'sum',
- cases: [],
- score: Math.floor(100 / cases.length) + 1,
- });
- for (let i = extra; i < cases.length; i++) {
- config.count++;
- config.subtasks[1].cases.push({
- id: config.count,
- input: checkFile(cases[i].input),
- output: checkFile(cases[i].output),
- });
- }
- }
- return config;
-}
-
-async function read1(folder: string, files: string[], checkFile, cfg, rst) {
- const subtask = {};
- for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
- const subtasks = [];
- for (const file of files) {
- for (const REG of RE1) {
- if (REG.reg.test(file)) {
- const data = REG.reg.exec(file);
- const sid = REG.subtask(data);
- const c = { input: file, output: '', id: REG.id(data) };
- for (const func of REG.output) {
- if (cfg.noOutputFile) c.output = '/dev/null';
- else c.output = func(data);
- if (c.output === '/dev/null' || fs.existsSync(path.resolve(folder, c.output))) {
- if (!subtask[sid]) {
- subtask[sid] = {
- time: parseTimeMS(cfg.time || '1s'),
- memory: parseMemoryMB(cfg.memory || '256m'),
- type: 'min',
- cases: [c],
- };
- } else if (!subtask[sid].cases) subtask[sid].cases = [c];
- else subtask[sid].cases.push(c);
- break;
- }
- }
- }
- }
- }
- for (const i in subtask) {
- subtask[i].cases.sort((a, b) => (a.id - b.id));
- subtasks.push(subtask[i]);
- }
- const base = Math.floor(100 / subtasks.length);
- const extra = subtasks.length - (100 % subtasks.length);
- const config = { count: 0, subtasks };
- const keys = Object.keys(subtask);
- for (let i = 0; i < keys.length; i++) {
- if (i >= extra) subtask[keys[i]].score = base + 1;
- else subtask[keys[i]].score = base;
- for (const j of subtask[keys[i]].cases) {
- config.count++;
- j.input = checkFile(j.input);
- j.output = checkFile(j.output);
- j.id = config.count;
- }
- }
- return config;
-}
-
-async function readAutoCases(folder: string, { next }, cfg, rst) {
- const config = {
- checker_type: 'default',
- count: 0,
- subtasks: [],
- judge_extra_files: [],
- user_extra_files: [],
- };
- const checkFile = ensureFile(folder);
- try {
- const files = await fs.readdir(folder);
- if (await fs.pathExists(path.resolve(folder, 'input'))) {
- const inputs = await fs.readdir(path.resolve(folder, 'input'));
- files.push(...inputs.map((i) => `input/${i}`));
- }
- if (await fs.pathExists(path.resolve(folder, 'output'))) {
- const outputs = await fs.readdir(path.resolve(folder, 'output'));
- files.push(...outputs.map((i) => `output/${i}`));
- }
- let result = await read0(folder, files, checkFile, cfg);
- if (!result.count) result = await read1(folder, files, checkFile, cfg, rst);
- Object.assign(config, result);
- if (cfg.isSelfSubmission) next({ message: { message: 'Found {0} testcases.', params: [config.count] } });
- } catch (e) {
- throw new SystemError('Cannot parse testdata.', [e.message, ...e.params]);
- }
- return config;
-}
+import { ensureFile, parseMemoryMB } from './utils';
function isValidConfig(config) {
if (config.count > (getConfig('testcases_max') || 100)) {
throw new FormatError('Too many testcases. Cancelled.');
}
- const time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length));
+ const time = sum(config.subtasks.map((subtask) => sum(subtask.cases.map((c) => c.time))));
if (time > (getConfig('total_time_limit') || 60) * 1000) {
throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]);
}
- const memMax = max(config.subtasks.map((subtask) => subtask.memory));
+ const memMax = max(config.subtasks.map((subtask) => max(subtask.cases.map((c) => c.memory))));
if (memMax > parseMemoryMB(getConfig('memoryMax'))) throw new FormatError('Memory limit larger than memory_max');
if (!['default', 'strict'].includes(config.checker_type || 'default') && !config.checker) {
throw new FormatError('You did not specify a checker.');
}
}
+async function collectFiles(folder: string) {
+ const files = await fs.readdir(folder);
+ await Promise.all(['input', 'output'].map(async (t) => {
+ if (await fs.pathExists(path.resolve(folder, t))) {
+ const f = await fs.readdir(path.resolve(folder, t));
+ files.push(...f.map((i) => `${t}/${i}`));
+ }
+ }));
+ return files;
+}
+
+export async function processTestdata(folder: string) {
+ let files = await fs.readdir(folder);
+ if (files.length <= 2) {
+ if (files.length === 2) files.splice(files.indexOf('version'), 1);
+ if (fs.statSync(path.resolve(folder, files[0])).isDirectory()) {
+ folder = path.resolve(folder, files[0]);
+ files = await fs.readdir(folder);
+ }
+ }
+ const ini = files.filter((i) => i.toLowerCase() === 'config.ini')[0];
+ if (!ini) return;
+ const t = fs.readFileSync(path.resolve(folder, ini), 'utf8');
+ await fs.writeFile(path.resolve(folder, 'config.ini'), t.toLowerCase());
+ for (const i of files) {
+ if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
+ if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
+ }
+ await Promise.all(['input', 'output'].flatMap(async (f) => {
+ const dir = path.resolve(folder, f);
+ const sf = await fs.readdir(dir);
+ return sf
+ .filter((i) => i !== i.toLowerCase())
+ .map((i) => fs.rename(`${dir}/${i}`, `${dir}/${i.toLowerCase()}`));
+ }));
+}
+
export default async function readCases(folder: string, cfg: Record = {}, args) {
const iniConfig = path.resolve(folder, 'config.ini');
const yamlConfig = path.resolve(folder, 'config.yaml');
const ymlConfig = path.resolve(folder, 'config.yml');
- let config;
- if (fs.existsSync(yamlConfig)) {
- config = { ...cfg, ...yaml.load(fs.readFileSync(yamlConfig).toString()) as object };
- } else if (fs.existsSync(ymlConfig)) {
- config = { ...cfg, ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object };
- } else if (fs.existsSync(iniConfig)) {
- try {
- config = { ...cfg, ...convertIniConfig(fs.readFileSync(iniConfig).toString()) };
- } catch (e) {
- throw changeErrorType(e, FormatError);
- }
- } else config = cfg;
- let result;
+ const config: Record = {
+ checker_type: 'default',
+ count: 0,
+ subtasks: [],
+ judge_extra_files: [],
+ user_extra_files: [],
+ ...cfg,
+ };
try {
- result = await readYamlCases(config, ensureFile(folder));
+ if (fs.existsSync(yamlConfig)) {
+ Object.assign(config, yaml.load(fs.readFileSync(yamlConfig).toString()));
+ } else if (fs.existsSync(ymlConfig)) {
+ Object.assign(config, yaml.load(fs.readFileSync(ymlConfig).toString()));
+ } else if (fs.existsSync(iniConfig)) {
+ Object.assign(config, convertIniConfig(fs.readFileSync(iniConfig).toString()));
+ }
} catch (e) {
throw changeErrorType(e, FormatError);
}
- let cases = result.outputs?.length || 0;
- cases += Math.sum((result.subtasks || []).map((subtask) => subtask.cases.length));
- if (!cases) {
- const c = await readAutoCases(folder, args, config, result);
- result.subtasks = c.subtasks;
- result.count = c.count;
+ const checkFile = ensureFile(folder);
+ const result = await readYamlCases(config, checkFile)
+ .catch((e) => { throw changeErrorType(e, FormatError); });
+ result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length));
+ if (!result.count) {
+ try {
+ result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg);
+ result.count = Math.sum(result.subtasks.map((i) => i.cases.length));
+ if (cfg.isSelfSubmission) args.next?.({ message: { message: 'Found {0} testcases.', params: [result.count] } });
+ } catch (e) {
+ throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]);
+ }
}
+ result.subtasks = normalizeSubtasks(result.subtasks || [], checkFile, config.time, config.memory);
if (result.key && args.key !== result.key) throw new FormatError('Incorrect secret key');
if (!result.key) isValidConfig(result);
return result;
diff --git a/packages/hydrojudge/src/check.ts b/packages/hydrojudge/src/check.ts
index 8155c878c..1223c6dab 100644
--- a/packages/hydrojudge/src/check.ts
+++ b/packages/hydrojudge/src/check.ts
@@ -16,6 +16,9 @@ export async function check(config: CheckConfig): Promise<[number, number, strin
}
export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise {
+ if (['default', 'strict'].includes(checkerType)) {
+ return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
+ }
if (!checkers[checkerType]) throw new SystemError('Unknown checker type {0}.', [checkerType]);
if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc };
const s = checker.replace('@', '.').split('.');
diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts
index eb767152b..76c170c5c 100644
--- a/packages/hydrojudge/src/compile.ts
+++ b/packages/hydrojudge/src/compile.ts
@@ -25,7 +25,7 @@ export = async function compile(
);
if (status !== STATUS.STATUS_ACCEPTED) throw new CompileError({ status, stdout, stderr });
if (!fileIds[target]) throw new CompileError({ stderr: 'Executable file not found.' });
- if (next) next({ compiler_text: compilerText(stdout, stderr) });
+ if (next) next({ compilerText: compilerText(stdout, stderr) });
return {
execute,
copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } },
diff --git a/packages/hydrojudge/src/config.ts b/packages/hydrojudge/src/config.ts
index 51cdbab56..564728377 100644
--- a/packages/hydrojudge/src/config.ts
+++ b/packages/hydrojudge/src/config.ts
@@ -1,4 +1,3 @@
-/* eslint-disable prefer-const */
import os from 'os';
import path from 'path';
import cac from 'cac';
@@ -9,19 +8,19 @@ const argv = cac().parse();
// eslint-disable-next-line @typescript-eslint/naming-convention
let CONFIG_FILE = path.resolve(os.homedir(), '.config', 'hydro', 'judge.yaml');
-let config = {
+const config = {
cache_dir: path.resolve(os.homedir(), '.cache', 'hydro', 'judge'),
tmp_dir: path.resolve(os.tmpdir(), 'hydro', 'judge'),
tmpfs_size: '256m',
stdio_size: '32m',
memoryMax: '512m',
strict_memory: false,
- retry_delay_sec: 15,
sandbox_host: 'http://localhost:5050',
testcases_max: 100,
total_time_limit: 60,
processLimit: 32,
parallelism: 2,
+ singleTaskParallelism: 2,
rerun: 0,
rate: 1,
config: null,
@@ -29,11 +28,12 @@ let config = {
env: `\
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/w`,
+ hosts: {},
secret: String.random(32),
};
-export function getConfig(key: string) {
- return global.Hydro ? global.Hydro.model.system.get(`hydrojudge.${key}`) : config[key];
+export function getConfig(key: keyof typeof config) {
+ return global.Hydro ? global.Hydro.model.system.get(`hydrojudge.${key}`) ?? config[key] : config[key];
}
if (fs.existsSync(path.resolve(process.cwd(), '.env'))) {
@@ -61,5 +61,5 @@ if (!global.Hydro) {
config.sandbox_host = path.resolve(process.env.EXECUTION_HOST || argv.options.sandbox);
}
const configFile = fs.readFileSync(CONFIG_FILE).toString();
- config = { ...config, ...yaml.load(configFile) as any };
+ Object.assign(config, yaml.load(configFile) as any);
}
diff --git a/packages/hydrojudge/src/daemon.ts b/packages/hydrojudge/src/daemon.ts
index 858f12fec..02cfe91ad 100644
--- a/packages/hydrojudge/src/daemon.ts
+++ b/packages/hydrojudge/src/daemon.ts
@@ -18,7 +18,7 @@ import './utils';
import { getConfig } from './config';
import * as Session from './hosts/index';
import log from './log';
-import { Queue, sleep } from './utils';
+import { Queue } from './utils';
declare global {
namespace NodeJS {
@@ -28,8 +28,8 @@ declare global {
}
}
}
-if (!global.onDestroy) global.onDestroy = [];
-if (!global.hosts) global.hosts = [];
+global.onDestroy ||= [];
+global.hosts ||= [];
let exit = false;
const terminate = async () => {
@@ -60,28 +60,16 @@ async function worker(queue: Queue) {
async function daemon() {
const _hosts = getConfig('hosts');
- const delay = getConfig('retry_delay_sec');
const hosts = {};
const queue = new Queue();
+ worker(queue).catch((e) => log.error(e));
for (const i in _hosts) {
_hosts[i].host = _hosts[i].host || i;
- hosts[i] = new Session[_hosts[i].type || 'vj4'](_hosts[i]);
+ hosts[i] = new Session[_hosts[i].type || 'hydro'](_hosts[i]);
await hosts[i].init();
}
+ for (const i in hosts) hosts[i].consume(queue);
global.hosts = hosts;
- worker(queue);
- for (const i in hosts) {
- while ('Orz twd2') {
- try {
- await hosts[i].consume(queue);
- break;
- } catch (e) {
- log.error(e, e.stack);
- log.info(`在 ${delay} 秒后重试`);
- await sleep(delay * 1000);
- }
- }
- }
}
if (require.main === module) daemon();
diff --git a/packages/hydrojudge/src/error.ts b/packages/hydrojudge/src/error.ts
index 29419e5ab..ff79b9a0b 100644
--- a/packages/hydrojudge/src/error.ts
+++ b/packages/hydrojudge/src/error.ts
@@ -5,7 +5,7 @@ export class CompileError extends Error {
stdout: string;
stderr: string;
status: string;
- type: string;
+ type = 'CompileError';
constructor(obj: string | CompileErrorInfo) {
super('Compile Error');
@@ -17,48 +17,29 @@ export class CompileError extends Error {
this.stderr = obj.stderr || '';
this.status = obj.status ? STATUS_TEXTS[obj.status] || '' : '';
}
- this.type = 'CompileError';
}
}
export class FormatError extends Error {
- type: string;
- params: any[];
+ type = 'FormatError';
- constructor(message: string, params = []) {
+ constructor(message: string, public params = []) {
super(message);
- this.type = 'FormatError';
- this.params = params;
}
}
export class RuntimeError extends Error {
- type: string;
- detail: any;
+ type = 'RuntimeError';
- constructor(detail: string, message: string) {
+ constructor(public detail: string, message: string) {
super(message);
- this.type = 'RuntimeError';
- this.detail = detail;
}
}
export class SystemError extends Error {
- type: string;
- params: any[];
+ type = 'SystemError';
- constructor(message: string, params = []) {
+ constructor(message: string, public params = []) {
super(message);
- this.type = 'SystemError';
- this.params = params;
- }
-}
-
-export class TooFrequentError extends Error {
- type: string;
-
- constructor(message: string) {
- super(message);
- this.type = 'TooFrequentError';
}
}
diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts
index 950975099..6824b5af0 100644
--- a/packages/hydrojudge/src/hosts/hydro.ts
+++ b/packages/hydrojudge/src/hosts/hydro.ts
@@ -9,12 +9,13 @@ import PQueue from 'p-queue';
import WebSocket from 'ws';
import { LangConfig } from '@hydrooj/utils/lib/lang';
import { STATUS } from '@hydrooj/utils/lib/status';
-import readCases from '../cases';
+import type { JudgeResultBody } from 'hydrooj';
+import readCases, { processTestdata } from '../cases';
import { getConfig } from '../config';
import { CompileError, FormatError, SystemError } from '../error';
import judge from '../judge';
import log from '../log';
-import { CopyInFile } from '../sandbox/interface';
+import type { CopyInFile } from '../sandbox/interface';
import * as sysinfo from '../sysinfo';
import * as tmpfs from '../tmpfs';
import {
@@ -82,21 +83,21 @@ class JudgeTask {
await this.doSubmission(startPromise);
} catch (e) {
if (e instanceof CompileError) {
- this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
+ this.next({ compilerText: compilerText(e.stdout, e.stderr) });
this.end({
- status: STATUS.STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_COMPILE_ERROR, score: 0, time: 0, memory: 0,
});
} else if (e instanceof FormatError) {
- this.next({ judge_text: 'Testdata configuration incorrect.' });
- this.next({ judge_text: { message: e.message, params: e.params } });
+ this.next({ message: 'Testdata configuration incorrect.' });
+ this.next({ message: { message: e.message, params: e.params } });
this.end({
- status: STATUS.STATUS_FORMAT_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_FORMAT_ERROR, score: 0, time: 0, memory: 0,
});
} else {
log.error(e);
this.next({ message: { message: e.message, params: e.params || [], ...argv.options.debug ? { stack: e.stack } : {} } });
this.end({
- status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time: 0, memory: 0,
});
}
} finally {
@@ -132,38 +133,21 @@ class JudgeTask {
await judge[type].judge(this, startPromise);
}
- next(data, id?: number) {
+ next(data: Partial, id?: number) {
log.debug('Next: %d %o', id, data);
data.key = 'next';
data.rid = new ObjectID(this.rid);
- if (data.time_ms) data.time = data.time_ms;
- if (data.memory_kb) data.memory = data.memory_kb;
- if (data.compiler_text) data.compilerText = data.compiler_text;
- if (data.judge_text) data.message = data.judge_text;
- delete data.time_ms;
- delete data.memory_kb;
- delete data.compiler_text;
- delete data.judge_text;
if (data.case) {
- data.case = {
- id,
- status: data.case.status,
- time: data.case.time_ms || data.case.time,
- memory: data.case.memory_kb || data.case.memory,
- message: data.case.message || data.case.judgeText || data.case.judge_text || '',
- };
+ data.case.id ||= id;
+ data.case.message ||= '';
}
this.ws.send(JSON.stringify(data));
}
- end(data) {
+ end(data: Partial) {
log.info('End: %o', data);
data.key = 'end';
- data.rid = new ObjectID(this.request.rid);
- data.time = data.time_ms ?? data.time;
- delete data.time_ms;
- data.memory = data.memory_kb ?? data.memory;
- delete data.memory_kb;
+ data.rid = this.request.rid;
this.ws.send(JSON.stringify(data));
}
}
@@ -221,7 +205,7 @@ export default class Hydro {
}
if (filenames.length) {
log.info(`Getting problem data: ${this.config.host}/${source}`);
- if (next) next({ judge_text: 'Syncing testdata, please wait...' });
+ if (next) next({ message: 'Syncing testdata, please wait...' });
await this.ensureLogin();
const res = await this.axios.post(`/d/${domainId}/judge/files`, {
pid: +pid,
@@ -248,7 +232,7 @@ export default class Hydro {
queue.start();
await Promise.all(tasks);
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
- await this.processData(filePath).catch(noop);
+ await processTestdata(filePath);
}
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
return filePath;
@@ -348,34 +332,6 @@ export default class Hydro {
}
}
- async processData(folder: string) { // eslint-disable-line class-methods-use-this
- let files = await fs.readdir(folder);
- let ini = false;
- if (files.length <= 2) {
- if (files.length === 2) files.splice(files.indexOf('version'), 1);
- const s = fs.statSync(path.resolve(folder, files[0]));
- if (s.isDirectory()) folder = path.resolve(folder, files[0]);
- }
- files = await fs.readdir(folder);
- for (const i of files) {
- if (i.toLowerCase() === 'config.ini') {
- ini = true;
- await fs.rename(`${folder}/${i}`, `${folder}/config.ini`);
- break;
- }
- }
- if (ini) {
- for (const i of files) {
- if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
- else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
- }
- files = await fs.readdir(`${folder}/input`);
- for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
- files = await fs.readdir(`${folder}/output`);
- for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`);
- }
- }
-
async retry(queue: Queue) {
this.consume(queue).catch(() => {
setTimeout(() => this.retry(queue), 30000);
diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts
index 14a589783..e9a9aab0a 100644
--- a/packages/hydrojudge/src/judge/default.ts
+++ b/packages/hydrojudge/src/judge/default.ts
@@ -8,10 +8,8 @@ import { Logger } from '../log';
import { del, run } from '../sandbox';
import { CmdFile } from '../sandbox/interface';
import signals from '../signals';
-import { parseFilename } from '../utils';
-import {
- Case, Context, ContextSubTask, SubTask,
-} from './interface';
+import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils';
+import { Context, ContextSubTask } from './interface';
const Score = {
sum: (a: number, b: number) => (a + b),
@@ -21,18 +19,18 @@ const Score = {
const logger = new Logger('judge/default');
-function judgeCase(c: Case, sid: string) {
+function judgeCase(c: NormalizedCase, sid: string) {
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
- || ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
+ || ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length)
) {
ctx.next({
case: {
status: STATUS.STATUS_CANCELED,
score: 0,
- time_ms: 0,
- memory_kb: 0,
+ time: 0,
+ memory: 0,
message: '',
},
addProgress: 100 / ctx.config.count,
@@ -76,7 +74,7 @@ function judgeCase(c: Case, sid: string) {
user_stdout: stdout,
user_stderr: stderr,
checker_type: ctx.config.checker_type,
- score: ctxSubtask.subtask.score,
+ score: c.score,
detail: ctx.config.detail ?? true,
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
});
@@ -100,10 +98,11 @@ function judgeCase(c: Case, sid: string) {
ctx.total_memory_usage_kb = Math.max(ctx.total_memory_usage_kb, memory_usage_kb);
ctx.next({
case: {
+ subtaskId: ctxSubtask.subtask.id,
status,
- score: 0,
- time_ms: time_usage_ms,
- memory_kb: memory_usage_kb,
+ score,
+ time: time_usage_ms,
+ memory: memory_usage_kb,
message,
},
addProgress: 100 / ctx.config.count,
@@ -126,7 +125,7 @@ function judgeCase(c: Case, sid: string) {
env: ctx.env,
});
const out = r.stdout.toString();
- if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
+ if (out.length) ctx.next({ compilerText: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
} catch (e) {
logger.info('Failed to run analysis');
@@ -137,7 +136,7 @@ function judgeCase(c: Case, sid: string) {
};
}
-function judgeSubtask(subtask: SubTask, sid: string) {
+function judgeSubtask(subtask: NormalizedSubtask, sid: string) {
return async (ctx: Context) => {
subtask.type = subtask.type || 'min';
const ctxSubtask = {
@@ -178,21 +177,17 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
),
ctx.next,
),
- (() => {
- if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
- return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
- }
- const copyIn = { user_code: ctx.code };
- for (const file of ctx.config.judge_extra_files) {
- copyIn[parseFilename(file)] = { src: file };
- }
- return compileChecker(
- ctx.getLang,
- ctx.config.checker_type,
- ctx.config.checker,
- copyIn,
- );
- })(),
+ compileChecker(
+ ctx.getLang,
+ ctx.config.checker_type,
+ ctx.config.checker,
+ {
+ user_code: ctx.code,
+ ...Object.fromEntries(
+ (ctx.config.judge_extra_files || []).map((i) => [parseFilename(i), { src: i }]),
+ ),
+ },
+ ),
]);
ctx.clean.push(ctx.execute.clean, ctx.checker.clean);
await startPromise;
@@ -203,7 +198,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
ctx.total_memory_usage_kb = 0;
ctx.total_time_usage_ms = 0;
ctx.rerun = getConfig('rerun') || 0;
- ctx.queue = new Queue({ concurrency: getConfig('parallelism') });
+ ctx.queue = new Queue({ concurrency: getConfig('singleTaskParallelism') });
ctx.failed = {};
for (const sid in ctx.config.subtasks) tasks.push(judgeSubtask(ctx.config.subtasks[sid], sid)(ctx));
const scores = await Promise.all(tasks);
@@ -219,7 +214,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
ctx.end({
status: ctx.total_status,
score: ctx.total_score,
- time_ms: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000,
- memory_kb: ctx.total_memory_usage_kb,
+ time: Math.floor(ctx.total_time_usage_ms * 1000000) / 1000000,
+ memory: ctx.total_memory_usage_kb,
});
};
diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts
index e99b33713..2e556ad07 100644
--- a/packages/hydrojudge/src/judge/interactive.ts
+++ b/packages/hydrojudge/src/judge/interactive.ts
@@ -5,10 +5,10 @@ import { getConfig } from '../config';
import { runPiped } from '../sandbox';
import signals from '../signals';
import { parse } from '../testlib';
-import { findFileSync, parseFilename } from '../utils';
import {
- Case, Context, ContextSubTask, SubTask,
-} from './interface';
+ findFileSync, NormalizedCase, NormalizedSubtask, parseFilename,
+} from '../utils';
+import { Context, ContextSubTask } from './interface';
const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h');
const Score = {
@@ -17,7 +17,7 @@ const Score = {
min: Math.min,
};
-function judgeCase(c: Case) {
+function judgeCase(c: NormalizedCase) {
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
ctx.executeInteractor.copyIn.in = c.input ? { src: c.input } : { content: '' };
ctx.executeInteractor.copyIn.out = c.output ? { src: c.output } : { content: '' };
@@ -50,7 +50,7 @@ function judgeCase(c: Case) {
if (code < 32) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] };
} else {
- const result = parse(resInteractor.stderr, ctxSubtask.subtask.score);
+ const result = parse(resInteractor.stderr, c.score);
status = result.status;
score = result.score;
message = result.message;
@@ -63,10 +63,11 @@ function judgeCase(c: Case) {
ctx.next({
status: STATUS.STATUS_JUDGING,
case: {
+ subtaskId: ctxSubtask.subtask.id,
status,
score,
- time_ms: time_usage_ms,
- memory_kb: memory_usage_kb,
+ time: time_usage_ms,
+ memory: memory_usage_kb,
message,
},
progress: Math.floor((c.id * 100) / ctx.config.count),
@@ -74,7 +75,7 @@ function judgeCase(c: Case) {
};
}
-function judgeSubtask(subtask: SubTask) {
+function judgeSubtask(subtask: NormalizedSubtask) {
return async (ctx: Context) => {
subtask.type = subtask.type || 'min';
const ctxSubtask = {
@@ -134,7 +135,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
ctx.end({
status: ctx.total_status,
score: ctx.total_score,
- time_ms: ctx.total_time_usage_ms,
- memory_kb: ctx.total_memory_usage_kb,
+ time: ctx.total_time_usage_ms,
+ memory: ctx.total_memory_usage_kb,
});
};
diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts
index b5f5f45d5..1ebb1f78b 100644
--- a/packages/hydrojudge/src/judge/interface.ts
+++ b/packages/hydrojudge/src/judge/interface.ts
@@ -1,31 +1,15 @@
import PQueue from 'p-queue';
-import { LangConfig } from '@hydrooj/utils/lib/lang';
+import type { NormalizedSubtask } from '@hydrooj/utils/lib/common';
+import type { LangConfig } from '@hydrooj/utils/lib/lang';
+import type { JudgeResultBody } from 'hydrooj';
import { Execute } from '../interface';
import { CopyInFile } from '../sandbox/interface';
export type Context = JudgeTaskInterface & RuntimeContext;
export interface JudgeTaskInterface {
- next(arg0: {
- compiler_text?: string;
- status?: number;
- message?: string;
- progress?: number;
- addProgress?: number;
- case?: {
- status: number;
- score?: number;
- time_ms: number;
- memory_kb: number;
- message: string;
- };
- }, arg1?: number): void;
- end(arg0: {
- status: number;
- score: number;
- time_ms: number;
- memory_kb: number;
- }): void;
+ next(data: Partial, id?: number): void;
+ end(data: Partial): void;
getLang: (name: string) => LangConfig;
stat: Record;
@@ -56,22 +40,20 @@ export interface RuntimeContext {
executeUser?: Execute;
}
-type ExtraFile = string[];
-
export interface Config {
type: string;
subType?: string;
time: string;
memory: string;
- subtasks?: SubTask[];
+ subtasks?: NormalizedSubtask[];
count?: number;
checker_type?: string;
detail?: boolean;
filename?: string;
- judge_extra_files?: ExtraFile;
- user_extra_files?: ExtraFile;
+ judge_extra_files: string[];
+ user_extra_files: string[];
template?: Record;
checker?: string;
@@ -86,23 +68,8 @@ export interface Config {
}[];
}
-export interface SubTask {
- time: number;
- memory: number;
- type: 'sum' | 'max' | 'min';
- score: number;
- cases: Record;
- if: string[];
-}
-
export interface ContextSubTask {
- subtask: SubTask;
+ subtask: NormalizedSubtask;
score: number;
status: number;
}
-
-export interface Case {
- id: number;
- input: string;
- output: string;
-}
diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts
index 278c1b538..0d6a374fb 100644
--- a/packages/hydrojudge/src/judge/objective.ts
+++ b/packages/hydrojudge/src/judge/objective.ts
@@ -29,11 +29,11 @@ export async function judge({
status: totalStatus,
progress: (100 * (+i + 1)) / config.outputs.length,
case: {
- status, score, time_ms: 0, memory_kb: 0, message: '',
+ status, score, time: 0, memory: 0, message: '',
},
}, +i + 1);
}
return end({
- status: totalStatus, score: totalScore, time_ms: 0, memory_kb: 0,
+ status: totalStatus, score: totalScore, time: 0, memory: 0,
});
}
diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts
index c746ea2d8..c87503ec8 100644
--- a/packages/hydrojudge/src/judge/run.ts
+++ b/packages/hydrojudge/src/judge/run.ts
@@ -12,8 +12,8 @@ import { Context } from './interface';
const failure = (status: number, message?: string) => ({
status,
score: 0,
- time_ms: 0,
- memory_kb: 0,
+ time: 0,
+ memory: 0,
message,
});
@@ -88,9 +88,11 @@ export const judge = async (ctx: Context) => {
ctx.next({
status,
case: {
+ subtaskId: 0,
status,
- time_ms: time_usage_ms,
- memory_kb: memory_usage_kb,
+ score: 100,
+ time: time_usage_ms,
+ memory: memory_usage_kb,
message: message.join('\n').substring(0, 102400),
},
});
@@ -115,7 +117,7 @@ export const judge = async (ctx: Context) => {
memory: 256,
});
const out = r.stdout.toString();
- if (out.length) ctx.next({ compiler_text: out.substring(0, 1024) });
+ if (out.length) ctx.next({ compilerText: out.substring(0, 1024) });
if (process.env.DEV) console.log(r);
} catch (e) {
logger.info('Failed to run analysis');
@@ -128,7 +130,7 @@ export const judge = async (ctx: Context) => {
ctx.end({
status,
score: status === STATUS.STATUS_ACCEPTED ? 100 : 0,
- time_ms: Math.floor(time_usage_ms * 1000000) / 1000000,
- memory_kb: memory_usage_kb,
+ time: Math.floor(time_usage_ms * 1000000) / 1000000,
+ memory: memory_usage_kb,
});
};
diff --git a/packages/hydrojudge/src/judge/submit_answer.ts b/packages/hydrojudge/src/judge/submit_answer.ts
index 67096be5c..699f58cdd 100644
--- a/packages/hydrojudge/src/judge/submit_answer.ts
+++ b/packages/hydrojudge/src/judge/submit_answer.ts
@@ -5,10 +5,8 @@ import { check, compileChecker } from '../check';
import { getConfig } from '../config';
import { FormatError } from '../error';
import { del, run } from '../sandbox';
-import { parseFilename } from '../utils';
-import {
- Case, Context, ContextSubTask, SubTask,
-} from './interface';
+import { NormalizedCase, NormalizedSubtask, parseFilename } from '../utils';
+import { Context, ContextSubTask } from './interface';
const Score = {
sum: (a: number, b: number) => (a + b),
@@ -16,18 +14,18 @@ const Score = {
min: Math.min,
};
-function judgeCase(c: Case, sid: string) {
+function judgeCase(c: NormalizedCase, sid: string) {
return async (ctx: Context, ctxSubtask: ContextSubTask) => {
if (ctx.errored || (ctx.failed[sid] && ctxSubtask.subtask.type === 'min')
|| (ctxSubtask.subtask.type === 'max' && ctxSubtask.score === ctxSubtask.subtask.score)
- || ((ctxSubtask.subtask.if || []).filter((i: string) => ctx.failed[i]).length)
+ || ((ctxSubtask.subtask.if || []).filter((i) => ctx.failed[i]).length)
) {
ctx.next({
case: {
status: STATUS.STATUS_CANCELED,
score: 0,
- time_ms: 0,
- memory_kb: 0,
+ time: 0,
+ memory: 0,
message: '',
},
addProgress: 100 / ctx.config.count,
@@ -74,7 +72,7 @@ function judgeCase(c: Case, sid: string) {
user_stdout: file,
user_stderr: { content: '' },
checker_type: ctx.config.checker_type,
- score: ctxSubtask.subtask.score,
+ score: c.score,
detail: ctx.config.detail ?? true,
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
});
@@ -87,8 +85,8 @@ function judgeCase(c: Case, sid: string) {
case: {
status,
score: 0,
- time_ms: 0,
- memory_kb: 0,
+ time: 0,
+ memory: 0,
message,
},
addProgress: 100 / ctx.config.count,
@@ -96,7 +94,7 @@ function judgeCase(c: Case, sid: string) {
};
}
-function judgeSubtask(subtask: SubTask, sid: string) {
+function judgeSubtask(subtask: NormalizedSubtask, sid: string) {
return async (ctx: Context) => {
subtask.type = subtask.type || 'min';
const ctxSubtask = {
@@ -124,9 +122,6 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.');
startPromise.then(() => ctx.next({ status: STATUS.STATUS_COMPILING }));
ctx.checker = await (() => {
- if (['default', 'strict'].includes(ctx.config.checker_type || 'default')) {
- return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
- }
const copyIn = { user_code: ctx.code };
for (const file of ctx.config.judge_extra_files) {
copyIn[parseFilename(file)] = { src: file };
@@ -160,7 +155,7 @@ export const judge = async (ctx: Context, startPromise = Promise.resolve()) => {
ctx.end({
status: ctx.total_status,
score: ctx.total_score,
- time_ms: 0,
- memory_kb: 0,
+ time: 0,
+ memory: 0,
});
};
diff --git a/packages/hydrojudge/src/log.ts b/packages/hydrojudge/src/log.ts
index 611e5da29..b67edf409 100644
--- a/packages/hydrojudge/src/log.ts
+++ b/packages/hydrojudge/src/log.ts
@@ -1,29 +1,9 @@
import { format, inspect, InspectOptions } from 'util';
import cac from 'cac';
+import { Time } from '@hydrooj/utils/lib/utils';
const argv = cac().parse();
-namespace Time {
- export const second = 1000;
- export const minute = second * 60;
- export const hour = minute * 60;
- export const day = hour * 24;
- export const week = day * 7;
- export function formatTimeShort(ms: number) {
- const abs = Math.abs(ms);
- if (abs >= day - hour / 2) {
- return `${Math.round(ms / day)}d`;
- } if (abs >= hour - minute / 2) {
- return `${Math.round(ms / hour)}h`;
- } if (abs >= minute - second / 2) {
- return `${Math.round(ms / minute)}m`;
- } if (abs >= second) {
- return `${Math.round(ms / second)}s`;
- }
- return `${ms}ms`;
- }
-}
-
const colors = [
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts
index 86d7f1f3f..feb6a5a0c 100644
--- a/packages/hydrojudge/src/sandbox.ts
+++ b/packages/hydrojudge/src/sandbox.ts
@@ -1,5 +1,6 @@
import cac from 'cac';
import fs from 'fs-extra';
+import PQueue from 'p-queue';
import { ParseEntry } from 'shell-quote';
import { STATUS } from '@hydrooj/utils/lib/status';
import { getConfig } from './config';
@@ -191,3 +192,9 @@ export async function run(execute: string, params?: Parameter): Promise run(execute, params), { priority });
+}
diff --git a/packages/hydrojudge/src/service.ts b/packages/hydrojudge/src/service.ts
index cace7bd8f..a8ac4f3c0 100644
--- a/packages/hydrojudge/src/service.ts
+++ b/packages/hydrojudge/src/service.ts
@@ -7,9 +7,10 @@ import fs from 'fs-extra';
import { noop } from 'lodash';
import { ObjectID } from 'mongodb';
import { STATUS } from '@hydrooj/utils/lib/status';
+import type { JudgeResultBody } from 'hydrooj';
import { Logger } from 'hydrooj/src/logger';
import * as monitor from 'hydrooj/src/service/monitor';
-import readCases from './cases';
+import readCases, { processTestdata } from './cases';
import { getConfig } from './config';
import { CompileError, FormatError, SystemError } from './error';
import { CopyInFile } from './sandbox/interface';
@@ -22,7 +23,6 @@ declare module 'hydrooj/src/interface' {
'hydrojudge.cache_dir': string;
'hydrojudge.tmp_dir': string;
'hydrojudge.tmpfs_size': string;
- 'hydrojudge.retry_delay_sec': number;
'hydrojudge.sandbox_host': string;
'hydrojudge.memoryMax': string;
'hydrojudge.testcases_max': number;
@@ -53,28 +53,6 @@ async function postInit() {
monitor.updateJudge({ mid, ...inf });
}, 1200000);
- async function processData(folder: string) {
- let files = await fs.readdir(folder);
- let ini = false;
- for (const i of files) {
- if (i.toLowerCase() === 'config.ini') {
- ini = true;
- await fs.rename(`${folder}/${i}`, `${folder}/config.ini`);
- break;
- }
- }
- if (ini) {
- for (const i of files) {
- if (i.toLowerCase() === 'input') await fs.rename(`${folder}/${i}`, `${folder}/input`);
- else if (i.toLowerCase() === 'output') await fs.rename(`${folder}/${i}`, `${folder}/output`);
- }
- files = await fs.readdir(`${folder}/input`);
- for (const i of files) await fs.rename(`${folder}/input/${i}`, `${folder}/input/${i.toLowerCase()}`);
- files = await fs.readdir(`${folder}/output`);
- for (const i of files) await fs.rename(`${folder}/output/${i}`, `${folder}/output/${i.toLowerCase()}`);
- }
- }
-
async function fetchCodeFile(name: string) {
const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_'));
await storage.get(`submission/${name}`, target);
@@ -103,39 +81,23 @@ async function postInit() {
}
fs.writeFileSync(path.join(filePath, 'etags'), JSON.stringify(version));
fs.writeFileSync(path.join(filePath, 'lastUsage'), new Date().getTime().toString());
- await processData(filePath).catch(noop);
+ await processTestdata(filePath);
return filePath;
}
function getNext(that) {
- return (data, id = 0) => {
+ return (data: Partial, id = 0) => {
logger.debug('Next: %d %o', id, data);
data.rid = new ObjectID(that.rid);
- if (data.time_ms) data.time = data.time_ms;
- if (data.memory_kb) data.memory = data.memory_kb;
- if (data.compiler_text) data.compilerText = data.compiler_text;
- delete data.time_ms;
- delete data.memory_kb;
- delete data.compiler_text;
- if (data.case) {
- data.case = {
- id,
- status: data.case.status,
- time: data.case.time_ms || data.case.time,
- memory: data.case.memory_kb || data.case.memory,
- message: data.case.message || data.case.judgeText || '',
- };
- }
+ if (data.case) data.case.id ||= id;
_judge.next(data);
};
}
function getEnd(rid: string) {
- return (data) => {
+ return (data: Partial) => {
data.key = 'end';
data.rid = new ObjectID(rid);
- data.time = data.time_ms ?? data.time;
- data.memory = data.memory_kb ?? data.memory;
logger.info('End: status=%d score=%d time=%dms memory=%dkb', data.status, data.score, data.time, data.memory);
_judge.end(data);
};
@@ -160,8 +122,8 @@ async function postInit() {
config: any;
env: any;
input?: string;
- next: (data: any, id?: number) => void;
- end: (data: any) => void;
+ next: ReturnType;
+ end: ReturnType;
tmpdir: string;
clean: (() => Promise)[];
folder: string;
@@ -204,20 +166,20 @@ async function postInit() {
await this.submission();
} catch (e) {
if (e instanceof CompileError) {
- this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
+ this.next({ compilerText: compilerText(e.stdout, e.stderr) });
this.end({
- status: STATUS.STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_COMPILE_ERROR, score: 0, time: 0, memory: 0,
});
} else if (e instanceof FormatError) {
this.next({ message: { message: e.message, params: e.params } });
this.end({
- status: STATUS.STATUS_FORMAT_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_FORMAT_ERROR, score: 0, time: 0, memory: 0,
});
} else {
logger.error(e);
this.next({ message: { message: e.message, params: e.params, ...process.env.DEV ? { stack: e.stack } : {} } });
this.end({
- status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
+ status: STATUS.STATUS_SYSTEM_ERROR, score: 0, time: 0, memory: 0,
});
}
}
diff --git a/packages/hydrojudge/src/sysinfo.ts b/packages/hydrojudge/src/sysinfo.ts
index 1c4650a57..ac2c0f63d 100644
--- a/packages/hydrojudge/src/sysinfo.ts
+++ b/packages/hydrojudge/src/sysinfo.ts
@@ -1,3 +1,4 @@
+// TODO this should be moved into web
import path from 'path';
import fs from 'fs-extra';
import { noop } from 'lodash';
@@ -30,6 +31,8 @@ int main(){
type: 'default',
time: '3s',
memory: '256m',
+ user_extra_files: [],
+ judge_extra_files: [],
},
stat: {},
clean: [],
diff --git a/packages/hydrojudge/src/testlib.ts b/packages/hydrojudge/src/testlib.ts
index d940a89b2..de824998a 100644
--- a/packages/hydrojudge/src/testlib.ts
+++ b/packages/hydrojudge/src/testlib.ts
@@ -29,7 +29,7 @@ export function parse(output: string, fullscore: number) {
if (p === 1) {
status = STATUS.STATUS_ACCEPTED;
score = fullscore;
- const base = output.split('points ')[1] || '';
+ const base = output.replace('points ', '') || '';
message = base.substring(base.indexOf(' '), 1024);
} else score = Math.floor(fullscore * p);
}
diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json
index ceb433fb3..4cc64ebed 100644
--- a/packages/hydrooj/package.json
+++ b/packages/hydrooj/package.json
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
- "version": "3.12.1",
+ "version": "3.13.8",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
@@ -12,7 +12,7 @@
},
"preferUnplugged": true,
"dependencies": {
- "@graphql-tools/schema": "^8.3.6",
+ "@graphql-tools/schema": "^8.3.11",
"@hydrooj/utils": "workspace:*",
"adm-zip": "0.5.5",
"ajv": "^8.11.0",
@@ -23,32 +23,32 @@
"emoji-regex": "^10.1.0",
"emojis-list": "2.1.0",
"esbuild": "0.14.3",
- "fs-extra": "^10.0.1",
- "graphql": "^16.3.0",
+ "fs-extra": "^10.1.0",
+ "graphql": "^16.5.0",
"graphql-scalars": "1.17.0",
"isbinaryfile": "^5.0.0",
"js-yaml": "^4.1.0",
"koa": "^2.13.4",
- "koa-body": "^4.2.0",
+ "koa-body": "^5.0.0",
"koa-compress": "^5.1.0",
"koa-proxies": "^0.12.2",
"koa-router": "^10.1.1",
"koa-static-cache": "^5.1.4",
"lodash": "^4.17.21",
- "lru-cache": "7.7.3",
+ "lru-cache": "7.9.0",
"mime-types": "^2.1.35",
"minio": "7.0.25",
"moment-timezone": "^0.5.34",
"mongodb": "^3.7.3",
- "nanoid": "^3.3.2",
- "nodemailer": "^6.7.3",
+ "nanoid": "^3.3.4",
+ "nodemailer": "^6.7.5",
"notp": "^2.0.3",
"p-queue": "^7.2.0",
"require-resolve-hook": "^1.1.0",
- "semver": "^7.3.6",
+ "semver": "^7.3.7",
"serialize-javascript": "^6.0.0",
"source-map-support": "^0.5.21",
- "superagent": "^7.1.2",
+ "superagent": "^7.1.3",
"thirty-two": "^1.0.2",
"tx2": "^1.0.5"
},
@@ -60,7 +60,7 @@
"@types/koa-compress": "^4.0.3",
"@types/koa-router": "^7.4.4",
"@types/koa-static-cache": "^5.1.1",
- "@types/lodash": "^4.14.181",
+ "@types/lodash": "^4.14.182",
"@types/lru-cache": "^7.6.1",
"@types/mime-types": "^2.1.1",
"@types/minio": "^7.0.12",
@@ -71,7 +71,7 @@
"@types/serialize-javascript": "^5.0.2",
"@types/source-map-support": "^0.5.4",
"@types/superagent": "^4.1.15",
- "moment": "^2.29.2",
+ "moment": "^2.29.3",
"pm2": "^5.2.0"
},
"peerDependencies": {
diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts
index 5a15bb04a..4cfb71851 100644
--- a/packages/hydrooj/src/entry/common.ts
+++ b/packages/hydrooj/src/entry/common.ts
@@ -178,6 +178,7 @@ export async function service(pending: string[], fail: string[]) {
}
}
for (const key in global.Hydro.service) {
+ if (key === 'server') continue;
const srv = global.Hydro.service[key];
if (!srv.started && srv.start) await srv.start();
}
diff --git a/packages/hydrooj/src/handler/compat.ts b/packages/hydrooj/src/handler/compat.ts
new file mode 100644
index 000000000..f25fb23d6
--- /dev/null
+++ b/packages/hydrooj/src/handler/compat.ts
@@ -0,0 +1,11 @@
+import { Handler, Route } from '../service/server';
+
+class ProblemCategoryCompatHandler extends Handler {
+ async get({ category }) {
+ this.response.redirect = this.url('problem_main', { query: { q: `category:${category}` } });
+ }
+}
+
+global.Hydro.handler.compat = () => {
+ Route('problem_category_compat', '/p/category/:category', ProblemCategoryCompatHandler);
+};
diff --git a/packages/hydrooj/src/handler/discussion.ts b/packages/hydrooj/src/handler/discussion.ts
index 98b39e9f4..7b4743445 100644
--- a/packages/hydrooj/src/handler/discussion.ts
+++ b/packages/hydrooj/src/handler/discussion.ts
@@ -240,7 +240,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
const msg = JSON.stringify({
message: 'User {0} mentioned you in {1:link}',
- params: [this.user.uname, this.request.path],
+ params: [this.user.uname, `/d/${domainId}${this.request.path}`],
});
for (const uid of uids) {
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
@@ -259,7 +259,7 @@ class DiscussionDetailHandler extends DiscussionHandler {
const uids = Object.keys(await user.getList(domainId, Array.from(targets))).map((i) => +i);
const msg = JSON.stringify({
message: 'User {0} mentioned you in {1:link}',
- params: [this.user.uname, this.request.path],
+ params: [this.user.uname, `/d/${domainId}${this.request.path}`],
});
for (const uid of uids) {
message.send(1, uid, msg, message.FLAG_RICHTEXT | message.FLAG_UNREAD);
diff --git a/packages/hydrooj/src/handler/import.ts b/packages/hydrooj/src/handler/import.ts
index 73b4d3cf1..b59d092ab 100644
--- a/packages/hydrooj/src/handler/import.ts
+++ b/packages/hydrooj/src/handler/import.ts
@@ -37,7 +37,7 @@ class ProblemImportHydroHandler extends Handler {
if (keepUser) this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
if (!this.request.files.file) throw new ValidationError('file');
const tmpdir = path.join(os.tmpdir(), 'hydro', `${Math.random()}.import`);
- const zip = new AdmZip(this.request.files.file.path);
+ const zip = new AdmZip(this.request.files.file.filepath);
await new Promise((resolve, reject) => {
zip.extractAllToAsync(tmpdir, true, (err) => {
if (err) reject(err);
diff --git a/packages/hydrooj/src/handler/index.ts b/packages/hydrooj/src/handler/index.ts
index c72beb7f5..d70cd464f 100644
--- a/packages/hydrooj/src/handler/index.ts
+++ b/packages/hydrooj/src/handler/index.ts
@@ -14,3 +14,4 @@ import './domain';
import './status';
import './api';
import './blog';
+import './compat';
diff --git a/packages/hydrooj/src/handler/judge.ts b/packages/hydrooj/src/handler/judge.ts
index e1fb46ee9..283289c52 100644
--- a/packages/hydrooj/src/handler/judge.ts
+++ b/packages/hydrooj/src/handler/judge.ts
@@ -41,19 +41,19 @@ export async function postJudge(rdoc: RecordDoc) {
await bus.serial('record/judge', rdoc, updated);
}
-export async function next(body: JudgeResultBody) {
+export async function next(body: Partial) {
body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.rid);
if (!rdoc) return;
const $set: Partial = {};
const $push: any = {};
if (body.case) {
- const c: TestCase = {
+ const c: Required = {
+ ...body.case,
id: body.case.id || 0,
- memory: body.case.memory,
- time: body.case.time,
+ subtaskId: body.case.subtaskId || 0,
+ score: body.case.score || 0,
message: body.case.message || '',
- status: body.case.status,
};
rdoc.testCases.push(c);
$push.testCases = c;
@@ -75,7 +75,7 @@ export async function next(body: JudgeResultBody) {
bus.broadcast('record/change', rdoc!, $set, $push);
}
-export async function end(body: JudgeResultBody) {
+export async function end(body: Partial) {
if (body.rid) body.rid = new ObjectID(body.rid);
let rdoc = await record.get(body.rid);
if (!rdoc) return;
diff --git a/packages/hydrooj/src/handler/misc.ts b/packages/hydrooj/src/handler/misc.ts
index 5c1a98d00..f2776ca0f 100644
--- a/packages/hydrooj/src/handler/misc.ts
+++ b/packages/hydrooj/src/handler/misc.ts
@@ -49,16 +49,17 @@ export class FilesHandler extends Handler {
if ((this.user._files?.length || 0) >= system.get('limit.user_files')) {
throw new ForbiddenError('File limit exceeded.');
}
- if (!this.request.files.file) throw new ValidationError('file');
- const f = statSync(this.request.files.file.path);
+ const file = this.request.files?.file;
+ if (!file) throw new ValidationError('file');
+ const f = statSync(file.filepath);
const size = Math.sum((this.user._files || []).map((i) => i.size)) + f.size;
if (size >= system.get('limit.user_files_size')) {
throw new ForbiddenError('File size limit exceeded.');
}
- if (!filename) filename = this.request.files.file.name || String.random(16);
+ if (!filename) filename = file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
if (this.user._files.filter((i) => i.name === filename).length) throw new BadRequestError('file exists');
- await storage.put(`user/${this.user._id}/${filename}`, this.request.files.file.path, this.user._id);
+ await storage.put(`user/${this.user._id}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`user/${this.user._id}/${filename}`);
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
if (!meta) throw new Error('Upload failed');
@@ -90,7 +91,7 @@ export class FSDownloadHandler extends Handler {
@param('noDisposition', Types.Boolean)
async get(domainId: string, uid: number, filename: string, noDisposition = false) {
const targetUser = await user.getById('system', uid);
- if (!targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new ForbiddenError('Access denied');
+ if (this.user._id !== uid && !targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new ForbiddenError('Access denied');
this.response.addHeader('Cache-Control', 'public');
const target = `user/${uid}/${filename}`;
const file = await storage.getMeta(target);
diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts
index 36b747ba3..00760623b 100644
--- a/packages/hydrooj/src/handler/problem.ts
+++ b/packages/hydrooj/src/handler/problem.ts
@@ -1,10 +1,10 @@
import AdmZip from 'adm-zip';
import { readFile, statSync } from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
-import { intersection, isSafeInteger } from 'lodash';
+import { flattenDeep, intersection, isSafeInteger } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
-import { sortFiles } from '@hydrooj/utils/lib/utils';
+import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
ContestNotFoundError, ContestNotLiveError,
@@ -125,7 +125,7 @@ export class ProblemHandler extends Handler {
page, ppcount, pcount, pdocs, psdict, qs,
}),
this.renderHTML('partials/problem_stat.html', { pcount, pcountRelation }),
- this.renderHTML('partials/problem_lucky.html', { category }),
+ this.renderHTML('partials/problem_lucky.html', { qs }),
])).map((i) => ({ html: i })),
raw: {
page, pcount, ppcount, pdocs, psdict, category,
@@ -138,8 +138,7 @@ export class ProblemHandler extends Handler {
export class ProblemMainHandler extends ProblemHandler {
@param('page', Types.PositiveInt, true)
@param('q', Types.Content, true)
- @param('category', Types.Name, true, null, parseCategory)
- async get(domainId: string, page = 1, q = '', category = []) {
+ async get(domainId: string, page = 1, q = '') {
this.response.template = 'problem_main.html';
// eslint-disable-next-line @typescript-eslint/no-shadow
const query = buildQuery(this.user);
@@ -148,11 +147,15 @@ export class ProblemMainHandler extends ProblemHandler {
let sort: string[];
let fail = false;
let pcountRelation = 'eq';
+ const category = flattenDeep(decodeURIComponent(q).split(' ')
+ .filter((i) => i.startsWith('category:'))
+ .map((i) => i.split('category:')[1]?.split(',')));
+ const text = q.split(' ').filter((i) => !i.startsWith('category:')).join(' ');
if (category.length) query.$and = category.map((tag) => ({ tag }));
- if (q) category.push(q);
+ if (text) category.push(text);
if (category.length) this.UiContext.extraTitleContent = category.join(',');
let total = 0;
- if (q) {
+ if (text) {
if (search) {
const result = await search(domainId, q, { skip: (page - 1) * system.get('pagination.problem') });
total = result.total;
@@ -166,7 +169,7 @@ export class ProblemMainHandler extends ProblemHandler {
}),
});
sort = result.hits;
- } else query.$text = { $search: q };
+ } else query.$text = { $search: text };
}
await bus.serial('problem/list', query, this);
// eslint-disable-next-line prefer-const
@@ -209,7 +212,7 @@ export class ProblemMainHandler extends ProblemHandler {
pdocs,
psdict,
category: category.join(','),
- qs: q ? `q=${encodeURIComponent(q)}` : '',
+ qs: q,
};
}
@@ -281,8 +284,11 @@ export class ProblemMainHandler extends ProblemHandler {
}
export class ProblemRandomHandler extends ProblemHandler {
- @param('category', Types.Name, true, null, parseCategory)
- async get(domainId: string, category: string[] = []) {
+ @param('q', Types.Content, true)
+ async get(domainId: string, qs = '') {
+ const category = flattenDeep(decodeURIComponent(qs).split(' ')
+ .filter((i) => i.startsWith('category:'))
+ .map((i) => i.split('category:')[1]?.split(',')));
const q = buildQuery(this.user);
if (category.length) q.$and = category.map((tag) => ({ tag }));
await bus.serial('problem/list', q, this);
@@ -486,13 +492,13 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
if (typeof config !== 'object' || config === null) throw new BadRequestError('Incorrect problem config');
const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : 65535;
if (file.size > sizeLimit) throw new ValidationError('file');
- if (file.size > 65535 || await isBinaryFile(file.path, file.size)) {
+ if (file.size > 65535 || await isBinaryFile(file.filepath, file.size)) {
const id = nanoid();
- await storage.put(`submission/${this.user._id}/${id}`, file.path, this.user._id);
- code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.name}`;
+ await storage.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id);
+ code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.originalFilename}`;
} else {
// TODO auto detect & convert encoding
- code = await readFile(file.path, 'utf-8');
+ code = await readFile(file.filepath, 'utf-8');
}
} else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code');
await this.limitRate('add_record', 60, system.get('limit.submission'));
@@ -525,6 +531,7 @@ export class ProblemManageHandler extends ProblemDetailHandler {
export class ProblemEditHandler extends ProblemManageHandler {
async get() {
+ this.response.body.additional_file = sortFiles(this.pdoc.additional_file || []);
this.response.template = 'problem_edit.html';
}
@@ -550,6 +557,23 @@ export class ProblemEditHandler extends ProblemManageHandler {
}
}
+export class ProblemConfigHandler extends ProblemManageHandler {
+ async get() {
+ if (this.pdoc.reference) throw new ForbiddenError('Cannot edit config of a referenced problem.');
+ this.response.body.testdata = sortFiles(this.pdoc.data || []);
+ const configFile = (this.pdoc.data || []).filter((i) => i.name.toLowerCase() === 'config.yaml');
+ this.response.body.config = '';
+ if (configFile.length > 0) {
+ try {
+ this.response.body.config = (await streamToBuffer(
+ await storage.get(`problem/${this.pdoc.domainId}/${this.pdoc.docId}/testdata/${configFile[0].name}`),
+ )).toString();
+ } catch (e) { /* ignore */ }
+ }
+ this.response.template = 'problem_config.html';
+ }
+}
+
export class ProblemFilesHandler extends ProblemDetailHandler {
notUsage = true;
@@ -608,12 +632,12 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit files of a referenced problem.');
if (!this.request.files.file) throw new ValidationError('file');
- if (!filename) filename = this.request.files.file.name || String.random(16);
+ if (!filename) filename = this.request.files.file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const files = [];
if (filename.endsWith('.zip')) {
- const zip = new AdmZip(this.request.files.file.path);
+ const zip = new AdmZip(this.request.files.file.filepath);
const entries = zip.getEntries();
for (const entry of entries) {
if (!entry.name) continue;
@@ -628,8 +652,8 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
files.push({
type,
name: filename,
- size: statSync(this.request.files.file.path).size,
- data: () => this.request.files.file.path,
+ size: statSync(this.request.files.file.filepath).size,
+ data: () => this.request.files.file.filepath,
});
}
if (!this.user.hasPriv(PRIV.PRIV_EDIT_SYSTEM)) {
@@ -831,6 +855,7 @@ export class ProblemCreateHandler extends Handler {
this.response.template = 'problem_edit.html';
this.response.body = {
page_name: 'problem_create',
+ additional_file: [],
};
}
@@ -887,11 +912,11 @@ export class ProblemPrefixListHandler extends Handler {
export async function apply() {
Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
- Route('problem_category', '/p/category/:category', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
+ Route('problem_config', '/p/:pid/config', ProblemConfigHandler);
Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_file_download', '/p/:pid/file/:filename', ProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
Route('problem_solution', '/p/:pid/solution', ProblemSolutionHandler, PERM.PERM_VIEW_PROBLEM);
diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts
index 6a90f5411..b21152259 100644
--- a/packages/hydrooj/src/handler/record.ts
+++ b/packages/hydrooj/src/handler/record.ts
@@ -183,7 +183,7 @@ class RecordDetailHandler extends Handler {
time: 0,
memory: 0,
testCases: [{
- status: 9, score: 0, time: 0, memory: 0, message: 'score canceled',
+ id: 0, subtaskId: 0, status: 9, score: 0, time: 0, memory: 0, message: 'score canceled',
}],
};
const [latest] = await Promise.all([
diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts
index f6ea50295..34a7d902a 100644
--- a/packages/hydrooj/src/interface.ts
+++ b/packages/hydrooj/src/interface.ts
@@ -2,7 +2,6 @@ import type fs from 'fs';
import type { Dictionary, NumericDictionary } from 'lodash';
import type { ItemBucketMetadata } from 'minio';
import type { Cursor, ObjectID } from 'mongodb';
-import type { Readable, Writable } from 'stream';
import type { ProblemDoc } from './model/problem';
type document = typeof import('./model/document');
@@ -127,10 +126,11 @@ export interface FileInfo {
}
export interface TestCaseConfig {
- input: string,
- output: string,
- time?: number,
- memory?: number,
+ input: string;
+ output: string;
+ time?: string;
+ memory?: string;
+ score?: number;
}
export enum ProblemType {
@@ -147,8 +147,8 @@ export enum SubtaskType {
}
export interface SubtaskConfig {
- time?: number;
- memory?: number;
+ time?: string;
+ memory?: string;
score?: number;
if?: number[];
id?: number;
@@ -207,6 +207,7 @@ export interface SampleContentNode {
sectionTitle: string,
payload: [string, string],
}
+// TODO drop contentNode support
export type ContentNode = PlainContentNode | TextContentNode | SampleContentNode;
export type Content = string | ContentNode[] | Record;
@@ -271,6 +272,8 @@ export interface ProblemStatusDoc extends StatusDoc {
export interface TestCase {
id?: number;
+ subtaskId?: number;
+ score?: number;
time: number;
memory: number;
status: number;
@@ -287,9 +290,9 @@ export interface RecordDoc {
score: number;
memory: number;
time: number;
- judgeTexts: string[];
+ judgeTexts: (string | JudgeMessage)[];
compilerTexts: string[];
- testCases: TestCase[];
+ testCases: Required[];
rejudged: boolean;
source?: string;
/** judge uid */
@@ -496,24 +499,25 @@ export interface Script {
validate: any,
}
+export interface JudgeMessage {
+ message: string;
+ params?: string[];
+ stack?: string;
+}
+
export interface JudgeResultBody {
+ key: string;
domainId: string;
rid: ObjectID;
judger?: number;
progress?: number;
addProgress?: number;
- case?: {
- id?: number;
- status: number;
- time: number;
- memory: number;
- message?: string;
- },
+ case?: TestCase,
status?: number;
score?: number;
time?: number;
memory?: number;
- message?: string;
+ message?: string | JudgeMessage;
compilerText?: string,
}
@@ -526,19 +530,6 @@ export interface Task {
[key: string]: any;
}
-export interface UploadStream extends Writable {
- id: ObjectID;
-}
-
-export interface HydroFileSystem {
- openUploadStream: (filename: string) => UploadStream
- openDownloadStream: (_id: ObjectID) => Readable
- // TODO
- find: (...args: any[]) => any
-}
-
-export type PathComponent = [string, string, Dictionary?, boolean?];
-
export interface BaseService {
started: boolean;
error?: Error | string;
@@ -636,7 +627,6 @@ export interface Model {
export interface Service {
bus: typeof import('./service/bus'),
db: typeof import('./service/db'),
- fs: HydroFileSystem,
monitor: typeof import('./service/monitor'),
server: typeof import('./service/server'),
storage: typeof import('./service/storage'),
diff --git a/packages/hydrooj/src/lib/difficulty.ts b/packages/hydrooj/src/lib/difficulty.ts
index c168f47c1..87d924d34 100644
--- a/packages/hydrooj/src/lib/difficulty.ts
+++ b/packages/hydrooj/src/lib/difficulty.ts
@@ -6,7 +6,7 @@ const _CACHE_INFO = {
function _LOGP(x: number) {
const sqrtPi = 2.506628274631; // Sqrt[Pi]
- return (2 * Math.exp(-1.0 * (Math.log(x) ** 2) * 2)) / x / sqrtPi;
+ return (2 * Math.exp(-2.0 * (Math.log(x) ** 2))) / x / sqrtPi;
}
function _intergrateEnsureCache(y: number) {
@@ -27,7 +27,7 @@ function _intergrateEnsureCache(y: number) {
return _CACHE_INFO;
}
-_intergrateEnsureCache(1000000);
+_intergrateEnsureCache(10000);
function _integrate(y: number) {
_intergrateEnsureCache(y);
@@ -38,7 +38,7 @@ function difficultyAlgorithm(nSubmit: number, nAccept: number) {
if (!nSubmit) return null;
const s = _integrate(nSubmit);
const acRate = nAccept / nSubmit;
- const ans = Math.round(10.0 - 1.30 * s * 10.0 * acRate);
+ const ans = Math.round(10 - 13 * s * acRate);
return Math.max(ans, 1);
}
diff --git a/packages/hydrooj/src/lib/testdataConfig.ts b/packages/hydrooj/src/lib/testdataConfig.ts
index 3607b5d06..c6a1fde45 100644
--- a/packages/hydrooj/src/lib/testdataConfig.ts
+++ b/packages/hydrooj/src/lib/testdataConfig.ts
@@ -1,13 +1,12 @@
import { load } from 'js-yaml';
import readYamlCases from '@hydrooj/utils/lib/cases';
import type { ProblemConfig, ProblemConfigFile } from '../interface';
+import { normalizeSubtasks, parseMemoryMB, parseTimeMS } from '../utils';
export async function parseConfig(config: string | ProblemConfigFile = {}) {
- let cfg: ProblemConfigFile = {};
- if (typeof config === 'string') {
- // TODO should validate here?
- cfg = await readYamlCases(load(config) as Record);
- } else if (typeof config === 'object') cfg = await readYamlCases(config);
+ const cfg: ProblemConfigFile = typeof config === 'string'
+ ? await readYamlCases(load(config) as Record)
+ : await readYamlCases(config);
const result: ProblemConfig = {
count: 0,
memoryMin: Number.MAX_SAFE_INTEGER,
@@ -18,23 +17,19 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
};
if (cfg.subType) result.subType = cfg.subType;
if (cfg.target) result.target = cfg.target;
- if (cfg.subtasks.length) {
- for (const subtask of cfg.subtasks) {
- result.memoryMax = Math.max(result.memoryMax, subtask.memory);
- result.memoryMin = Math.min(result.memoryMin, subtask.memory);
- result.timeMax = Math.max(result.timeMax, subtask.time);
- result.timeMin = Math.min(result.timeMin, subtask.time);
+ if (cfg.subtasks?.length) {
+ for (const subtask of normalizeSubtasks(cfg.subtasks as any || [], (i) => i, cfg.time, cfg.memory)) {
+ result.memoryMax = Math.max(result.memoryMax, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
+ result.memoryMin = Math.min(result.memoryMin, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));
+ result.timeMax = Math.max(result.timeMax, ...subtask.cases.map((i) => parseTimeMS(i.time)));
+ result.timeMin = Math.min(result.timeMin, ...subtask.cases.map((i) => parseTimeMS(i.time)));
}
- } else if (cfg.time || cfg.memory) {
+ } else {
if (cfg.time) result.timeMax = result.timeMin = cfg.time as unknown as number;
if (cfg.memory) result.memoryMax = result.memoryMin = cfg.memory as unknown as number;
}
- if (result.memoryMax < result.memoryMin) {
- result.memoryMax = result.memoryMin = 256;
- }
- if (result.timeMax < result.timeMin) {
- result.timeMax = result.timeMin = 1000;
- }
+ if (result.memoryMax < result.memoryMin) result.memoryMax = result.memoryMin = 256;
+ if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
if (cfg.langs) result.langs = cfg.langs;
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
if (cfg.filename && result.type === 'default') {
diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts
index 4e355638d..3703e8b96 100644
--- a/packages/hydrooj/src/model/contest.ts
+++ b/packages/hydrooj/src/model/contest.ts
@@ -50,7 +50,7 @@ const acm = buildContestRule({
check: () => { },
statusSort: { accept: -1, time: 1 },
submitAfterAccept: false,
- showScoreboard: () => true,
+ showScoreboard: (tdoc, now) => now > tdoc.beginAt,
showSelfRecord: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
@@ -297,7 +297,7 @@ const ioi = buildContestRule({
submitAfterAccept: false,
showRecord: (tdoc, now) => now > tdoc.endAt,
showSelfRecord: () => true,
- showScoreboard: () => true,
+ showScoreboard: (tdoc, now) => now > tdoc.beginAt,
});
const homework = buildContestRule({
diff --git a/packages/hydrooj/src/service/layers/renderer.ts b/packages/hydrooj/src/service/layers/renderer.ts
index 566fab208..e7544fcce 100644
--- a/packages/hydrooj/src/service/layers/renderer.ts
+++ b/packages/hydrooj/src/service/layers/renderer.ts
@@ -40,7 +40,7 @@ export default (router, logger) => async (ctx: KoaContext, next) => {
}
for (const key in kwargs.query || {}) {
if (query[key] instanceof ObjectID) query[key] = kwargs.query[key].toHexString();
- else query[key] = encodeURIComponent(kwargs.query[key].toString());
+ else query[key] = kwargs.query[key].toString();
}
try {
const { anchor } = args;
@@ -52,7 +52,7 @@ export default (router, logger) => async (ctx: KoaContext, next) => {
|| (host instanceof Array
? (!host.includes(request.host))
: request.host !== host)
- )) withDomainId = domainId;
+ )) withDomainId ||= domainId;
res = router.url(name, args, { query }).toString();
if (anchor) res = `${res}#${anchor}`;
if (withDomainId) res = `/d/${withDomainId}${res}`;
diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts
index c15ceaee7..417ed376c 100644
--- a/packages/hydrooj/src/service/server.ts
+++ b/packages/hydrooj/src/service/server.ts
@@ -95,7 +95,7 @@ app.on('error', (error) => {
}
});
wsServer.on('error', (error) => {
- console.log(123, error);
+ console.log('Websocket server error:', error);
});
const ignoredLimit = `,${argv.options.ignoredLimit},`;
@@ -139,7 +139,7 @@ ${ctx.response.status} ${endTime - startTime}ms ${ctx.response.length}`);
}));
const layers = [baseLayer, rendererLayer(router, logger), responseLayer(router), userLayer];
app.use(async (ctx, next) => await next().catch(console.error)).use(domainLayer);
- layers.forEach((layer) => router.use(layer));
+ layers.forEach((layer) => router.use(layer as any));
wsServer.on('connection', async (socket, request) => {
const ctx: any = app.createContext(request, {} as any);
await domainLayer(ctx, () => baseLayer(ctx, () => layers[1](ctx, () => userLayer(ctx, () => { }))));
diff --git a/packages/import-qduoj/handler.ts b/packages/import-qduoj/handler.ts
index d34319add..44758be22 100644
--- a/packages/import-qduoj/handler.ts
+++ b/packages/import-qduoj/handler.ts
@@ -119,9 +119,9 @@ class ImportQduojHandler extends Handler {
async post({ domainId }) {
if (!this.request.files.file) throw new ValidationError('file');
- const stat = await fs.stat(this.request.files.file.path);
+ const stat = await fs.stat(this.request.files.file.filepath);
if (stat.size > 128 * 1024 * 1024) throw new ValidationError('file', 'File too large');
- await this.fromFile(domainId, this.request.files.file.path);
+ await this.fromFile(domainId, this.request.files.file.filepath);
this.response.redirect = this.url('problem_main');
}
}
diff --git a/packages/import-qduoj/package.json b/packages/import-qduoj/package.json
index cd260cd91..35ab4d7cb 100644
--- a/packages/import-qduoj/package.json
+++ b/packages/import-qduoj/package.json
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/import-qduoj",
- "version": "1.3.3",
+ "version": "1.3.4",
"description": "Import QDUOJ problem export",
"main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git",
@@ -10,6 +10,6 @@
"dependencies": {
"adm-zip": "0.5.5",
"decode-html": "^2.0.0",
- "fs-extra": "^10.0.1"
+ "fs-extra": "^10.1.0"
}
}
diff --git a/packages/login-with-github/package.json b/packages/login-with-github/package.json
index 2e5bbcca4..7df12780e 100644
--- a/packages/login-with-github/package.json
+++ b/packages/login-with-github/package.json
@@ -13,6 +13,6 @@
"hydrooj": "*"
},
"dependencies": {
- "superagent": "^7.1.2"
+ "superagent": "^7.1.3"
}
}
diff --git a/packages/login-with-google/package.json b/packages/login-with-google/package.json
index e951bdd1c..c4e588642 100644
--- a/packages/login-with-google/package.json
+++ b/packages/login-with-google/package.json
@@ -13,6 +13,6 @@
"hydrooj": "*"
},
"dependencies": {
- "superagent": "^7.1.2"
+ "superagent": "^7.1.3"
}
}
diff --git a/packages/migrate-hustoj/README.md b/packages/migrate-hustoj/README.md
deleted file mode 100644
index 7fea86811..000000000
--- a/packages/migrate-hustoj/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# migrate-hustoj
-
-这个模块用于从HustOJ的数据库导入数据。
-
-## 迁移须知
-
-迁移前,您需要指定数据来源。
-迁移过程中,Hydro 的内容会被**清空**,以下内容将被迁移:
-
-题目与测试数据,用户列表。
-
-迁移过程中请保持网络畅通。
diff --git a/packages/migrate-hustoj/locale/zh.yaml b/packages/migrate-hustoj/locale/zh.yaml
deleted file mode 100644
index 989f26537..000000000
--- a/packages/migrate-hustoj/locale/zh.yaml
+++ /dev/null
@@ -1 +0,0 @@
-migrate from hustoj: 从 HustOJ 导入
\ No newline at end of file
diff --git a/packages/migrate-vijos/lib.ts b/packages/migrate-vijos/lib.ts
deleted file mode 100644
index b0eac4509..000000000
--- a/packages/migrate-vijos/lib.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { } from 'hydrooj';
-
-const { md5, sha1 } = global.Hydro.lib;
-
-interface Udoc {
- uname: string,
- unameLower: string,
-}
-
-function hash(password: string, salt: string, udoc: Udoc) {
- const { uname, unameLower } = udoc;
- const pmd5 = md5(password);
- const mixedSha1 = sha1(md5(unameLower + pmd5) + salt + sha1(pmd5 + salt));
- return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`;
-}
-
-global.Hydro.lib['hash.vj2'] = hash;
diff --git a/packages/migrate-vijos/locale/zh.yaml b/packages/migrate-vijos/locale/zh.yaml
deleted file mode 100644
index e66715a60..000000000
--- a/packages/migrate-vijos/locale/zh.yaml
+++ /dev/null
@@ -1 +0,0 @@
-migrate from vijos: 从 Vijos 导入
\ No newline at end of file
diff --git a/packages/migrate-vijos/package.json b/packages/migrate-vijos/package.json
deleted file mode 100644
index dce316e16..000000000
--- a/packages/migrate-vijos/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "@hydrooj/migrate-vijos",
- "version": "1.5.1",
- "main": "package.json",
- "repository": "git@github.com:hydro-dev/Hydro.git",
- "author": "undefined ",
- "preferUnplugged": true,
- "license": "AGPL-3.0-or-later",
- "devDependencies": {
- "@types/mongodb": "^3.6.20"
- },
- "dependencies": {
- "mongodb": "^3.7.3"
- }
-}
diff --git a/packages/migrate-vijos/README.md b/packages/migrate/README.md
similarity index 57%
rename from packages/migrate-vijos/README.md
rename to packages/migrate/README.md
index 146a77293..cf08632fb 100644
--- a/packages/migrate-vijos/README.md
+++ b/packages/migrate/README.md
@@ -1,17 +1,21 @@
-# migrate-vijos
+# migrate
-这个模块用于从Vijos4.0的数据库导入数据。
-## 迁移须知
+这个模块用于从其他系统批量导入数据。
+迁移过程中请保持网络畅通。
+## migrate-vijos
+
+从Vijos4.0的数据库导入数据。
迁移前,您需要指定数据来源。
**请不要**将 Hydro 正在使用的数据库设置为数据来源的数据库。
-迁移过程中,Hydro 的以下内容会被**清空**:
-
+迁移过程中,Hydro 的以下内容会被**清空**:
题目列表,提交记录,用户列表,比赛列表,比赛成绩表,训练列表,训练进度,站内消息,题解,讨论
-
以下内容将被迁移:
-
题目与测试数据、题解,讨论与回复,比赛、训练、作业相关数据,提交记录,用户列表,站内消息。
-迁移过程中请保持网络畅通。
+## migrate-hustoj
+
+从HustOJ的数据库导入数据。
+迁移过程中,Hydro 的内容会被**清空**,以下内容将被迁移:
+题目与测试数据,用户列表。
diff --git a/packages/migrate-hustoj/lib.ts b/packages/migrate/lib.ts
similarity index 55%
rename from packages/migrate-hustoj/lib.ts
rename to packages/migrate/lib.ts
index ce21fa5ff..b4266f256 100644
--- a/packages/migrate-hustoj/lib.ts
+++ b/packages/migrate/lib.ts
@@ -2,7 +2,7 @@ import { md5, sha1 } from 'hydrooj/src/lib/crypto';
const RE_MD5 = /^[\da-f]{32}$/;
-function hash($password: string, $saved: string) {
+global.Hydro.lib['hash.hust'] = ($password: string, $saved: string) => {
$password = md5($password);
if (RE_MD5.test($saved)) return $password === $saved;
const $svd = Buffer.from($saved, 'base64').toString('hex');
@@ -13,6 +13,11 @@ function hash($password: string, $saved: string) {
]).toString('base64');
if ($hash.trim() === $saved.trim()) return true;
return false;
-}
+};
-global.Hydro.lib['hash.hust'] = hash;
+global.Hydro.lib['hash.vj2'] = (password: string, salt: string, udoc: { uname: string; unameLower: string }) => {
+ const { uname, unameLower } = udoc;
+ const pmd5 = md5(password);
+ const mixedSha1 = sha1(md5(unameLower + pmd5) + salt + sha1(pmd5 + salt));
+ return `${Buffer.from(uname).toString('base64')}|${mixedSha1}`;
+};
diff --git a/packages/migrate/locale/zh.yaml b/packages/migrate/locale/zh.yaml
new file mode 100644
index 000000000..915ee05a2
--- /dev/null
+++ b/packages/migrate/locale/zh.yaml
@@ -0,0 +1,2 @@
+migrate from hustoj: 从 HustOJ 导入
+migrate from vijos: 从 Vijos 导入
\ No newline at end of file
diff --git a/packages/migrate-hustoj/package.json b/packages/migrate/package.json
similarity index 63%
rename from packages/migrate-hustoj/package.json
rename to packages/migrate/package.json
index dead68f3b..9da63c31e 100644
--- a/packages/migrate-hustoj/package.json
+++ b/packages/migrate/package.json
@@ -1,20 +1,17 @@
{
- "name": "@hydrooj/migrate-hustoj",
- "version": "0.1.4",
+ "name": "@hydrooj/migrate",
+ "version": "0.1.0",
"main": "package.json",
"repository": "git@github.com:hydro-dev/Hydro.git",
"author": "undefined ",
"license": "AGPL-3.0-or-later",
- "preferUnplugged": true,
- "scripts": {
- "lint": "eslint lib.ts script.ts --fix",
- "build": "tsc"
- },
"devDependencies": {
+ "@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21"
},
"dependencies": {
"@hydrooj/utils": "workspace:*",
+ "mongodb": "^3.7.3",
"mysql": "^2.18.1"
}
}
diff --git a/packages/migrate/script.ts b/packages/migrate/script.ts
new file mode 100644
index 000000000..adb212de4
--- /dev/null
+++ b/packages/migrate/script.ts
@@ -0,0 +1,2 @@
+import './scripts/hustoj';
+import './scripts/vijos';
diff --git a/packages/migrate-hustoj/script.ts b/packages/migrate/scripts/hustoj.ts
similarity index 100%
rename from packages/migrate-hustoj/script.ts
rename to packages/migrate/scripts/hustoj.ts
diff --git a/packages/migrate-vijos/script.ts b/packages/migrate/scripts/vijos.ts
similarity index 99%
rename from packages/migrate-vijos/script.ts
rename to packages/migrate/scripts/vijos.ts
index 0aa6ffea7..4af1515a6 100644
--- a/packages/migrate-vijos/script.ts
+++ b/packages/migrate/scripts/vijos.ts
@@ -200,9 +200,13 @@ const tasks = {
},
record: async (doc) => {
if (doc.hidden) return null;
- const testCases: TestCase[] = [];
+ const testCases: Required[] = [];
+ let i = 1;
for (const c of doc.cases || []) {
testCases.push({
+ id: i++,
+ subtaskId: 0,
+ score: 0,
status: c.status,
time: c.time_ms || c.time,
memory: c.memory_kb || c.memory,
diff --git a/packages/recaptcha/package.json b/packages/recaptcha/package.json
index aae34fa89..daefa1486 100644
--- a/packages/recaptcha/package.json
+++ b/packages/recaptcha/package.json
@@ -8,6 +8,6 @@
"license": "AGPL-3.0-or-later",
"preferUnplugged": true,
"dependencies": {
- "superagent": "^7.1.2"
+ "superagent": "^7.1.3"
}
}
diff --git a/packages/ui-default/.eslintrc.js b/packages/ui-default/.eslintrc.js
index 09bf98b73..20983ca61 100644
--- a/packages/ui-default/.eslintrc.js
+++ b/packages/ui-default/.eslintrc.js
@@ -81,7 +81,7 @@ module.exports = {
indent: [
'error',
2,
- { SwitchCase: 0 },
+ { SwitchCase: 1 },
],
'max-len': ['error', 150],
quotes: 'warn',
@@ -115,6 +115,7 @@ module.exports = {
'no-underscore-dangle': 'off',
'no-await-in-loop': 'off',
'no-lonely-if': 'off',
+ 'no-nested-ternary': 'off',
'no-param-reassign': 'off',
'no-script-url': 'off',
'no-bitwise': 'off',
diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js
index 44712341f..f9b4a8f8c 100644
--- a/packages/ui-default/build/config/webpack.js
+++ b/packages/ui-default/build/config/webpack.js
@@ -81,6 +81,7 @@ export default function (env = {}) {
mode: (env.production || env.measure) ? 'production' : 'development',
profile: true,
context: root(),
+ devtool: env.production ? 'source-map' : false,
entry: {
hydro: './entry.js',
polyfill: './polyfill.ts',
@@ -100,8 +101,6 @@ export default function (env = {}) {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
vj: root(),
- 'js-yaml': root('utils/yamlCompact'),
- 'real-js-yaml': require.resolve('js-yaml'),
},
},
module: {
@@ -205,12 +204,12 @@ export default function (env = {}) {
patterns: [
{ from: root('static') },
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
- { from: root('.build/sharedworker.js'), to: 'sharedworker.js' },
],
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: env.production ? '"production"' : '"debug"',
+ VERSION: JSON.stringify(require('@hydrooj/ui-default/package.json').version),
},
}),
new webpack.LoaderOptionsPlugin({
@@ -224,10 +223,10 @@ export default function (env = {}) {
filename: '[name].[hash:10].worker.js',
customLanguages: [{
label: 'yaml',
- entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/monaco.contribution'),
+ entry: require.resolve('monaco-yaml/index.js'),
worker: {
id: 'vs/language/yaml/yamlWorker',
- entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/yaml.worker.js'),
+ entry: require.resolve('monaco-yaml/yaml.worker.js'),
},
}],
}),
diff --git a/packages/ui-default/build/main.ts b/packages/ui-default/build/main.ts
index 916ce631a..c343cb8a6 100644
--- a/packages/ui-default/build/main.ts
+++ b/packages/ui-default/build/main.ts
@@ -3,7 +3,6 @@
import cac from 'cac';
import fs from 'fs-extra';
import webpack from 'webpack';
-import esbuild from 'esbuild';
import WebpackDevServer from 'webpack-dev-server';
import gulp from 'gulp';
import log from 'fancy-log';
@@ -18,15 +17,6 @@ const argv = cac().parse();
async function runWebpack({
watch, production, measure, dev,
}) {
- const result = esbuild.build({
- bundle: true,
- format: 'iife',
- entryPoints: [root('sharedworker.ts')],
- outfile: root('.build/sharedworker.js'),
- watch: dev || watch,
- minify: production,
- });
- if (!dev && !watch) await result;
const compiler = webpack(webpackConfig({ watch, production, measure }));
if (dev) {
const server = new WebpackDevServer(compiler, {
@@ -36,7 +26,7 @@ async function runWebpack({
stats: 'none',
index: root('public'),
proxy: {
- context: (path) => !path.includes('sharedworker.js'),
+ context: (path) => !path.includes('sockjs-node'),
target: 'http://localhost:2333',
ws: true,
},
@@ -98,6 +88,10 @@ async function main() {
if (fs.existsSync('public/default.theme.css')) {
fs.copyFileSync('public/default.theme.css', `public/default-${pkg.version}.theme.css`);
}
+ if (argv.options.production) {
+ const files = fs.readdirSync('public');
+ files.filter((i) => /(^[in]\..+|worker)\.js\.map$/.test(i)).forEach((i) => fs.removeSync(`public/${i}`));
+ }
}
process.chdir(dir);
}
diff --git a/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx
index ca54c1b3b..9b2987419 100644
--- a/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM from 'react-dom/client';
import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
@@ -33,15 +33,12 @@ export default class DomainSelectAutoComplete extends AutoComplete {
attach() {
const value = this.$dom.val();
- ReactDOM.render(
- { this.ref = ref; }}
- value={value}
- onChange={this.onChange}
- multi={this.options.multi}
- />,
- this.container,
- );
+ ReactDOM.createRoot(this.container).render( { this.ref = ref; }}
+ value={value}
+ onChange={this.onChange}
+ multi={this.options.multi}
+ />);
}
}
diff --git a/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx
index 8d5500960..b50261df1 100644
--- a/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM from 'react-dom/client';
import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
@@ -33,14 +33,13 @@ export default class ProblemSelectAutoComplete extends AutoComplete {
attach() {
const value = this.$dom.val();
- ReactDOM.render(
+ ReactDOM.createRoot(this.container).render(
{ this.ref = ref; }}
value={value}
multi={this.options.multi}
onChange={this.onChange}
/>,
- this.container,
);
}
}
diff --git a/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx
index 7fd6c1ca7..01652016a 100644
--- a/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM from 'react-dom/client';
import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete, { AutoCompleteOptions } from '.';
@@ -38,14 +38,13 @@ export default class UserSelectAutoComplete extends AutoC
attach() {
const value = this.$dom.val();
- ReactDOM.render(
+ ReactDOM.createRoot(this.container).render(
{ this.ref = ref; }}
value={value}
multi={this.options.multi}
onChange={this.onChange}
/>,
- this.container,
);
}
}
diff --git a/packages/ui-default/components/autocomplete/autocomplete.page.styl b/packages/ui-default/components/autocomplete/autocomplete.page.styl
index dd4fc3e28..ff150f4fe 100644
--- a/packages/ui-default/components/autocomplete/autocomplete.page.styl
+++ b/packages/ui-default/components/autocomplete/autocomplete.page.styl
@@ -53,7 +53,7 @@
white-space: nowrap
text-overflow: ellipsis
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
font-size: 12px
cursor: pointer
padding: 4px
@@ -78,19 +78,19 @@
&>div
flex-grow: 1
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: transparent
&[aria-selected='true']
background-color: #fafafa
font-weight: 600
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: #1890ff
&[data-focus='true']
background-color: #e6f7ff
cursor: pointer
- &>span.bp3-icon>svg
+ &>span.bp4-icon>svg
color: currentColor
diff --git a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx
index 5b79bc4cc..f23a4bf50 100644
--- a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx
@@ -17,6 +17,7 @@ export interface AutoCompleteProps- {
*/
height?: string;
disabled?: boolean;
+ placeholder?: string;
disabledHint?: string;
listStyle?: React.CSSProperties;
cacheKey?: string;
@@ -128,8 +129,10 @@ const AutoComplete = forwardRef(function Impl
(props: AutoCompleteProps, re
else onChange(selectedKeys.join(','));
};
+ let first = !multi;
useEffect(() => {
- dispatchChange();
+ if (first) first = false;
+ else dispatchChange();
const ids = [];
for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
if (!ids.length) return;
@@ -293,6 +296,7 @@ const AutoComplete = forwardRef(function Impl(props: AutoCompleteProps, re
if (allowEmptyQuery) handleInputChange();
setFocused(true);
}}
+ placeholder={props.placeholder}
onBlur={() => setFocused(false)}
onKeyDown={handleInputKeyDown}
defaultValue={multi ? '' : selectedKeys.join(',')}
@@ -342,6 +346,7 @@ AutoComplete.propTypes = {
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
+ placeholder: PropTypes.string,
};
AutoComplete.defaultProps = {
diff --git a/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
index 70d185a00..f8cd5acd8 100644
--- a/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
+++ b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx
@@ -33,7 +33,7 @@ CustomSelectAutoComplete.propTypes = {
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
- data: PropTypes.any,
+ data: PropTypes.arrayOf(PropTypes.any),
};
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
diff --git a/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx
new file mode 100644
index 000000000..4ff950c0c
--- /dev/null
+++ b/packages/ui-default/components/autocomplete/components/FileSelectAutoComplete.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import AutoComplete from './AutoComplete';
+
+type DefaultProps = {
+ _id: string;
+ name: string;
+} & string;
+
+const FileSelectAutoComplete = React.forwardRef((props, ref) => (
+
+ ref={ref as any}
+ fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
+ queryItems={(query) => props.data.filter((i) => (i.name
+ ? i.name.toLowerCase().match(query.toLowerCase())
+ : i.toString().toLowerCase().match(query.toLowerCase())
+ ))}
+ itemText={(item) => `${item.name || item}`}
+ itemKey={(item) => `${item._id?.toString() || item.name || item}`}
+ renderItem={(item) => item.name || item._id || item} // TODO icon
+ allowEmptyQuery
+ {...props}
+ />
+));
+
+FileSelectAutoComplete.propTypes = {
+ width: PropTypes.string,
+ height: PropTypes.string,
+ listStyle: PropTypes.object,
+ onChange: PropTypes.func.isRequired,
+ multi: PropTypes.bool,
+ selectedKeys: PropTypes.arrayOf(PropTypes.string),
+ allowEmptyQuery: PropTypes.bool,
+ freeSolo: PropTypes.bool,
+ freeSoloConverter: PropTypes.func,
+ data: PropTypes.arrayOf(PropTypes.any),
+};
+
+FileSelectAutoComplete.displayName = 'FileSelectAutoComplete';
+
+FileSelectAutoComplete.defaultProps = {
+ width: '100%',
+ height: 'auto',
+ listStyle: {},
+ multi: false,
+ selectedKeys: [],
+ freeSolo: false,
+ freeSoloConverter: (input) => input,
+};
+
+export default FileSelectAutoComplete;
diff --git a/packages/ui-default/components/autocomplete/index.tsx b/packages/ui-default/components/autocomplete/index.tsx
index bee7074a1..1e1ad8361 100644
--- a/packages/ui-default/components/autocomplete/index.tsx
+++ b/packages/ui-default/components/autocomplete/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM from 'react-dom/client';
import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoCompleteFC from './components/AutoComplete';
@@ -64,7 +64,7 @@ export default class AutoComplete extends DOMAttachedObject {
attach() {
const value = this.$dom.val();
- ReactDOM.render(
+ ReactDOM.createRoot(this.container).render(
{ this.ref = ref; }}
height="34px"
@@ -76,7 +76,6 @@ export default class AutoComplete extends DOMAttachedObject {
multi={this.options.multi}
freeSolo={this.options.multi}
/>,
- this.container,
);
}
diff --git a/packages/ui-default/components/discussion/comments.page.tsx b/packages/ui-default/components/discussion/comments.page.tsx
index 2cd6cc709..2ca04b4a0 100644
--- a/packages/ui-default/components/discussion/comments.page.tsx
+++ b/packages/ui-default/components/discussion/comments.page.tsx
@@ -1,7 +1,7 @@
import 'jquery.easing';
import * as React from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM from 'react-dom/client';
import { Popover } from '@blueprintjs/core';
import { AutoloadPage } from 'vj/misc/Page';
import CommentBox from 'vj/components/discussion/CommentBox';
@@ -256,7 +256,7 @@ const commentsPage = new AutoloadPage('commentsPage', () => {
const canUseReaction = $('[data-op="react"]').length > 0;
$('[data-op="react"]').each((i, e) => {
- ReactDOM.render( , e);
+ ReactDOM.createRoot(e).render( );
});
$(document).on('click', '.reaction', async (e) => {
if (!canUseReaction) {
diff --git a/packages/ui-default/components/editor/index.ts b/packages/ui-default/components/editor/index.ts
index 285d05d7a..ce1eb0ecf 100644
--- a/packages/ui-default/components/editor/index.ts
+++ b/packages/ui-default/components/editor/index.ts
@@ -62,7 +62,7 @@ export default class VjEditor extends DOMAttachedObject {
? monaco.editor.getModel(monaco.Uri.parse(model))
|| monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model))
: model;
- this.model.setValue(value);
+ if (!this.options.model) this.model.setValue(value);
const cfg: import('../monaco').default.editor.IStandaloneEditorConstructionOptions = {
theme,
lineNumbers: 'on',
diff --git a/packages/ui-default/components/hotkey/hotkey.page.js b/packages/ui-default/components/hotkey/hotkey.page.js
index ab174d564..e4537a6df 100644
--- a/packages/ui-default/components/hotkey/hotkey.page.js
+++ b/packages/ui-default/components/hotkey/hotkey.page.js
@@ -38,15 +38,15 @@ function testElementHotkey(hotkey, $element, attr) {
const [defStr, trigger] = singleDef.split(':');
if (isHotkeyMatch(hotkey, defStr)) {
switch (trigger) {
- case 'submit':
- $element.closest('form').trigger('submit');
- break;
- case undefined:
- $element.trigger('click');
- break;
- default:
- $element.trigger(trigger);
- break;
+ case 'submit':
+ $element.closest('form').trigger('submit');
+ break;
+ case undefined:
+ $element.trigger('click');
+ break;
+ default:
+ $element.trigger(trigger);
+ break;
}
}
});
diff --git a/packages/ui-default/components/menu/menu.page.styl b/packages/ui-default/components/menu/menu.page.styl
index 4b301cc37..48777ce75 100644
--- a/packages/ui-default/components/menu/menu.page.styl
+++ b/packages/ui-default/components/menu/menu.page.styl
@@ -42,6 +42,9 @@
opacity: 0.85
a.menu__link, button.menu__link
+ &.half
+ padding-right: 0
+ width: 50%
&.disabled
cursor: default
opacity: 0.5
diff --git a/packages/ui-default/components/message/index.page.js b/packages/ui-default/components/message/index.page.js
deleted file mode 100644
index a396f23a5..000000000
--- a/packages/ui-default/components/message/index.page.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { AutoloadPage } from 'vj/misc/Page';
-import { InfoDialog } from 'vj/components/dialog';
-import VjNotification from 'vj/components/notification/index';
-import { FLAG_ALERT } from 'vj/constant/message';
-import i18n from 'vj/utils/i18n';
-import tpl from 'vj/utils/tpl';
-
-const messagePage = new AutoloadPage('messagePage', (pagename) => {
- if (pagename === 'home_messages' || !UserContext._id) return;
-
- if (UserContext.unreadMsg) {
- new VjNotification({
- type: 'info',
- message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
- duration: 5000,
- action: () => window.open('/home/messages', '_blank'),
- }).show();
- }
-
- const onmessage = (message) => {
- const msg = JSON.parse(message);
- console.log('Received message', msg);
- if (msg.mdoc.flag & FLAG_ALERT) {
- // Is alert
- return new InfoDialog({
- cancelByClickingBack: false,
- $body: tpl`
-
- ${{ templateRaw: true, html: i18n('Message from {0}:', `
${msg.mdoc.from}
`) }}
-
${i18n(msg.mdoc.content)}
-
`,
- }).open();
- }
- if (msg.udoc._id === 1 && msg.mdoc.flag & 4) {
- // System notification
- return new VjNotification({
- message: i18n('You received a system message, click here to view.'),
- duration: 15000,
- action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
- }).show();
- }
- // Is message
- return new VjNotification({
- title: msg.udoc.uname,
- avatar: msg.udoc.avatarUrl,
- message: msg.mdoc.content,
- duration: 15000,
- action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
- }).show();
- };
-
- const path = new URL('/home/messages-conn', window.location.href).toString();
- const worker = new SharedWorker('/sharedworker.js', { name: 'Hydro Messages Shared Worker' });
- worker.port.start();
- worker.port.postMessage({ type: 'init', path, cookie: document.cookie });
- worker.port.onmessage = (e) => {
- if (e.data.type !== 'message') return;
- if (document.hidden) {
- worker.port.postMessage({ type: 'fail' });
- } else {
- worker.port.postMessage({ type: 'success' });
- onmessage(e.data.payload);
- }
- };
-});
-
-export default messagePage;
diff --git a/packages/ui-default/components/message/index.page.ts b/packages/ui-default/components/message/index.page.ts
new file mode 100644
index 000000000..b6728dff6
--- /dev/null
+++ b/packages/ui-default/components/message/index.page.ts
@@ -0,0 +1,122 @@
+import { AutoloadPage } from 'vj/misc/Page';
+import { InfoDialog } from 'vj/components/dialog';
+import VjNotification from 'vj/components/notification/index';
+import ReconnectingWebsocket from 'reconnecting-websocket';
+import { FLAG_ALERT } from 'vj/constant/message';
+import i18n from 'vj/utils/i18n';
+import tpl from 'vj/utils/tpl';
+import { nanoid } from 'nanoid';
+
+const onmessage = (msg) => {
+ console.log('Received message', msg);
+ if (msg.mdoc.flag & FLAG_ALERT) {
+ // Is alert
+ return new InfoDialog({
+ cancelByClickingBack: false,
+ $body: tpl`
+
+ ${{ templateRaw: true, html: i18n('Message from {0}:', `
${msg.mdoc.from}
`) }}
+
${i18n(msg.mdoc.content)}
+
`,
+ }).open();
+ }
+ // Is message
+ return new VjNotification({
+ ...(msg.udoc._id === 1 && msg.mdoc.flag & 4)
+ ? { message: i18n('You received a system message, click here to view.') }
+ : {
+ title: msg.udoc.uname,
+ avatar: msg.udoc.avatarUrl,
+ message: msg.mdoc.content,
+ },
+ duration: 15000,
+ action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
+ }).show();
+};
+
+const messagePage = new AutoloadPage('messagePage', (pagename) => {
+ if (pagename === 'home_messages' || !UserContext._id) return;
+ if (UserContext.unreadMsg) {
+ new VjNotification({
+ type: 'info',
+ message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
+ duration: 5000,
+ action: () => window.open('/home/messages', '_blank'),
+ }).show();
+ }
+ if (!window.BroadcastChannel) {
+ console.error('BoardcastChannel not supported');
+ return;
+ }
+
+ let isMaster = false;
+ const selfId = nanoid();
+ const channel = new BroadcastChannel('hydro-messages');
+ const pages = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
+ pages.push(selfId);
+ pages.sort();
+ localStorage.setItem('pages', JSON.stringify(pages));
+
+ window.onunload = () => {
+ const c = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
+ localStorage.setItem('pages', JSON.stringify(c.filter((i) => i !== selfId)));
+ if (!isMaster) return;
+ localStorage.removeItem('page.master');
+ channel?.postMessage({ type: 'master' });
+ };
+
+ function asMaster() {
+ console.log('init as master');
+ isMaster = true;
+ localStorage.setItem('page.master', selfId);
+ const masterChannel = new BroadcastChannel('hydro-messages');
+ const url = new URL('/home/messages-conn', window.location.href.replace('http', 'ws'));
+ // TODO handle a better way for cookie
+ url.searchParams.append('sid', document.cookie);
+ const sock = new ReconnectingWebsocket(url.toString());
+ const pending = {};
+ sock.onopen = () => console.log('Connected');
+ sock.onerror = console.error;
+ sock.onclose = (...args) => console.log('Closed', ...args);
+ sock.onmessage = async (message) => {
+ if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
+ const payload = JSON.parse(message.data);
+ const id = nanoid();
+ masterChannel.postMessage({ type: 'message', id, payload });
+ const success = await new Promise((resolve) => {
+ pending[id] = resolve;
+ setTimeout(() => {
+ delete pending[id];
+ resolve(false);
+ }, 1000);
+ });
+ if (!success && window.Notification?.permission === 'granted') {
+ const notification = new window.Notification(
+ payload.udoc.uname || 'Hydro Notification',
+ {
+ icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
+ body: payload.mdoc.content,
+ },
+ );
+ notification.onclick = () => window.open('/home/messages');
+ }
+ };
+ masterChannel.onmessage = (msg) => {
+ if (msg.data.type === 'message-push') pending[msg.data.id]?.(true);
+ };
+ }
+
+ channel.onmessage = (msg) => {
+ if (msg.data.type === 'message' && !document.hidden) {
+ channel.postMessage({ type: 'message-push', id: msg.data.id });
+ onmessage(msg.data.payload);
+ }
+ if (msg.data.type === 'master') {
+ const c = JSON.parse(localStorage.getItem('pages') || '[]') as string[];
+ if (c[0] === selfId) asMaster();
+ }
+ };
+ if (!localStorage.getItem('page.master')) asMaster();
+});
+
+export default messagePage;
diff --git a/packages/ui-default/components/messagepad/reducers/activeId.js b/packages/ui-default/components/messagepad/reducers/activeId.js
index 036642c14..8af2782c8 100644
--- a/packages/ui-default/components/messagepad/reducers/activeId.js
+++ b/packages/ui-default/components/messagepad/reducers/activeId.js
@@ -21,14 +21,14 @@ function scrollToViewport() {
export default function reducer(state = null, action) {
switch (action.type) {
- case 'DIALOGUES_SWITCH_TO': {
- scrollToViewport();
- return action.payload;
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- return action.meta.dialogueId;
- }
- default:
- return state;
+ case 'DIALOGUES_SWITCH_TO': {
+ scrollToViewport();
+ return action.payload;
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ return action.meta.dialogueId;
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/dialogues.js b/packages/ui-default/components/messagepad/reducers/dialogues.js
index e56009a5a..d6e55d7bf 100644
--- a/packages/ui-default/components/messagepad/reducers/dialogues.js
+++ b/packages/ui-default/components/messagepad/reducers/dialogues.js
@@ -2,52 +2,52 @@ import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const { messages } = action.payload;
- return { ...state, ..._.keyBy(messages, 'udoc._id') };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- if (state[user._id]) return state;
- return {
- ...state,
- [user._id]: {
- _id: user._id,
- udoc: { ...user },
- messages: [],
- },
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- const { mdoc } = action.payload;
- return {
- ...state,
- [id]: {
- ...state[id],
- messages: [
- ...state[id].messages,
- mdoc,
- ],
- },
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { mdoc, udoc } = action.payload;
- const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
- return {
- ...state,
- [to]: {
- ...state[to] || {},
- udoc,
- messages: [
- ...state[to]?.messages || [],
- mdoc,
- ],
- },
- };
- }
- default:
- return state;
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const { messages } = action.payload;
+ return { ...state, ..._.keyBy(messages, 'udoc._id') };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
+ if (state[user._id]) return state;
+ return {
+ ...state,
+ [user._id]: {
+ _id: user._id,
+ udoc: { ...user },
+ messages: [],
+ },
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ const { mdoc } = action.payload;
+ return {
+ ...state,
+ [id]: {
+ ...state[id],
+ messages: [
+ ...state[id].messages,
+ mdoc,
+ ],
+ },
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { mdoc, udoc } = action.payload;
+ const to = mdoc.from === UserContext._id ? mdoc.to : mdoc.from;
+ return {
+ ...state,
+ [to]: {
+ ...state[to] || {},
+ udoc,
+ messages: [
+ ...state[to]?.messages || [],
+ mdoc,
+ ],
+ },
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/inputs.js b/packages/ui-default/components/messagepad/reducers/inputs.js
index a0856a741..a188073e6 100644
--- a/packages/ui-default/components/messagepad/reducers/inputs.js
+++ b/packages/ui-default/components/messagepad/reducers/inputs.js
@@ -2,42 +2,42 @@ import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const dialogues = action.payload.messages;
- return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- return {
- ...state,
- [user._id]: '',
- };
- }
- case 'DIALOGUES_INPUT_CHANGED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: action.payload,
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: '',
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { udoc } = action.payload;
- if (!state[udoc._id]) {
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const dialogues = action.payload.messages;
+ return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, ''])) };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
return {
...state,
- [udoc._id]: '',
+ [user._id]: '',
};
}
- return state;
- }
- default:
- return state;
+ case 'DIALOGUES_INPUT_CHANGED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: action.payload,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: '',
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { udoc } = action.payload;
+ if (!state[udoc._id]) {
+ return {
+ ...state,
+ [udoc._id]: '',
+ };
+ }
+ return state;
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/messagepad/reducers/isPosting.js b/packages/ui-default/components/messagepad/reducers/isPosting.js
index 4bb841122..ecd04cbff 100644
--- a/packages/ui-default/components/messagepad/reducers/isPosting.js
+++ b/packages/ui-default/components/messagepad/reducers/isPosting.js
@@ -3,47 +3,47 @@ import Notification from 'vj/components/notification';
export default function reducer(state = {}, action) {
switch (action.type) {
- case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
- const dialogues = action.payload.messages;
- return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
- }
- case 'DIALOGUES_CREATE': {
- const { user } = action.payload;
- return {
- ...state,
- [user._id]: false,
- };
- }
- case 'DIALOGUES_POST_SEND_PENDING': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: true,
- };
- }
- case 'DIALOGUES_POST_SEND_REJECTED': {
- Notification.error(action.payload.message);
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: false,
- };
- }
- case 'DIALOGUES_POST_SEND_FULFILLED': {
- const id = action.meta.dialogueId;
- return {
- ...state,
- [id]: false,
- };
- }
- case 'DIALOGUES_MESSAGE_PUSH': {
- const { udoc } = action.payload;
- return {
- ...state,
- [udoc._id]: false,
- };
- }
- default:
- return state;
+ case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
+ const dialogues = action.payload.messages;
+ return { ...state, ..._.fromPairs(_.map(dialogues, (d) => [d._id, false])) };
+ }
+ case 'DIALOGUES_CREATE': {
+ const { user } = action.payload;
+ return {
+ ...state,
+ [user._id]: false,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_PENDING': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: true,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_REJECTED': {
+ Notification.error(action.payload.message);
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: false,
+ };
+ }
+ case 'DIALOGUES_POST_SEND_FULFILLED': {
+ const id = action.meta.dialogueId;
+ return {
+ ...state,
+ [id]: false,
+ };
+ }
+ case 'DIALOGUES_MESSAGE_PUSH': {
+ const { udoc } = action.payload;
+ return {
+ ...state,
+ [udoc._id]: false,
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/monaco/languages/yaml.ts b/packages/ui-default/components/monaco/languages/yaml.ts
index 544d0fd85..84a576182 100644
--- a/packages/ui-default/components/monaco/languages/yaml.ts
+++ b/packages/ui-default/components/monaco/languages/yaml.ts
@@ -1,4 +1,4 @@
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import { setDiagnosticsOptions } from 'monaco-yaml';
/** @type {Record} */
const problemConfigSchemaDef = {
@@ -8,6 +8,9 @@ const problemConfigSchemaDef = {
properties: {
input: { type: 'string' },
output: { type: 'string' },
+ time: { $ref: '#/def/time' },
+ memory: { $ref: '#/def/memory' },
+ score: { $ref: '#/def/score', description: 'score' },
},
required: ['input'],
additionalProperties: false,
@@ -28,7 +31,7 @@ const problemConfigSchemaDef = {
additionalProperties: false,
},
time: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([mu]?)s?$' },
- memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kmg])b?$' },
+ memory: { type: 'string', pattern: '^([0-9]+(?:\\.[0-9]*)?)([kKmMgG])[bB]?$' },
score: { type: 'integer', maximum: 100, minimum: 1 },
};
@@ -37,7 +40,7 @@ const problemConfigSchema = {
type: 'object',
def: problemConfigSchemaDef,
properties: {
- redirect: { type: 'string', pattern: '[0-9a-z_-]+\\/[0-9]+' },
+ redirect: { type: 'string', pattern: '[0-9a-zA-Z_-]+\\/[0-9]+' },
key: { type: 'string', pattern: '[0-9a-f]{32}' },
type: { enum: ['default', 'interactive', 'submit_answer', 'objective', 'remote_judge'] },
subType: { type: 'string' },
@@ -75,8 +78,7 @@ const problemConfigSchema = {
additionalProperties: false,
};
-// @ts-ignore
-monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({
+setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
hover: true,
@@ -86,7 +88,7 @@ monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({
{
uri: 'https://hydro.js.org/schema/problemConfig.json',
fileMatch: ['hydro://problem/file/config.yaml'],
- schema: problemConfigSchema,
+ schema: problemConfigSchema as any,
},
{
uri: `${UiContext.cdn_prefix}manage/setting/schema.json`,
diff --git a/packages/ui-default/components/objective-question/index.jsx b/packages/ui-default/components/objective-question/index.jsx
index 8a81c97c7..c810c25a3 100644
--- a/packages/ui-default/components/objective-question/index.jsx
+++ b/packages/ui-default/components/objective-question/index.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { sortBy } from 'lodash';
import request from 'vj/utils/request';
+import Notification from 'vj/components/notification';
export default class ObjectiveContainer extends React.PureComponent {
constructor(args) {
diff --git a/packages/ui-default/components/problemconfig/BasicForm.tsx b/packages/ui-default/components/problemconfig/BasicForm.tsx
new file mode 100644
index 000000000..0d34abcaf
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/BasicForm.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import type { RootState } from './reducer/index';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+
+export function FormItem({
+ columns, label, children, helpText = '', disableLabel = false, ...props
+}) {
+ return (
+
+
+ {!disableLabel && i18n(label)}
+ {children}
+ {helpText && ({i18n(helpText)}
)}
+
+
+ );
+}
+
+type KeyType = {
+ [Q in keyof K]: K[Q] extends T ? Q : never;
+}[keyof K];
+
+export function ManagedInput({ placeholder, formKey }: { placeholder?: string, formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const dispatch = useDispatch();
+ return (
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
+ }}
+ className="textbox"
+ />
+ );
+}
+
+export function ManagedSelect({ options, formKey }: { options: string[], formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const dispatch = useDispatch();
+ return (
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
+ }}
+ className="select"
+ >
+ {options.map((o) => ({o} ))}
+
+ );
+}
+
+export function SingleFileSelect({ formKey }: { formKey: KeyType }) {
+ const value = useSelector((state: RootState) => state.config[formKey]);
+ const Files = useSelector((state: RootState) => state.testdata);
+ const dispatch = useDispatch();
+ return (
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
+ />
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx
new file mode 100644
index 000000000..27ab9d6e1
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import type { editor } from 'monaco-editor';
+import { connect } from 'react-redux';
+import { load } from 'vj/components/monaco/loader';
+import Editor from 'vj/components/editor';
+import { diffLines } from 'diff';
+import yaml from 'js-yaml';
+import type { ProblemConfigFile, TestCaseConfig } from 'hydrooj/src/interface';
+
+const mapStateToProps = (state) => ({
+ config: state.config,
+});
+const mapDispatchToProps = (dispatch) => ({
+ handleUpdateCode: (code) => {
+ dispatch({
+ type: 'CONFIG_CODE_UPDATE',
+ payload: code,
+ });
+ },
+});
+
+interface Props {
+ config: object;
+ handleUpdateCode: Function;
+}
+
+const configKey = [
+ 'type', 'subType', 'target', 'score', 'time',
+ 'memory', 'filename', 'checker_type', 'checker', 'interactor',
+ 'user_extra_files', 'judge_extra_files', 'detail', 'outputs', 'redirect',
+ 'cases', 'subtasks', 'langs',
+];
+
+const subtasksKey = [
+ 'time', 'memory', 'score', 'if', 'id',
+ 'type', 'cases',
+];
+
+const casesKey = ['time', 'memory', 'input', 'output'];
+
+export function configYamlFormat(config: ProblemConfigFile) {
+ const formatConfig: ProblemConfigFile = {};
+ configKey.forEach((key) => {
+ if (config[key] !== undefined) {
+ if (key === 'subType' && ['single', 'multi'].includes(config[key]) && config.type !== 'submit_answer') return;
+ if (key === 'checker_type' && config.type !== 'default') return;
+ if (key === 'checker'
+ && (['default', 'strict'].includes(formatConfig.checker_type) || !formatConfig.checker_type)) return;
+ if (key === 'interactor' && config.type !== 'interactive') return;
+ if (key === 'subtasks') {
+ formatConfig[key] = [];
+ config[key].forEach((subtask) => {
+ const formatSubtask: object = {};
+ subtasksKey.forEach((subtaskKey) => {
+ if (subtask && subtask[subtaskKey] !== undefined) {
+ formatSubtask[subtaskKey] = subtask[subtaskKey];
+ }
+ });
+ formatConfig[key].push(formatSubtask);
+ });
+ } else if (key === 'cases') {
+ formatConfig[key] = [];
+ config[key].forEach((caseItem) => {
+ const formatCase: TestCaseConfig = {
+ time: '1000ms', memory: '256MB', input: '', output: '',
+ };
+ casesKey.forEach((caseKey) => {
+ if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey];
+ else delete formatCase[caseKey];
+ });
+ formatConfig[key].push(formatCase);
+ });
+ } else formatConfig[key] = config[key];
+ }
+ });
+ if (formatConfig.type === 'objective') {
+ Object.keys(formatConfig).filter((i) => !['type', 'outputs'].includes(i)).forEach((i) => delete formatConfig[i]);
+ formatConfig.outputs = formatConfig.outputs || [];
+ }
+ Object.keys(formatConfig).filter((i) => i.startsWith('__')).forEach((i) => delete formatConfig[i]);
+ return formatConfig;
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent {
+ disposable = [];
+ containerElement: HTMLElement;
+ private __preventUpdate = false;
+ private __preventFormat = false;
+
+ editor: editor.IStandaloneCodeEditor;
+ model: editor.ITextModel;
+ vjEditor: Editor;
+
+ async componentDidMount() {
+ const { monaco } = await load(['yaml']);
+ const uri = monaco.Uri.parse('hydro://problem/file/config.yaml');
+ this.model = monaco.editor.createModel(yaml.dump(configYamlFormat(this.props.config)), 'yaml', uri);
+ this.vjEditor = Editor.getOrConstruct($(this.containerElement), {
+ language: 'yaml',
+ model: this.model,
+ onChange: (value: string) => {
+ this.__preventUpdate = true;
+ if (!this.__preventFormat) this.props.handleUpdateCode(value);
+ this.__preventUpdate = false;
+ },
+ }) as Editor;
+ this.editor = this.vjEditor.editor;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.__preventUpdate || !this.model) return;
+ if (yaml.dump(prevProps.config) !== yaml.dump(this.props.config)) {
+ this.__preventFormat = true;
+ const curValue = this.model.getValue();
+ const diff = diffLines(curValue, yaml.dump(configYamlFormat(this.props.config)));
+ const ops = [];
+ let cursor = 1;
+ for (const line of diff) {
+ if (line.added) {
+ let range = this.model.getFullModelRange();
+ range = range.setStartPosition(cursor, 0);
+ range = range.setEndPosition(cursor, 0);
+ ops.push({ range, text: line.value });
+ } else if (line.removed) {
+ let range = this.model.getFullModelRange();
+ range = range.setStartPosition(cursor, 0);
+ cursor += line.count;
+ range = range.setEndPosition(cursor, 0);
+ ops.push({ range, text: '' });
+ } else cursor += line.count;
+ }
+ this.model.pushEditOperations([], ops, undefined);
+ this.__preventFormat = false;
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.vjEditor) this.vjEditor.destory();
+ if (this.model) this.model.dispose();
+ if (this.editor) this.editor.dispose();
+ this.disposable.map((i) => i.dispose());
+ }
+
+ assignRef = (component) => {
+ this.containerElement = component;
+ };
+
+ render() {
+ return (
+
+
+ );
+ }
+});
diff --git a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
new file mode 100644
index 000000000..5aeb8c490
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import {
+ Card, Tab, Tabs, InputGroup, Tag,
+} from '@blueprintjs/core';
+import type { RootState } from './reducer/index';
+import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+import { FormItem } from './BasicForm';
+import ProblemType from './ProblemType';
+import { TaskConfig } from './SubtaskTable';
+
+function FileIOConfig() {
+ const filename = useSelector((state: RootState) => state.config.filename);
+ const dispatch = useDispatch();
+ return (
+
+
+
+
+ .in/.out}
+ value={filename || ''}
+ onChange={(ev) => {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value });
+ }}
+ fill
+ />
+
+
+
+
+ );
+}
+
+function ExtraFilesConfig() {
+ const Files = useSelector((state: RootState) => state.testdata);
+ const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files) || [];
+ const judgeExtraFiles = useSelector((state: RootState) => state.config.judge_extra_files) || [];
+ const dispatch = useDispatch();
+ const userRef = React.useRef();
+ const judgeRef = React.useRef();
+ React.useEffect(() => {
+ userRef.current.setSelectedKeys(userExtraFiles);
+ }, [JSON.stringify(userExtraFiles)]);
+ React.useEffect(() => {
+ judgeRef.current.setSelectedKeys(judgeExtraFiles);
+ }, [JSON.stringify(judgeExtraFiles)]);
+ return (
+
+
+
+ );
+}
+
+function LangConfig() {
+ const langs = useSelector((state: RootState) => state.config.langs) || [];
+ const prefixes = new Set(Object.keys(LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
+ const data = Object.keys(LANGS).filter((i) => !prefixes.has(i))
+ .map((i) => ({ name: LANGS[i].display, _id: i }));
+ const dispatch = useDispatch();
+ const ref = React.useRef();
+ const selectedKeys = langs.filter((i) => !prefixes.has(i));
+ React.useEffect(() => {
+ ref.current.setSelectedKeys(selectedKeys);
+ }, [JSON.stringify(selectedKeys)]);
+ return (
+
+ {
+ const value = val.split(',');
+ value.push(...Array.from(new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0]))));
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'langs', value });
+ }}
+ multi
+ />
+
+ );
+}
+
+export default function ProblemConfigForm() {
+ const Type = useSelector((state: RootState) => state.config.type);
+ return (
+
+
+ {Type === 'default' &&
}
+ {!['submit_answer', 'objective'].includes(Type) && (
+ <>
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/ProblemType.tsx b/packages/ui-default/components/problemconfig/ProblemType.tsx
new file mode 100644
index 000000000..3525a6d80
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/ProblemType.tsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import {
+ Card, Switch, Tab, Tabs,
+} from '@blueprintjs/core';
+import type { RootState } from './reducer/index';
+import { FormItem, ManagedSelect, SingleFileSelect } from './BasicForm';
+
+export default function ProblemType() {
+ const Type = useSelector((state: RootState) => state.config.type);
+ const checkerType = useSelector((state: RootState) => state.config.checker_type);
+ const filename = useSelector((state: RootState) => state.config.filename);
+ const subType = useSelector((state: RootState) => state.config.subType);
+ const dispatch = useDispatch();
+ const dispatcher = (base) => (value) => dispatch({ ...base, value });
+ return (
+
+
+
+ {i18n('Problem Type')}
+
+
+ {i18n('CheckerType')}
+
+
+
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
+ }}
+ />
+
+
+ )}
+ />
+ {['testlib', 'other'].map((i) => (
+
+ {i === 'other' && (
+
+
+
+ )}
+
+
+
+
+ )}
+ />
+ ))}
+
+ )}
+ />
+
+
+
+
+
+ )}
+ />
+
+
+ {
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'subType', value: subType === 'multi' ? 'single' : 'multi' });
+ }}
+ />
+
+
+ dispatch(({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value }))}
+ className="textbox"
+ />
+
+
+ )}
+ />
+ {i18n('Unsupported configure this type of problem. Please refer to the documentation.')}
)}
+ />
+
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/SubtaskTable.tsx b/packages/ui-default/components/problemconfig/SubtaskTable.tsx
new file mode 100644
index 000000000..9bc1583c3
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/SubtaskTable.tsx
@@ -0,0 +1,186 @@
+import { Card, NumericInput, Tag } from '@blueprintjs/core';
+import { pick, isEqual } from 'lodash';
+import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { SubtaskConfig } from 'hydrooj/src/interface';
+import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
+import i18n from 'vj/utils/i18n';
+import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
+import { FormItem } from './BasicForm';
+import { RootState } from './reducer';
+import { CasesTable } from './TestCasesTable';
+
+const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
+
+export function SubtasksIds({ index }) {
+ const ids = useSelector((state: RootState) => state.config.subtasks.map((i) => (i ? i.id : undefined)).filter((i) => i !== undefined).join(','));
+ const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
+ const dispatch = useDispatch();
+ return (
+
+ if
+
+ i.length)}
+ selectedKeys={subtaskIf?.map(String) || []}
+ onChange={(val) => dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
+ })}
+ multi
+ />
+
+
+ );
+}
+
+export function SubtasksTable({ index }) {
+ const keys = ['id', 'type', 'score', 'time', 'memory'];
+ const subtask = useSelector((state: RootState) => pick(state.config.subtasks[index], keys), eq);
+ const defaultTime = useSelector((state: RootState) => state.config.time);
+ const defaultMemory = useSelector((state: RootState) => state.config.memory);
+ const dispatch = useDispatch();
+ const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent | number) => {
+ let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
+ if (value === 0) value = '';
+ if (value && suffix) value += suffix;
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value,
+ });
+ };
+ if (!subtask || Object.keys(subtask).length === 0) {
+ return (
+
+ Subtasks #{index + 1}
+ {i18n('Failed to parse subtask.')}
+
+ );
+ }
+ return (
+
+ Subtasks #{index + 1}
+ dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' })}>
+ dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' })}
+ >
+
+
+
+
+
+ );
+}
+
+function GlobalTaskConfig() {
+ const time = useSelector((state: RootState) => state.config.time);
+ const memory = useSelector((state: RootState) => state.config.memory);
+ const dispatch = useDispatch();
+ const dispatcher = (key: string, suffix = '') => (ev: React.ChangeEvent | number) => {
+ let value = typeof ev !== 'object' ? ev : ev.currentTarget?.value;
+ if (value === 0) value = '';
+ if (value && suffix) value += suffix;
+ dispatch({ type: 'CONFIG_FORM_UPDATE', key, value });
+ };
+ return (
+ <>
+
+ ms}
+ value={time ? parseTimeMS(time, false).toString() : ''}
+ placeholder={parseTimeMS('1000ms').toString()}
+ onValueChange={dispatcher('time', 'ms')}
+ buttonPosition="none"
+ fill
+ />
+
+
+ MB}
+ value={memory ? parseMemoryMB(memory, false).toString() : ''}
+ placeholder={parseMemoryMB('256m').toString()}
+ onValueChange={dispatcher('memory', 'MB')}
+ buttonPosition="none"
+ fill
+ />
+
+ >
+ );
+}
+
+export function TaskConfig() {
+ const len = useSelector((state: RootState) => state.config.subtasks?.length);
+ const dispatch = useDispatch();
+ return (
+
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/TestCasesTable.tsx b/packages/ui-default/components/problemconfig/TestCasesTable.tsx
new file mode 100644
index 000000000..689f46be9
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/TestCasesTable.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import i18n from 'vj/utils/i18n';
+import { isEqual } from 'lodash';
+import { Tag, NumericInput } from '@blueprintjs/core';
+import { parseTimeMS, parseMemoryMB } from '@hydrooj/utils/lib/common';
+import type { SubtaskConfig } from 'hydrooj/src/interface';
+import type { RootState } from './reducer/index';
+import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
+
+const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
+const eqArr = (a: any[], b: any[]) => isEqual(a, b);
+
+export function TestCaseEntry({ index, subindex }) {
+ const testcase = useSelector((state: RootState) => state.config.subtasks[index].cases[subindex], eq);
+ const Files = useSelector((state: RootState) => state.testdata, eqArr);
+ const defaultTime = useSelector((state: RootState) => state.config.subtasks[index].time || state.config.time);
+ const defaultMemory = useSelector((state: RootState) => state.config.subtasks[index].memory || state.config.memory);
+ const dispatch = useDispatch();
+ const dispatcher = (casesKey: string, valueSuffix = '') => (ev: React.ChangeEvent | number) => {
+ let value = typeof ev !== 'object' ? ev : ev.currentTarget.value;
+ if (value === 0) value = '';
+ if (valueSuffix && value) value += valueSuffix;
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-edit',
+ casesId: subindex,
+ casesKey,
+ value,
+ });
+ };
+ if (!testcase || Object.keys(testcase).length === 0) {
+ return (
+ {i18n('Failed to parse testcase.')}
+ );
+ }
+ return (
+
+
+ ms}
+ value={testcase.time ? parseTimeMS(testcase.time, false).toString() : ''}
+ placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
+ onValueChange={dispatcher('time', 'ms')}
+ buttonPosition="none"
+ fill
+ />
+
+
+ MB}
+ value={testcase.memory ? parseMemoryMB(testcase.memory, false).toString() : ''}
+ placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
+ onValueChange={dispatcher('memory', 'MB')}
+ buttonPosition="none"
+ fill
+ />
+
+ {['input', 'output'].map((t) => (
+
+
+
+ ))}
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
+ })}
+ >
+
+
+
+ );
+}
+
+export function CasesTable({ index }) {
+ const len = useSelector((state: RootState) => state.config.subtasks[index].cases?.length);
+ const dispatch = useDispatch();
+ return (
+
+
+
+ {i18n('Time')}
+ {i18n('Memory')}
+ {i18n('Input')}
+ {i18n('Output')}
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-add',
+ value: { input: '', output: '' },
+ })}
+ >
+
+
+
+
+
+
+ {i18n('Time')}
+ {i18n('Memory')}
+ {i18n('Input')}
+ {i18n('Output')}
+
+ dispatch({
+ type: 'CONFIG_SUBTASK_UPDATE',
+ id: index,
+ key: 'cases-add',
+ value: { input: '', output: '' },
+ })}
+ >
+
+
+
+ {len > 0 && [...Array(len).keys()].map((i) => )}
+
+
+ );
+}
diff --git a/packages/ui-default/components/problemconfig/reducer/config.ts b/packages/ui-default/components/problemconfig/reducer/config.ts
new file mode 100644
index 000000000..711168ca8
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/reducer/config.ts
@@ -0,0 +1,69 @@
+import yaml from 'js-yaml';
+import type { ProblemConfigFile } from 'hydrooj/src/interface';
+import { cloneDeep } from 'lodash';
+import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
+
+type State = ProblemConfigFile & { __loaded: boolean };
+
+export default function reducer(state = { type: 'default', __loaded: false } as State, action): State {
+ switch (action.type) {
+ case 'CONFIG_LOAD_FULFILLED': {
+ return { ...state, ...yaml.load(action.payload.config) as object, __loaded: true };
+ }
+ case 'CONFIG_FORM_UPDATE': {
+ const next = { ...state, [action.key]: action.value };
+ if (action.key === 'score' && action.value) next.score = +next.score;
+ if (action.key === 'checker_type' && action.value === 'other') next.checker_type = 'syzoj';
+ if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
+ return next;
+ }
+ case 'CONFIG_CODE_UPDATE': {
+ try {
+ return { ...state, ...yaml.load(action.payload) as object };
+ } catch {
+ return state;
+ }
+ }
+ case 'CONFIG_AUTOCASES_UPDATE': {
+ const next = { ...state };
+ const { subtasks } = action;
+ for (const subtask of subtasks) {
+ if (subtask.time === parseTimeMS(state.time || '1s')) delete subtask.time;
+ if (subtask.memory === parseMemoryMB(state.memory || '256m')) delete subtask.memory;
+ if (subtask.time) subtask.time += 'ms';
+ if (subtask.memory) subtask.memory += 'MB';
+ }
+ if (subtasks.length === 0) next.subtasks = [];
+ else {
+ next.subtasks = subtasks.map((subtask) => (
+ { ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
+ }
+ return next;
+ }
+ case 'CONFIG_SUBTASK_UPDATE': {
+ const subtasks = cloneDeep(state.subtasks);
+ const subsubtasks = cloneDeep(state.subtasks[action.id]);
+ if (action.value !== '' && ['score', 'id'].includes(action.key)) action.value = +action.value;
+ if (action.key === 'if' && action.value.join('') !== '') action.value = action.value.map((i) => +i);
+ if (action.key.split('-')[0] === 'cases') {
+ if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
+ else if (action.key === 'cases-edit') {
+ if (action.value === '') delete subsubtasks.cases[action.casesId][action.casesKey];
+ else subsubtasks.cases[action.casesId][action.casesKey] = action.value;
+ } else if (action.key === 'cases-delete') {
+ subsubtasks.cases = subsubtasks.cases.filter((k, v) => v !== action.value);
+ }
+ } else if (action.key === 'add') {
+ return { ...state, subtasks: [...subtasks, { time: state.time || '1s', memory: state.memory || '256m', cases: [] }] };
+ } else if (action.key === 'delete') return { ...state, subtasks: subtasks.filter((k, v) => v !== action.id) };
+ else {
+ if (action.value === '' || (action.key === 'if' && action.value.join('') === '')) delete subsubtasks[action.key];
+ else subsubtasks[action.key] = action.value;
+ }
+ subtasks[action.id] = subsubtasks;
+ return { ...state, subtasks };
+ }
+ default:
+ return state;
+ }
+}
diff --git a/packages/ui-default/components/problemconfig/reducer/index.ts b/packages/ui-default/components/problemconfig/reducer/index.ts
new file mode 100644
index 000000000..3ddfbafbd
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/reducer/index.ts
@@ -0,0 +1,11 @@
+import { combineReducers } from 'redux';
+import config from './config';
+import testdata from './testdata';
+
+const reducer = combineReducers({
+ config,
+ testdata,
+});
+
+export default reducer;
+export type RootState = ReturnType;
diff --git a/packages/ui-default/components/problemconfig/reducer/testdata.ts b/packages/ui-default/components/problemconfig/reducer/testdata.ts
new file mode 100644
index 000000000..34fb0fb88
--- /dev/null
+++ b/packages/ui-default/components/problemconfig/reducer/testdata.ts
@@ -0,0 +1,16 @@
+export default function reducer(state = [], action) {
+ switch (action.type) {
+ case 'CONFIG_LOAD_FULFILLED': {
+ return state.concat(action.payload.testdata);
+ }
+ case 'CONFIG_DELETE_TESTDATA': {
+ const next = state.filter((i) => i.name !== action.value[0]);
+ return next;
+ }
+ case 'CONFIG_ADD_TESTDATA': {
+ return state.concat([action.value]);
+ }
+ default:
+ return state;
+ }
+}
diff --git a/packages/ui-default/components/scratchpad/reducers/records.ts b/packages/ui-default/components/scratchpad/reducers/records.ts
index 4c44b09e6..3b8b0d655 100644
--- a/packages/ui-default/components/scratchpad/reducers/records.ts
+++ b/packages/ui-default/components/scratchpad/reducers/records.ts
@@ -5,36 +5,36 @@ export default function reducer(state = {
items: {},
}, action) {
switch (action.type) {
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
- const { rdocs } = action.payload;
- return {
- ...state,
- rows: _.map(rdocs, '_id'),
- items: _.keyBy(rdocs, '_id'),
- };
- }
- case 'SCRATCHPAD_RECORDS_PUSH': {
- const { rdoc } = action.payload;
- const rows = [...state.rows];
- if (!rows.includes(rdoc._id)) {
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
+ const { rdocs } = action.payload;
+ return {
+ ...state,
+ rows: _.map(rdocs, '_id'),
+ items: _.keyBy(rdocs, '_id'),
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_PUSH': {
+ const { rdoc } = action.payload;
+ const rows = [...state.rows];
+ if (!rows.includes(rdoc._id)) {
+ return {
+ ...state,
+ rows: [rdoc._id, ...state.rows],
+ items: {
+ ...state.items,
+ [rdoc._id]: rdoc,
+ },
+ };
+ }
return {
...state,
- rows: [rdoc._id, ...state.rows],
items: {
...state.items,
[rdoc._id]: rdoc,
},
};
}
- return {
- ...state,
- items: {
- ...state.items,
- [rdoc._id]: rdoc,
- },
- };
- }
- default:
- return state;
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/scratchpad/reducers/ui.ts b/packages/ui-default/components/scratchpad/reducers/ui.ts
index 257868f14..ca374c480 100644
--- a/packages/ui-default/components/scratchpad/reducers/ui.ts
+++ b/packages/ui-default/components/scratchpad/reducers/ui.ts
@@ -19,93 +19,93 @@ export default function reducer(state = {
isPosting: false,
}, action) {
switch (action.type) {
- case 'SCRATCHPAD_UI_CHANGE_SIZE': {
- const { uiElement, size } = action.payload;
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- size,
- },
- };
- }
- case 'SCRATCHPAD_UI_SET_VISIBILITY': {
- const { uiElement, visibility } = action.payload;
- localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- visible: visibility,
- },
- };
- }
- case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
- const { uiElement } = action.payload;
- localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
- return {
- ...state,
- [uiElement]: {
- ...state[uiElement],
- visible: !state[uiElement].visible,
- },
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_PENDING':
- case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
- return {
- ...state,
- isPosting: true,
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
- case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
- Notification.success(i18n('Submitted.'));
- if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
- state.records.visible = true;
+ case 'SCRATCHPAD_UI_CHANGE_SIZE': {
+ const { uiElement, size } = action.payload;
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ size,
+ },
+ };
}
- return {
- ...state,
- isPosting: false,
- };
- }
- case 'SCRATCHPAD_POST_PRETEST_REJECTED':
- case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
- Notification.error(action.payload.message);
- return {
- ...state,
- isPosting: false,
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: true,
- },
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
- Notification.error(action.payload.message);
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: false,
- },
- };
- }
- case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
- return {
- ...state,
- records: {
- ...state.records,
- isLoading: false,
- },
- };
- }
- default:
- return state;
+ case 'SCRATCHPAD_UI_SET_VISIBILITY': {
+ const { uiElement, visibility } = action.payload;
+ localStorage.setItem(`scratchpad/${uiElement}`, visibility.toString());
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ visible: visibility,
+ },
+ };
+ }
+ case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
+ const { uiElement } = action.payload;
+ localStorage.setItem(`scratchpad/${uiElement}`, (!state[uiElement].visible).toString());
+ return {
+ ...state,
+ [uiElement]: {
+ ...state[uiElement],
+ visible: !state[uiElement].visible,
+ },
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_PENDING':
+ case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
+ return {
+ ...state,
+ isPosting: true,
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
+ case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
+ Notification.success(i18n('Submitted.'));
+ if (action.type === 'SCRATCHPAD_POST_SUBMIT_FULFILLED' && UiContext.canViewRecord) {
+ state.records.visible = true;
+ }
+ return {
+ ...state,
+ isPosting: false,
+ };
+ }
+ case 'SCRATCHPAD_POST_PRETEST_REJECTED':
+ case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
+ Notification.error(action.payload.message);
+ return {
+ ...state,
+ isPosting: false,
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: true,
+ },
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
+ Notification.error(action.payload.message);
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: false,
+ },
+ };
+ }
+ case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
+ return {
+ ...state,
+ records: {
+ ...state.records,
+ isLoading: false,
+ },
+ };
+ }
+ default:
+ return state;
}
}
diff --git a/packages/ui-default/components/socket/index.js b/packages/ui-default/components/socket/index.js
index 0435d062b..6bb9b0e3b 100644
--- a/packages/ui-default/components/socket/index.js
+++ b/packages/ui-default/components/socket/index.js
@@ -5,7 +5,6 @@ export default class Sock {
const i = new URL(url, window.location.href);
i.protocol = i.protocol.replace('http', 'ws');
this.url = i.toString();
- this.closed = false;
this.sock = new ReconnectingWebSocket(this.url);
this.sock.onopen = () => {
console.log('Connected');
@@ -13,17 +12,14 @@ export default class Sock {
};
this.sock.onclose = ({ code, reason }) => {
console.warn('Connection closed, ', code, reason);
- if (code >= 4000) this.closed = true;
+ if (code >= 4000) this.close();
this.onclose?.(code, reason);
};
this.sock.onmessage = (message) => {
if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message);
const msg = JSON.parse(message.data);
- if (msg.event === 'auth') {
- if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') {
- this.close();
- }
- } else this.onmessage?.(message);
+ if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') this.close();
+ else this.onmessage?.(message);
};
}
@@ -32,7 +28,6 @@ export default class Sock {
}
close() {
- this.closed = true;
this.sock?.close?.();
}
}
diff --git a/packages/ui-default/components/zipDownloader/index.js b/packages/ui-default/components/zipDownloader/index.js
index 56ea2f88d..eb3c3e183 100644
--- a/packages/ui-default/components/zipDownloader/index.js
+++ b/packages/ui-default/components/zipDownloader/index.js
@@ -51,6 +51,7 @@ export default async function download(filename, targets) {
stream,
};
} catch (e) {
+ window.captureException?.(e);
stopDownload();
Notification.error(i18n('Download Error', [target.filename, e.toString()]));
}
@@ -144,6 +145,7 @@ export async function downloadProblemSet(pids, name = 'Export') {
}
await download(`${name}.zip`, targets);
} catch (e) {
+ window.captureException?.(e);
Notification.error(`${e.message} ${e.params?.[0]}`);
}
}
diff --git a/packages/ui-default/entry.js b/packages/ui-default/entry.js
index 8cc02a54c..2e2524581 100644
--- a/packages/ui-default/entry.js
+++ b/packages/ui-default/entry.js
@@ -4,6 +4,7 @@ window.Hydro = {
components: {},
utils: {},
node_modules: {},
+ version: process.env.VERSION,
};
window.externalModules = {};
diff --git a/packages/ui-default/hydro.ts b/packages/ui-default/hydro.ts
index 848db2ab4..20d45eaae 100644
--- a/packages/ui-default/hydro.ts
+++ b/packages/ui-default/hydro.ts
@@ -64,6 +64,7 @@ async function load() {
try {
await func(currentPageName, loadPage(1, type));
} catch (e) {
+ (window as any).captureException?.(e);
Notification.warn(`Failed to call '${type}Loading' of ${page.name}`);
console.error(`Failed to call '${type}Loading' of ${page.name}\n${e.stack}`);
console.error(e);
diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml
index 429064326..4a7ae97f2 100644
--- a/packages/ui-default/locales/zh.yaml
+++ b/packages/ui-default/locales/zh.yaml
@@ -86,6 +86,7 @@ Attend Contest: 参加比赛
Attend contests: 参加比赛
Attended: 已参加
author: 作者
+Auto Read Tasks: 自动识别任务
Be Copied: 被复制
Begin at: 开始于
Begin Date: 开始日期
@@ -117,6 +118,8 @@ Change Email: 修改电子邮件
Change Password: 修改密码
Change Theme: 切换主题
Check In: 签到
+Checker: 比较器
+CheckerType: 比较器类型
Chinese: 中文
Choose the background image in your profile page.: 选择您资料页面的背景图片。
Claim homework: 认领作业
@@ -138,6 +141,7 @@ Compile Error: 编译错误
Compilers' Version and Parameters: 编译器版本及参数
Complete: 完成
Completed: 已完成
+Config: 配置
Confirm deleting the selected roles?: 您确定删除所选角色吗?
Confirm deleting this comment? Its replies will be deleted as well.: 确认删除这个评论吗?回复会被同时删除。
Confirm deleting this reply?: 确认删除这个回复吗?
@@ -268,6 +272,7 @@ domain: 域
Domain: 域
Don't have an account?: 还没有账户?
Done: 已结束
+Download All: 下载全部
Download Dataset: 下载数据集
Download Selected: 下载选中
Download: 下载
@@ -318,7 +323,10 @@ Export as CSV: 导出为 CSV
Export as HTML: 导出为 HTML
Extension (days): 最长延期 (日)
Extension Score Penalty: 延期递交扣分规则
+Extra Files Config: 额外文件设置
Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。
+Failed to parse subtask.: 无法解析子任务信息。
+Failed to parse testcase.: 无法解析测试点信息。
Feedback: 反馈
Field {0} or {1} validation failed.: 字段 {0} 或 {1} 验证失败。
Field {0} validation failed.: 字段 {0} 验证失败。
@@ -382,6 +390,7 @@ Host: 主持人
hour(s): 小时
If enabled, source code will be emailed to you after the submission is accepted.: 如果启用,在您通过题目后,源代码会以 Email 的形式发送给您。
If left blank, the built-in template of the corresponding language will be used.: 若留空,则将使用对应语言的内置代码模板。
+Ignore trailing space and enter.: 忽略行末空格与文件尾回车。
Images With Size: 指定大小的图片
Images: 图片
Import from: 导入自
@@ -400,6 +409,7 @@ Information: 信息
Initialize: 初始化
Inline codes: 行内代码
Inline formulas: 行内公式
+Input: 输入
Insert Code: 插入代码
Insert Image: 插入图像
Installed Modules: 已安装的模块
@@ -414,12 +424,16 @@ Is rated: 计入排名
It works!: 它成了!
Italic: 斜体
Join: 加入
+Judge Config: 评测设置
Judge Status: 评测状态
+judge_extra_files: 评测额外文件
judge_playground: 评测游乐场
Judged At: 评测时间
Judged By: 评测机
Judging Queue: 评测队列
Keep current expiration: 保持当前过期设置
+Langs Config: 语言设置
+langs: 语言
Language: 语言
last active at: 最后活动于
last login at: 最后登录于
@@ -531,6 +545,7 @@ Or, with automatically filled invitation code: 或者,这是可以自动填写
Ordered List: 有序列表
Original Score: 原始分数
OS Info: 系统信息
+Output: 输出
Over {0} problems: 超过 {0} 道题目
Owner: 所有者
page.problem_detail.sidebar.show_category: 点击显示
@@ -582,6 +597,8 @@ Problem Solution List: 题解列表
Problem Solution: 题解列表
Problem Tags Visibility: 题目标签可见性
Problem Tags: 题目标签
+Problem Type: 题目类型
+problem_config: 评测设置
problem_create: 创建题目
problem_edit: 编辑题目
problem_import: 导入题目
@@ -755,6 +772,7 @@ SuperUser: 超级管理员
Support: 支持
Switch account: 切换账户
Tags: 标签
+Task Settings: 任务设置
tasks_list: 任务列表
Technical Information: 技术信息
Temporary session: 临时会话
@@ -811,9 +829,11 @@ Uncompleted: 未完成
Unique: 唯一
Unknown field {0}.: 未知字段 {0}。
Unknown: 未知
+Unlimited: 无限制
Unordered List: 无序列表
Unstar Topic: 取消收藏
Unstar: 取消收藏
+Unsupported configure this type of problem. Please refer to the documentation.: 暂不支持快捷配置此类题目。请参照文档操作。
Update Permission: 更新权限
Update Settings: 更新设置
Update: 更新
@@ -830,12 +850,14 @@ Usage exceeded.: 用量超限。
Use admin specificed: 由管理员指定
Use algorithm calculated: 使用算法计算
Use average of above: 使用上面的平均值
+Use Subtasks Mode: 使用子任务模式
User {0} already exists.: 用户 {0} 已存在。
User {0} not found.: 用户 {0} 不存在。
User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域
User ID: 用户 ID
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
user_detail: 用户
+user_extra_files: 编译额外文件
user_login: 登录
user_logout: 登出
user_lostpass: 忘记密码
diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json
index 255ab91a7..e23d6985d 100644
--- a/packages/ui-default/package.json
+++ b/packages/ui-default/package.json
@@ -1,24 +1,24 @@
{
"name": "@hydrooj/ui-default",
- "version": "4.35.0",
+ "version": "4.37.6",
"author": "undefined ",
"license": "AGPL-3.0",
"main": "hydro.js",
"repository": "https://github.com/hydro-dev/Hydro.git",
"preferUnplugged": true,
"scripts": {
- "lint": "eslint ."
+ "lint": "eslint"
},
"devDependencies": {
- "@blueprintjs/core": "^3.54.0",
- "@fontsource/dm-mono": "^4.5.7",
- "@fontsource/fira-code": "^4.5.8",
- "@fontsource/inconsolata": "^4.5.4",
- "@fontsource/jetbrains-mono": "^4.5.5",
- "@fontsource/pt-mono": "^4.5.5",
- "@fontsource/roboto-mono": "^4.5.5",
- "@fontsource/source-code-pro": "^4.5.6",
- "@fontsource/ubuntu-mono": "^4.5.6",
+ "@blueprintjs/core": "^4.3.0",
+ "@fontsource/dm-mono": "^4.5.8",
+ "@fontsource/fira-code": "^4.5.9",
+ "@fontsource/inconsolata": "^4.5.6",
+ "@fontsource/jetbrains-mono": "^4.5.8",
+ "@fontsource/pt-mono": "^4.5.8",
+ "@fontsource/roboto-mono": "^4.5.7",
+ "@fontsource/source-code-pro": "^4.5.9",
+ "@fontsource/ubuntu-mono": "^4.5.9",
"@hydrooj/utils": "workspace:*",
"@milkdown/core": "^6.0.1",
"@milkdown/plugin-clipboard": "^6.0.1",
@@ -42,8 +42,8 @@
"@types/json-schema": "^7.0.11",
"@types/katex": "^0.14.0",
"@types/qrcode": "^1.4.2",
+ "@types/react-dom": "^18.0.3",
"@types/redux-logger": "^3.0.9",
- "@undefined-moe/monaco-yaml": "^2.5.1",
"ansi_up": "^5.1.0",
"autoprefixer": "^9.8.8",
"browser-update": "^3.3.38",
@@ -51,19 +51,19 @@
"chunk-progress-webpack-plugin": "^2.0.1",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
- "clipboard": "^2.0.10",
+ "clipboard": "^2.0.11",
"copy-webpack-plugin": "^6.4.1",
"css-loader": "^4.3.0",
+ "diff": "^5.0.0",
"diff-dom": "^4.2.3",
"echarts": "^5.3.2",
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^2.18.0",
- "eslint": "^8.12.0",
"fancy-log": "^2.0.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
- "graphiql": "^1.8.3",
+ "graphiql": "^1.8.9",
"gulp": "^4.0.2",
"gulp-cli": "^2.3.0",
"gulp-iconfont": "^11.0.1",
@@ -76,31 +76,32 @@
"jquery.transit": "^0.9.12",
"material-icons": "^1.10.9",
"mini-css-extract-plugin": "^1.6.2",
- "moment": "^2.29.1",
- "monaco-editor": "^0.31.1",
+ "moment": "^2.29.3",
+ "monaco-editor": "^0.33.0",
"monaco-editor-nls": "^2.0.0",
"monaco-editor-webpack-plugin": "^7.0.1",
- "monaco-themes": "^0.4.0",
- "nanoid": "^3.3.2",
+ "monaco-themes": "^0.4.2",
+ "monaco-yaml": "^4.0.0-alpha.3",
+ "nanoid": "^3.3.4",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pickadate": "^3.6.4",
"plugin-error": "^1.0.1",
"postcss-loader": "^3.0.0",
"prism-themes": "^1.9.0",
- "prismjs": "^1.27.0",
+ "prismjs": "^1.28.0",
"prop-types": "^15.8.1",
"qrcode": "^1.5.0",
"queue-microtask": "^1.2.3",
"raw-loader": "^4.0.2",
- "react": "^17.0.2",
- "react-dnd": "^16.0.0",
- "react-dnd-html5-backend": "^16.0.0",
- "react-dom": "^17.0.2",
- "react-redux": "^7.2.8",
+ "react": "^18.1.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
+ "react-dom": "^18.1.0",
+ "react-redux": "^8.0.1",
"react-split-pane": "^0.1.92",
"reconnecting-websocket": "^4.4.0",
- "redux": "^4.1.2",
+ "redux": "^4.2.0",
"redux-logger": "^3.0.6",
"redux-promise-middleware": "^6.1.2",
"redux-thunk": "^2.4.1",
@@ -118,7 +119,7 @@
"timeago-react": "^3.0.4",
"timeago.js": "^4.0.2",
"vinyl-buffer": "^1.0.1",
- "web-streams-polyfill": "^3.2.0",
+ "web-streams-polyfill": "^3.2.1",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-dev-server": "^3.11.3",
@@ -126,13 +127,13 @@
},
"dependencies": {
"esbuild": "0.14.3",
- "fs-extra": "^10.0.1",
+ "fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"katex": "^0.15.3",
"lodash": "^4.17.21",
- "markdown-it": "^12.3.2",
- "markdown-it-anchor": "^8.4.1",
+ "markdown-it": "^13.0.1",
+ "markdown-it-anchor": "^8.6.4",
"markdown-it-footnote": "^3.0.3",
"markdown-it-imsize": "^2.0.1",
"markdown-it-mark": "^3.0.1",
diff --git a/packages/ui-default/pages/api.page.tsx b/packages/ui-default/pages/api.page.tsx
index e2fcef103..87f5ab589 100644
--- a/packages/ui-default/pages/api.page.tsx
+++ b/packages/ui-default/pages/api.page.tsx
@@ -2,7 +2,7 @@
import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import React from 'react';
-import { render } from 'react-dom';
+import ReactDOM from 'react-dom/client';
const Logo = () => Hydro API Console ;
const defaultQuery = `\
@@ -43,7 +43,7 @@ const page = new NamedPage('api', async () => {
/>
);
- render( , document.getElementById('graphiql'));
+ ReactDOM.createRoot(document.getElementById('graphiql')).render( );
});
export default page;
diff --git a/packages/ui-default/pages/home_files.page.js b/packages/ui-default/pages/home_files.page.js
index 4b8a816cd..e2f3ee0c2 100644
--- a/packages/ui-default/pages/home_files.page.js
+++ b/packages/ui-default/pages/home_files.page.js
@@ -40,12 +40,12 @@ async function handleClickUpload(files) {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
@@ -88,6 +88,7 @@ async function handleClickUpload(files) {
Notification.success(i18n('File uploaded successfully.'));
await pjax.request({ push: false });
} catch (e) {
+ window.captureException?.(e);
console.error(e);
Notification.error(i18n('File upload failed: {0}', e.toString()));
} finally {
diff --git a/packages/ui-default/pages/home_messages.page.jsx b/packages/ui-default/pages/home_messages.page.jsx
index 698492aea..d6b2258f4 100644
--- a/packages/ui-default/pages/home_messages.page.jsx
+++ b/packages/ui-default/pages/home_messages.page.jsx
@@ -1,4 +1,6 @@
import { NamedPage } from 'vj/misc/Page';
+import React from 'react';
+import { createRoot } from 'react-dom/client';
import api, { gql } from 'vj/utils/api';
import loadReactRedux from 'vj/utils/loadReactRedux';
import parseQueryString from 'vj/utils/parseQueryString';
@@ -26,9 +28,7 @@ const page = new NamedPage('home_messages', () => {
const { default: WebSocket } = await import('../components/socket');
const { default: MessagePadApp } = await import('../components/messagepad');
const { default: MessagePadReducer } = await import('../components/messagepad/reducers');
- const {
- React, render, Provider, store,
- } = await loadReactRedux(MessagePadReducer);
+ const { Provider, store } = await loadReactRedux(MessagePadReducer);
reduxStore = store;
@@ -71,7 +71,7 @@ const page = new NamedPage('home_messages', () => {
return this;
};
- render(
+ createRoot($('#messagePad').get(0)).render(
{
@@ -84,7 +84,6 @@ const page = new NamedPage('home_messages', () => {
}}
/>
,
- $('#messagePad').get(0),
);
}
diff --git a/packages/ui-default/pages/homework_main.page.js b/packages/ui-default/pages/homework_main.page.js
index 4dc9a6d58..842e223b2 100644
--- a/packages/ui-default/pages/homework_main.page.js
+++ b/packages/ui-default/pages/homework_main.page.js
@@ -19,16 +19,16 @@ const page = new NamedPage('homework_main', () => {
calendar.getDom().appendTo('[name="calendar_entry"]');
$('[name="homework_display"]').change((ev) => {
switch (ev.currentTarget.value) {
- case 'calendar':
- $('.homework__list').hide();
- $('[name="calendar_entry"]').show();
- break;
- case 'list':
- $('.homework__list').show();
- $('[name="calendar_entry"]').hide();
- break;
- default:
- throw new Error('Unexpected display parameter');
+ case 'calendar':
+ $('.homework__list').hide();
+ $('[name="calendar_entry"]').show();
+ break;
+ case 'list':
+ $('.homework__list').show();
+ $('[name="calendar_entry"]').hide();
+ break;
+ default:
+ throw new Error('Unexpected display parameter');
}
});
}
diff --git a/packages/ui-default/pages/problem_config.page.styl b/packages/ui-default/pages/problem_config.page.styl
new file mode 100644
index 000000000..ef43e08a1
--- /dev/null
+++ b/packages/ui-default/pages/problem_config.page.styl
@@ -0,0 +1,66 @@
+.page--problem_config
+ .section--problem-sidebar-testdata
+ .problem-sidebar-testdata__detail
+ max-height: 270px
+ overflow-y: auto
+
+ .col--size
+ width: 80px
+ .col--operation
+ width: 40px
+ text-align: center
+
+ .textbox,.select
+ margin: 0
+
+ .bp4-tab-panel
+ margin-top: 5px
+
+ .problem-config-form
+ font-family: var(--code-font-family)
+
+ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button
+ -webkit-appearance: none !important
+ input[type="number"]
+ -moz-appearance: textfield
+
+ .bp4-tag.bp4-minimal
+ background: transparent
+ .bp4-input-action
+ pointer-events: none
+ .bp4-input-group
+ input
+ appearance: none
+ display: block
+ width: 100%
+ font-size: 0.875rem
+ height: 2.1875rem
+ line-height: 1.2
+ padding: 0.3125rem;
+ border: 1px solid #ddd
+ color: inherit
+ border-radius: 0
+ background-color: transparent
+ outline: 2px solid transparent
+ transition: outline-color 0.2s, border-color 0.2s
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1)
+ box-shadow: none
+
+
+ .data-table
+ tr
+ border-top: 0
+ border-bottom: 0
+ height: auto
+ td
+ padding: 0
+ .thead td
+ text-align: center
+ td input, td div
+ background: transparent
+
+ .hover-row.status-bar
+ display: none
+
+ input::placeholder
+ color: rgba(95,107,124,.6)!important
\ No newline at end of file
diff --git a/packages/ui-default/pages/problem_config.page.tsx b/packages/ui-default/pages/problem_config.page.tsx
new file mode 100644
index 000000000..f73dbad86
--- /dev/null
+++ b/packages/ui-default/pages/problem_config.page.tsx
@@ -0,0 +1,203 @@
+import { NamedPage } from 'vj/misc/Page';
+import request from 'vj/utils/request';
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import loadReactRedux from 'vj/utils/loadReactRedux';
+import i18n from 'vj/utils/i18n';
+import yaml from 'js-yaml';
+import Notification from 'vj/components/notification';
+import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
+import download from 'vj/components/zipDownloader';
+import { size, readSubtasksFromFiles, normalizeSubtasks } from '@hydrooj/utils/lib/common';
+import tpl from 'vj/utils/tpl';
+import { SubtaskType } from 'hydrooj/src/interface';
+import { configYamlFormat } from 'vj/components/problemconfig/ProblemConfigEditor';
+
+function onBeforeUnload(e) {
+ e.returnValue = '';
+}
+
+const page = new NamedPage('problem_config', () => {
+ let reduxStore;
+
+ async function handleClickUpload() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.multiple = true;
+ input.click();
+ await new Promise((resolve) => { input.onchange = resolve; });
+ const { files } = input;
+ const dialog = new Dialog({
+ $body: `
+
+
+
+
`,
+ });
+ try {
+ Notification.info(i18n('Uploading files...'));
+ window.addEventListener('beforeunload', onBeforeUnload);
+ dialog.open();
+ const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
+ const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
+ const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
+ const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
+ for (const i in files) {
+ if (Number.isNaN(+i)) continue;
+ const file = files[i];
+ const data = new FormData();
+ data.append('filename', file.name);
+ data.append('file', file);
+ data.append('type', 'testdata');
+ data.append('operation', 'upload_file');
+ await request.postFile('./files', data, {
+ xhr() {
+ const xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener('loadstart', () => {
+ $fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
+ $fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
+ $uploadLabel.text(i18n('Uploading... ({0}%)', 0));
+ $uploadProgress.width(0);
+ });
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const percentComplete = Math.round((e.loaded / e.total) * 100);
+ if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
+ else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
+ $uploadProgress.width(`${percentComplete}%`);
+ }
+ }, false);
+ return xhr;
+ },
+ });
+ reduxStore.dispatch({
+ type: 'CONFIG_ADD_TESTDATA',
+ value: {
+ _id: file.name,
+ name: file.name,
+ size: file.size,
+ },
+ });
+ $('.testdata-table tbody').append(
+ $(tpl`
+ ${file.name}
+ ${size(file.size)}
+
+ `),
+ );
+ }
+ window.removeEventListener('beforeunload', onBeforeUnload);
+ Notification.success(i18n('File uploaded successfully.'));
+ } catch (e) {
+ console.error(e);
+ Notification.error(i18n('File upload failed: {0}', e.toString()));
+ } finally {
+ dialog.close();
+ }
+ }
+
+ async function handleClickRemove(ev: JQuery.ClickEvent
) {
+ const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
+ const action = await new ConfirmDialog({
+ $body: tpl.typoMsg(i18n('Confirm to delete the file?')),
+ }).open();
+ if (action !== 'yes') return;
+ try {
+ await request.post('./files', {
+ operation: 'delete_files',
+ files: file,
+ type: 'testdata',
+ });
+ Notification.success(i18n('File have been deleted.'));
+ reduxStore.dispatch({
+ type: 'CONFIG_DELETE_TESTDATA',
+ value: file,
+ });
+ $(ev.currentTarget).parent().parent().remove();
+ } catch (error) {
+ Notification.error(error.message);
+ }
+ }
+
+ async function handleClickDownloadAll() {
+ const files = reduxStore.getState().testdata.map((i) => i.name);
+ const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'testdata' });
+ const targets = [];
+ for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
+ await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
+ }
+
+ async function uploadConfig(config: object) {
+ const configYaml = yaml.dump(configYamlFormat(config));
+ Notification.info(i18n('Saving file...'));
+ const data = new FormData();
+ data.append('filename', 'config.yaml');
+ data.append('file', new Blob([configYaml], { type: 'text/plain' }));
+ data.append('type', 'testdata');
+ data.append('operation', 'upload_file');
+ await request.postFile('./files', data);
+ Notification.success(i18n('File saved.'));
+ window.location.reload();
+ }
+
+ async function mountComponent() {
+ const [{ default: ProblemConfigEditor }, { default: ProblemConfigForm }, { default: ProblemConfigReducer }] = await Promise.all([
+ import('vj/components/problemconfig/ProblemConfigEditor'),
+ import('vj/components/problemconfig/ProblemConfigForm'),
+ import('vj/components/problemconfig/reducer'),
+ ]);
+
+ const { Provider, store } = await loadReactRedux(ProblemConfigReducer);
+
+ reduxStore = store;
+
+ store.dispatch({
+ type: 'CONFIG_LOAD',
+ payload: request.get(),
+ });
+ const unsubscribe = store.subscribe(() => {
+ // TODO set yaml schema
+ const state = store.getState();
+ if (!state.config.__loaded) return;
+ if (state.config.cases) {
+ const score = state.config.score * state.config.cases.length;
+ state.config.subtasks = [{ type: 'sum' as SubtaskType, score: score && score < 100 ? score : 100, cases: state.config.cases }];
+ delete state.config.cases;
+ delete state.config.score;
+ }
+ if (state.config.subtasks) return;
+ const testdata = (state.testdata || []).map((i) => i.name);
+ unsubscribe();
+ const subtasks = readSubtasksFromFiles(testdata, state.config);
+ store.dispatch({
+ type: 'CONFIG_AUTOCASES_UPDATE',
+ subtasks: normalizeSubtasks(subtasks, (i) => i, state.config.time, state.config.memory, true),
+ });
+ });
+ createRoot($('#ProblemConfig').get(0)).render(
+
+
+ uploadConfig(store.getState().config)}>{i18n('Submit')}
+ ,
+ );
+ }
+
+ mountComponent();
+
+ $(document).on('click', '[name="testdata__upload"]', () => handleClickUpload());
+ $(document).on('click', '[name="testdata__delete"]', (ev) => handleClickRemove(ev));
+ $(document).on('click', '[name="testdata__download__all"]', () => handleClickDownloadAll());
+});
+
+export default page;
diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx
index 4d2890a8d..49fc75686 100644
--- a/packages/ui-default/pages/problem_detail.page.jsx
+++ b/packages/ui-default/pages/problem_detail.page.jsx
@@ -1,4 +1,6 @@
import yaml from 'js-yaml';
+import ReactDOM, { createRoot } from 'react-dom/client';
+import React from 'react';
import { getScoreColor } from '@hydrooj/utils/lib/status';
import { NamedPage } from 'vj/misc/Page';
import { downloadProblemSet } from 'vj/components/zipDownloader';
@@ -132,9 +134,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
const { default: WebSocket } = await import('../components/socket');
const { default: ScratchpadApp } = await import('../components/scratchpad');
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
- const {
- React, render, unmountComponentAtNode, Provider, store,
- } = await loadReactRedux(ScratchpadReducer);
+ const { Provider, store } = await loadReactRedux(ScratchpadReducer);
const sock = new WebSocket(UiContext.pretestConnUrl);
sock.onmessage = (message) => {
@@ -145,17 +145,15 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
});
};
+ const root = createRoot($('#scratchpad').get(0));
renderReact = () => {
- render(
+ root.render(
,
- $('#scratchpad').get(0),
);
};
- unmountReact = () => {
- unmountComponentAtNode($('#scratchpad').get(0));
- };
+ unmountReact = () => root.unmount();
reactLoaded = true;
$('.loader-container').hide();
}
@@ -192,17 +190,12 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
const props = yaml.load(s);
if (!(props instanceof Array)) return;
$('.outer-loader-container').show();
- const [{ default: Objective }, React, ReactDOM] = await Promise.all([
- import('vj/components/objective-question/index'),
- import('react'),
- import('react-dom'),
- ]);
+ const { default: Objective } = await import('vj/components/objective-question/index');
- ReactDOM.render(
+ ReactDOM.createRoot($('.problem-content').get(0)).render(
,
- $('.problem-content').get(0),
);
$('.outer-loader-container').hide();
} catch (e) { }
diff --git a/packages/ui-default/pages/problem_edit.page.js b/packages/ui-default/pages/problem_edit.page.js
index ec97f3928..759ccec00 100644
--- a/packages/ui-default/pages/problem_edit.page.js
+++ b/packages/ui-default/pages/problem_edit.page.js
@@ -3,10 +3,13 @@ import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import i18n from 'vj/utils/i18n';
-import { ConfirmDialog } from 'vj/components/dialog';
+import Dialog, { ConfirmDialog } from 'vj/components/dialog';
import Dropdown from 'vj/components/dropdown/Dropdown';
import Editor from 'vj/components/editor/index';
import Notification from 'vj/components/notification';
+import { slideDown, slideUp } from 'vj/utils/slide';
+import download from 'vj/components/zipDownloader';
+import { size } from '@hydrooj/utils/lib/common';
const categories = {};
const dirtyCategories = [];
@@ -18,6 +21,10 @@ function setDomSelected($dom, selected) {
else $dom.removeClass('selected');
}
+function onBeforeUnload(e) {
+ e.returnValue = '';
+}
+
async function updateSelection() {
dirtyCategories.forEach(({ type, category, subcategory }) => {
let item = categories[category];
@@ -143,6 +150,21 @@ function buildCategoryFilter() {
});
}
+async function handleSection(ev, sidebar, type) {
+ const $section = $(ev.currentTarget).closest(`.section--problem-sidebar-${sidebar}`);
+ if ($section.is(`.${type}d, .animating`)) return;
+ $section.addClass('animating');
+ const $detail = $section.find(`.section--problem-sidebar-${sidebar}__detail`);
+ if (type === 'expand') {
+ await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 });
+ } else {
+ await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 });
+ }
+ $section.addClass(type === 'expand' ? 'expanded' : 'collapsed');
+ $section.removeClass(type === 'expand' ? 'collapsed' : 'expanded');
+ $section.removeClass('animating');
+}
+
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
@@ -170,6 +192,105 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
buildCategoryFilter();
parseCategorySelection();
+ async function handleClickUpload() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.multiple = true;
+ input.click();
+ await new Promise((resolve) => { input.onchange = resolve; });
+ const { files } = input;
+ const dialog = new Dialog({
+ $body: `
+
+
+
+ `,
+ });
+ try {
+ Notification.info(i18n('Uploading files...'));
+ window.addEventListener('beforeunload', onBeforeUnload);
+ dialog.open();
+ const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
+ const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
+ const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
+ const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
+ for (const i in files) {
+ if (Number.isNaN(+i)) continue;
+ const file = files[i];
+ const data = new FormData();
+ data.append('filename', file.name);
+ data.append('file', file);
+ data.append('type', 'additional_file');
+ data.append('operation', 'upload_file');
+ await request.postFile('./files', data, {
+ xhr() {
+ const xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener('loadstart', () => {
+ $fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
+ $fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
+ $uploadLabel.text(i18n('Uploading... ({0}%)', 0));
+ $uploadProgress.width(0);
+ });
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const percentComplete = Math.round((e.loaded / e.total) * 100);
+ if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
+ else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
+ $uploadProgress.width(`${percentComplete}%`);
+ }
+ }, false);
+ return xhr;
+ },
+ });
+ $('.additionalfile-table tbody').append(
+ $(tpl`
+ ${file.name}
+ ${size(file.size)}
+
+ `),
+ );
+ }
+ window.removeEventListener('beforeunload', onBeforeUnload);
+ Notification.success(i18n('File uploaded successfully.'));
+ } catch (e) {
+ console.error(e);
+ Notification.error(i18n('File upload failed: {0}', e.toString()));
+ } finally {
+ dialog.close();
+ }
+ }
+
+ async function handleClickRemove(ev) {
+ const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
+ const action = await new ConfirmDialog({
+ $body: tpl.typoMsg(i18n('Confirm to delete the file?')),
+ }).open();
+ if (action !== 'yes') return;
+ try {
+ await request.post('./files', {
+ operation: 'delete_files',
+ files: file,
+ type: 'testdata',
+ });
+ Notification.success(i18n('File have been deleted.'));
+ $(ev.currentTarget).parent().parent().remove();
+ } catch (error) {
+ Notification.error(error.message);
+ }
+ }
+
+ async function handleClickDownloadAll() {
+ const files = $('.additionalfile-table tr').map(function () { return $(this).attr('data-filename'); }).get();
+ const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'additional_file' });
+ const targets = [];
+ for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
+ await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
+ }
+
setInterval(() => {
$('img').each(function () {
if (this.src.startsWith('file://')) {
@@ -232,4 +353,11 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
ev.preventDefault();
}
});
+ $(document).on('click', '[name="additional_file__upload"]', () => handleClickUpload());
+ $(document).on('click', '[name="additional_file__delete"]', (ev) => handleClickRemove(ev));
+ $(document).on('click', '[name="additional_file__download"]', () => handleClickDownloadAll());
+ $(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand'));
+ $(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse'));
+ $(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand'));
+ $(document).on('click', '[name="tags__section__collapse"]', (ev) => handleSection(ev, 'tags', 'collapse'));
});
diff --git a/packages/ui-default/pages/problem_edit.page.styl b/packages/ui-default/pages/problem_edit.page.styl
index ca626adbc..7662dc97f 100644
--- a/packages/ui-default/pages/problem_edit.page.styl
+++ b/packages/ui-default/pages/problem_edit.page.styl
@@ -1,4 +1,4 @@
-.page--problem_edit
+.page--problem_edit,.page--problem_create
.section__tab-header-item
border-top: 3px solid transparent;
transition: border-color 0.2s, background 0.2s, color 0.2s;
@@ -10,4 +10,14 @@
background: #f8f8f8;
&.tab--active
- border-color: #ed5f82;
\ No newline at end of file
+ border-color: #ed5f82;
+
+ .section--problem-sidebar-additional_file
+ .problem-sidebar-additional_file__detail
+ height: 270px
+ overflow-y: auto
+
+ .col--size
+ width: 80px
+ .col--operation
+ width: 40px
\ No newline at end of file
diff --git a/packages/ui-default/pages/problem_files.page.js b/packages/ui-default/pages/problem_files.page.js
index 4692c389f..1f633665f 100644
--- a/packages/ui-default/pages/problem_files.page.js
+++ b/packages/ui-default/pages/problem_files.page.js
@@ -51,12 +51,12 @@ const page = new NamedPage('problem_files', () => {
const dialog = new Dialog({
$body: `
-
-
+
-
-
+
`,
});
try {
@@ -100,6 +100,7 @@ const page = new NamedPage('problem_files', () => {
Notification.success(i18n('File uploaded successfully.'));
await pjax.request({ push: false });
} catch (e) {
+ window.captureException?.(e);
console.error(e);
Notification.error(i18n('File upload failed: {0}', e.toString()));
} finally {
@@ -238,6 +239,7 @@ const page = new NamedPage('problem_files', () => {
const res = await request.get(link);
content = await request.get(res.url, undefined, { dataType: 'text' });
} catch (e) {
+ window.captureException?.(e);
Notification.error(i18n('Failed to load file: {0}', e.message));
throw e;
}
diff --git a/packages/ui-default/pages/problem_main.page.js b/packages/ui-default/pages/problem_main.page.js
index 33f299941..0620ec4f2 100644
--- a/packages/ui-default/pages/problem_main.page.js
+++ b/packages/ui-default/pages/problem_main.page.js
@@ -7,7 +7,6 @@ import Dropdown from 'vj/components/dropdown/Dropdown';
import createHint from 'vj/components/hint';
import { downloadProblemSet } from 'vj/components/zipDownloader';
import pjax from 'vj/utils/pjax';
-import substitute from 'vj/utils/substitute';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import delay from 'vj/utils/delay';
@@ -52,19 +51,14 @@ async function updateSelection(sendRequest = true) {
.map((s) => s.split(',')[0]));
// drop the category if its subcategory is selected
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
- let url;
- if (requestTags.length === 0) {
- url = UiContext.getProblemUrlWithoutCategory;
- } else {
- url = substitute(decodeURIComponent(UiContext.getProblemUrlWithCategory), {
- category: requestTags
- .map((tag) => tag.split(',').map(encodeURIComponent).join(','))
- .join(','), // build a beautiful URL
- });
- }
- const q = $('[name="q"]').val();
- if (q) url += `?q=${q}`;
- pjax.request({ url });
+ let q = $('[name="q"]').val().split(' ').filter((i) => !i.startsWith('category:')).join(' ');
+ if (requestTags.length) q = requestTags.map((i) => `category:${i}`).join(' ') + (q ? ` ${q}` : '');
+ const url = new URL(window.location.href);
+ if (!q) url.searchParams.delete('q');
+ else url.searchParams.set('q', q);
+ url.searchParams.delete('page');
+ pjax.request({ url: url.toString() });
+ $('[name="q"]').val(q);
}
}
@@ -230,7 +224,7 @@ function processElement(ele) {
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
}
-const page = new NamedPage(['problem_main', 'problem_category'], () => {
+const page = new NamedPage(['problem_main'], () => {
const doc = document.body;
doc.className += ' display-mode';
$('.section.display-mode').removeClass('display-mode');
diff --git a/packages/ui-default/sharedworker.ts b/packages/ui-default/sharedworker.ts
deleted file mode 100644
index 798ed7045..000000000
--- a/packages/ui-default/sharedworker.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import ReconnectingWebsocket from 'reconnecting-websocket';
-
-console.log('SharedWorker init');
-const ports: MessagePort[] = [];
-let conn: ReconnectingWebsocket;
-
-function initSharedConn(path: string, sid: string) {
- if (conn) return;
- console.log('Init connection for', path);
- const url = new URL(path);
- url.protocol = url.protocol.replace('http', 'ws');
- url.searchParams.append('sid', sid);
- const sock = new ReconnectingWebsocket(url.toString());
- conn = sock;
- sock.onopen = () => console.log('Connected');
- sock.onerror = console.error;
- sock.onclose = (...args) => console.log('Closed', ...args);
- sock.onmessage = async (message) => {
- if (process.env.NODE_ENV !== 'production') console.log('SharedWorker.port.onmessage: ', message);
- const payload = JSON.parse(message.data);
- if (payload.event === 'auth') {
- if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
- conn?.close();
- conn = null;
- }
- return;
- }
- for (const port of ports) {
- port.postMessage({ type: 'message', payload: message.data });
- const success = await new Promise((resolve) => {
- const handle = (msg) => {
- if (['success', 'fail'].includes(msg.data.type)) {
- port.removeEventListener('message', handle);
- resolve(msg.data.type === 'success');
- }
- };
- port.addEventListener('message', handle);
- });
- if (success) return;
- }
- console.log('Failed to push notification');
- // TODO try system notification
- };
-}
-
-// @ts-ignore
-onconnect = function (e) { // eslint-disable-line no-undef
- const port: MessagePort = e.ports[0];
- port.addEventListener('message', (msg) => {
- initSharedConn(msg.data.path, msg.data.cookie);
- });
- port.start();
- ports.push(port);
-};
diff --git a/packages/ui-default/templates/components/problem.html b/packages/ui-default/templates/components/problem.html
index fda06daca..d81ca4366 100644
--- a/packages/ui-default/templates/components/problem.html
+++ b/packages/ui-default/templates/components/problem.html
@@ -21,7 +21,7 @@
{%- if pdoc.tag|length > 0 %}
{%- if not inline %}
{% endif -%}
{%- else -%}
diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html
index f22c726fa..64b521654 100644
--- a/packages/ui-default/templates/contest_detail.html
+++ b/packages/ui-default/templates/contest_detail.html
@@ -11,14 +11,14 @@
{{ tdoc.title }}
- {{ _(model.contest.statusText(tdoc)) }}
- {{ _(model.contest.RULES[tdoc.rule].TEXT) }}
- {{ _('Start at') }}: {{ contest.render_time(tdoc.beginAt) }}
- {{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
- {{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}
- {{ tdoc['attend']|default(0) }}
+ {{ _(model.contest.statusText(tdoc)) }}
+ {{ _(model.contest.RULES[tdoc.rule].TEXT) }}
+ {{ _('Start at') }}: {{ contest.render_time(tdoc.beginAt) }}
+ {{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
+ {{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}
+ {{ tdoc['attend']|default(0) }}
{% if tsdoc.attend %}
- {{ _('Attended') }}
+ {{ _('Attended') }}
{% endif %}
diff --git a/packages/ui-default/templates/domain_join_applications.html b/packages/ui-default/templates/domain_join_applications.html
index ef504dba1..bbb4f1a03 100644
--- a/packages/ui-default/templates/domain_join_applications.html
+++ b/packages/ui-default/templates/domain_join_applications.html
@@ -7,10 +7,10 @@
{{ _('User can join this domain by visiting the following URL') }}:
-
{{ url_prefix|safe }}domain/join?domainId={{ handler.args.domainId }}
+
{{ url_prefix|safe }}d/{{ handler.args.domainId }}/domain/join
{% if joinSettings['method'] == model.domain.JOIN_METHOD_CODE %}
{{ _('Or, with automatically filled invitation code') }}:
-
{{ url_prefix|safe }}domain/join?domainId={{ handler.args.domainId }}&code={{ joinSettings.code|urlencode }}
+
{{ url_prefix|safe }}d/{{ handler.args.domainId }}/domain/join?code={{ joinSettings.code|urlencode }}
{% endif %}
{% if joinSettings.expire %}
diff --git a/packages/ui-default/templates/partials/category.html b/packages/ui-default/templates/partials/category.html
index d7ec78f0b..fd507f421 100644
--- a/packages/ui-default/templates/partials/category.html
+++ b/packages/ui-default/templates/partials/category.html
@@ -2,13 +2,13 @@
{%- for category, sub_categories in model.system.get('problem.categories')|parseYaml -%}
{% if sub_categories | length > 0 %}
{%- for sub_category in sub_categories -%}
- {{ sub_category }}
+ {{ sub_category }}
{%- endfor -%}
diff --git a/packages/ui-default/templates/partials/problem-sidebar-information.html b/packages/ui-default/templates/partials/problem-sidebar-information.html
index 7700ade61..42c5d28a6 100644
--- a/packages/ui-default/templates/partials/problem-sidebar-information.html
+++ b/packages/ui-default/templates/partials/problem-sidebar-information.html
@@ -11,24 +11,6 @@
{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB
{{ _('Difficulty') }}
{{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}
- {{ _('Category') }}
-
- {% if pdoc['category'] %}
-
- {%- for category in pdoc['category'] -%}
- {{ category }}
- {% if loop.index < pdoc['category']|length %}
- {{ _('problem-subcategory-delim') }}
- {% endif %}
- {%- endfor -%}
-
-
- {{ _('page.problem_detail.sidebar.show_category') }}
-
- {% else %}
- {{ _('(None)') }}
- {% endif %}
-
{{ _('Tags') }}
{{ problem.render_problem_tags(pdoc, true) }}
diff --git a/packages/ui-default/templates/partials/problem_list.html b/packages/ui-default/templates/partials/problem_list.html
index 5796bd017..ad6c49d33 100644
--- a/packages/ui-default/templates/partials/problem_list.html
+++ b/packages/ui-default/templates/partials/problem_list.html
@@ -6,7 +6,7 @@
{% if not pdocs.length %}
{{ nothing.render('Sorry, there is no problem in the problem set') }}
{% else %}
- {{ paginator.render(page, ppcount, position='top', add_qs=qs) }}
+ {{ paginator.render(page, ppcount, position='top', add_qs=('q=' + qs if qs else '')) }}
@@ -73,6 +73,6 @@
{%- endfor -%}
- {{ paginator.render(page, ppcount, add_qs=qs) }}
+ {{ paginator.render(page, ppcount, add_qs=('q=' + qs if qs else '')) }}
{% endif %}
diff --git a/packages/ui-default/templates/partials/problem_lucky.html b/packages/ui-default/templates/partials/problem_lucky.html
index 19e5c466d..9413a2526 100644
--- a/packages/ui-default/templates/partials/problem_lucky.html
+++ b/packages/ui-default/templates/partials/problem_lucky.html
@@ -1,6 +1,6 @@
-
+
{{ _('Lucky') }}
diff --git a/packages/ui-default/templates/partials/problem_sidebar_additional_file.html b/packages/ui-default/templates/partials/problem_sidebar_additional_file.html
new file mode 100644
index 000000000..4d4ef2413
--- /dev/null
+++ b/packages/ui-default/templates/partials/problem_sidebar_additional_file.html
@@ -0,0 +1,48 @@
+{% import "components/nothing.html" as nothing with context %}
+{% import "components/problem.html" as problem with context %}
+
\ No newline at end of file
diff --git a/packages/ui-default/templates/partials/problem_sidebar_normal.html b/packages/ui-default/templates/partials/problem_sidebar_normal.html
index 3aa8a43c1..89c960cb6 100644
--- a/packages/ui-default/templates/partials/problem_sidebar_normal.html
+++ b/packages/ui-default/templates/partials/problem_sidebar_normal.html
@@ -83,6 +83,9 @@
+
{% endif %}
{% if page_name == 'problem_detail' %}
@@ -100,14 +103,14 @@
-{% if page_name != 'problem_detail' and page_name != 'homework_detail_problem' and page_name != 'contest_detail_problem' %}
+{% if page_name not in ['problem_config', 'problem_detail', 'homework_detail_problem', 'contest_detail_problem'] %}
{% include "partials/problem-sidebar-information.html" %}
-{% elif page_name != 'contest_detail_problem' %}
+{% elif page_name != 'contest_detail_problem' and page_name != 'problem_config' %}
diff --git a/packages/ui-default/templates/problem_edit.html b/packages/ui-default/templates/problem_edit.html
index f1e2bd4fc..25bf26976 100644
--- a/packages/ui-default/templates/problem_edit.html
+++ b/packages/ui-default/templates/problem_edit.html
@@ -92,14 +92,15 @@
-
+
diff --git a/packages/ui-default/templates/problem_main.html b/packages/ui-default/templates/problem_main.html
index 9621521ab..f80888050 100644
--- a/packages/ui-default/templates/problem_main.html
+++ b/packages/ui-default/templates/problem_main.html
@@ -3,8 +3,6 @@
{% extends "layout/basic.html" %}
{% block content %}
{{ set(UiContext, {
- getProblemUrlWithCategory: url('problem_category', category='{category}'),
- getProblemUrlWithoutCategory: url('problem_main'),
currentCategory: category
}) }}