Skip to content

Commit 191353f

Browse files
committed
bug fixes & improvements for on site contest
1 parent effc7da commit 191353f

19 files changed

Lines changed: 383 additions & 68 deletions

File tree

framework/eslint-config/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
"repository": "https://github.com/hydro-dev/Hydro",
77
"dependencies": {
88
"@antfu/eslint-config": "^6.2.0",
9-
"@eslint-react/eslint-plugin": "^2.3.4",
10-
"@typescript-eslint/eslint-plugin": "^8.46.4",
11-
"@typescript-eslint/parser": "^8.46.4",
9+
"@eslint-react/eslint-plugin": "^2.3.5",
10+
"@typescript-eslint/eslint-plugin": "^8.47.0",
11+
"@typescript-eslint/parser": "^8.47.0",
1212
"eslint-plugin-de-morgan": "^2.0.0",
1313
"eslint-plugin-github": "^6.0.0",
1414
"eslint-plugin-react-hooks": "^7.0.1",

framework/utils/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@hydrooj/register": "workspace:^",
1616
"cac": "^6.7.14",
1717
"fs-extra": "^11.3.2",
18-
"js-yaml": "^4.1.0",
18+
"js-yaml": "^4.1.1",
1919
"reggol": "^2.1.0",
2020
"search-query-parser": "^1.6.0",
2121
"systeminformation": "^5.27.11"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"mongodb-memory-server": "10.3.0",
5454
"nyc": "^17.1.0",
5555
"ora": "^9.0.0",
56-
"oxlint": "^1.28.0",
56+
"oxlint": "^1.29.0",
5757
"package-json": "^10.0.1",
5858
"semver": "^7.7.3",
5959
"simple-git": "^3.30.0",

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"dependencies": {
1111
"allotment": "^1.20.4",
1212
"diff": "^8.0.2",
13-
"js-yaml": "^4.1.0",
13+
"js-yaml": "^4.1.1",
1414
"lodash": "^4.17.21",
1515
"react-dnd": "^16.0.1",
1616
"react-dnd-html5-backend": "^16.0.1",
@@ -21,7 +21,7 @@
2121
"@types/diff": "^8.0.0",
2222
"@types/js-yaml": "^4.0.9",
2323
"@types/lodash": "^4.17.20",
24-
"sass": "^1.94.0"
24+
"sass": "^1.94.1"
2525
},
2626
"peerDependencies": {
2727
"react": "*",

packages/hydrooj/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"emoji-regex": "^10.6.0",
3333
"emojis-list": "2.1.0",
3434
"fs-extra": "^11.3.2",
35-
"js-yaml": "^4.1.0",
35+
"js-yaml": "^4.1.1",
3636
"koa-proxies": "^0.12.4",
3737
"koa-static-cache": "^5.1.4",
3838
"lodash": "^4.17.21",
@@ -63,7 +63,7 @@
6363
"@types/lodash": "^4.17.20",
6464
"@types/mime-types": "^3.0.1",
6565
"@types/mongodb-uri": "^0.9.4",
66-
"@types/nodemailer": "^7.0.3",
66+
"@types/nodemailer": "^7.0.4",
6767
"@types/notp": "^2.0.5",
6868
"@types/semver": "^7.7.1",
6969
"@types/superagent": "^8.1.9",

packages/hydrooj/src/handler/contest.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Context, Service } from '../context';
1212
import {
1313
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
1414
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
15-
InvalidTokenError, NotAssignedError, NotFoundError, PermissionError, ValidationError,
15+
InvalidTokenError, MethodNotAllowedError, NotAssignedError, NotFoundError, PermissionError, ValidationError,
1616
} from '../error';
1717
import { FileInfo, ScoreboardConfig, Tdoc } from '../interface';
1818
import { PERM, PRIV, STATUS } from '../model/builtin';
@@ -126,6 +126,11 @@ export class ContestDetailBaseHandler extends Handler {
126126
args: { tid, prefix: 'contest_problemlist' },
127127
checker: () => this.tsdoc?.attend || contest.isDone(this.tdoc),
128128
},
129+
{
130+
name: 'contest_print',
131+
args: { tid, prefix: 'contest_print' },
132+
checker: () => this.tdoc.allowPrint && (this.tsdoc?.attend || this.user.own(this.tdoc) || this.user.hasPerm(PERM.PERM_EDIT_CONTEST)),
133+
},
129134
{
130135
name: 'contest_scoreboard',
131136
args: { tid, prefix: 'contest_scoreboard' },
@@ -197,6 +202,24 @@ export class ContestPrintHandler extends ContestDetailBaseHandler {
197202
this.response.template = 'contest_print.html';
198203
}
199204

205+
async post() {
206+
if (this.args.operation) return;
207+
if (this.args.file_contents && this.args.original_name) {
208+
try {
209+
await (this.postPrint as any)({
210+
...this.args,
211+
title: this.args.original_name,
212+
content: Buffer.from(this.args.file_contents, 'base64').toString('utf-8'),
213+
});
214+
this.response.body = { success: true, output: '' };
215+
} catch (e) {
216+
this.response.body = { success: false, output: e.message };
217+
} finally {
218+
delete this.response.redirect;
219+
}
220+
} else throw new MethodNotAllowedError('POST');
221+
}
222+
200223
@param('tid', Types.ObjectId)
201224
@param('title', Types.Title, true)
202225
@param('content', Types.Content, true)
@@ -758,8 +781,10 @@ export class ContestScoreboardHandler extends ContestDetailBaseHandler {
758781
@param('tid', Types.ObjectId)
759782
@param('view', Types.String, true)
760783
async get(domainId: string, tid: ObjectId, viewId = 'default') {
761-
if (!contest.canShowScoreboard.call(this, this.tdoc, true)) throw new ContestScoreboardHiddenError(tid);
762-
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
784+
if (!this.user.own(this.tdoc)) {
785+
if (!contest.canShowScoreboard.call(this, this.tdoc, true)) throw new ContestScoreboardHiddenError(tid);
786+
if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid);
787+
}
763788
const view = this.ctx.scoreboard.getView(viewId);
764789
if (!view) throw new NotFoundError(`View ${viewId} not found`);
765790
const args = {};
@@ -846,6 +871,8 @@ export async function apply(ctx: Context) {
846871
ctx.Route('contest_problemlist', '/contest/:tid/problems', ContestProblemListHandler, PERM.PERM_VIEW_CONTEST);
847872
ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
848873
ctx.Route('contest_print', '/contest/:tid/print', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
874+
// Support for DOMJudge printfile
875+
ctx.Route('contest_print_alt', '/contest/:tid/api/printing/team', ContestPrintHandler, PERM.PERM_VIEW_CONTEST);
849876
ctx.Route('contest_manage', '/contest/:tid/management', ContestManagementHandler);
850877
ctx.Route('contest_clarification', '/contest/:tid/clarification', ContestClarificationHandler);
851878
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);

packages/hydrooj/src/handler/problem.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,10 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
519519
await storage.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id);
520520
files.code = `${this.user._id}/${id}#${file.originalFilename}`;
521521
}
522-
} else if (code.length > lengthLimit) throw new ValidationError('code');
522+
} else {
523+
code = code.replace(/\r\n/g, '\n');
524+
if (code.length > lengthLimit) throw new ValidationError('code');
525+
}
523526
const rid = await record.add(
524527
domainId, this.pdoc.docId, this.user._id, lang, code, true,
525528
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: 'judge' },

packages/hydrooj/src/lib/testdataConfig.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { load } from 'js-yaml';
2-
import { normalizeSubtasks, ProblemConfigFile } from '@hydrooj/common';
2+
import { normalizeSubtasks, ProblemConfigFile, readSubtasksFromFiles } from '@hydrooj/common';
33
import { readYamlCases } from '@hydrooj/common/cases';
44
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
55
import type { ProblemConfig } from '../interface';
66

7-
export async function parseConfig(config: string | ProblemConfigFile = {}) {
7+
export async function parseConfig(config: string | ProblemConfigFile = {}, files: string[]) {
88
const cfg: ProblemConfigFile = typeof config === 'string'
99
? await readYamlCases(load(config) as Record<string, any>)
1010
: await readYamlCases(config);
1111
const result: ProblemConfig = {
12-
count: 0,
12+
count: Object.keys(cfg.answers || {}).length || Math.sum((cfg.subtasks || []).map((s) => s.cases.length)),
1313
memoryMin: Number.MAX_SAFE_INTEGER,
1414
memoryMax: 0,
1515
timeMin: Number.MAX_SAFE_INTEGER,
@@ -19,6 +19,7 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
1919
};
2020
if (cfg.subType) result.subType = cfg.subType;
2121
if (cfg.target) result.target = cfg.target;
22+
result.count ||= Math.sum(readSubtasksFromFiles(files, cfg).map((i) => i.cases.length));
2223
if (cfg.subtasks?.length) {
2324
for (const subtask of normalizeSubtasks(cfg.subtasks as any || [], (i) => i, cfg.time, cfg.memory)) {
2425
result.memoryMax = Math.max(result.memoryMax, ...subtask.cases.map((i) => parseMemoryMB(i.memory)));

packages/hydrooj/src/model/contest.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function buildContestRule<T>(def: Partial<ContestRule<T>>, baseRule: Cont
9898
}
9999

100100
const acm = buildContestRule({
101-
TEXT: 'ACM/ICPC',
101+
TEXT: 'XCPC',
102102
check: () => { },
103103
statusSort: { accept: -1, time: 1 },
104104
submitAfterAccept: false,
@@ -276,6 +276,7 @@ const acm = buildContestRule({
276276
delete rdoc.memory;
277277
rdoc.testCases = [];
278278
rdoc.judgeTexts = [];
279+
delete rdoc.progress;
279280
delete rdoc.subtasks;
280281
delete rdoc.score;
281282
return rdoc;
@@ -908,7 +909,7 @@ export async function updateStatus(
908909
}: { status?: STATUS, score?: number, subtasks?: Record<number, SubtaskResult>, lang?: string } = {},
909910
) {
910911
const tdoc = await get(domainId, tid);
911-
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid);
912+
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED && !isLocked(tdoc)) await addBalloon(domainId, tid, uid, rid, pid);
912913
const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', {
913914
rid, pid, status, score, subtasks, lang,
914915
}, 'rid');

packages/hydrooj/src/model/problem.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,25 @@ interface ProblemCreateOptions {
7575
reference?: { domainId: string, pid: number };
7676
}
7777

78+
const PROJECTION_BASE: Field[] = [
79+
'_id', 'domainId', 'docType', 'docId', 'pid',
80+
'owner', 'title',
81+
];
82+
7883
export class ProblemModel {
7984
static PROJECTION_CONTEST_LIST: Field[] = [
80-
'_id', 'domainId', 'docType', 'docId', 'pid',
81-
'owner', 'title',
85+
...PROJECTION_BASE, 'config',
8286
];
8387

8488
static PROJECTION_LIST: Field[] = [
85-
...ProblemModel.PROJECTION_CONTEST_LIST,
89+
...PROJECTION_BASE,
8690
'nSubmit', 'nAccept', 'difficulty', 'tag', 'hidden',
8791
'stats',
8892
];
8993

9094
static PROJECTION_CONTEST_DETAIL: Field[] = [
9195
...ProblemModel.PROJECTION_CONTEST_LIST,
92-
'content', 'html', 'data', 'config', 'additional_file',
96+
'content', 'html', 'data', 'additional_file',
9397
'reference', 'maintainer',
9498
];
9599

@@ -198,7 +202,7 @@ export class ProblemModel {
198202
.project(buildProjection(projection)).limit(1).toArray())[0];
199203
if (!res) return null;
200204
try {
201-
if (!rawConfig) res.config = await parseConfig(res.config);
205+
if (!rawConfig) res.config = await parseConfig(res.config, res.data?.map((i) => i.name) || []);
202206
} catch (e) {
203207
res.config = `Cannot parse: ${e.message}`;
204208
}
@@ -390,7 +394,7 @@ export class ProblemModel {
390394
}
391395
for (const pdoc of pdocs) {
392396
try {
393-
pdoc.config = await parseConfig(pdoc.config as string);
397+
pdoc.config = await parseConfig(pdoc.config as string, pdoc.data?.map((i) => i.name) || []);
394398
} catch (e) {
395399
pdoc.config = `Cannot parse: ${e.message}`;
396400
}
@@ -552,16 +556,29 @@ export class ProblemModel {
552556
const tag = (pdoc.tag || []).map((t) => t.toString());
553557
let configChanged = false;
554558
let config: ProblemConfigFile = {};
555-
if (await fs.exists('testdata/config.yaml')) {
559+
if (await fs.exists(path.join(tmpdir, i, 'testdata/config.yaml'))) {
556560
try {
557-
config = yaml.load(await fs.readFile('testdata/config.yaml', 'utf-8'));
561+
config = yaml.load(await fs.readFile(path.join(tmpdir, i, 'testdata/config.yaml'), 'utf-8'));
558562
} catch (e) {
559563
// TODO: report this as a warning
560564
}
561565
}
566+
if (await fs.exists(path.join(tmpdir, i, 'domjudge-problem.ini'))) {
567+
const djConfig = (await fs.readFile(path.join(tmpdir, i, 'domjudge-problem.ini'), 'utf-8') as string)
568+
.split('\n').map((line: string) => line.split('=').map((lines: string) => lines.trim()));
569+
const djConfigJson: any = {};
570+
for (const [key, value] of djConfig) {
571+
djConfigJson[key] = value;
572+
}
573+
if (djConfigJson.timelimit) {
574+
config.time = `${djConfigJson.timelimit * 1000}ms`;
575+
configChanged = true;
576+
}
577+
if (djConfigJson.externalid) pid = djConfigJson.externalid;
578+
}
562579
if ((pdoc as any).limits) {
563-
config.time = (pdoc as any).limits.time_limit;
564-
config.memory = (pdoc as any).limits.memory;
580+
config.time = (pdoc as any).limits.time_limit ? `${(pdoc as any).limits.time_limit * 1000}ms` : config.time || undefined;
581+
config.memory = (pdoc as any).limits.memory ? `${(pdoc as any).limits.memory}m` : config.memory || undefined;
565582
configChanged = true;
566583
}
567584
const docId = overridePid
@@ -587,9 +604,8 @@ export class ProblemModel {
587604
if (!f.isDirectory()) continue;
588605
const sub = await fs.readdir(loc);
589606
for (const file of sub) {
590-
await (f.name === 'sample' ? ProblemModel.addAdditionalFile
591-
: f.name === 'secret' ? ProblemModel.addTestdata
592-
: null)?.(domainId, docId, file, path.join(loc, file));
607+
if (f.name === 'sample') await ProblemModel.addAdditionalFile(domainId, docId, file, path.join(loc, file));
608+
await ProblemModel.addTestdata(domainId, docId, file, path.join(loc, file));
593609
}
594610
}
595611
for (const [f, loc] of await getFiles('output_validators')) {
@@ -598,10 +614,10 @@ export class ProblemModel {
598614
for (const file of sub) {
599615
if (file === 'testlib.h') continue;
600616
await ProblemModel.addTestdata(domainId, docId, file, path.join(loc, file));
601-
if (file === 'checker') {
617+
if (f.name === 'checker') {
602618
config.checker_type = 'testlib';
603619
config.checker = file;
604-
} else if (file === 'interactor') {
620+
} else if (f.name === 'interactor') {
605621
config.type = ProblemType.Interactive;
606622
config.interactor = file;
607623
}
@@ -624,14 +640,24 @@ export class ProblemModel {
624640
config.judge_extra_files = Array.from(new Set(config.judge_extra_files.concat(f.name)));
625641
configChanged = true;
626642
}
643+
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', Buffer.from(yaml.dump(config)));
627644
let count = 0;
628645
for (const [f, loc] of await getFiles('std')) {
629646
if (!f.isFile()) continue;
630647
count++;
631648
if (count > 5) continue;
632649
await RecordModel.add(domainId, docId, operator, f.name.split('.')[1], await fs.readFile(loc, 'utf-8'), true);
633650
}
634-
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', yaml.dump(config));
651+
for (const [f, loc] of await getFiles('submissions')) {
652+
if (f.isFile()) continue;
653+
const sub = await fs.readdir(loc);
654+
for (const file of sub) {
655+
if (file.endsWith('.zip')) continue;
656+
const code = await fs.readFile(path.join(loc, file), 'utf-8');
657+
await RecordModel.add(domainId, docId, operator, file.split('.')[1], `// ${file}: ${loc}\n${code}`, true);
658+
}
659+
}
660+
if (configChanged) await ProblemModel.addTestdata(domainId, docId, 'config.yaml', Buffer.from(yaml.dump(config)));
635661
const message = `${overridePid ? 'Updated' : 'Imported'} problem ${pdoc.pid || docId} (${title})`;
636662
(process.env.HYDRO_CLI ? logger.info : progress)?.(message);
637663
} catch (e) {

0 commit comments

Comments
 (0)