diff --git a/install/centos-7.sh b/install/centos-7.sh index 7a69d2648..31b8488b6 100644 --- a/install/centos-7.sh +++ b/install/centos-7.sh @@ -30,7 +30,7 @@ sysctl -p # Install NodeJS echo "Installing NodeJS" -curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash +curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash export NVM_DIR=/root/.nvm [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm install 14 diff --git a/install/ubuntu-1604.sh b/install/ubuntu-1604.sh index ba97fe414..a540e1c37 100644 --- a/install/ubuntu-1604.sh +++ b/install/ubuntu-1604.sh @@ -17,7 +17,7 @@ mkdir -p /data/db /data/file # Install NodeJS echo "Installing NodeJS" -curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash +curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash export NVM_DIR=/root/.nvm [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14 diff --git a/install/ubuntu-1804.sh b/install/ubuntu-1804.sh index 1dc8844cf..9e8803e5d 100644 --- a/install/ubuntu-1804.sh +++ b/install/ubuntu-1804.sh @@ -17,7 +17,7 @@ mkdir -p /data/db /data/file # Install NodeJS echo "Installing NodeJS" -curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash +curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash export NVM_DIR=/root/.nvm [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14 diff --git a/install/ubuntu-2004.sh b/install/ubuntu-2004.sh index ea65ba33a..272309be2 100644 --- a/install/ubuntu-2004.sh +++ b/install/ubuntu-2004.sh @@ -17,7 +17,7 @@ mkdir -p /data/db /data/file # Install NodeJS echo "Installing NodeJS" -curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | bash +curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.36.0/install.sh | sed 's/raw.githubusercontent.com\/nvm-sh\/nvm\//cdn.jsdelivr.net\/gh\/nvm-sh\/nvm@/g' | sed 's/github.com\/nvm-sh\/nvm.git/gitee.com\/imirror\/nvm/g' | bash export NVM_DIR=/root/.nvm [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" NVM_NODEJS_ORG_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/nodejs-release nvm install 14 diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 998ae9079..faa9d9f6e 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.4.19", + "version": "2.5.1", "os": [ "linux" ], diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index afae6a5a0..c93cb6237 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -1,7 +1,9 @@ import fs from 'fs-extra'; import path from 'path'; import yaml from 'js-yaml'; -import { Dictionary, sum } from 'lodash'; +import { sum } from 'lodash'; +import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases'; +import { changeErrorType } from '@hydrooj/utils/lib/utils'; import { FormatError, SystemError } from './error'; import { parseTimeMS, parseMemoryMB, ensureFile } from './utils'; import { getConfig } from './config'; @@ -86,8 +88,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) { const config = { count: 0, subtasks: [{ - time_limit_ms: parseTimeMS(cfg.time || '1s'), - memory_limit_mb: parseMemoryMB(cfg.memory || '256m'), + time: parseTimeMS(cfg.time || '1s'), + memory: parseMemoryMB(cfg.memory || '256m'), type: 'sum', cases: [], score: Math.floor(100 / cases.length), @@ -103,8 +105,8 @@ async function read0(folder: string, files: string[], checkFile, cfg) { } if (extra < cases.length) { config.subtasks.push({ - time_limit_ms: parseTimeMS(cfg.time || '1s'), - memory_limit_mb: parseMemoryMB(cfg.memory || '256m'), + time: parseTimeMS(cfg.time || '1s'), + memory: parseMemoryMB(cfg.memory || '256m'), type: 'sum', cases: [], score: Math.floor(100 / cases.length) + 1, @@ -132,8 +134,8 @@ async function read1(folder: string, files: string[], checkFile, cfg) { if (fs.existsSync(path.resolve(folder, c.output))) { if (!subtask[REG.subtask(data)]) { subtask[REG.subtask(data)] = [{ - time_limit_ms: parseTimeMS(cfg.time || '1s'), - memory_limit_mb: parseMemoryMB(cfg.memory || '256m'), + time: parseTimeMS(cfg.time || '1s'), + memory: parseMemoryMB(cfg.memory || '256m'), type: 'min', cases: [c], }]; @@ -193,107 +195,11 @@ async function readAutoCases(folder, { next }, cfg) { return config; } -export async function readYamlCases(folder: string, cfg: Dictionary = {}, args) { - const config: any = { - checker_type: 'default', - count: 0, - subtasks: [], - judge_extra_files: [], - user_extra_files: [], - }; - const next = args.next; - const checkFile = ensureFile(folder); - config.checker_type = cfg.checker_type || 'default'; - if (cfg.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); - if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); - if (cfg.judge_extra_files) { - if (typeof cfg.judge_extra_files === 'string') { - config.judge_extra_files = [checkFile(cfg.judge_extra_files, 'Cannot find judge extra file {0}.')]; - } else if (cfg.judge_extra_files instanceof Array) { - for (const file of cfg.judge_extra_files) { - config.judge_extra_files.push(checkFile(file, 'Cannot find judge extra file {0}.')); - } - } else throw new FormatError('Invalid judge_extra_files config.'); - } - if (cfg.user_extra_files) { - if (typeof cfg.user_extra_files === 'string') { - config.user_extra_files = [checkFile(cfg.user_extra_files, 'Cannot find user extra file {0}.')]; - } else if (cfg.user_extra_files instanceof Array) { - for (const file of cfg.user_extra_files) { - config.user_extra_files.push(checkFile(file, 'Cannot find user extra file {0}.')); - } - } else throw new FormatError('Invalid user_extra_files config.'); - } - if (cfg.outputs) { - config.type = 'submit_answer'; - } else if (cfg.cases) { - config.subtasks = [{ - score: parseInt(cfg.score, 10) || Math.floor(100 / cfg.cases.length), - time_limit_ms: parseTimeMS(cfg.time || '1s'), - memory_limit_mb: parseMemoryMB(cfg.memory || '512m'), - cases: [], - type: 'sum', - }]; - for (const c of cfg.cases) { - config.count++; - config.subtasks[0].cases.push({ - input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', - output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null', - id: config.count, - }); - } - } else if (cfg.subtasks) { - for (const subtask of cfg.subtasks) { - const cases = []; - for (const c of subtask.cases) { - config.count++; - cases.push({ - input: c.input ? checkFile(c.input, 'Cannot find input file {0}.') : '/dev/null', - output: c.output ? checkFile(c.output, 'Cannot find output file {0}.') : '/dev/null', - id: config.count, - }); - } - config.subtasks.push({ - score: parseInt(subtask.score, 10), - if: subtask.if || [], - cases, - time_limit_ms: parseTimeMS(subtask.time || cfg.time || '1s'), - memory_limit_mb: parseMemoryMB(subtask.memory || cfg.memory || '512m'), - }); - } - } else { - const c = await readAutoCases(folder, { next }, cfg); - config.subtasks = c.subtasks; - config.count = c.count; - } - if (config.type === 'submit_answer' && !cfg.outputs) throw new FormatError('outputs config not found'); - return Object.assign(cfg, config); -} - -function convertIniConfig(ini: string) { - const f = ini.split('\n'); - const count = parseInt(f[0], 10); - const res = { subtasks: [] }; - for (let i = 1; i <= count; i++) { - if (!f[i] || !f[i].trim()) throw new FormatError('Testcada count incorrect.'); - const [input, output, time, score, memory] = f[i].split('|'); - const cur = { - cases: [{ input: `input/${input.toLowerCase()}`, output: `output/${output.toLowerCase()}` }], - score: parseInt(score, 10), - time: `${time}s`, - memory: '512m', - }; - if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`; - res.subtasks.push(cur); - } - return res; -} - function isValidConfig(config) { if (config.count > (getConfig('testcases_max') || 100)) { throw new FormatError('Too many testcases. Cancelled.'); } - const total_time = sum(config.subtasks.map((subtask) => subtask.time_limit_ms * subtask.cases.length)); + const total_time = sum(config.subtasks.map((subtask) => subtask.time * subtask.cases.length)); if (total_time > (getConfig('total_time_limit') || 60) * 1000) { throw new FormatError('Total time limit longer than {0}s. Cancelled.', [+getConfig('total_time_limit') || 60]); } @@ -309,9 +215,23 @@ export default async function readCases(folder: string, cfg: Record } else if (fs.existsSync(ymlConfig)) { config = { ...yaml.load(fs.readFileSync(ymlConfig).toString()) as object, ...cfg }; } else if (fs.existsSync(iniConfig)) { - config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg }; + try { + config = { ...convertIniConfig(fs.readFileSync(iniConfig).toString()), ...cfg }; + } catch (e) { + throw changeErrorType(e, FormatError); + } } else config = cfg; - const result = await readYamlCases(folder, config, args); + let result; + try { + result = await readYamlCases(config, ensureFile(folder)); + } catch (e) { + throw changeErrorType(e, FormatError); + } + if (!(result.outputs || result.subtasks.length)) { + const c = await readAutoCases(folder, args, cfg); + config.subtasks = c.subtasks; + config.count = c.count; + } isValidConfig(result); return result; } diff --git a/packages/hydrojudge/src/judge/default.ts b/packages/hydrojudge/src/judge/default.ts index 7fbdf154d..3e4a6b2c0 100644 --- a/packages/hydrojudge/src/judge/default.ts +++ b/packages/hydrojudge/src/judge/default.ts @@ -50,8 +50,8 @@ function judgeCase(c, sid) { stderr, copyIn, copyOut, - time_limit_ms: ctxSubtask.subtask.time_limit_ms * ctx.execute.time, - memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, + time: ctxSubtask.subtask.time * ctx.execute.time, + memory: ctxSubtask.subtask.memory, }, ); const { code, time_usage_ms, memory_usage_kb } = res; @@ -62,9 +62,9 @@ function judgeCase(c, sid) { let message: any = ''; let score = 0; if (status === STATUS.STATUS_ACCEPTED) { - if (time_usage_ms > ctxSubtask.subtask.time_limit_ms * ctx.execute.time) { + if (time_usage_ms > ctxSubtask.subtask.time * ctx.execute.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; - } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { + } else if (memory_usage_kb > ctxSubtask.subtask.memory * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; } else { [status, score, message] = await check({ diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index 8b9e56330..8e81be8dc 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -77,8 +77,8 @@ export const judge = async (ctx) => { stderr, copyIn: execute.copyIn, copyOut, - time_limit_ms: 1000, - memory_limit_mb: 512, + time: 1000, + memory: 512, }, ); const { code, time_usage_ms, memory_usage_kb } = res; diff --git a/packages/hydrojudge/src/judge/interactive.ts b/packages/hydrojudge/src/judge/interactive.ts index 008658acd..47711f894 100644 --- a/packages/hydrojudge/src/judge/interactive.ts +++ b/packages/hydrojudge/src/judge/interactive.ts @@ -24,21 +24,21 @@ function judgeCase(c) { { execute: ctx.executeUser.execute.replace(/\$\{name\}/g, 'code'), copyIn: ctx.executeUser.copyIn, - time_limit_ms: ctxSubtask.subtask.time_limit_ms, - memory_limit_mb: ctxSubtask.subtask.memory_limit_mb, + time: ctxSubtask.subtask.time, + memory: ctxSubtask.subtask.memory, }, { execute: `${ctx.executeInteractor.execute.replace(/\$\{name\}/g, 'interactor')} /w/in /w/tout`, copyIn: ctx.executeInteractor.copyIn, - time_limit_ms: ctxSubtask.subtask.time_limit_ms * 2, - memory_limit_mb: ctxSubtask.subtask.memory_limit_mb * 2, + time: ctxSubtask.subtask.time * 2, + memory: ctxSubtask.subtask.memory * 2, }, ]); let status; let score = 0; let message: any = ''; - if (time_usage_ms > ctxSubtask.subtask.time_limit_ms) { + if (time_usage_ms > ctxSubtask.subtask.time) { status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; - } else if (memory_usage_kb > ctxSubtask.subtask.memory_limit_mb * 1024) { + } else if (memory_usage_kb > ctxSubtask.subtask.memory * 1024) { status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; } else if (code) { status = STATUS.STATUS_RUNTIME_ERROR; diff --git a/packages/hydrojudge/src/judge/run.ts b/packages/hydrojudge/src/judge/run.ts index 6cd04efd6..f80fd4f3e 100644 --- a/packages/hydrojudge/src/judge/run.ts +++ b/packages/hydrojudge/src/judge/run.ts @@ -69,8 +69,8 @@ export const judge = async (ctx) => { stderr, copyIn, copyOut, - time_limit_ms: parseTimeMS(ctx.config.time || '1s'), - memory_limit_mb: parseMemoryMB(ctx.config.memory || '128m'), + time: parseTimeMS(ctx.config.time || '1s'), + memory: parseMemoryMB(ctx.config.memory || '128m'), }, ); const { code, time_usage_ms, memory_usage_kb } = res; diff --git a/packages/hydrojudge/src/sandbox.ts b/packages/hydrojudge/src/sandbox.ts index 64a2e1674..7da948604 100644 --- a/packages/hydrojudge/src/sandbox.ts +++ b/packages/hydrojudge/src/sandbox.ts @@ -25,8 +25,8 @@ const statusMap = { function proc({ execute = '', - time_limit_ms = 16000, - memory_limit_mb = 1024, + time = 16000, + memory = 1024, process_limit = 32, stdin = '', copyIn = {}, copyOut = [], copyOutCached = [], } = {}) { @@ -39,9 +39,9 @@ function proc({ { name: 'stdout', max: 1024 * 1024 * size }, { name: 'stderr', max: 1024 * 1024 * size }, ], - cpuLimit: time_limit_ms * 1000 * 1000, - realCpuLimit: time_limit_ms * 3000 * 1000, - memoryLimit: memory_limit_mb * 1024 * 1024, + cpuLimit: time * 1000 * 1000, + realCpuLimit: time * 3000 * 1000, + memoryLimit: memory * 1024 * 1024, procLimit: process_limit, copyIn, copyOut, @@ -58,7 +58,7 @@ async function adaptResult(result, params) { files: result.files, code: result.exitStatus, }; - if (ret.time_usage_ms >= (params.time_limit_ms || 16000)) { + if (ret.time_usage_ms >= (params.time || 16000)) { ret.status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; } ret.files = result.files || {}; diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 95d3427f1..94beae1fd 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.21.19", + "version": "2.22.1", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/entry/cli.ts b/packages/hydrooj/src/entry/cli.ts index f50efdddc..1a4d7d806 100644 --- a/packages/hydrooj/src/entry/cli.ts +++ b/packages/hydrooj/src/entry/cli.ts @@ -1,11 +1,13 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable import/no-dynamic-require */ import { argv } from 'yargs'; +import { ObjectID } from 'mongodb'; import { - lib, service, model, - builtinLib, builtinModel, + lib, service, model, script, + builtinLib, builtinModel, builtinScript, } from './common'; import options from '../options'; +import { validate } from '../lib/validator'; import * as bus from '../service/bus'; import db from '../service/db'; @@ -20,8 +22,23 @@ function parseParameters(fn: Function) { return result === null ? [] : result; } +async function runScript(name: string, arg: any) { + if (!global.Hydro.script[name]) return console.error('Script %s not found.', name); + validate(global.Hydro.script[name].validate, arg); + return await global.Hydro.script[name].run(arg, console.info); +} + async function cli() { - const [, modelName, func, ...args] = argv._; + const [, modelName, func, ...args] = argv._ as [string, string, string, ...any[]]; + if (modelName === 'script') { + let arg: any; + try { + arg = JSON.parse(args.join(' ')); + } catch (e) { + return console.error('Invalid argument'); + } + return await runScript(func, arg); + } if (!global.Hydro.model[modelName]) { return console.error(`Model ${modelName} doesn't exist.`); } @@ -45,6 +62,11 @@ async function cli() { console.error(`Too few arguments. Min ${parameterMin}`); return console.error(parameters.join(', ')); } + for (let i = 0; i < args.length; i++) { + if (ObjectID.isValid(args[i])) { + args[i] = new ObjectID(args[i]); + } + } let result = global.Hydro.model[modelName][func](...args); if (result instanceof Promise) result = await result; return console.log(result); @@ -59,22 +81,18 @@ export async function load() { require('../options'); const opts = options(); await db.start(opts); - const modelSystem = require('../model/system'); - const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([ - 'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region', - 'file.endPointForUser', 'file.endPointForJudge', - ]); - const sopts = { - endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge, - }; const storage = require('../service/storage'); - await storage.start(sopts); + await storage.start(); for (const i of builtinLib) require(`../lib/${i}`); await lib(pending, fail); + const systemModel = require('../model/system'); + await systemModel.runConfig(); require('../service/gridfs'); await service(pending, fail); for (const i of builtinModel) require(`../model/${i}`); await model(pending, fail); + for (const i of builtinScript) require(`../script/${i}`); + await script(pending, fail, []); await bus.parallel('app/started'); await cli(); } diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts index 2b12037d8..779ff1434 100644 --- a/packages/hydrooj/src/entry/common.ts +++ b/packages/hydrooj/src/entry/common.ts @@ -12,8 +12,7 @@ const logger = new Logger('common', true); export const builtinLib = [ 'jwt', 'download', 'i18n', 'mail', 'useragent', 'crypto', 'misc', 'paginate', 'hash.hydro', 'rank', - 'validator', 'ui', 'testdata.convert.ini', 'testdataConfig', 'difficulty', - 'content', + 'validator', 'ui', 'testdataConfig', 'difficulty', 'content', ]; export const builtinModel = [ diff --git a/packages/hydrooj/src/entry/master.ts b/packages/hydrooj/src/entry/master.ts index 6081bf151..b6b19b252 100644 --- a/packages/hydrooj/src/entry/master.ts +++ b/packages/hydrooj/src/entry/master.ts @@ -57,15 +57,8 @@ export async function load(call: Entry) { await modelSystem.set('file.secretKey', process.env.MINIO_SECRET_KEY); await modelSystem.set('file.endPoint', 'http://localhost:9000/'); } - const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([ - 'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region', - 'file.endPointForUser', 'file.endPointForJudge', - ]); - const sopts = { - endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge, - }; const storage = require('../service/storage'); - storage.start(sopts); + await storage.start(); require('../service/monitor'); for (const i of builtinModel) require(`../model/${i}`); const scripts = require('../upgrade').default; @@ -75,16 +68,10 @@ export async function load(call: Entry) { while (dbVer < expected) { logger.info('Upgrading database: from %d to %d', dbVer, expected); const func = scripts[dbVer]; - if (typeof func !== 'function') { + if (typeof func !== 'function' || (isFresh && func.toString().includes('_FRESH_INSTALL_IGNORE'))) { dbVer++; continue; } - if (isFresh) { - if (func.toString().includes('_FRESH_INSTALL_IGNORE')) { - dbVer++; - continue; - } - } const result = await func(); if (!result) break; dbVer++; diff --git a/packages/hydrooj/src/entry/worker.ts b/packages/hydrooj/src/entry/worker.ts index 6b690c38d..2f4e5caf3 100644 --- a/packages/hydrooj/src/entry/worker.ts +++ b/packages/hydrooj/src/entry/worker.ts @@ -34,15 +34,8 @@ export async function load() { const modelSystem = require('../model/system'); await modelSystem.runConfig(); if (argv.loaderDetail) logger.info('finish: config'); - const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = modelSystem.getMany([ - 'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region', - 'file.endPointForUser', 'file.endPointForJudge', - ]); - const sopts = { - endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge, - }; const storage = require('../service/storage'); - storage.start(sopts); + await storage.start(); if (argv.loaderDetail) logger.info('finish: storage.connect'); for (const i of builtinLib) { let t; diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 2660ec52d..209d373c0 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -96,6 +96,14 @@ class ContestDetailHandler extends ContestHandler { await contest.attend(domainId, tid, this.user._id); this.back(); } + + @param('tid', Types.ObjectID) + async postDelete(domainId: string, tid: ObjectID) { + const tdoc = await contest.get(domainId, tid); + if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_CONTEST); + await contest.del(domainId, tid); + this.response.redirect = this.url('contest_main'); + } } class ContestBoardcastHandler extends ContestHandler { diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 72c5fc688..70026eb4d 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -108,6 +108,14 @@ class HomeworkDetailHandler extends HomeworkHandler { await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK); this.back(); } + + @param('tid', Types.ObjectID) + async postDelete(domainId: string, tid: ObjectID) { + const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); + if (tdoc.owner !== this.user._id) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); + await contest.del(domainId, tid, document.TYPE_HOMEWORK); + this.response.redirect = this.url('homework_main'); + } } class HomeworkDetailProblemHandler extends HomeworkHandler { diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 50584f275..efab4ce4f 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -6,11 +6,12 @@ import { SolutionNotFoundError, ProblemNotFoundError, BadRequestError, } from '../error'; import { - Pdoc, User, Rdoc, PathComponent, + Pdoc, User, Rdoc, PathComponent, ProblemStatusDoc, } from '../interface'; import paginate from '../lib/paginate'; import { isPid } from '../lib/validator'; import difficultyAlgorithm from '../lib/difficulty'; +import { parseConfig } from '../lib/testdataConfig'; import * as system from '../model/system'; import problem from '../model/problem'; import record from '../model/record'; @@ -156,8 +157,12 @@ export class ProblemRandomHandler extends ProblemHandler { } } +export interface PdocWithPsdoc extends Pdoc { + psdoc?: ProblemStatusDoc +} + export class ProblemDetailHandler extends ProblemHandler { - pdoc: Pdoc; + pdoc: PdocWithPsdoc; udoc: User; @route('pid', Types.Name, true, null, parsePid) @@ -189,6 +194,15 @@ export class ProblemDetailHandler extends ProblemHandler { // e.g. ![img](a.jpg) will navigate to ![img](./pid/file/a.jpg) this.response.body.pdoc.content = this.response.body.pdoc.content .replace(/\(file:\/\//g, `(./${this.pdoc.docId}/file/`); + // Get time and memory limit + try { + this.response.body.pdoc.config = await parseConfig(this.pdoc.config); + } catch (e) { + this.response.body.pdoc.config = `Cannot parse: ${e.message}`; + } + if (this.pdoc.psdoc) { + this.response.body.rdoc = await record.get(this.domainId, this.pdoc.psdoc.rid); + } } @param('pid', Types.UnsignedInt) @@ -574,6 +588,13 @@ export class ProblemCreateHandler extends Handler { } } +export class ProblemPrefixListHandler extends Handler { + @param('prefix', Types.Name) + async get(domainId: string, prefix: string) { + this.response.body = await problem.getPrefixList(domainId, prefix); + } +} + export async function apply() { Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_category', '/p/category/:category', ProblemCategoryHandler, PERM.PERM_VIEW_PROBLEM); @@ -589,6 +610,7 @@ export async function apply() { Route('problem_solution_raw', '/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_solution_reply_raw', '/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionRawHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_create', '/problem/create', ProblemCreateHandler, PERM.PERM_CREATE_PROBLEM); + Route('problem_prefix_list', '/problem/list', ProblemPrefixListHandler, PERM.PERM_VIEW_PROBLEM); Connection('problem_pretest_conn', '/conn/pretest', ProblemPretestConnectionHandler, PERM.PERM_SUBMIT_PROBLEM); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 95bfad782..ae6e19ff2 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -189,7 +189,6 @@ declare module './model/problem' { data: FileInfo[], additional_file: FileInfo[], hidden: boolean, - acMsg?: string, html?: boolean, difficulty?: number, /** @deprecated */ @@ -546,7 +545,6 @@ export interface Lib extends Record { rank: typeof import('./lib/rank'), rating: typeof import('./lib/rating'), testdataConfig: typeof import('./lib/testdataConfig'), - 'testdata.convert.ini': typeof import('./lib/testdata.convert.ini'), useragent: typeof import('./lib/useragent'), validator: typeof import('./lib/validator'), template?: any, diff --git a/packages/hydrooj/src/lib/testdata.convert.ini.ts b/packages/hydrooj/src/lib/testdata.convert.ini.ts deleted file mode 100644 index 976d19928..000000000 --- a/packages/hydrooj/src/lib/testdata.convert.ini.ts +++ /dev/null @@ -1,22 +0,0 @@ -import yaml from 'js-yaml'; - -function convert(ini: string) { - const f = ini.split('\n'); - const count = parseInt(f[0], 10); - const res = { subtasks: [] }; - for (let i = 1; i <= count; i++) { - const [input, output, time, score, memory] = f[i].split('|'); - const cur = { - cases: [{ input: `input/${input}`, output: `output/${output}` }], - score: parseInt(score, 10), - time: `${time}s`, - memory: '512m', - }; - if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`; - res.subtasks.push(cur); - } - return yaml.dump(res); -} - -global.Hydro.lib['testdata.convert.ini'] = convert; -export = convert; diff --git a/packages/hydrooj/src/lib/testdataConfig.ts b/packages/hydrooj/src/lib/testdataConfig.ts index b858c9d75..7d16e4d4b 100644 --- a/packages/hydrooj/src/lib/testdataConfig.ts +++ b/packages/hydrooj/src/lib/testdataConfig.ts @@ -1,23 +1,7 @@ -import Zip from 'adm-zip'; +import readYamlCases from '@hydrooj/utils/lib/cases'; import { load } from 'js-yaml'; import type { ProblemConfig } from '../interface'; -export async function readConfig(file: string | Buffer) { - const data = new Zip(file); - const entries = data.getEntries(); - for (const entry of entries) { - if (entry.name.toLowerCase() === 'config.yaml') { - return entry.getData().toString(); - } - if (entry.name.toLowerCase() === 'config.ini') { - const ini = entry.getData().toString(); - const conv = global.Hydro.lib['testdata.convert.ini']; - return conv(ini); - } - } - return ''; -} - interface ParseResult { count: number, memoryMax: number, @@ -37,24 +21,26 @@ export async function parseConfig(config: string | ProblemConfig) { let cfg: ProblemConfig = {}; if (typeof config === 'string') { // TODO should validate here? - cfg = load(config) as ProblemConfig; + cfg = await readYamlCases(load(config) as Record) as ProblemConfig; } else cfg = config; - if (cfg.cases) { - for (const c of cfg.cases) { - result.memoryMax = Math.max(result.memoryMax, c.memory); - result.memoryMin = Math.min(result.memoryMin, c.memory); - result.timeMax = Math.max(result.timeMax, c.time); - result.timeMin = Math.min(result.timeMin, c.time); - result.count++; - } - } else if (cfg.subtasks) { + 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); } + } else if (cfg.time || cfg.memory) { + 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; + } + return result; } -global.Hydro.lib.testdataConfig = { readConfig, parseConfig }; +global.Hydro.lib.testdataConfig = { parseConfig }; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 60e9239f8..27e630c07 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -1,6 +1,6 @@ import type { Readable } from 'stream'; import { ObjectID, FilterQuery } from 'mongodb'; -import { Dictionary, pick } from 'lodash'; +import { Dictionary, escapeRegExp, pick } from 'lodash'; import { STATUS } from './builtin'; import * as document from './document'; import domain from './domain'; @@ -28,8 +28,7 @@ export class ProblemModel { static PROJECTION_PUBLIC: Field[] = [ ...ProblemModel.PROJECTION_LIST, - 'content', 'html', 'data', 'config', 'acMsg', - 'additional_file', + 'content', 'html', 'data', 'config', 'additional_file', ]; static extend(getter: Getter) { @@ -209,6 +208,13 @@ export class ProblemModel { return Object.assign(r, l); } + static async getPrefixList(domainId: string, prefix: string) { + prefix = prefix.toLowerCase(); + const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi'); + const filter = { $or: [{ pid: { $regex } }, { title: { $regex } }] }; + return await document.getMulti(domainId, document.TYPE_PROBLEM, filter, ['domainId', 'docId', 'pid', 'title']).toArray(); + } + static async getListStatus(domainId: string, uid: number, pids: number[]) { const psdocs = await ProblemModel.getMultiStatus( domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, @@ -258,7 +264,6 @@ ProblemModel.extend((docId, pid) => ({ additional_file: [], hidden: true, config: '', - acMsg: '', difficulty: 0, })); diff --git a/packages/hydrooj/src/model/system.ts b/packages/hydrooj/src/model/system.ts index 077b8eab8..9c1a41327 100644 --- a/packages/hydrooj/src/model/system.ts +++ b/packages/hydrooj/src/model/system.ts @@ -67,7 +67,7 @@ export async function runConfig() { } const config = await coll.find().toArray(); for (const i of config) cache[i._id] = i.value; - bus.emit('database/config'); + await bus.emit('database/config'); } bus.on('system/setting', (args) => { diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 407a2cc72..75373d6a0 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -1,4 +1,5 @@ import { Collection } from 'mongodb'; +import { escapeRegExp } from 'lodash'; import * as system from './system'; import token from './token'; import * as setting from './setting'; @@ -224,7 +225,7 @@ class UserModel { @ArgMethod static async getPrefixList(domainId: string, prefix: string, limit: number = 50) { prefix = prefix.toLowerCase(); - const $regex = new RegExp(`\\A\\Q${prefix}\\E`, 'gmi'); + const $regex = new RegExp(`\\A${escapeRegExp(prefix)}`, 'gmi'); const uids = await coll.find({ unameLower: { $regex } }) .limit(limit).map((doc) => doc._id).toArray(); const users = []; diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index 9aa051167..902b3cb3f 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -38,7 +38,7 @@ export interface EventMap extends Record { 'message/run': (command: string) => VoidReturn 'database/connect': (db: Db) => void - 'database/config': () => void + 'database/config': () => VoidReturn 'system/setting': (args: Record) => VoidReturn diff --git a/packages/hydrooj/src/service/storage.ts b/packages/hydrooj/src/service/storage.ts index 318ce3693..80d2cda9c 100644 --- a/packages/hydrooj/src/service/storage.ts +++ b/packages/hydrooj/src/service/storage.ts @@ -79,9 +79,15 @@ class StorageService { private opts: StorageOptions; private replaceWithAlternativeUrlFor: Record<'user' | 'judge', (originalUrl: string) => string>; - async start(opts: StorageOptions) { + async start() { try { - this.opts = opts; + const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = system.getMany([ + 'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region', + 'file.endPointForUser', 'file.endPointForJudge', + ]); + this.opts = { + endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge, + }; this.client = new Client({ ...parseMainEndpointUrl(this.opts.endPoint), accessKey: this.opts.accessKey, @@ -103,14 +109,7 @@ class StorageService { logger.warn('Storage init fail. will retry later.'); this.error = e.toString(); setTimeout(async () => { - const [endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge] = system.getMany([ - 'file.endPoint', 'file.accessKey', 'file.secretKey', 'file.bucket', 'file.region', - 'file.endPointForUser', 'file.endPointForJudge', - ]); - const sopts = { - endPoint, accessKey, secretKey, bucket, region, endPointForUser, endPointForJudge, - }; - await this.start(sopts); + await this.start(); }, 10000); } } diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index bd3ddc2b1..a49235f5f 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -4,21 +4,21 @@ import { FilterQuery, ObjectID } from 'mongodb'; import AdmZip from 'adm-zip'; import Queue from 'p-queue'; import yaml from 'js-yaml'; +import { convertIniConfig } from '@hydrooj/utils/lib/cases'; import { Progress } from './ui'; +import { Rdoc } from './interface'; import { Logger } from './logger'; import { streamToBuffer } from './utils'; import { iterateAllDomain, iterateAllProblem } from './pipelineUtils'; import gridfs from './service/gridfs'; import storage from './service/storage'; import db from './service/db'; +import difficultyAlgorithm from './lib/difficulty'; import problem from './model/problem'; import domain from './model/domain'; import * as document from './model/document'; import * as system from './model/system'; -import difficultyAlgorithm from './lib/difficulty'; import { STATUS } from './model/builtin'; -import RecordModel from './model/record'; -import { Rdoc } from './interface'; const logger = new Logger('upgrade'); type UpgradeScript = void | (() => Promise); @@ -262,6 +262,33 @@ const scripts: UpgradeScript[] = [ } return true; }, + async function _20_21() { + await iterateAllProblem([], async (pdoc) => { + let config: string; + try { + const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.yaml`); + config = (await streamToBuffer(file)).toString('utf-8'); + logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.yaml`); + } catch (e) { + try { + const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.yml`); + config = (await streamToBuffer(file)).toString('utf-8'); + logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.yml`); + } catch (err) { + try { + const file = await storage.get(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/config.ini`); + config = yaml.dump(convertIniConfig((await streamToBuffer(file)).toString('utf-8'))); + logger.info(`Loaded config for ${pdoc.domainId}/${pdoc.docId} from config.ini`); + } catch (error) { + logger.warn('Config for %s/%s(%s) not found', pdoc.domainId, pdoc.docId, pdoc.pid || pdoc.docId); + // no config found + } + } + } + if (config) await problem.edit(pdoc.domainId, pdoc.docId, { config }); + }); + return true; + }, ]; export default scripts; diff --git a/packages/hydrooj/test/db.ts b/packages/hydrooj/test/db.ts index 9812e8c99..cb58498c7 100644 --- a/packages/hydrooj/test/db.ts +++ b/packages/hydrooj/test/db.ts @@ -11,7 +11,7 @@ export async function connect() { while (dbVer < expected) { const func = scripts[dbVer]; dbVer++; - if (func.toString().includes('_FRESH_INSTALL_IGNORE')) continue; + if (typeof func !== 'function' || func.toString().includes('_FRESH_INSTALL_IGNORE')) continue; // eslint-disable-next-line no-await-in-loop await func(); } diff --git a/packages/import-qduoj/package.json b/packages/import-qduoj/package.json index ba3622670..aec5ff718 100644 --- a/packages/import-qduoj/package.json +++ b/packages/import-qduoj/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/import-qduoj", - "version": "1.1.6", + "version": "1.1.7", "description": "Import QDUOJ problem export", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", @@ -11,4 +11,4 @@ "decode-html": "^2.0.0", "fs-extra": "^9.1.0" } -} \ No newline at end of file +} diff --git a/packages/migrate-vijos/package.json b/packages/migrate-vijos/package.json index f29552922..5db7ddb9b 100644 --- a/packages/migrate-vijos/package.json +++ b/packages/migrate-vijos/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/migrate-vijos", - "version": "1.4.0", + "version": "1.4.1", "main": "package.json", "repository": "git@github.com:hydro-dev/Hydro.git", "author": "undefined ", diff --git a/packages/migrate-vijos/script.ts b/packages/migrate-vijos/script.ts index cef6a06ce..ed64baff6 100644 --- a/packages/migrate-vijos/script.ts +++ b/packages/migrate-vijos/script.ts @@ -74,7 +74,6 @@ const tasks = { return res; }, }, - ac_msg: 'acMsg', parent_doc_id: { field: 'parentId', processer: (parentId, doc) => { diff --git a/packages/ui-default/backendlib/template.js b/packages/ui-default/backendlib/template.js index 5bbc511e2..ac2159461 100644 --- a/packages/ui-default/backendlib/template.js +++ b/packages/ui-default/backendlib/template.js @@ -88,7 +88,7 @@ class Loader extends nunjucks.Loader { }; } let fullpath = null; - if (typeof argv.template !== 'string') argv.template = path.resolve(__dirname, '..', 'template'); + if (typeof argv.template !== 'string') argv.template = path.resolve(__dirname, '..', 'templates'); const p = path.resolve(process.cwd(), argv.template, name); if (fs.existsSync(p)) fullpath = p; if (!fullpath) { diff --git a/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.js b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.js new file mode 100644 index 000000000..6a3d89586 --- /dev/null +++ b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.js @@ -0,0 +1,41 @@ +import _ from 'lodash'; +import tpl from 'vj/utils/tpl'; +import request from 'vj/utils/request'; +import DOMAttachedObject from 'vj/components/DOMAttachedObject'; +import AutoComplete from './index'; + +function getText(pdoc) { + return pdoc.docId; +} + +function getItems(prefix) { + return request.get('/problem/list', { prefix }); +} + +function renderItem(pdoc) { + return tpl` +
+
+
${pdoc.pid ? `${pdoc.pid} ` : ''}${pdoc.title}
+
ID = ${pdoc.docId}
+
+
+ `; +} + +export default class ProblemSelectAutoComplete extends AutoComplete { + static DOMAttachKey = 'vjProblemSelectAutoCompleteInstance'; + + constructor($dom, options) { + super($dom, { + classes: 'problem-select', + items: getItems, + render: renderItem, + text: getText, + ...options, + }); + } +} + +_.assign(ProblemSelectAutoComplete, DOMAttachedObject); +window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/index.js b/packages/ui-default/components/autocomplete/index.js index c9ca90a0e..6dc0c1f7f 100644 --- a/packages/ui-default/components/autocomplete/index.js +++ b/packages/ui-default/components/autocomplete/index.js @@ -22,6 +22,7 @@ export default class AutoComplete extends DOMAttachedObject { clearDefaultValue: true, position: 'bottom left', classes: '', + multi: false, ...options, }; this.clear(this.options.clearDefaultValue); @@ -46,9 +47,7 @@ export default class AutoComplete extends DOMAttachedObject { } clear(clearValue = true) { - if (clearValue) { - this.$dom.val(''); - } + if (clearValue) this.$dom.val(''); this._value = null; this.lastText = null; } @@ -80,19 +79,14 @@ export default class AutoComplete extends DOMAttachedObject { } onKeyUp(ev) { - if (ev.which === 27) { - // ESC + if (ev.which === 27 /* ESC */) { this.close(); return; } - if (this.$dom.val() === this.lastText) { - return; - } + if (this.$dom.val() === this.lastText) return; this.lastText = this.$dom.val(); this.updateOpenState(); - if (this.isOpen) { - this.renderList(); - } + if (this.isOpen) this.renderList(); } onMenuClick(ev) { @@ -109,7 +103,8 @@ export default class AutoComplete extends DOMAttachedObject { this._value = item; this.$dom.trigger('vjAutoCompleteSelect', item); if (text != null) { - this.$dom.val(text); + if (!this.options.multi) this.$dom.val(text); + else this.$dom.val(`${this.$dom.val() + text}, `); } this.close(); } @@ -141,7 +136,11 @@ export default class AutoComplete extends DOMAttachedObject { } async renderList() { - const val = this.$dom.val(); + let val = this.$dom.val(); + if (this.options.multi) { + const elems = val.split(',').map((i) => i.trim()); + val = elems[elems.length - 1]; + } const items = await this.getItems(val); const html = this.getHtml(items); this.currentItems = items; diff --git a/packages/ui-default/components/autocomplete/problemselectautocomplete.page.styl b/packages/ui-default/components/autocomplete/problemselectautocomplete.page.styl new file mode 100644 index 000000000..d7910b617 --- /dev/null +++ b/packages/ui-default/components/autocomplete/problemselectautocomplete.page.styl @@ -0,0 +1,6 @@ +.menu.problem-select + width: rem(200px) + +.problem-select__uid + font-size: rem($font-size-small) + color: $userselect-uid-color diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 44a6391bd..8aec11bfe 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.5.14", + "version": "4.5.16", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/contest_edit.page.js b/packages/ui-default/pages/contest_edit.page.js new file mode 100644 index 000000000..6aeeb3e67 --- /dev/null +++ b/packages/ui-default/pages/contest_edit.page.js @@ -0,0 +1,34 @@ +import { NamedPage } from 'vj/misc/Page'; +import { ConfirmDialog } from 'vj/components/dialog'; +import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; +import tpl from 'vj/utils/tpl'; +import i18n from 'vj/utils/i18n'; +import request from 'vj/utils/request'; + +const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { + ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true }); + if (pagename.endsWith('edit')) { + let confirmed = false; + $(document).on('click', '[name="operation"]', (ev) => { + ev.preventDefault(); + if (confirmed) { + return request.post('.', { operation: 'delete' }).then((res) => { + window.location.href = res.url; + }); + } + const message = `Confirm deleting this ${pagename.split('_')[0]}? Its status will be deleted as well.`; + return new ConfirmDialog({ + $body: tpl` +
+

${i18n(message)}

+
`, + }).open().then((action) => { + if (action !== 'yes') return; + confirmed = true; + ev.target.click(); + }); + }); + } +}); + +export default page; diff --git a/packages/ui-default/templates/components/form.html b/packages/ui-default/templates/components/form.html index 9be28d33c..226e5a185 100644 --- a/packages/ui-default/templates/components/form.html +++ b/packages/ui-default/templates/components/form.html @@ -47,6 +47,7 @@ {% set disabled = args.disabled or false %} {% set required = args.required or false %} {% set autofocus = args.autofocus or false %} +{% set noAutocomplete = args.noAutocomplete or false %} {% set extra_style = args.extra_style or false %} {% set date = args.date or false %} {% set time = args.time or false %} @@ -54,6 +55,7 @@ {% if disabled %}disabled{% endif %} {% if required %}required{% endif %} {% if autofocus %}autofocus data-autofocus{% endif %} + {% if noAutocomplete %}autocomplete="off"{% endif %} {% if date %}data-pick-date{% endif %} {% if time %}data-pick-time{% endif %} {% if extra_style %}style="{{extra_style}}"{% endif %} diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 4cff1e971..7ca52e245 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -62,7 +62,9 @@ columns:null, label:'Problems', name:'pids', - value:pids + value:pids, + noAutocomplete:true, + placeholder:_("Seperated with ','") }) }} {{ form.form_textarea({ columns:null, @@ -77,6 +79,11 @@ + {% if tdoc.owner == handler.user._id or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %} + + {% endif %} {% else %} + {% if tdoc.owner == handler.user._id or handler.user.hasPerm(perm.PERM_EDIT_HOMEWORK) %} + + {% endif %} {% else %}