mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
judge: support generate testdata (#695)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
b83b2e792e
commit
55b4e1aaf0
27 files changed
+453
-67
No files matched your search
@@ -50,8 +50,8 @@ const testlibFile = {
|
|||||||
src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'),
|
src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function compileWithTestlib(
|
export async function compileLocalFile(
|
||||||
src: string, type: 'checker' | 'validator' | 'interactor',
|
src: string, type: 'checker' | 'validator' | 'interactor' | 'generator' | 'std',
|
||||||
getLang, copyIn: CopyIn, withTestlib = true, next?: any,
|
getLang, copyIn: CopyIn, withTestlib = true, next?: any,
|
||||||
) {
|
) {
|
||||||
const s = src.replace('@', '.').split('.');
|
const s = src.replace('@', '.').split('.');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
JudgeResultBody, ObjectId, RecordModel, SettingModel,
|
JudgeResultBody, ObjectId, RecordModel, SettingModel,
|
||||||
StorageModel, SystemModel, TaskModel,
|
StorageModel, SystemModel, TaskModel,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { end, next } from 'hydrooj/src/handler/judge';
|
import { end, next, processJudgeFileCallback } from 'hydrooj/src/handler/judge';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { FormatError, SystemError } from '../error';
|
import { FormatError, SystemError } from '../error';
|
||||||
import { Context } from '../judge/interface';
|
import { Context } from '../judge/interface';
|
||||||
@@ -46,6 +46,9 @@ const session = {
|
|||||||
if (doThrow) throw new SystemError('Unsupported language {0}.', [lang]);
|
if (doThrow) throw new SystemError('Unsupported language {0}.', [lang]);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
async postFile(target: string, filename: string, filepath: string) {
|
||||||
|
return await processJudgeFileCallback(new ObjectId(target), filename, filepath);
|
||||||
|
},
|
||||||
async cacheOpen(source: string, files: any[]) {
|
async cacheOpen(source: string, files: any[]) {
|
||||||
const filePath = path.join(getConfig('cache_dir'), source);
|
const filePath = path.join(getConfig('cache_dir'), source);
|
||||||
await fs.ensureDir(filePath);
|
await fs.ensureDir(filePath);
|
||||||
@@ -84,9 +87,10 @@ export async function postInit(ctx) {
|
|||||||
logger.debug('Record not found: %o', t);
|
logger.debug('Record not found: %o', t);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await (new JudgeTask(session, Object.assign(rdoc, t))).handle().catch(logger.error);
|
await (new JudgeTask(session, JSON.parse(JSON.stringify(Object.assign(rdoc, t))))).handle().catch(logger.error);
|
||||||
};
|
};
|
||||||
const parallelism = Math.max(getConfig('parallelism'), 2);
|
const parallelism = Math.max(getConfig('parallelism'), 2);
|
||||||
for (let i = 1; i < parallelism; i++) TaskModel.consume({ type: 'judge' }, handle);
|
for (let i = 1; i < parallelism; i++) TaskModel.consume({ type: 'judge' }, handle);
|
||||||
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
|
||||||
|
TaskModel.consume({ type: 'generate' }, handle);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import * as sysinfo from '@hydrooj/utils/lib/sysinfo';
|
|||||||
import type { JudgeResultBody } from 'hydrooj';
|
import type { JudgeResultBody } from 'hydrooj';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { FormatError, SystemError } from '../error';
|
import { FormatError, SystemError } from '../error';
|
||||||
|
import { Session } from '../interface';
|
||||||
import log from '../log';
|
import log from '../log';
|
||||||
import { JudgeTask } from '../task';
|
import { JudgeTask } from '../task';
|
||||||
import { Lock } from '../utils';
|
import { Lock } from '../utils';
|
||||||
@@ -18,7 +19,7 @@ function removeNixPath(text: string) {
|
|||||||
return text.replace(/\/nix\/store\/[a-z0-9]{32}-/g, '/nix/');
|
return text.replace(/\/nix\/store\/[a-z0-9]{32}-/g, '/nix/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Hydro {
|
export default class Hydro implements Session {
|
||||||
ws: WebSocket;
|
ws: WebSocket;
|
||||||
language: Record<string, LangConfig>;
|
language: Record<string, LangConfig>;
|
||||||
|
|
||||||
@@ -134,6 +135,10 @@ export default class Hydro {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async postFile(target: string, file: string) {
|
||||||
|
await this.post('judge/upload', { target }).attach('file', fs.createReadStream(file));
|
||||||
|
}
|
||||||
|
|
||||||
getLang(name: string, doThrow = true) {
|
getLang(name: string, doThrow = true) {
|
||||||
if (this.language[name]) return this.language[name];
|
if (this.language[name]) return this.language[name];
|
||||||
if (name === 'cpp' && this.language.cc) return this.language.cc;
|
if (name === 'cpp' && this.language.cc) return this.language.cc;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import type { LangConfig } from '@hydrooj/utils/lib/lang';
|
||||||
import { NormalizedSubtask } from '@hydrooj/utils/lib/utils';
|
import { NormalizedSubtask } from '@hydrooj/utils/lib/utils';
|
||||||
|
import type { JudgeRequest as OrigJudgeRequest, ObjectId } from 'hydrooj';
|
||||||
import { JudgeResultBody, ProblemConfigFile } from 'hydrooj';
|
import { JudgeResultBody, ProblemConfigFile } from 'hydrooj';
|
||||||
import { CopyInFile } from './sandbox';
|
import { CopyInFile } from './sandbox';
|
||||||
|
import type { JudgeTask } from './task';
|
||||||
|
|
||||||
export interface Execute {
|
export interface Execute {
|
||||||
execute: string;
|
execute: string;
|
||||||
@@ -16,3 +19,18 @@ export interface ParsedConfig extends Omit<ProblemConfigFile, 'time' | 'memory'
|
|||||||
memory: number;
|
memory: number;
|
||||||
subtasks: NormalizedSubtask[];
|
subtasks: NormalizedSubtask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replace ObjectId to string
|
||||||
|
export type JudgeRequest = {
|
||||||
|
[K in keyof OrigJudgeRequest]: OrigJudgeRequest[K] extends ObjectId ? string : OrigJudgeRequest[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
getLang: (name: string) => LangConfig;
|
||||||
|
getNext: (task: JudgeTask) => NextFunction;
|
||||||
|
getEnd: (task: JudgeTask) => NextFunction;
|
||||||
|
cacheOpen: (source: string, files: any[], next?: NextFunction) => Promise<string>;
|
||||||
|
fetchFile: (target: string) => Promise<string>;
|
||||||
|
postFile: (target: string, filename: string, file: string) => Promise<void>;
|
||||||
|
config: { detail: boolean, host?: string };
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ export const judge = async (ctx: Context) => await runFlow(ctx, {
|
|||||||
compile: async () => {
|
compile: async () => {
|
||||||
[ctx.execute, ctx.checker] = await Promise.all([
|
[ctx.execute, ctx.checker] = await Promise.all([
|
||||||
ctx.compile(ctx.lang, ctx.code),
|
ctx.compile(ctx.lang, ctx.code),
|
||||||
ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type),
|
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
judgeCase,
|
judgeCase,
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||||
|
import { SystemError } from 'hydrooj';
|
||||||
|
import { CopyInFile, runQueued } from '../sandbox';
|
||||||
|
import client from '../sandbox/client';
|
||||||
|
import signals from '../signals';
|
||||||
|
import { JudgeTask } from '../task';
|
||||||
|
import { parseMemoryMB, parseTimeMS } from '../utils';
|
||||||
|
|
||||||
|
export const judge = async (ctx: JudgeTask) => {
|
||||||
|
ctx.stat.judge = new Date();
|
||||||
|
ctx.next({ status: STATUS.STATUS_COMPILING });
|
||||||
|
if (!('content' in ctx.code)) throw new SystemError('Unsupported input');
|
||||||
|
const [generator, std] = ctx.code.content.toString().split('\n').map((i) => i.trim());
|
||||||
|
if (generator.includes('/') || generator === '..') throw new SystemError('Invalid input');
|
||||||
|
if (std.includes('/') || std === '..') throw new SystemError('Invalid input');
|
||||||
|
const [executeGenerator, executeStd] = await Promise.all([
|
||||||
|
ctx.compileLocalFile('generator', generator),
|
||||||
|
ctx.compileLocalFile('std', std),
|
||||||
|
]);
|
||||||
|
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
|
||||||
|
let totalTime = 0;
|
||||||
|
let totalMemory = 0;
|
||||||
|
let totalStatus = 0;
|
||||||
|
|
||||||
|
async function runGenerator(i: number) {
|
||||||
|
const res = await runQueued(
|
||||||
|
`${executeGenerator.execute} ${i}`,
|
||||||
|
{
|
||||||
|
stdin: { content: ctx.input || '' },
|
||||||
|
copyIn: executeGenerator.copyIn,
|
||||||
|
copyOut: ['stdout'],
|
||||||
|
time: parseTimeMS('2s'),
|
||||||
|
memory: parseMemoryMB('256m'),
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.in`);
|
||||||
|
ctx.clean.push(() => {
|
||||||
|
if (fs.existsSync(tmp)) fs.removeSync(tmp);
|
||||||
|
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
code, signalled, time, memory, fileIds,
|
||||||
|
} = res;
|
||||||
|
let { status } = res;
|
||||||
|
const message: string[] = [];
|
||||||
|
if (time > parseTimeMS(ctx.config.time || '2s')) {
|
||||||
|
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
|
} else if (memory > parseMemoryMB('256m') * 1024) {
|
||||||
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
|
} else if (code) {
|
||||||
|
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||||
|
if (code < 32 && signalled) message.push(`ExitCode: ${code} (${signals[code]})`);
|
||||||
|
else message.push(`ExitCode: ${code}`);
|
||||||
|
}
|
||||||
|
totalTime += time;
|
||||||
|
totalMemory = Math.max(memory, totalMemory);
|
||||||
|
totalStatus = Math.max(status, totalStatus);
|
||||||
|
if (status === STATUS.STATUS_ACCEPTED) {
|
||||||
|
await client.getFile(fileIds['output'], tmp);
|
||||||
|
await ctx.session.postFile(ctx.request.rid.toString(), `${i}.in`, tmp);
|
||||||
|
}
|
||||||
|
ctx.next({
|
||||||
|
case: {
|
||||||
|
id: i,
|
||||||
|
subtaskId: 1,
|
||||||
|
status,
|
||||||
|
score: 0,
|
||||||
|
time,
|
||||||
|
memory,
|
||||||
|
message: message.join('\n').substring(0, 102400),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return status === STATUS.STATUS_ACCEPTED ? tmp : null;
|
||||||
|
}
|
||||||
|
async function runStd(i: number, stdin: CopyInFile) {
|
||||||
|
const res = await runQueued(
|
||||||
|
`${executeStd.execute} ${i}`,
|
||||||
|
{
|
||||||
|
stdin,
|
||||||
|
copyIn: executeStd.copyIn,
|
||||||
|
copyOut: ['stdout'],
|
||||||
|
time: parseTimeMS('2s'),
|
||||||
|
memory: parseMemoryMB('256m'),
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.out`);
|
||||||
|
ctx.clean.push(() => {
|
||||||
|
if (fs.existsSync(tmp)) fs.removeSync(tmp);
|
||||||
|
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
code, signalled, time, memory, fileIds,
|
||||||
|
} = res;
|
||||||
|
let { status } = res;
|
||||||
|
const message: string[] = [];
|
||||||
|
if (time > parseTimeMS(ctx.config.time || '2s')) {
|
||||||
|
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
|
} else if (memory > parseMemoryMB('256m') * 1024) {
|
||||||
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
|
} else if (code) {
|
||||||
|
status = STATUS.STATUS_RUNTIME_ERROR;
|
||||||
|
if (code < 32 && signalled) message.push(`ExitCode: ${code} (${signals[code]})`);
|
||||||
|
else message.push(`ExitCode: ${code}`);
|
||||||
|
}
|
||||||
|
totalTime += time;
|
||||||
|
totalMemory = Math.max(memory, totalMemory);
|
||||||
|
totalStatus = Math.max(status, totalStatus);
|
||||||
|
if (status === STATUS.STATUS_ACCEPTED) {
|
||||||
|
await client.getFile(fileIds['output'], tmp);
|
||||||
|
await ctx.session.postFile(ctx.request.rid.toString(), `${i}.out`, tmp);
|
||||||
|
}
|
||||||
|
ctx.next({
|
||||||
|
case: {
|
||||||
|
id: i,
|
||||||
|
subtaskId: 2,
|
||||||
|
status,
|
||||||
|
score: 0,
|
||||||
|
time,
|
||||||
|
memory,
|
||||||
|
message: message.join('\n').substring(0, 102400),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return status === STATUS.STATUS_ACCEPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const result = await runGenerator(i);
|
||||||
|
if (result) await runStd(i, { src: result });
|
||||||
|
}
|
||||||
|
ctx.stat.done = new Date();
|
||||||
|
if (process.env.DEV) ctx.next({ message: JSON.stringify(ctx.stat) });
|
||||||
|
ctx.end({
|
||||||
|
status: totalStatus,
|
||||||
|
score: totalStatus === STATUS.STATUS_ACCEPTED ? 100 : 0,
|
||||||
|
time: totalTime,
|
||||||
|
memory: totalMemory,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,8 +11,8 @@ export async function judge(ctx: Context) {
|
|||||||
ctx.next({ status: STATUS.STATUS_COMPILING, progress: 0 });
|
ctx.next({ status: STATUS.STATUS_COMPILING, progress: 0 });
|
||||||
const [execute, checker, validator, input] = await Promise.all([
|
const [execute, checker, validator, input] = await Promise.all([
|
||||||
ctx.compile(ctx.lang, ctx.code),
|
ctx.compile(ctx.lang, ctx.code),
|
||||||
ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type),
|
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
|
||||||
ctx.compileWithTestlib('validator', ctx.config.validator),
|
ctx.compileLocalFile('validator', ctx.config.validator),
|
||||||
ctx.session.fetchFile(ctx.files.hack),
|
ctx.session.fetchFile(ctx.files.hack),
|
||||||
]);
|
]);
|
||||||
ctx.clean.push(() => fs.unlink(input));
|
ctx.clean.push(() => fs.unlink(input));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as def from './default';
|
import * as def from './default';
|
||||||
|
import * as generate from './generate';
|
||||||
import * as hack from './hack';
|
import * as hack from './hack';
|
||||||
import * as interactive from './interactive';
|
import * as interactive from './interactive';
|
||||||
import { Context } from './interface';
|
import { Context } from './interface';
|
||||||
@@ -7,5 +8,5 @@ import * as run from './run';
|
|||||||
import * as submit_answer from './submit_answer';
|
import * as submit_answer from './submit_answer';
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
default: def, interactive, run, submit_answer, objective, hack,
|
default: def, generate, interactive, run, submit_answer, objective, hack,
|
||||||
} as Record<string, { judge(ctx: Context): Promise<void> }>;
|
} as Record<string, { judge(ctx: Context): Promise<void> }>;
|
||||||
@@ -68,7 +68,7 @@ export const judge = async (ctx: Context) => await runFlow(ctx, {
|
|||||||
compile: async () => {
|
compile: async () => {
|
||||||
[ctx.executeUser, ctx.executeInteractor] = await Promise.all([
|
[ctx.executeUser, ctx.executeInteractor] = await Promise.all([
|
||||||
ctx.compile(ctx.lang, ctx.code),
|
ctx.compile(ctx.lang, ctx.code),
|
||||||
ctx.compileWithTestlib('interactor', ctx.config.interactor),
|
ctx.compileLocalFile('interactor', ctx.config.interactor),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
judgeCase,
|
judgeCase,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function judgeCase(c: NormalizedCase) {
|
|||||||
export const judge = async (ctx: Context) => {
|
export const judge = async (ctx: Context) => {
|
||||||
await runFlow(ctx, {
|
await runFlow(ctx, {
|
||||||
compile: async () => {
|
compile: async () => {
|
||||||
ctx.checker = await ctx.compileWithTestlib('checker', ctx.config.checker, ctx.config.checker_type);
|
ctx.checker = await ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type);
|
||||||
},
|
},
|
||||||
judgeCase,
|
judgeCase,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import superagent from 'superagent';
|
import superagent from 'superagent';
|
||||||
import { getConfig } from '../config';
|
import { getConfig } from '../config';
|
||||||
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
|
import { SandboxRequest, SandboxResult, SandboxVersion } from './interface';
|
||||||
@@ -8,7 +9,15 @@ const client = new Proxy({
|
|||||||
run(req: SandboxRequest): Promise<SandboxResult[]> {
|
run(req: SandboxRequest): Promise<SandboxResult[]> {
|
||||||
return superagent.post(`${url}/run`).send(req).then((res) => res.body);
|
return superagent.post(`${url}/run`).send(req).then((res) => res.body);
|
||||||
},
|
},
|
||||||
getFile(fileId: string): Promise<Buffer> {
|
getFile(fileId: string, dest?: string): Promise<Buffer> {
|
||||||
|
if (dest) {
|
||||||
|
const w = fs.createWriteStream(dest);
|
||||||
|
superagent.get(`${url}/file/${fileId}`).pipe(w);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
w.on('finish', () => resolve(null));
|
||||||
|
w.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
return superagent.get(`${url}/file/${fileId}`).responseType('arraybuffer').then((res) => res.body);
|
return superagent.get(`${url}/file/${fileId}`).responseType('arraybuffer').then((res) => res.body);
|
||||||
},
|
},
|
||||||
deleteFile(fileId: string): Promise<void> {
|
deleteFile(fileId: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
import { basename } from 'path';
|
import { basename, join } from 'path';
|
||||||
import { fs } from '@hydrooj/utils';
|
import { fs } from '@hydrooj/utils';
|
||||||
import { LangConfig } from '@hydrooj/utils/lib/lang';
|
|
||||||
import { STATUS } from '@hydrooj/utils/lib/status';
|
import { STATUS } from '@hydrooj/utils/lib/status';
|
||||||
import type {
|
import type {
|
||||||
FileInfo, JudgeMeta, JudgeRequest, JudgeResultBody, TestCase,
|
FileInfo, JudgeMeta, JudgeResultBody, TestCase,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import readCases from './cases';
|
import readCases from './cases';
|
||||||
import checkers from './checkers';
|
import checkers from './checkers';
|
||||||
import compile, { compileWithTestlib } from './compile';
|
import compile, { compileLocalFile } from './compile';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { CompileError, FormatError } from './error';
|
import { CompileError, FormatError } from './error';
|
||||||
import { Execute, NextFunction, ParsedConfig } from './interface';
|
import {
|
||||||
|
Execute, JudgeRequest, ParsedConfig, Session,
|
||||||
|
} from './interface';
|
||||||
import judge from './judge';
|
import judge from './judge';
|
||||||
import { Logger } from './log';
|
import { Logger } from './log';
|
||||||
import { CopyIn, CopyInFile, runQueued } from './sandbox';
|
import { CopyIn, CopyInFile, runQueued } from './sandbox';
|
||||||
import { compilerText, md5 } from './utils';
|
import { compilerText, md5 } from './utils';
|
||||||
|
|
||||||
interface Session {
|
|
||||||
getLang: (name: string) => LangConfig;
|
|
||||||
getNext: (task: JudgeTask) => NextFunction;
|
|
||||||
getEnd: (task: JudgeTask) => NextFunction;
|
|
||||||
cacheOpen: (source: string, files: any[], next?: NextFunction) => Promise<string>;
|
|
||||||
fetchFile: (target: string) => Promise<string>;
|
|
||||||
config: { detail: boolean, host?: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = new Logger('judge');
|
const logger = new Logger('judge');
|
||||||
|
|
||||||
export class JudgeTask {
|
export class JudgeTask {
|
||||||
@@ -121,13 +113,14 @@ export class JudgeTask {
|
|||||||
isSelfSubmission: this.meta.problemOwner === this.request.uid,
|
isSelfSubmission: this.meta.problemOwner === this.request.uid,
|
||||||
key: md5(`${this.source}/${getConfig('secret')}`),
|
key: md5(`${this.source}/${getConfig('secret')}`),
|
||||||
lang: this.lang,
|
lang: this.lang,
|
||||||
langConfig: ['objective', 'submit_answer'].includes(this.request.config.type) ? null : this.session.getLang(this.lang),
|
langConfig: (this.request.type === 'generate' || ['objective', 'submit_answer'].includes(this.request.config.type))
|
||||||
|
? null : this.session.getLang(this.lang),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.stat.judge = new Date();
|
this.stat.judge = new Date();
|
||||||
const type = this.request.contest?.toString() === '000000000000000000000000' ? 'run'
|
const type = this.request.contest?.toString() === '000000000000000000000000' ? 'run'
|
||||||
: this.files?.hack
|
: this.request.type === 'generate' ? 'generate'
|
||||||
? 'hack'
|
: this.files?.hack ? 'hack'
|
||||||
: this.config.type || 'default';
|
: this.config.type || 'default';
|
||||||
if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]);
|
if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]);
|
||||||
await judge[type].judge(this);
|
await judge[type].judge(this);
|
||||||
@@ -142,17 +135,19 @@ export class JudgeTask {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async compileWithTestlib(type: 'interactor' | 'validator' | 'checker', file: string, checkerType?: string) {
|
async compileLocalFile(type: 'interactor' | 'validator' | 'checker' | 'generator' | 'std', file: string, checkerType?: string) {
|
||||||
if (type === 'checker' && ['default', 'strict'].includes(checkerType)) return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
if (type === 'checker' && ['default', 'strict'].includes(checkerType)) return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) };
|
||||||
if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
if (type === 'checker' && !checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]);
|
||||||
const withTestlib = type !== 'checker' || checkerType === 'testlib';
|
const withTestlib = type !== 'std' && (type !== 'checker' || checkerType === 'testlib');
|
||||||
|
const extra = type === 'std' ? this.config.user_extra_files : this.config.judge_extra_files;
|
||||||
const copyIn = {
|
const copyIn = {
|
||||||
user_code: this.code,
|
user_code: this.code,
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
(this.config.judge_extra_files || []).map((i) => [basename(i), { src: i }]),
|
(extra || []).map((i) => [basename(i), { src: i }]),
|
||||||
),
|
),
|
||||||
} as CopyIn;
|
} as CopyIn;
|
||||||
const result = await compileWithTestlib(file, type, this.session.getLang, copyIn, withTestlib, this.next);
|
if (!file.startsWith('/')) file = join(this.folder, file);
|
||||||
|
const result = await compileLocalFile(file, type, this.session.getLang, copyIn, withTestlib, this.next);
|
||||||
this.clean.push(result.clean);
|
this.clean.push(result.clean);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
|
import sanitize from 'sanitize-filename';
|
||||||
|
import {
|
||||||
|
FileLimitExceededError, ForbiddenError, ProblemIsReferencedError, ValidationError,
|
||||||
|
} from '../error';
|
||||||
import {
|
import {
|
||||||
JudgeResultBody, ProblemConfigFile, RecordDoc, Task, TestCase,
|
JudgeResultBody, ProblemConfigFile, RecordDoc, Task, TestCase,
|
||||||
} from '../interface';
|
} from '../interface';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as builtin from '../model/builtin';
|
import * as builtin from '../model/builtin';
|
||||||
import { STATUS } from '../model/builtin';
|
import { PERM, STATUS } from '../model/builtin';
|
||||||
import * as contest from '../model/contest';
|
import * as contest from '../model/contest';
|
||||||
import domain from '../model/domain';
|
import domain from '../model/domain';
|
||||||
import problem from '../model/problem';
|
import problem from '../model/problem';
|
||||||
import record from '../model/record';
|
import record from '../model/record';
|
||||||
import * as setting from '../model/setting';
|
import * as setting from '../model/setting';
|
||||||
import storage from '../model/storage';
|
import storage from '../model/storage';
|
||||||
|
import * as system from '../model/system';
|
||||||
import task from '../model/task';
|
import task from '../model/task';
|
||||||
|
import user from '../model/user';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import { updateJudge } from '../service/monitor';
|
import { updateJudge } from '../service/monitor';
|
||||||
import {
|
import {
|
||||||
@@ -189,11 +196,46 @@ export class SubmissionDataDownloadHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function processJudgeFileCallback(rid: ObjectId, filename: string, filePath: string) {
|
||||||
|
const rdoc = await record.get(rid);
|
||||||
|
const [pdoc, udoc] = await Promise.all([
|
||||||
|
problem.get(rdoc.domainId, rdoc.pid),
|
||||||
|
user.getById(rdoc.domainId, rdoc.uid),
|
||||||
|
]);
|
||||||
|
if (!udoc.own(pdoc, PERM.PERM_EDIT_PROBLEM_SELF) && !udoc.hasPerm(PERM.PERM_EDIT_PROBLEM)) throw new ForbiddenError();
|
||||||
|
if (pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if ((pdoc.data?.length || 0)
|
||||||
|
+ (pdoc.additional_file?.length || 0)
|
||||||
|
>= system.get('limit.problem_files_max')) {
|
||||||
|
throw new FileLimitExceededError('count');
|
||||||
|
}
|
||||||
|
const size = Math.sum(
|
||||||
|
(pdoc.data || []).map((i) => i.size),
|
||||||
|
(pdoc.additional_file || []).map((i) => i.size),
|
||||||
|
stat.size,
|
||||||
|
);
|
||||||
|
if (size >= system.get('limit.problem_files_max_size')) {
|
||||||
|
throw new FileLimitExceededError('size');
|
||||||
|
}
|
||||||
|
await problem.addTestdata(pdoc.domainId, pdoc.docId, sanitize(filename), fs.createReadStream(filePath), udoc._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JudgeFileUpdateHandler extends Handler {
|
||||||
|
@post('rid', Types.ObjectId)
|
||||||
|
@post('name', Types.Filename)
|
||||||
|
async post(domainId: string, rid: ObjectId, filename: string) {
|
||||||
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
|
await processJudgeFileCallback(rid, filename, this.request.files.file.filepath);
|
||||||
|
this.response.body = { ok: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class JudgeConnectionHandler extends ConnectionHandler {
|
class JudgeConnectionHandler extends ConnectionHandler {
|
||||||
category = '#judge';
|
category = '#judge';
|
||||||
processing: Task[] = [];
|
processing: Task[] = [];
|
||||||
closed = false;
|
closed = false;
|
||||||
query: any = { type: 'judge' };
|
query: any = { type: { $in: ['judge', 'generate'] } };
|
||||||
rdocs: Record<string, RecordDoc> = {};
|
rdocs: Record<string, RecordDoc> = {};
|
||||||
ip: string;
|
ip: string;
|
||||||
concurrency = 1;
|
concurrency = 1;
|
||||||
@@ -282,6 +324,7 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
|||||||
|
|
||||||
export async function apply(ctx) {
|
export async function apply(ctx) {
|
||||||
ctx.Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
ctx.Route('judge_files_download', '/judge/files', JudgeFilesDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
||||||
|
ctx.Route('judge_files_upload', '/judge/upload', JudgeFileUpdateHandler, builtin.PRIV.PRIV_JUDGE);
|
||||||
ctx.Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
ctx.Route('judge_submission_download', '/judge/code', SubmissionDataDownloadHandler, builtin.PRIV.PRIV_JUDGE);
|
||||||
ctx.Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
|
ctx.Connection('judge_conn', '/judge/conn', JudgeConnectionHandler, builtin.PRIV.PRIV_JUDGE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
|||||||
}
|
}
|
||||||
if (pretest) {
|
if (pretest) {
|
||||||
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest as string;
|
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest as string;
|
||||||
if (!['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
|
if (!['default', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
|
||||||
throw new ProblemNotAllowPretestError('type');
|
throw new ProblemNotAllowPretestError('type');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,7 +543,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
|
|||||||
}
|
}
|
||||||
const rid = await record.add(
|
const rid = await record.add(
|
||||||
domainId, this.pdoc.docId, this.user._id, lang, code, true,
|
domainId, this.pdoc.docId, this.user._id, lang, code, true,
|
||||||
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: tid ? 'contest' : 'judge' },
|
pretest ? { input, type: 'pretest' } : { contest: tid, files, type: 'judge' },
|
||||||
);
|
);
|
||||||
const rdoc = await record.get(domainId, rid);
|
const rdoc = await record.get(domainId, rid);
|
||||||
if (!pretest) {
|
if (!pretest) {
|
||||||
@@ -698,6 +698,12 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
} else this.response.template = 'problem_files.html';
|
} else this.response.template = 'problem_files.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async post() {
|
||||||
|
if (this.args.operation === 'get_links') return;
|
||||||
|
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
||||||
|
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||||
|
}
|
||||||
|
|
||||||
@post('files', Types.Set)
|
@post('files', Types.Set)
|
||||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
async postGetLinks(domainId: string, files: Set<string>, type = 'testdata') {
|
async postGetLinks(domainId: string, files: Set<string>, type = 'testdata') {
|
||||||
@@ -730,10 +736,8 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
@post('filename', Types.Filename, true)
|
@post('filename', Types.Filename, true)
|
||||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
|
||||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
|
|
||||||
if (!this.request.files.file) throw new ValidationError('file');
|
if (!this.request.files.file) throw new ValidationError('file');
|
||||||
filename ||= this.request.files.file.originalFilename || String.random(16);
|
filename ||= this.request.files.file.originalFilename || String.random(16);
|
||||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
|
||||||
const files = [];
|
const files = [];
|
||||||
if (filename.endsWith('.zip') && type === 'testdata') {
|
if (filename.endsWith('.zip') && type === 'testdata') {
|
||||||
let zip: AdmZip;
|
let zip: AdmZip;
|
||||||
@@ -744,7 +748,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
}
|
}
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.name) continue;
|
if (!entry.name || entry.isDirectory) continue;
|
||||||
files.push({
|
files.push({
|
||||||
type,
|
type,
|
||||||
name: sanitize(entry.name),
|
name: sanitize(entry.name),
|
||||||
@@ -788,9 +792,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
@post('newNames', Types.ArrayOf(Types.Filename))
|
@post('newNames', Types.ArrayOf(Types.Filename))
|
||||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
async postRenameFiles(domainId: string, files: string[], newNames: string[], type = 'testdata') {
|
async postRenameFiles(domainId: string, files: string[], newNames: string[], type = 'testdata') {
|
||||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('rename files');
|
|
||||||
if (files.length !== newNames.length) throw new ValidationError('files', 'newNames');
|
if (files.length !== newNames.length) throw new ValidationError('files', 'newNames');
|
||||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
|
||||||
await Promise.all(files.map(async (file, index) => {
|
await Promise.all(files.map(async (file, index) => {
|
||||||
const newName = newNames[index];
|
const newName = newNames[index];
|
||||||
if (type === 'testdata') await problem.renameTestdata(domainId, this.pdoc.docId, file, newName, this.user._id);
|
if (type === 'testdata') await problem.renameTestdata(domainId, this.pdoc.docId, file, newName, this.user._id);
|
||||||
@@ -802,12 +804,21 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
|
|||||||
@post('files', Types.ArrayOf(Types.Filename))
|
@post('files', Types.ArrayOf(Types.Filename))
|
||||||
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
@post('type', Types.Range(['testdata', 'additional_file']), true)
|
||||||
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
|
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
|
||||||
if (this.pdoc.reference) throw new ProblemIsReferencedError('delete files');
|
|
||||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
|
||||||
if (type === 'testdata') await problem.delTestdata(domainId, this.pdoc.docId, files, this.user._id);
|
if (type === 'testdata') await problem.delTestdata(domainId, this.pdoc.docId, files, this.user._id);
|
||||||
else await problem.delAdditionalFile(domainId, this.pdoc.docId, files, this.user._id);
|
else await problem.delAdditionalFile(domainId, this.pdoc.docId, files, this.user._id);
|
||||||
this.back();
|
this.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@post('std', Types.Filename)
|
||||||
|
@post('gen', Types.Filename)
|
||||||
|
async postGenerateTestdata(domainId: string, std: string, gen: string) {
|
||||||
|
if (!this.pdoc.data.find((i) => i.name === std)) throw new BadRequestError();
|
||||||
|
if (!this.pdoc.data.find((i) => i.name === gen)) throw new BadRequestError();
|
||||||
|
const rid = await record.add(domainId, this.pdoc.docId, this.user._id, '_', `${gen}\n${std}`, true, {
|
||||||
|
type: 'generate',
|
||||||
|
});
|
||||||
|
this.response.redirect = this.url('record_detail', { rid });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemFileDownloadHandler extends ProblemDetailHandler {
|
export class ProblemFileDownloadHandler extends ProblemDetailHandler {
|
||||||
|
|||||||
@@ -342,11 +342,13 @@ export interface JudgeMeta {
|
|||||||
problemOwner: number;
|
problemOwner: number;
|
||||||
hackRejudge?: string;
|
hackRejudge?: string;
|
||||||
rejudge?: boolean;
|
rejudge?: boolean;
|
||||||
|
// FIXME stricter types
|
||||||
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
|
export interface JudgeRequest extends Omit<RecordDoc, '_id' | 'testCases'> {
|
||||||
priority: number;
|
priority: number;
|
||||||
type: 'judge';
|
type: 'judge' | 'generate';
|
||||||
rid: ObjectId;
|
rid: ObjectId;
|
||||||
config: ProblemConfigFile;
|
config: ProblemConfigFile;
|
||||||
meta: JudgeMeta;
|
meta: JudgeMeta;
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) {
|
|||||||
if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
|
if (result.timeMax < result.timeMin) result.timeMax = result.timeMin = 1000;
|
||||||
if (cfg.langs) result.langs = cfg.langs;
|
if (cfg.langs) result.langs = cfg.langs;
|
||||||
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
if (cfg.redirect) result.redirect = cfg.redirect.split('/', 2) as any;
|
||||||
if (cfg.filename && result.type === 'default') {
|
if (cfg.filename && result.type === 'default') result.subType = cfg.filename;
|
||||||
result.type = 'fileio';
|
|
||||||
result.subType = cfg.filename;
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export default class RecordModel {
|
|||||||
'files',
|
'files',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static RECORD_PRETEST = new ObjectId('000000000000000000000000');
|
||||||
|
static RECORD_GENERATE = new ObjectId('000000000000000000000001');
|
||||||
|
|
||||||
static async submissionPriority(uid: number, base: number = 0) {
|
static async submissionPriority(uid: number, base: number = 0) {
|
||||||
const timeRecent = await RecordModel.coll
|
const timeRecent = await RecordModel.coll
|
||||||
.find({ _id: { $gte: Time.getObjectID(moment().add(-30, 'minutes')) }, uid, rejudged: { $ne: true } })
|
.find({ _id: { $gte: Time.getObjectID(moment().add(-30, 'minutes')) }, uid, rejudged: { $ne: true } })
|
||||||
@@ -82,8 +85,9 @@ export default class RecordModel {
|
|||||||
}
|
}
|
||||||
meta = { ...meta, problemOwner: pdoc.owner };
|
meta = { ...meta, problemOwner: pdoc.owner };
|
||||||
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
|
||||||
const type = (pdoc.config.type === 'remote_judge' && rdoc.contest?.toHexString() !== '0'.repeat(24)) ? 'remotejudge' : 'judge';
|
let type = 'judge';
|
||||||
config.type = pdoc.config.type === 'fileio' ? 'default' : pdoc.config.type as any;
|
if (pdoc.config.type === 'remote_judge' && rdoc.contest?.toHexString() !== '0'.repeat(24)) type = 'remotejudge';
|
||||||
|
else if (meta?.type === 'generate') type = 'generate';
|
||||||
return await task.addMany(rids.map((rid) => ({
|
return await task.addMany(rids.map((rid) => ({
|
||||||
...(pdoc.config as any),
|
...(pdoc.config as any),
|
||||||
priority,
|
priority,
|
||||||
@@ -104,7 +108,7 @@ export default class RecordModel {
|
|||||||
contest?: ObjectId,
|
contest?: ObjectId,
|
||||||
input?: string,
|
input?: string,
|
||||||
files?: Record<string, string>,
|
files?: Record<string, string>,
|
||||||
type: 'judge' | 'rejudge' | 'contest' | 'pretest' | 'hack',
|
type: 'judge' | 'rejudge' | 'pretest' | 'hack' | 'generate',
|
||||||
} = { type: 'judge' },
|
} = { type: 'judge' },
|
||||||
) {
|
) {
|
||||||
const data: RecordDoc = {
|
const data: RecordDoc = {
|
||||||
@@ -125,6 +129,7 @@ export default class RecordModel {
|
|||||||
judgeAt: null,
|
judgeAt: null,
|
||||||
rejudged: false,
|
rejudged: false,
|
||||||
};
|
};
|
||||||
|
let isContest = !!args.contest;
|
||||||
if (args.contest) data.contest = args.contest;
|
if (args.contest) data.contest = args.contest;
|
||||||
if (args.files) data.files = args.files;
|
if (args.files) data.files = args.files;
|
||||||
if (args.type === 'rejudge') {
|
if (args.type === 'rejudge') {
|
||||||
@@ -132,12 +137,16 @@ export default class RecordModel {
|
|||||||
data.rejudged = true;
|
data.rejudged = true;
|
||||||
} else if (args.type === 'pretest') {
|
} else if (args.type === 'pretest') {
|
||||||
data.input = args.input || '';
|
data.input = args.input || '';
|
||||||
data.contest = new ObjectId('000000000000000000000000');
|
isContest = false;
|
||||||
|
data.contest = RecordModel.RECORD_PRETEST;
|
||||||
}
|
}
|
||||||
const res = await RecordModel.coll.insertOne(data);
|
const res = await RecordModel.coll.insertOne(data);
|
||||||
if (addTask) {
|
if (addTask) {
|
||||||
const priority = await RecordModel.submissionPriority(uid, args.type === 'pretest' ? -20 : (args.type === 'contest' ? 50 : 0));
|
const priority = await RecordModel.submissionPriority(uid, args.type === 'pretest' ? -20 : (isContest ? 50 : 0));
|
||||||
await RecordModel.judge(domainId, res.insertedId, priority, args.type === 'contest' ? { detail: false } : {}, { rejudge: data.rejudged });
|
await RecordModel.judge(domainId, res.insertedId, priority, isContest ? { detail: false } : {}, {
|
||||||
|
type: args.type,
|
||||||
|
rejudge: data.rejudged,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return res.insertedId;
|
return res.insertedId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Schedule } from '../interface';
|
|||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as bus from '../service/bus';
|
import * as bus from '../service/bus';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
import RecordModel from './record';
|
||||||
|
|
||||||
const logger = new Logger('model/schedule');
|
const logger = new Logger('model/schedule');
|
||||||
const coll = db.collection('schedule');
|
const coll = db.collection('schedule');
|
||||||
@@ -136,7 +137,7 @@ export async function apply(ctx: Context) {
|
|||||||
ctx.worker = Worker;
|
ctx.worker = Worker;
|
||||||
|
|
||||||
Worker.addHandler('task.daily', async () => {
|
Worker.addHandler('task.daily', async () => {
|
||||||
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectId('000000000000000000000000') });
|
await RecordModel.coll.deleteMany({ contest: { $in: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } });
|
||||||
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
||||||
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
||||||
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
|
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ObjectId } from 'mongodb';
|
|
||||||
import Schema from 'schemastery';
|
import Schema from 'schemastery';
|
||||||
import { STATUS } from '../model/builtin';
|
import { STATUS } from '../model/builtin';
|
||||||
import * as document from '../model/document';
|
import * as document from '../model/document';
|
||||||
|
import RecordModel from '../model/record';
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
|
||||||
const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1, 0] } });
|
const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1, 0] } });
|
||||||
@@ -10,7 +10,7 @@ const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1
|
|||||||
export async function udoc(report) {
|
export async function udoc(report) {
|
||||||
report({ message: 'Udoc' });
|
report({ message: 'Udoc' });
|
||||||
const pipeline = [
|
const pipeline = [
|
||||||
{ $match: { contest: { $ne: new ObjectId('000000000000000000000000') } } },
|
{ $match: { contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } } },
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||||
@@ -49,7 +49,7 @@ export async function udoc(report) {
|
|||||||
export async function psdoc(report) {
|
export async function psdoc(report) {
|
||||||
report({ message: 'Psdoc' });
|
report({ message: 'Psdoc' });
|
||||||
const pipeline = [
|
const pipeline = [
|
||||||
{ $match: { contest: { $ne: new ObjectId('000000000000000000000000') } } },
|
{ $match: { contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } } },
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||||
@@ -68,7 +68,7 @@ export async function pdoc(report) {
|
|||||||
const pipeline = [
|
const pipeline = [
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
contest: { $ne: new ObjectId('000000000000000000000000') },
|
contest: { $nin: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] },
|
||||||
status: { $ne: STATUS.STATUS_CANCELED },
|
status: { $ne: STATUS.STATUS_CANCELED },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { assign } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
|
import FileSelectAutoCompleteFC from './components/FileSelectAutoComplete';
|
||||||
|
|
||||||
|
interface FileSelectOptions {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = React.forwardRef<any, any>((props, ref) => {
|
||||||
|
const [value, setValue] = React.useState(props.value);
|
||||||
|
return (
|
||||||
|
<FileSelectAutoCompleteFC
|
||||||
|
ref={ref as any}
|
||||||
|
height="auto"
|
||||||
|
selectedKeys={value.split(',').map((i) => i.trim()).filter((i) => i)}
|
||||||
|
onChange={(v) => {
|
||||||
|
setValue(v);
|
||||||
|
props.onChange(v);
|
||||||
|
}}
|
||||||
|
data={props.data}
|
||||||
|
multi={props.multi}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||||
|
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||||
|
options: FileSelectOptions & AutoCompleteOptions;
|
||||||
|
|
||||||
|
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||||
|
super($dom, {
|
||||||
|
classes: 'custom-select',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
value(): Multi extends true ? number[] : string {
|
||||||
|
if (this.options.multi) return this.ref?.getSelectedItemKeys().map((i) => +i) ?? this.$dom.val();
|
||||||
|
return this.ref?.getSelectedItems()[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
const value = this.$dom.val();
|
||||||
|
ReactDOM.createRoot(this.container).render(
|
||||||
|
<Component
|
||||||
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
data={this.options.data}
|
||||||
|
value={value}
|
||||||
|
multi={this.options.multi}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(FileSelectAutoComplete, DOMAttachedObject);
|
||||||
|
window.Hydro.components.FileSelectAutoComplete = FileSelectAutoComplete;
|
||||||
@@ -100,7 +100,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let canUsePretest = ['default', 'fileio'].includes(UiContext.pdoc.config?.type);
|
let canUsePretest = UiContext.pdoc.config?.type === 'default';
|
||||||
const langInfo = availableLangs[this.props.editorLang];
|
const langInfo = availableLangs[this.props.editorLang];
|
||||||
if (UiContext.pdoc.config?.type === 'remote_judge' && langInfo) {
|
if (UiContext.pdoc.config?.type === 'remote_judge' && langInfo) {
|
||||||
if (langInfo.pretest) canUsePretest = true;
|
if (langInfo.pretest) canUsePretest = true;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { i18n } from 'vj/utils';
|
|||||||
|
|
||||||
export default function reducer(state = {
|
export default function reducer(state = {
|
||||||
pretest: {
|
pretest: {
|
||||||
visible: ['default', 'fileio'].includes(UiContext.pdoc.config?.type)
|
visible: UiContext.pdoc.config?.type === 'default'
|
||||||
? localStorage.getItem('scratchpad/pretest') === 'true'
|
? localStorage.getItem('scratchpad/pretest') === 'true'
|
||||||
: false,
|
: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
__langname: English
|
__langname: English
|
||||||
|
'File IO: {0}': 'File IO: {0}'
|
||||||
Can be Extended For: Extension
|
Can be Extended For: Extension
|
||||||
Current Status: Status
|
Current Status: Status
|
||||||
Handled Requests: Req
|
Handled Requests: Req
|
||||||
@@ -27,7 +28,6 @@ problem_main: Problem Set
|
|||||||
problem_solution: Problem Solution
|
problem_solution: Problem Solution
|
||||||
problem_submit: Problem Submit
|
problem_submit: Problem Submit
|
||||||
problem_type.default: 'Type: Default'
|
problem_type.default: 'Type: Default'
|
||||||
problem_type.fileio: 'Type: FileIO ({0})'
|
|
||||||
problem_type.interactive: 'Type: Interactive'
|
problem_type.interactive: 'Type: Interactive'
|
||||||
problem_type.objective: 'Type: Objective'
|
problem_type.objective: 'Type: Objective'
|
||||||
problem_type.remote_judge: 'Type: RemoteJudge'
|
problem_type.remote_judge: 'Type: RemoteJudge'
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ __langname: 简体中文
|
|||||||
'Current dataset: {0}': '当前测试数据: {0}'
|
'Current dataset: {0}': '当前测试数据: {0}'
|
||||||
'Discussion: Terms Of Service': 讨论区服务条款
|
'Discussion: Terms Of Service': 讨论区服务条款
|
||||||
'Drag and drop testcases here:': '拖拽测试点到此处:'
|
'Drag and drop testcases here:': '拖拽测试点到此处:'
|
||||||
|
'Enter a new name for the file: ': 请输入新的文件名:
|
||||||
'Failed to get credential: {0}': '获取认证数据失败: {0}'
|
'Failed to get credential: {0}': '获取认证数据失败: {0}'
|
||||||
|
'File IO: {0}': 文件IO:{0}
|
||||||
'File upload failed: {0}': 文件上传失败:{0}
|
'File upload failed: {0}': 文件上传失败:{0}
|
||||||
'Format: category 1, category 2, ..., category n': 格式:分类1, 分类2, ..., 分类n
|
'Format: category 1, category 2, ..., category n': 格式:分类1, 分类2, ..., 分类n
|
||||||
'Hello, {0}! You can click following link to active your new email of your {1} account:': 您好,{0}!您可以点击以下链接来激活您 {1} 账户的新电子邮件地址:
|
'Hello, {0}! You can click following link to active your new email of your {1} account:': 您好,{0}!您可以点击以下链接来激活您 {1} 账户的新电子邮件地址:
|
||||||
@@ -50,6 +52,8 @@ __langname: 简体中文
|
|||||||
(None): (无)
|
(None): (无)
|
||||||
(Not changed): 未更改
|
(Not changed): 未更改
|
||||||
6-Digit Code: 6位代码
|
6-Digit Code: 6位代码
|
||||||
|
A data generator is a program that generates testdata. It should output result to stdout.: 数据生成器是一个生成测试数据的程序。它应该将结果输出到标准输出。
|
||||||
|
A standard program is a program that solves the problem. It should output answer to stdout.: 标准程序是一个解决问题的程序。它应该将答案输出到标准输出。
|
||||||
Aborted: 已放弃
|
Aborted: 已放弃
|
||||||
About {0}: 关于 {0}
|
About {0}: 关于 {0}
|
||||||
About Markdown: 关于 Markdown
|
About Markdown: 关于 Markdown
|
||||||
@@ -245,6 +249,7 @@ Current Status: 当前状态
|
|||||||
currently offline: 目前离线
|
currently offline: 目前离线
|
||||||
Daily update check: 每日自动检查更新
|
Daily update check: 每日自动检查更新
|
||||||
Dark: 暗色
|
Dark: 暗色
|
||||||
|
Data Generator: 数据生成器
|
||||||
Data of Problem {0} not found.: 题目 {0} 的数据缺失。
|
Data of Problem {0} not found.: 题目 {0} 的数据缺失。
|
||||||
Data of problem {1} not found.: 题目 {1} 的数据未找到。
|
Data of problem {1} not found.: 题目 {1} 的数据未找到。
|
||||||
Data of record {0} not found.: 记录 {0} 的数据未找到。
|
Data of record {0} not found.: 记录 {0} 的数据未找到。
|
||||||
@@ -358,7 +363,6 @@ End Time: 结束时间
|
|||||||
Enroll Training: 参加训练
|
Enroll Training: 参加训练
|
||||||
Enrolled: 已参加
|
Enrolled: 已参加
|
||||||
Enrollees: 参加人数
|
Enrollees: 参加人数
|
||||||
'Enter a new name for the file: ': 请输入新的文件名:
|
|
||||||
error: 错误
|
error: 错误
|
||||||
Errors detected in the config. Confirm save?: 配置中存在错误。确认保存?
|
Errors detected in the config. Confirm save?: 配置中存在错误。确认保存?
|
||||||
Evaluated difficulty: 估计的难度
|
Evaluated difficulty: 估计的难度
|
||||||
@@ -398,6 +402,9 @@ Freeze scoreboard with N minutes remaining: 在赛事剩余N分钟时封榜
|
|||||||
fs_upload: 上传文件
|
fs_upload: 上传文件
|
||||||
Gender Visibility: 性别可见性
|
Gender Visibility: 性别可见性
|
||||||
Gender: 性别
|
Gender: 性别
|
||||||
|
Generate testdata is not supported for this problem type.: 此题目类型不支持生成测试数据。
|
||||||
|
Generate Testdata: 生成测试数据
|
||||||
|
Generate: 生成
|
||||||
Gravatar URL Prefix: Gravatar地址前缀
|
Gravatar URL Prefix: Gravatar地址前缀
|
||||||
Group / UID: 小组或 UID
|
Group / UID: 小组或 UID
|
||||||
Group Name: 名称
|
Group Name: 名称
|
||||||
@@ -686,7 +693,6 @@ problem_main: 题库
|
|||||||
problem_solution: 题解
|
problem_solution: 题解
|
||||||
problem_submit: 递交代码
|
problem_submit: 递交代码
|
||||||
problem_type.default: 传统题
|
problem_type.default: 传统题
|
||||||
problem_type.fileio: '文件IO ({0})'
|
|
||||||
problem_type.interactive: 交互题
|
problem_type.interactive: 交互题
|
||||||
problem_type.objective: 客观题
|
problem_type.objective: 客观题
|
||||||
problem_type.remote_judge: 远端评测题
|
problem_type.remote_judge: 远端评测题
|
||||||
@@ -845,6 +851,7 @@ Sorry: 对不起
|
|||||||
Sort by: 排序
|
Sort by: 排序
|
||||||
Source type: 来源类型
|
Source type: 来源类型
|
||||||
Standard mode: 标准模式
|
Standard mode: 标准模式
|
||||||
|
Standard Program: 标准程序
|
||||||
Star Topic: 收藏讨论
|
Star Topic: 收藏讨论
|
||||||
Star: 收藏
|
Star: 收藏
|
||||||
Starred Users: 关注用户
|
Starred Users: 关注用户
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import $ from 'jquery';
|
|||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog/index';
|
import FileSelectAutoComplete from 'vj/components/autocomplete/FileSelectAutoComplete';
|
||||||
|
import { ActionDialog, ConfirmDialog, InfoDialog } from 'vj/components/dialog/index';
|
||||||
import createHint from 'vj/components/hint';
|
import createHint from 'vj/components/hint';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import { previewFile } from 'vj/components/preview/preview.page';
|
import { previewFile } from 'vj/components/preview/preview.page';
|
||||||
@@ -318,6 +319,48 @@ const page = new NamedPage('problem_files', () => {
|
|||||||
handleClickUpload(type, files);
|
handleClickUpload(type, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGenerateTestdata(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const gen = $('[name=gen]').val();
|
||||||
|
const std = $('[name=std]').val();
|
||||||
|
if (!gen) {
|
||||||
|
Notification.error(i18n('Please input the generator.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!std) {
|
||||||
|
Notification.error(i18n('Please input the standard program.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await request.post('', {
|
||||||
|
operation: 'generate_testdata',
|
||||||
|
gen,
|
||||||
|
std,
|
||||||
|
});
|
||||||
|
if (res.error) Notification.error(res.error);
|
||||||
|
else {
|
||||||
|
Notification.success(i18n('Generating...'));
|
||||||
|
await new InfoDialog({
|
||||||
|
$body: tpl`
|
||||||
|
<div class="typo">
|
||||||
|
<iframe src="${res.url}"
|
||||||
|
scrolling="yes" border="0" frameborder="no" framespacing="0" width="100%" style="height: 70vh;"></iframe>
|
||||||
|
</div>`,
|
||||||
|
width: `${window.innerWidth - 200}px`,
|
||||||
|
height: `${window.innerHeight - 100}px`,
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error([error.message, ...error.params].join(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UiContext.pdoc) {
|
||||||
|
FileSelectAutoComplete.getOrConstruct($('[name=gen]'), { data: UiContext.pdoc.data });
|
||||||
|
FileSelectAutoComplete.getOrConstruct($('[name=std]'), { data: UiContext.pdoc.data });
|
||||||
|
$('[name="generate_testdata"]').on('click', handleGenerateTestdata);
|
||||||
|
}
|
||||||
|
|
||||||
if ($('[name="upload_testdata"]').length) {
|
if ($('[name="upload_testdata"]').length) {
|
||||||
$(document).on('click', '[name="upload_testdata"]', () => handleClickUpload('testdata'));
|
$(document).on('click', '[name="upload_testdata"]', () => handleClickUpload('testdata'));
|
||||||
$(document).on('click', '[name="upload_file"]', () => handleClickUpload('additional_file'));
|
$(document).on('click', '[name="upload_file"]', () => handleClickUpload('additional_file'));
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _('ID') }}: {{ pdoc.docId }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-book">{{ _('problem_type.' + pdoc.config.type).format(pdoc.config.subType) }}</span>
|
||||||
|
{% if pdoc.config.type == 'default' and pdoc.config.subType %}
|
||||||
|
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-book">{{ _('File IO: {0}').format(pdoc.config.subType) }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
{% if pdoc.config.type != 'objective' and pdoc.config.type != 'submit_answer' %}
|
||||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-stopwatch">{{ pdoc.config.timeMax if pdoc.config.timeMax == pdoc.config.timeMin else pdoc.config.timeMin+'~'+pdoc.config.timeMax }}ms</span>
|
||||||
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-comparison">{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</span>
|
<span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-comparison">{{ pdoc.config.memoryMax if pdoc.config.memoryMax == pdoc.config.memoryMin else pdoc.config.memoryMin+'~'+pdoc.config.memoryMax }}MiB</span>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% import "components/nothing.html" as nothing with context %}
|
||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -27,6 +28,36 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if not reference and (handler.user.own(pdoc, perm.PERM_EDIT_PROBLEM_SELF) or handler.user.hasPerm(perm.PERM_EDIT_PROBLEM)) %}
|
||||||
|
{{ set(UiContext, {
|
||||||
|
pdoc: pdoc
|
||||||
|
}) }}
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__header">
|
||||||
|
<h1 class="section__title">{{ _('Generate Testdata') }}(Beta)</h1>
|
||||||
|
</div>
|
||||||
|
{{ noscript_note.render() }}
|
||||||
|
<div class="section__body generate">
|
||||||
|
{% if pdoc.config.type === 'default' %}
|
||||||
|
{{ form.form_text({
|
||||||
|
columns: 12,
|
||||||
|
label:'Data Generator',
|
||||||
|
name:'gen',
|
||||||
|
help_text:'A data generator is a program that generates testdata. It should output result to stdout.'
|
||||||
|
}) }}
|
||||||
|
{{ form.form_text({
|
||||||
|
columns: 12,
|
||||||
|
label:'Standard Program',
|
||||||
|
name:'std',
|
||||||
|
help_text:'A standard program is a program that solves the problem. It should output answer to stdout.'
|
||||||
|
}) }}
|
||||||
|
<button class="rounded button" name="generate_testdata">{{ _('Generate') }}</button>
|
||||||
|
{% else %}
|
||||||
|
{{ nothing.render('Generate testdata is not supported for this problem type.') }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-4 columns">
|
<div class="medium-4 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
Reference in new issue
Block a user