mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
bug fixes & improvements for on site contest
This commit is contained in:
19 files changed
+383
-68
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in new issue
Block a user