bug fixes & improvements for on site contest

This commit is contained in:
undefined committed 2025-11-18 17:59:59 +08:00
1 parent effc7da312
commit 191353fbbc
19 files changed
+383 -68

No files matched your search

+2 -2
View File
@@ -32,7 +32,7 @@
"emoji-regex": "^10.6.0",
"emojis-list": "2.1.0",
"fs-extra": "^11.3.2",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"koa-proxies": "^0.12.4",
"koa-static-cache": "^5.1.4",
"lodash": "^4.17.21",
@@ -63,7 +63,7 @@
"@types/lodash": "^4.17.20",
"@types/mime-types": "^3.0.1",
"@types/mongodb-uri": "^0.9.4",
"@types/nodemailer": "^7.0.3",
"@types/nodemailer": "^7.0.4",
"@types/notp": "^2.0.5",
"@types/semver": "^7.7.1",
"@types/superagent": "^8.1.9",
+30 -3
View File
@@ -12,7 +12,7 @@ import { Context, Service } from '../context';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
InvalidTokenError, NotAssignedError, NotFoundError, PermissionError, ValidationError,
InvalidTokenError, MethodNotAllowedError, NotAssignedError, NotFoundError, PermissionError, ValidationError,
} from '../error';
import { FileInfo, ScoreboardConfig, Tdoc } from '../interface';
import { PERM, PRIV, STATUS } from '../model/builtin';
@@ -126,6 +126,11 @@ export class ContestDetailBaseHandler extends Handler {
args: { tid, prefix: 'contest_problemlist' },
checker: () => this.tsdoc?.attend || contest.isDone(this.tdoc),
},
{
name: 'contest_print',
args: { tid, prefix: 'contest_print' },
checker: () => this.tdoc.allowPrint && (this.tsdoc?.attend || this.user.own(this.tdoc) || this.user.hasPerm(PERM.PERM_EDIT_CONTEST)),
},
{
name: 'contest_scoreboard',
args: { tid, prefix: 'contest_scoreboard' },
@@ -197,6 +202,24 @@ export class ContestPrintHandler extends ContestDetailBaseHandler {
this.response.template = 'contest_print.html';
}
async post() {
if (this.args.operation) return;
if (this.args.file_contents && this.args.original_name) {
try {
await (this.postPrint as any)({
...this.args,
title: this.args.original_name,
content: Buffer.from(this.args.file_contents, 'base64').toString('utf-8'),
});
this.response.body = { success: true, output: '' };
} catch (e) {
this.response.body = { success: false, output: e.message };
} finally {
delete this.response.redirect;
}
} else throw new MethodNotAllowedError('POST');
}
@param('tid', Types.ObjectId)
@param('title', Types.Title, true)
@param('content', Types.Content, true)
@@ -758,8 +781,10 @@ export class ContestScoreboardHandler extends ContestDetailBaseHandler {
@param('tid', Types.ObjectId)
@param('view', Types.String, true)
async get(domainId: string, tid: ObjectId, viewId = 'default') {
if (!contest.canShowScoreboard.call(this, this.tdoc, true)) throw new ContestScoreboardHiddenError(tid);
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
if (!this.user.own(this.tdoc)) {
if (!contest.canShowScoreboard.call(this, this.tdoc, true)) throw new ContestScoreboardHiddenError(tid);
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
}
const view = this.ctx.scoreboard.getView(viewId);
if (!view) throw new NotFoundError(`View ${viewId} not found`);
const args = {};
@@ -846,6 +871,8 @@ export async function apply(ctx: Context) {
ctx.Route('contest_problemlist', '/contest/:tid/problems', ContestProblemListHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_print', '/contest/:tid/print', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
// Support for DOMJudge printfile
ctx.Route('contest_print_alt', '/contest/:tid/api/printing/team', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_manage', '/contest/:tid/management', ContestManagementHandler);
ctx.Route('contest_clarification', '/contest/:tid/clarification', ContestClarificationHandler);
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
+4 -1
View File
@@ -519,7 +519,10 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
await storage.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id);
files.code = `${this.user._id}/${id}#${file.originalFilename}`;
}
} else if (code.length > lengthLimit) throw new ValidationError('code');
} else {
code = code.replace(/\r\n/g, '\n');
if (code.length > lengthLimit) throw new ValidationError('code');
}
const rid = await record.add(
domainId, this.pdoc.docId, this.user._id, lang, code, true,
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: 'judge' },
+4 -3
View File
@@ -1,15 +1,15 @@
import { load } from 'js-yaml';
import { normalizeSubtasks, ProblemConfigFile } from '@hydrooj/common';
import { normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles } from '@hydrooj/common';
import { readYamlCases } from '@hydrooj/common/cases';
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
import type { ProblemConfig } from '../interface';
export async function parseConfig(config: string | ProblemConfigFile = {}) {
export async function parseConfig(config: string | ProblemConfigFile = {}, files: string[]) {
const cfg: ProblemConfigFile = typeof config === 'string'
? await readYamlCases(load(config) as Record<string, any>)
: await readYamlCases(config);
const result: ProblemConfig = {
count: 0,
count: Object.keys(cfg.answers || {}).length || Math.sum((cfg.subtasks || []).map((s) => s.cases.length)),
memoryMin: Number.MAX_SAFE_INTEGER,
memoryMax: 0,
timeMin: Number.MAX_SAFE_INTEGER,
@@ -19,6 +19,7 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
};
if (cfg.subType) result.subType = cfg.subType;
if (cfg.target) result.target = cfg.target;
result.count ||= Math.sum(readSubtasksFromFiles(files, cfg).map((i) => i.cases.length));
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)));
+3 -2
View File
@@ -98,7 +98,7 @@ export function buildContestRule<T>(def: Partial<ContestRule<T>>, baseRule: Cont
}
const acm = buildContestRule({
TEXT: 'ACM/ICPC',
TEXT: 'XCPC',
check: () => { },
statusSort: { accept: -1, time: 1 },
submitAfterAccept: false,
@@ -276,6 +276,7 @@ const acm = buildContestRule({
delete rdoc.memory;
rdoc.testCases = [];
rdoc.judgeTexts = [];
delete rdoc.progress;
delete rdoc.subtasks;
delete rdoc.score;
return rdoc;
@@ -908,7 +909,7 @@ export async function updateStatus(
}: { status?: STATUS, score?: number, subtasks?: Record<number, SubtaskResult>, lang?: string } = {},
) {
const tdoc = await get(domainId, tid);
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid);
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED && !isLocked(tdoc)) await addBalloon(domainId, tid, uid, rid, pid);
const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', {
rid, pid, status, score, subtasks, lang,
}, 'rid');
+42 -16
View File
@@ -75,21 +75,25 @@ interface ProblemCreateOptions {
reference?: { domainId: string, pid: number };
}
const PROJECTION_BASE: Field[] = [
'_id', 'domainId', 'docType', 'docId', 'pid',
'owner', 'title',
];
export class ProblemModel {
static PROJECTION_CONTEST_LIST: Field[] = [
'_id', 'domainId', 'docType', 'docId', 'pid',
'owner', 'title',
...PROJECTION_BASE, 'config',
];
static PROJECTION_LIST: Field[] = [
...ProblemModel.PROJECTION_CONTEST_LIST,
...PROJECTION_BASE,
'nSubmit', 'nAccept', 'difficulty', 'tag', 'hidden',
'stats',
];
static PROJECTION_CONTEST_DETAIL: Field[] = [
...ProblemModel.PROJECTION_CONTEST_LIST,
'content', 'html', 'data', 'config', 'additional_file',
'content', 'html', 'data', 'additional_file',
'reference', 'maintainer',
];
@@ -198,7 +202,7 @@ export class ProblemModel {
.project(buildProjection(projection)).limit(1).toArray())[0];
if (!res) return null;
try {
if (!rawConfig) res.config = await parseConfig(res.config);
if (!rawConfig) res.config = await parseConfig(res.config, res.data?.map((i) => i.name) || []);
} catch (e) {
res.config = `Cannot parse: ${e.message}`;
}
@@ -390,7 +394,7 @@ export class ProblemModel {
}
for (const pdoc of pdocs) {
try {
pdoc.config = await parseConfig(pdoc.config as string);
pdoc.config = await parseConfig(pdoc.config as string, pdoc.data?.map((i) => i.name) || []);
} catch (e) {
pdoc.config = `Cannot parse: ${e.message}`;
}
@@ -552,16 +556,29 @@ export class ProblemModel {
const tag = (pdoc.tag || []).map((t) => t.toString());
let configChanged = false;
let config: ProblemConfigFile = {};
if (await fs.exists('testdata/config.yaml')) {
if (await fs.exists(path.join(tmpdir, i, 'testdata/config.yaml'))) {
try {
config = yaml.load(await fs.readFile('testdata/config.yaml', 'utf-8'));
config = yaml.load(await fs.readFile(path.join(tmpdir, i, 'testdata/config.yaml'), 'utf-8'));
} catch (e) {
// TODO: report this as a warning
}
}
if (await fs.exists(path.join(tmpdir, i, 'domjudge-problem.ini'))) {
const djConfig = (await fs.readFile(path.join(tmpdir, i, 'domjudge-problem.ini'), 'utf-8') as string)
.split('\n').map((line: string) => line.split('=').map((lines: string) => lines.trim()));
const djConfigJson: any = {};
for (const [key, value] of djConfig) {
djConfigJson[key] = value;
}
if (djConfigJson.timelimit) {
config.time = `${djConfigJson.timelimit * 1000}ms`;
configChanged = true;
}
if (djConfigJson.externalid) pid = djConfigJson.externalid;
}
if ((pdoc as any).limits) {
config.time = (pdoc as any).limits.time_limit;
config.memory = (pdoc as any).limits.memory;
config.time = (pdoc as any).limits.time_limit ? `${(pdoc as any).limits.time_limit * 1000}ms` : config.time || undefined;
config.memory = (pdoc as any).limits.memory ? `${(pdoc as any).limits.memory}m` : config.memory || undefined;
configChanged = true;
}
const docId = overridePid
@@ -587,9 +604,8 @@ export class ProblemModel {
if (!f.isDirectory()) continue;
const sub = await fs.readdir(loc);
for (const file of sub) {
await (f.name === 'sample' ? ProblemModel.addAdditionalFile
: f.name === 'secret' ? ProblemModel.addTestdata
: null)?.(domainId, docId, file, path.join(loc, file));
if (f.name === 'sample') await ProblemModel.addAdditionalFile(domainId, docId, file, path.join(loc, file));
await ProblemModel.addTestdata(domainId, docId, file, path.join(loc, file));
}
}
for (const [f, loc] of await getFiles('output_validators')) {
@@ -598,10 +614,10 @@ export class ProblemModel {
for (const file of sub) {
if (file === 'testlib.h') continue;
await ProblemModel.addTestdata(domainId, docId, file, path.join(loc, file));
if (file === 'checker') {
if (f.name === 'checker') {
config.checker_type = 'testlib';
config.checker = file;
} else if (file === 'interactor') {
} else if (f.name === 'interactor') {
config.type = ProblemType.Interactive;
config.interactor = file;
}
@@ -624,6 +640,7 @@ export class ProblemModel {
config.judge_extra_files = Array.from(new Set(config.judge_extra_files.concat(f.name)));
configChanged = true;
}
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', Buffer.from(yaml.dump(config)));
let count = 0;
for (const [f, loc] of await getFiles('std')) {
if (!f.isFile()) continue;
@@ -631,7 +648,16 @@ export class ProblemModel {
if (count > 5) continue;
await RecordModel.add(domainId, docId, operator, f.name.split('.')[1], await fs.readFile(loc, 'utf-8'), true);
}
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', yaml.dump(config));
for (const [f, loc] of await getFiles('submissions')) {
if (f.isFile()) continue;
const sub = await fs.readdir(loc);
for (const file of sub) {
if (file.endsWith('.zip')) continue;
const code = await fs.readFile(path.join(loc, file), 'utf-8');
await RecordModel.add(domainId, docId, operator, file.split('.')[1], `// ${file}: ${loc}\n${code}`, true);
}
}
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', Buffer.from(yaml.dump(config)));
const message = `${overridePid ? 'Updated' : 'Imported'} problem ${pdoc.pid || docId} (${title})`;
(process.env.HYDRO_CLI ? logger.info : progress)?.(message);
} catch (e) {
+6 -1
View File
@@ -627,7 +627,12 @@ export const coreScripts: MigrationScript[] = [
};
for (const file of files) {
const [, domainId, tid, type, name] = file.path.split('/');
const tdoc = await contest.get(domainId, new ObjectId(tid));
let tdoc;
try {
tdoc = await contest.get(domainId, new ObjectId(tid));
} catch (e) {
continue;
}
if (!tdoc) console.error('Contest not found', file.path);
if (tdoc.rule === 'homework' || !name) {
await rename(file.path, `contest/${domainId}/${tid}/public/${name || type}`);