From c01f8042cebe45f815e857bd358805af8709d469 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 27 Jun 2022 01:42:51 +0800 Subject: [PATCH 01/15] ui: add objecttiveNew ui --- .../ui-default/pages/problem_detail.page.jsx | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 5740081c6..977922785 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -7,6 +7,7 @@ import { downloadProblemSet } from 'vj/components/zipDownloader'; import loadReactRedux from 'vj/utils/loadReactRedux'; import delay from 'vj/utils/delay'; import pjax from 'vj/utils/pjax'; +import request from 'vj/utils/request'; class ProblemPageExtender { constructor() { @@ -201,6 +202,56 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } catch (e) { } } + async function loadObjectiveNew() { + $('.outer-loader-container').show(); + const ans = {}; + let objectiveNum = 0; + $('.problem-content .typo').children().each((i, e) => { + if (e.innerText.includes('{{ input }}')) { + objectiveNum++; + $(e).text($(e).text().replace(/{{ input }}/g, '?')); + $(e).after(`
+ +
`); + } + if (e.innerText.includes('{{ select }}')) { + objectiveNum++; + $(e).text($(e).text().replace(/{{ select }}/g, '')); + $(e).next('ul').children().each((j, ele) => { + $(ele).after(`
+ ${String.fromCharCode(65 + j)}. + + ${ele.innerHTML} +
`); + $(ele).remove(); + }); + } + }); + if (objectiveNum) { + $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length + ? '' + : ''); + $('input.objective-input').on('input', (e) => { + ans[e.target.name] = e.target.value; + }); + $('input[type="submit"]').on('click', (e) => { + e.preventDefault(); + request + .post(`${window.location.href}/submit`, { + lang: '_', + code: yaml.dump(ans), + }) + .then((res) => { + window.location.href = res.url; + }) + .catch((err) => { + Notification.error(err.message); + }); + }); + } + $('.outer-loader-container').hide(); + } + async function initChart() { if (!Object.keys(UiContext.pdoc.stats || {}).length) { $('#submission-status-placeholder').parent().hide(); @@ -273,7 +324,9 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor $('span.tags').css('display', 'inline-block'); }); $('[name="problem-sidebar__download"]').on('click', handleClickDownloadProblem); - if (UiContext.pdoc.config?.type === 'objective') loadObjective(); + const { type } = UiContext.pdoc.config || {}; + if (type === 'objective') loadObjective(); + if (type === 'objectiveNew') loadObjectiveNew(); if (pagename !== 'contest_detail_problem') initChart(); }); From e509bde41ba9999cd1b96210657ca58c5535ee17 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 27 Jun 2022 10:38:21 +0800 Subject: [PATCH 02/15] ui: add multiselect question --- .../ui-default/pages/problem_detail.page.jsx | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 977922785..b3cca2cf3 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -205,29 +205,48 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor async function loadObjectiveNew() { $('.outer-loader-container').show(); const ans = {}; - let objectiveNum = 0; + let objNum = 0; + const reg = /{{ (input|select|multiselect)\([a-zA-Z0-9]+\) }}/g; $('.problem-content .typo').children().each((i, e) => { - if (e.innerText.includes('{{ input }}')) { - objectiveNum++; - $(e).text($(e).text().replace(/{{ input }}/g, '?')); - $(e).after(`
- -
`); + if (e.tagName === 'PRE' && !e.children[0].className.includes('#input')) return; + const objQuestions = []; + let objQuestion = reg.exec(e.innerText); + while (objQuestion != null) { + objQuestions.push(objQuestion); + objQuestion = reg.exec(e.innerText); } - if (e.innerText.includes('{{ select }}')) { - objectiveNum++; - $(e).text($(e).text().replace(/{{ select }}/g, '')); - $(e).next('ul').children().each((j, ele) => { - $(ele).after(`
+ objQuestions.forEach((obj) => { + objNum++; + const [objData, objType] = obj; + const objID = objData.replace(/{{ (input|select|multiselect)\(([a-zA-Z0-9]+)\) }}/, '$2'); + if (objType === 'input') { + $(e).html($(e).html().replace(objData, `
+ +
`)); + } else if (objType === 'select') { + $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); + $(e).next('ul').children().each((j, ele) => { + $(ele).after(`
+ ${String.fromCharCode(65 + j)}. + + ${ele.innerHTML} +
`); + $(ele).remove(); + }); + } else if (objType === 'multiselect') { + $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); + $(e).next('ul').children().each((j, ele) => { + $(ele).after(`
${String.fromCharCode(65 + j)}. - + ${ele.innerHTML}
`); - $(ele).remove(); - }); - } + $(ele).remove(); + }); + } + }); }); - if (objectiveNum) { + if (objNum) { $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length ? '' : ''); From 76745639c10e00b39c9ea566df39a437df0ddf19 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 27 Jun 2022 15:00:58 +0800 Subject: [PATCH 03/15] judge: change objective config format --- packages/hydrojudge/src/cases.ts | 2 +- packages/hydrojudge/src/judge/interface.ts | 5 +- packages/hydrojudge/src/judge/objective.ts | 109 +++++++++++++++--- packages/hydrooj/src/interface.ts | 2 +- .../components/monaco/languages/yaml.ts | 13 ++- .../ui-default/pages/problem_detail.page.jsx | 65 +++-------- packages/utils/lib/cases.ts | 30 ++--- 7 files changed, 139 insertions(+), 87 deletions(-) diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index baf38cd5d..d28917de0 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -88,7 +88,7 @@ export default async function readCases(folder: string, cfg: Record const checkFile = ensureFile(folder); const result = await readYamlCases(config, checkFile) .catch((e) => { throw changeErrorType(e, FormatError); }); - result.count = result.outputs?.length || Math.sum((result.subtasks || []).map((s) => s.cases.length)); + result.count = Object.keys(result.answers || {}).length || Math.sum((result.subtasks || []).map((s) => s.cases.length)); if (!result.count) { try { result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg); diff --git a/packages/hydrojudge/src/judge/interface.ts b/packages/hydrojudge/src/judge/interface.ts index 1ebb1f78b..3f34a6688 100644 --- a/packages/hydrojudge/src/judge/interface.ts +++ b/packages/hydrojudge/src/judge/interface.ts @@ -62,10 +62,7 @@ export interface Config { hack?: string; interactor?: string; - outputs?: { - output: string, - score: number, - }[]; + answers?: Record; } export interface ContextSubTask { diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts index 0d6a374fb..2acaa0d64 100644 --- a/packages/hydrojudge/src/judge/objective.ts +++ b/packages/hydrojudge/src/judge/objective.ts @@ -1,4 +1,6 @@ +import assert from 'assert'; import { readFile } from 'fs-extra'; +import yaml from 'js-yaml'; import { STATUS } from '@hydrooj/utils/lib/status'; import { Context } from './interface'; @@ -11,27 +13,98 @@ export async function judge({ : ('content' in code) ? code.content.toString().replace(/\r/g, '') : ''; - const outputs = answer.split('\n'); + let answers; + try { + answers = yaml.load(answer); + assert(typeof answers === 'object'); + } catch (e) { + return end({ + status: STATUS.STATUS_WRONG_ANSWER, + score: 0, + message: 'Unable to parse answer.', + time: 0, + memory: 0, + }); + } let totalScore = 0; let totalStatus = 0; - for (const i in config.outputs) { - const c = config.outputs[i]; - outputs[i] = outputs[i] || ''; - let status = STATUS.STATUS_WRONG_ANSWER; - let score = 0; - if (outputs[i].trim() === (c.output || c[0]).trim()) { - score = c.score || c[1]; - status = STATUS.STATUS_ACCEPTED; + for (const key in config.answers) { + const [subtaskId, caseId] = key.split('-').map(Number); + const ansInfo = config.answers[key] as [string | string[], number]; + const score = ansInfo[1]; + const baseInfo = { + subtaskId, + id: caseId, + time: 0, + memory: 0, + }; + if (typeof ansInfo[0] === 'string') { + if (answer[0].trim() === answers[key].trim()) { + totalScore += score; + totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED); + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_ACCEPTED, + score, + message: 'Correct', + }, + }, +caseId); + } else { + totalStatus = STATUS.STATUS_WRONG_ANSWER; + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_WRONG_ANSWER, + score: 0, + message: 'Incorrect', + }, + }, +caseId); + } + } else { + const stdAns = new Set(ansInfo[0]); + const ans = new Set(answers[key]); + const correct = stdAns.size === ans.size && [...stdAns].every((x) => ans.has(x)); + const partialCorrect = [...stdAns].some((x) => ans.has(x)) && [...ans].every((x) => stdAns.has(x)); + if (correct) { + totalScore += score; + totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED); + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_ACCEPTED, + score, + message: 'Correct', + }, + }, +caseId); + } else if (partialCorrect) { + totalScore += Math.floor(score / 2); + totalStatus = STATUS.STATUS_WRONG_ANSWER; + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_WRONG_ANSWER, + score: Math.floor(score / 2), + message: 'Partially Correct', + }, + }, +caseId); + } else { + totalStatus = STATUS.STATUS_WRONG_ANSWER; + next({ + status: totalStatus, + case: { + ...baseInfo, + status: STATUS.STATUS_WRONG_ANSWER, + score: 0, + message: 'Incorrect', + }, + }, +caseId); + } } - totalScore += score; - totalStatus = Math.max(status, totalStatus); - next({ - status: totalStatus, - progress: (100 * (+i + 1)) / config.outputs.length, - case: { - status, score, time: 0, memory: 0, message: '', - }, - }, +i + 1); } return end({ status: totalStatus, score: totalScore, time: 0, memory: 0, diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index c8e29de2b..df67241c5 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -170,7 +170,7 @@ export interface ProblemConfigFile { user_extra_files?: string[]; judge_extra_files?: string[]; detail?: boolean; - outputs?: [string, number][]; + answers?: Record; redirect?: string; cases?: TestCaseConfig[]; subtasks?: SubtaskConfig[]; diff --git a/packages/ui-default/components/monaco/languages/yaml.ts b/packages/ui-default/components/monaco/languages/yaml.ts index 84a576182..df6e9a887 100644 --- a/packages/ui-default/components/monaco/languages/yaml.ts +++ b/packages/ui-default/components/monaco/languages/yaml.ts @@ -70,9 +70,16 @@ const problemConfigSchema = { }, additionalProperties: false, }, - outputs: { - type: 'array', - minLength: 1, + answers: { + type: 'object', + patternProperties: { + '^\\d+(-\\d+)?$': { + type: 'array', + minLength: 2, + maxLength: 2, + }, + }, + additionalProperties: false, }, }, additionalProperties: false, diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index b3cca2cf3..c18663d44 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -1,5 +1,5 @@ import yaml from 'js-yaml'; -import ReactDOM, { createRoot } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; import React from 'react'; import { getScoreColor } from '@hydrooj/utils/lib/status'; import { NamedPage } from 'vj/misc/Page'; @@ -174,39 +174,10 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } async function loadObjective() { - try { - let s = ''; - try { - s = JSON.parse(UiContext.pdoc.content); - } catch { - s = UiContext.pdoc.content; - } - if (typeof s === 'object') { - const langs = Object.keys(s); - const f = langs.filter((i) => i.startsWith(UserContext.viewLang)); - if (s[UserContext.viewLang]) s = s[UserContext.viewLang]; - else if (f.length) s = s[f[0]]; - else s = s[langs[0]]; - } - const props = yaml.load(s); - if (!(props instanceof Array)) return; - $('.outer-loader-container').show(); - const { default: Objective } = await import('vj/components/objective-question/index'); - - ReactDOM.createRoot($('.problem-content').get(0)).render( -
- -
, - ); - $('.outer-loader-container').hide(); - } catch (e) { } - } - - async function loadObjectiveNew() { $('.outer-loader-container').show(); const ans = {}; let objNum = 0; - const reg = /{{ (input|select|multiselect)\([a-zA-Z0-9]+\) }}/g; + const reg = /{{ (input|select|multiselect)\(\d+(-\d+)?\) }}/g; $('.problem-content .typo').children().each((i, e) => { if (e.tagName === 'PRE' && !e.children[0].className.includes('#input')) return; const objQuestions = []; @@ -218,29 +189,32 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor objQuestions.forEach((obj) => { objNum++; const [objData, objType] = obj; - const objID = objData.replace(/{{ (input|select|multiselect)\(([a-zA-Z0-9]+)\) }}/, '$2'); + const objID = objData.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2'); if (objType === 'input') { - $(e).html($(e).html().replace(objData, `
- -
`)); + $(e).html($(e).html().replace(objData, ` +
+ +
+ `)); } else if (objType === 'select') { $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); $(e).next('ul').children().each((j, ele) => { - $(ele).after(`
- ${String.fromCharCode(65 + j)}. - - ${ele.innerHTML} -
`); + $(ele).after(` +
+ + ${String.fromCharCode(65 + j)}. ${ele.innerHTML} +
`); $(ele).remove(); }); } else if (objType === 'multiselect') { $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); $(e).next('ul').children().each((j, ele) => { - $(ele).after(`
- ${String.fromCharCode(65 + j)}. - - ${ele.innerHTML} -
`); + $(ele).after(` +
+ + ${String.fromCharCode(65 + j)}. ${ele.innerHTML} +
+ `); $(ele).remove(); }); } @@ -345,7 +319,6 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor $('[name="problem-sidebar__download"]').on('click', handleClickDownloadProblem); const { type } = UiContext.pdoc.config || {}; if (type === 'objective') loadObjective(); - if (type === 'objectiveNew') loadObjectiveNew(); if (pagename !== 'contest_detail_problem') initChart(); }); diff --git a/packages/utils/lib/cases.ts b/packages/utils/lib/cases.ts index faa5a0bea..72980e132 100644 --- a/packages/utils/lib/cases.ts +++ b/packages/utils/lib/cases.ts @@ -7,21 +7,24 @@ export default async function readYamlCases(cfg: Record = {}, check judge_extra_files: [], user_extra_files: [], }; - if (cfg.checker) { - if (!cfg.checker.includes('.')) { - config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false); + if (cfg.type === 'objective') { + if (cfg.checker || cfg.interactor) throw new Error('You cannot use checker or interactor for objective questions'); + } else { + if (cfg.checker) { + if (!cfg.checker.includes('.')) { + config.checker = findFileSync(`@hydrooj/hydrojudge/vendor/testlib/checkers/${cfg.checker}.cpp`, false); + } + if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); } - if (!config.checker) config.checker = checkFile(cfg.checker, 'Cannot find checker {0}.'); + if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); + ['judge', 'user'].forEach((n) => { + const conf = cfg[`${n}_extra_files`]; + if (!conf) return; + if (conf instanceof Array) { + config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`)); + } else throw new Error(`Invalid ${n}_extra_files config.`); + }); } - if (cfg.interactor) config.interactor = checkFile(cfg.interactor, 'Cannot find interactor {0}.'); - ['judge', 'user'].forEach((n) => { - const conf = cfg[`${n}_extra_files`]; - if (!conf) return; - if (conf instanceof Array) { - config[`${n}_extra_files`] = conf.map((file) => checkFile(file, `Cannot find ${n} extra file {0}.`)); - } else throw new Error(`Invalid ${n}_extra_files config.`); - }); - if (cfg.outputs) config.type = cfg.type || 'objective'; if (cfg.cases?.length) { config.subtasks = [{ cases: cfg.cases, @@ -30,7 +33,6 @@ export default async function readYamlCases(cfg: Record = {}, check } if (cfg.time) config.time = parseTimeMS(cfg.time); if (cfg.memory) config.memory = parseMemoryMB(cfg.memory); - if (config.type === 'objective' && !cfg.outputs?.length) throw new Error('outputs config not found'); return Object.assign(cfg, config); } From 0195ba361eb9d89f1a20b0701c64b09a9a6137aa Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 27 Jun 2022 15:26:35 +0800 Subject: [PATCH 04/15] judge&ui: fix bugs --- packages/hydrojudge/src/judge/objective.ts | 2 +- packages/ui-default/pages/problem_detail.page.jsx | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts index 2acaa0d64..a278325a9 100644 --- a/packages/hydrojudge/src/judge/objective.ts +++ b/packages/hydrojudge/src/judge/objective.ts @@ -39,7 +39,7 @@ export async function judge({ memory: 0, }; if (typeof ansInfo[0] === 'string') { - if (answer[0].trim() === answers[key].trim()) { + if (ansInfo[0].trim() === answers[key].trim()) { totalScore += score; totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED); next({ diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index c18663d44..34bc18c23 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -224,9 +224,18 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length ? '' : ''); - $('input.objective-input').on('input', (e) => { + $('input.objective-input[type!=checkbox]').on('input', (e) => { ans[e.target.name] = e.target.value; }); + $('input.objective-input[type=checkbox]').on('input', (e) => { + if (e.target.checked) { + if (ans[e.target.name] === undefined) ans[e.target.name] = []; + ans[e.target.name].push(e.target.value); + } else { + ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value); + } + console.log(ans); + }); $('input[type="submit"]').on('click', (e) => { e.preventDefault(); request From 00bdea78d2186e065ad690cc87bf6b167d00cb59 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 27 Jun 2022 16:52:55 +0800 Subject: [PATCH 05/15] judge: fix type check --- packages/hydrojudge/src/judge/objective.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts index a278325a9..0f8712369 100644 --- a/packages/hydrojudge/src/judge/objective.ts +++ b/packages/hydrojudge/src/judge/objective.ts @@ -13,9 +13,9 @@ export async function judge({ : ('content' in code) ? code.content.toString().replace(/\r/g, '') : ''; - let answers; + let answers: { [x: string]: string | string[] } = {}; try { - answers = yaml.load(answer); + answers = yaml.load(answer) as { [x: string]: string | string[] }; assert(typeof answers === 'object'); } catch (e) { return end({ @@ -39,7 +39,7 @@ export async function judge({ memory: 0, }; if (typeof ansInfo[0] === 'string') { - if (ansInfo[0].trim() === answers[key].trim()) { + if (ansInfo[0].trim() === (answers[key] as string).trim()) { totalScore += score; totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED); next({ From df85677ea5cdb56d24a32fb196520bf83a1b4084 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 27 Jun 2022 20:14:40 +0800 Subject: [PATCH 06/15] ui: simplify --- .../ui-default/pages/problem_detail.page.jsx | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 34bc18c23..b44829996 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -176,51 +176,37 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor async function loadObjective() { $('.outer-loader-container').show(); const ans = {}; - let objNum = 0; + let cnt = 0; const reg = /{{ (input|select|multiselect)\(\d+(-\d+)?\) }}/g; $('.problem-content .typo').children().each((i, e) => { if (e.tagName === 'PRE' && !e.children[0].className.includes('#input')) return; - const objQuestions = []; - let objQuestion = reg.exec(e.innerText); - while (objQuestion != null) { - objQuestions.push(objQuestion); - objQuestion = reg.exec(e.innerText); - } - objQuestions.forEach((obj) => { - objNum++; - const [objData, objType] = obj; - const objID = objData.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2'); - if (objType === 'input') { - $(e).html($(e).html().replace(objData, ` -
- + const questions = []; + let q; + while (q = reg.exec(e.innerText)) questions.push(q); // eslint-disable-line no-cond-assign + for (const [info, type] of questions) { + cnt++; + const id = info.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2'); + if (type === 'input') { + $(e).html($(e).html().replace(info, ` +
+
`)); - } else if (objType === 'select') { - $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); + } else { + $(e).html($(e).html().replace(info, '')); $(e).next('ul').children().each((j, ele) => { $(ele).after(` -
- - ${String.fromCharCode(65 + j)}. ${ele.innerHTML} -
`); - $(ele).remove(); - }); - } else if (objType === 'multiselect') { - $(e).html($(e).html().replace(objData, `Question${objNum}_${objID}:`)); - $(e).next('ul').children().each((j, ele) => { - $(ele).after(` -
- +
+ ${String.fromCharCode(65 + j)}. ${ele.innerHTML}
`); $(ele).remove(); }); } - }); + } }); - if (objNum) { + if (cnt) { $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length ? '' : ''); From 0f150337f25024a92e76ae53d4c0484ca925f131 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 28 Jun 2022 15:22:06 +0800 Subject: [PATCH 07/15] core: objective: add upgrade script --- packages/hydrooj/src/upgrade.ts | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 19e0adfdc..403ee470d 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -17,6 +17,7 @@ import * as contest from './model/contest'; import * as discussion from './model/discussion'; import * as document from './model/document'; import domain from './model/domain'; +import MessageModel from './model/message'; import problem from './model/problem'; import RecordModel from './model/record'; import StorageModel from './model/storage'; @@ -715,6 +716,74 @@ const scripts: UpgradeScript[] = [ } return true; }, + async function _62_63() { + const _FRESH_INSTALL_IGNORE = 1; + const uids = new Set(); + await iterateAllDomain(async (ddoc) => { + const pdocs = await problem.getMulti(ddoc._id, { config: /type: objective/ }) + .project({ + config: 1, content: 1, docId: 1, owner: 1, + }).toArray(); + for (const pdoc of pdocs) { + try { + const config = yaml.load(pdoc.config as string) as any; + if (config.type !== 'objective') continue; + config.answers = {}; + let cnt = 0; + for (const l of config.outputs) { + cnt++; + config.answers[cnt] = l; + } + + function processSingleLanguage(content: string) { // eslint-disable-line no-inner-declarations + let text = ''; + try { + let scnt = 0; + const doc = yaml.load(content); + if (!(doc instanceof Array)) return content; + for (const s of doc) { + scnt++; + text += `${scnt}. ${s.desc}\n`; + if (s.choices) { + text += `{{ select(${scnt}) }}\n`; + const isPrefixed = s.choices.every((c) => /^[A-Z]\./.test(c)); + let selectionId = 64; + for (const c of s.choices) { + selectionId++; + text += `- ${isPrefixed ? c.replace(/^[A-Z]\./, '') : c}\n`; + if (config.answers[scnt][0] === c) config.answers[scnt][0] = String.fromCharCode(selectionId); + } + } else text += `{{ input(${scnt}) }}\n`; + text += '\n'; + } + } catch (e) { console.error(e); } + return text; + } + + try { + const langs = JSON.parse(pdoc.content); + if (typeof langs === 'object' && !(langs instanceof Array)) { + for (const lang in langs) { + if (typeof langs[lang] !== 'string') continue; + langs[lang] = processSingleLanguage(langs[lang]); + } + delete config.outputs; + await problem.edit(ddoc._id, pdoc.docId, { content: JSON.stringify(langs) }); + } + } catch (e) { + const content = processSingleLanguage(pdoc.content); + await problem.edit(ddoc._id, pdoc.docId, { content }); + } + uids.add(pdoc.owner); + await problem.addTestdata(ddoc._id, pdoc.docId, 'config.yaml', Buffer.from(yaml.dump(config))); + } catch (e) { console.error(e); } + } + }); + for (const uid of uids) { + await MessageModel.send(1, uid, '我们更新了客观题的配置格式,已有题目已自动转换,查看文档获得更多信息。', MessageModel.FLAG_UNREAD); + } + return true; + }, ]; export default scripts; From af3294f30cd77bb701d249d2a07004db2a09b95d Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 15:53:06 +0800 Subject: [PATCH 08/15] ui: follow edit --- .../components/problemconfig/ProblemConfigEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index 27ab9d6e1..ab8b2955f 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -74,8 +74,8 @@ export function configYamlFormat(config: ProblemConfigFile) { } }); if (formatConfig.type === 'objective') { - Object.keys(formatConfig).filter((i) => !['type', 'outputs'].includes(i)).forEach((i) => delete formatConfig[i]); - formatConfig.outputs = formatConfig.outputs || []; + Object.keys(formatConfig).filter((i) => !['type', 'answers'].includes(i)).forEach((i) => delete formatConfig[i]); + formatConfig.answers = config.answers || {}; } Object.keys(formatConfig).filter((i) => i.startsWith('__')).forEach((i) => delete formatConfig[i]); return formatConfig; From 061698a548df550383cf6b933fd71dda4cb6a8be Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 16:11:48 +0800 Subject: [PATCH 09/15] ui: avoid wrong transform --- packages/ui-default/pages/problem_detail.page.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index b44829996..1c945fe9e 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -193,6 +193,10 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
`)); } else { + if ($(e).next()[0].tagName !== 'UL') { + cnt--; + return; + } $(e).html($(e).html().replace(info, '')); $(e).next('ul').children().each((j, ele) => { $(ele).after(` From d7f72617c8d3c29dbbe94c19f451f2e952ef6f0a Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 16:17:05 +0800 Subject: [PATCH 10/15] judge&ui: fix bugs --- packages/hydrojudge/src/judge/objective.ts | 4 ++-- packages/ui-default/pages/problem_detail.page.jsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/hydrojudge/src/judge/objective.ts b/packages/hydrojudge/src/judge/objective.ts index 0f8712369..0ac588490 100644 --- a/packages/hydrojudge/src/judge/objective.ts +++ b/packages/hydrojudge/src/judge/objective.ts @@ -15,7 +15,7 @@ export async function judge({ : ''; let answers: { [x: string]: string | string[] } = {}; try { - answers = yaml.load(answer) as { [x: string]: string | string[] }; + answers = yaml.load(answer) as any; assert(typeof answers === 'object'); } catch (e) { return end({ @@ -39,7 +39,7 @@ export async function judge({ memory: 0, }; if (typeof ansInfo[0] === 'string') { - if (ansInfo[0].trim() === (answers[key] as string).trim()) { + if (ansInfo[0].trim() === (answers[key] as any).trim()) { totalScore += score; totalStatus = Math.max(totalStatus, STATUS.STATUS_ACCEPTED); next({ diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 1c945fe9e..afb555b62 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -8,6 +8,7 @@ import loadReactRedux from 'vj/utils/loadReactRedux'; import delay from 'vj/utils/delay'; import pjax from 'vj/utils/pjax'; import request from 'vj/utils/request'; +import tpl from 'vj/utils/tpl'; class ProblemPageExtender { constructor() { @@ -187,7 +188,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor cnt++; const id = info.replace(/{{ (input|select|multiselect)\((\d+(-\d+)?)\) }}/, '$2'); if (type === 'input') { - $(e).html($(e).html().replace(info, ` + $(e).html($(e).html().replace(info, tpl`
@@ -199,7 +200,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } $(e).html($(e).html().replace(info, '')); $(e).next('ul').children().each((j, ele) => { - $(ele).after(` + $(ele).after(tpl`
${String.fromCharCode(65 + j)}. ${ele.innerHTML} From 4529a501dab1f86a30bca363895cdc8423b87f33 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 16:54:28 +0800 Subject: [PATCH 11/15] core: fix bugs --- packages/hydrooj/src/upgrade.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 403ee470d..2bbe6ff62 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -727,7 +727,7 @@ const scripts: UpgradeScript[] = [ for (const pdoc of pdocs) { try { const config = yaml.load(pdoc.config as string) as any; - if (config.type !== 'objective') continue; + if (config.type !== 'objective' || !config.outputs) continue; config.answers = {}; let cnt = 0; for (const l of config.outputs) { @@ -756,7 +756,7 @@ const scripts: UpgradeScript[] = [ } else text += `{{ input(${scnt}) }}\n`; text += '\n'; } - } catch (e) { console.error(e); } + } catch (e) { console.error(e); return content; } return text; } @@ -772,6 +772,7 @@ const scripts: UpgradeScript[] = [ } } catch (e) { const content = processSingleLanguage(pdoc.content); + delete config.outputs; await problem.edit(ddoc._id, pdoc.docId, { content }); } uids.add(pdoc.owner); From 8d30a642e3c6fedca445ecd3e649d6bfa4f93a21 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 17:00:12 +0800 Subject: [PATCH 12/15] version: bump version --- packages/hydrojudge/package.json | 2 +- packages/hydrooj/package.json | 2 +- packages/ui-default/package.json | 2 +- packages/utils/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 6825f8540..d9bf6181b 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "2.20.7", + "version": "2.21.0", "main": "package.json", "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 816df4d5d..cc6e93ec5 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.13.19", + "version": "3.14.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index f5a98ccf9..544a06755 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.37.24", + "version": "4.38.0", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/utils/package.json b/packages/utils/package.json index 5515d9607..315530fd8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/utils", - "version": "1.2.7", + "version": "1.3.0", "description": "hydrooj utils", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", From 05ec5e4737b23d4edc364c700f48fdda94c4d5e8 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 28 Jun 2022 18:19:22 +0800 Subject: [PATCH 13/15] ui: remove unused file --- packages/hydrooj/src/upgrade.ts | 3 +- .../components/objective-question/index.jsx | 83 ------------------- 2 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 packages/ui-default/components/objective-question/index.jsx diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 2bbe6ff62..81614060b 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -767,15 +767,14 @@ const scripts: UpgradeScript[] = [ if (typeof langs[lang] !== 'string') continue; langs[lang] = processSingleLanguage(langs[lang]); } - delete config.outputs; await problem.edit(ddoc._id, pdoc.docId, { content: JSON.stringify(langs) }); } } catch (e) { const content = processSingleLanguage(pdoc.content); - delete config.outputs; await problem.edit(ddoc._id, pdoc.docId, { content }); } uids.add(pdoc.owner); + delete config.outputs; await problem.addTestdata(ddoc._id, pdoc.docId, 'config.yaml', Buffer.from(yaml.dump(config))); } catch (e) { console.error(e); } } diff --git a/packages/ui-default/components/objective-question/index.jsx b/packages/ui-default/components/objective-question/index.jsx deleted file mode 100644 index c810c25a3..000000000 --- a/packages/ui-default/components/objective-question/index.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { sortBy } from 'lodash'; -import request from 'vj/utils/request'; -import Notification from 'vj/components/notification'; - -export default class ObjectiveContainer extends React.PureComponent { - constructor(args) { - super(args); - this.state = {}; - this.onChange = this.onChange.bind(this); - this.onSubmit = this.onSubmit.bind(this); - } - - onChange(ev) { - this.setState({ [ev.target.name]: ev.target.value }); - } - - onSubmit(ev) { - ev.preventDefault(); - const entries = sortBy(Object.entries(this.state).map((i) => [+i[0], i[1]])); - const total = entries[entries.length - 1][0]; - const ans = new Array(total).fill(''); - entries.forEach((i) => ans[i[0]] = i[1]); // eslint-disable-line - request - .post(this.props.target, { - lang: '_', - code: ans.join('\n'), - }) - .then((res) => { - window.location.href = res.url; - }) - .catch((e) => { - Notification.error(e.message); - }); - } - - Textbox(args, name) { - return ( - - ); - } - - Radio(args, name) { - return ( - <> - {name + 1}. {args.desc.split('\n').map((i, index) => { - if (index) return <>
{i}; - return i; - })} - {args.choices.map((i) => ( - - ))} - - ); - } - - render() { - return ( -
- {this.props.panel.map((i, name) => ( -
-
- {i.choices ? this.Radio(i, name) : this.Textbox(i, name)} -
-
- ))} - {document.getElementsByClassName('nav__item--round').length - ? - : } -
- ); - } -} From 6ec215df358d4df550e9e2cd456dddcf00d5d21a Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 28 Jun 2022 18:29:17 +0800 Subject: [PATCH 14/15] ui: i18n for submission --- packages/ui-default/pages/problem_detail.page.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index afb555b62..7ab730b51 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -9,6 +9,7 @@ import delay from 'vj/utils/delay'; import pjax from 'vj/utils/pjax'; import request from 'vj/utils/request'; import tpl from 'vj/utils/tpl'; +import i18n from 'vj/utils/i18n'; class ProblemPageExtender { constructor() { @@ -213,8 +214,8 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor }); if (cnt) { $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length - ? '' - : ''); + ? `` + : ``); $('input.objective-input[type!=checkbox]').on('input', (e) => { ans[e.target.name] = e.target.value; }); @@ -225,12 +226,11 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } else { ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value); } - console.log(ans); }); $('input[type="submit"]').on('click', (e) => { e.preventDefault(); request - .post(`${window.location.href}/submit`, { + .post(UiContext.postSubmitUrl, { lang: '_', code: yaml.dump(ans), }) From 5f9d83dce4c6e6135e80de770fd18e641da873a8 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 28 Jun 2022 19:03:02 +0800 Subject: [PATCH 15/15] ui: hide normal submit when objective --- packages/ui-default/pages/problem_detail.page.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 7ab730b51..e81f1a37f 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -242,6 +242,8 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor }); }); } + $('.scratchpad--hide').hide(); + $('.non-scratchpad--hide').hide(); $('.outer-loader-container').hide(); } @@ -317,8 +319,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor $('span.tags').css('display', 'inline-block'); }); $('[name="problem-sidebar__download"]').on('click', handleClickDownloadProblem); - const { type } = UiContext.pdoc.config || {}; - if (type === 'objective') loadObjective(); + if (UiContext.pdoc.config?.type === 'objective') loadObjective(); if (pagename !== 'contest_detail_problem') initChart(); });