mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
utils&ui: move public utils & add upload
This commit is contained in:
1 parent
1f498484d2
commit
4ccb838a06
13 files changed
+703
-397
No files matched your search
@@ -2,7 +2,8 @@ import path from 'path';
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { max, sum } from 'lodash';
|
import { max, sum } from 'lodash';
|
||||||
import readYamlCases, { convertIniConfig, readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/cases';
|
import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases';
|
||||||
|
import { readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||||
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
import { changeErrorType } from '@hydrooj/utils/lib/utils';
|
||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { FormatError, SystemError } from './error';
|
import { FormatError, SystemError } from './error';
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { isBinaryFile } from 'isbinaryfile';
|
|||||||
import { intersection, isSafeInteger } from 'lodash';
|
import { intersection, isSafeInteger } from 'lodash';
|
||||||
import { FilterQuery, ObjectID } from 'mongodb';
|
import { FilterQuery, ObjectID } from 'mongodb';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/cases';
|
|
||||||
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
|
||||||
import {
|
import {
|
||||||
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
|
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
|
||||||
@@ -39,9 +38,6 @@ import { registerResolver, registerValue } from './api';
|
|||||||
|
|
||||||
export const parseCategory = (value: string) => value.replace(/,/g, ',').split(',').map((e) => e.trim());
|
export const parseCategory = (value: string) => value.replace(/,/g, ',').split(',').map((e) => e.trim());
|
||||||
export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value);
|
export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value);
|
||||||
function ensureFile(testdata) {
|
|
||||||
return (file: string) => testdata.filter((i) => i === file)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
registerValue('FileInfo', [
|
registerValue('FileInfo', [
|
||||||
['_id', 'String!'],
|
['_id', 'String!'],
|
||||||
@@ -568,13 +564,6 @@ export class ProblemConfigHandler extends ProblemManageHandler {
|
|||||||
)).toString();
|
)).toString();
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
const testdata = (this.pdoc.data || []).map((i) => i.name);
|
|
||||||
const checkFile = ensureFile(testdata);
|
|
||||||
let autocases = await readCasesFromFiles(testdata, checkFile, {});
|
|
||||||
if (!autocases.count) {
|
|
||||||
autocases = await readSubtasksFromFiles(testdata, checkFile, {}, { subtasks: [] });
|
|
||||||
}
|
|
||||||
this.response.body.autocases = autocases;
|
|
||||||
this.response.template = 'problem_config.html';
|
this.response.template = 'problem_config.html';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import i18n from 'vj/utils/i18n';
|
import i18n from 'vj/utils/i18n';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { Switch, Tab, Tabs } from '@blueprintjs/core';
|
import { Switch, Tab, Tabs } from '@blueprintjs/core';
|
||||||
import type { TestCaseConfig } from 'hydrooj/src/interface';
|
import type { SubtaskConfig, TestCaseConfig } from 'hydrooj/src/interface';
|
||||||
import type { RootState } from './reducer/index';
|
import type { RootState } from './reducer/index';
|
||||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ const SelectValue = {
|
|||||||
task_type: ['min', 'max', 'sum'],
|
task_type: ['min', 'max', 'sum'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const eq = (a: TestCaseConfig[], b: TestCaseConfig[]) => isEqual(a, b);
|
const eq = (a: SubtaskConfig[], b: SubtaskConfig[]) => isEqual(a, b);
|
||||||
|
const eq1 = (a: TestCaseConfig, b: TestCaseConfig) => isEqual(a, b);
|
||||||
|
|
||||||
function FormItem({
|
function FormItem({
|
||||||
columns, label, children, helpText = '', disableLabel = false, ...props
|
columns, label, children, helpText = '', disableLabel = false, ...props
|
||||||
@@ -185,14 +186,90 @@ function ExtraFilesConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CasesSubCasesTable({ index, subindex }) {
|
||||||
|
const subcases = useSelector((state: RootState) => (index === -1
|
||||||
|
? state.config.cases[subindex] : state.config.subtasks[index].cases[subindex]), eq1);
|
||||||
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const dispatcher = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (ev) => {
|
||||||
|
dispatch({
|
||||||
|
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||||
|
id: index,
|
||||||
|
key: 'cases-edit',
|
||||||
|
casesId: subindex,
|
||||||
|
casesKey,
|
||||||
|
value: ev.currentTarget.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const dispatcher1 = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (val) => {
|
||||||
|
dispatch({
|
||||||
|
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||||
|
id: index,
|
||||||
|
key: 'cases-edit',
|
||||||
|
casesId: subindex,
|
||||||
|
casesKey,
|
||||||
|
value: val,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
{
|
||||||
|
index === -1 && (
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
value={subcases.time || ''}
|
||||||
|
onChange={dispatcher('time')}
|
||||||
|
className="textbox"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
value={subcases.memory || ''}
|
||||||
|
onChange={dispatcher('memory')}
|
||||||
|
className="textbox"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<td>
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[subcases.input]}
|
||||||
|
onChange={dispatcher1('input')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<CustomSelectAutoComplete
|
||||||
|
width="100%"
|
||||||
|
data={Files}
|
||||||
|
selectedKeys={[subcases.output]}
|
||||||
|
onChange={dispatcher1('output')}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
onClick={() => dispatch({
|
||||||
|
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
|
||||||
|
})}
|
||||||
|
><span className="icon icon-close"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CasesTable({ index }) {
|
function CasesTable({ index }) {
|
||||||
const [time, setTime] = useState('');
|
const [time, setTime] = useState('');
|
||||||
const [memory, setMemory] = useState('');
|
const [memory, setMemory] = useState('');
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [output, setOutput] = useState('');
|
const [output, setOutput] = useState('');
|
||||||
const cases = useSelector((state: RootState) => (index === -1 ? state.config.cases : state.config.subtasks[index].cases), eq);
|
const cases = useSelector((state: RootState) => (index === -1 ? state.config.cases : state.config.subtasks[index].cases), eq);
|
||||||
console.log(cases);
|
|
||||||
const Files = useSelector((state: RootState) => state.testdata);
|
const Files = useSelector((state: RootState) => state.testdata);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const outputRef = useRef(null);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
@@ -223,29 +300,29 @@ function CasesTable({ index }) {
|
|||||||
<th>
|
<th>
|
||||||
{i18n('Input')}<br />
|
{i18n('Input')}<br />
|
||||||
<CustomSelectAutoComplete
|
<CustomSelectAutoComplete
|
||||||
|
ref={inputRef}
|
||||||
width="100%"
|
width="100%"
|
||||||
data={Files}
|
data={Files}
|
||||||
selectedKeys={[input]}
|
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{i18n('Output')}<br />
|
{i18n('Output')}<br />
|
||||||
<CustomSelectAutoComplete
|
<CustomSelectAutoComplete
|
||||||
|
ref={outputRef}
|
||||||
width="100%"
|
width="100%"
|
||||||
data={Files}
|
data={Files}
|
||||||
selectedKeys={[output]}
|
|
||||||
onChange={setOutput}
|
onChange={setOutput}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th className="col--operation">
|
||||||
<span className="icon icon-wrench"></span><br />
|
<span className="icon icon-wrench"></span><br />
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTime('');
|
setTime('');
|
||||||
setMemory('');
|
setMemory('');
|
||||||
setInput('');
|
inputRef?.current.setQuery('');
|
||||||
setOutput('');
|
outputRef?.current.setQuery('');
|
||||||
dispatch({
|
dispatch({
|
||||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||||
id: index,
|
id: index,
|
||||||
@@ -255,34 +332,13 @@ function CasesTable({ index }) {
|
|||||||
} : { input, output },
|
} : { input, output },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
><span className="icon icon-add">{i18n('Add')}</span>
|
><span className="icon icon-add" />
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{cases && cases.map((k, v) => (
|
{cases && cases.map((k, v) => <CasesSubCasesTable index={index} subindex={v} key={JSON.stringify(k)} />)}
|
||||||
<tr key={JSON.stringify(k)}>
|
|
||||||
{
|
|
||||||
index === -1 && (
|
|
||||||
<>
|
|
||||||
<td>{k.time}</td>
|
|
||||||
<td>{k.memory}</td>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<td>{k.input}</td>
|
|
||||||
<td>{k.output}</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
onClick={() => dispatch({
|
|
||||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: v,
|
|
||||||
})}
|
|
||||||
><span className="icon icon-close"></span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
@@ -298,7 +354,7 @@ function SubtasksTable({ data, index }) {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<span>Subtasks #{index} </span>
|
<span>Subtasks #{index + 1} </span>
|
||||||
<a onClick={() => { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' }); }}><span className="icon icon-add"></span></a>
|
<a onClick={() => { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' }); }}><span className="icon icon-add"></span></a>
|
||||||
<a
|
<a
|
||||||
style={index === 0 ? { display: 'none' } : {}}
|
style={index === 0 ? { display: 'none' } : {}}
|
||||||
@@ -376,7 +432,7 @@ function SubtasksTable({ data, index }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskConfig() {
|
function TaskConfig({ onAutoLoad }) {
|
||||||
const subtasks = useSelector((state: RootState) => state.config.subtasks, eq);
|
const subtasks = useSelector((state: RootState) => state.config.subtasks, eq);
|
||||||
const cases = useSelector((state: RootState) => state.config.cases, eq);
|
const cases = useSelector((state: RootState) => state.config.cases, eq);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -399,9 +455,8 @@ function TaskConfig() {
|
|||||||
? subtasks && <ul>{subtasks.map((k, v) => <SubtasksTable data={k} index={v} key={k.id} />)}</ul>
|
? subtasks && <ul>{subtasks.map((k, v) => <SubtasksTable data={k} index={v} key={k.id} />)}</ul>
|
||||||
|| cases && (<CasesTable index={-1} />)
|
|| cases && (<CasesTable index={-1} />)
|
||||||
: (
|
: (
|
||||||
<a
|
<a onClick={() => onAutoLoad()}>
|
||||||
onClick={() => dispatch({ type: 'CONFIG_AUTOCASES_UPDATE' })}
|
<span className="icon icon-settings"> {i18n('Auto Read Tasks')}</span>
|
||||||
><span className="icon icon-settings"> {i18n('Auto Read Tasks')}</span>
|
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -443,11 +498,11 @@ function LangConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProblemConfigForm() {
|
export default function ProblemConfigForm({ onAutoLoad }) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<BasicInfo />
|
<BasicInfo />
|
||||||
<TaskConfig />
|
<TaskConfig onAutoLoad={onAutoLoad} />
|
||||||
<ExtraFilesConfig />
|
<ExtraFilesConfig />
|
||||||
<LangConfig />
|
<LangConfig />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
let autocases = { subtasks: [] };
|
|
||||||
|
|
||||||
export default function reducer(state = { type: 'default' } as ProblemConfigFile, action): ProblemConfigFile {
|
export default function reducer(state = { type: 'default' } as ProblemConfigFile, action): ProblemConfigFile {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'CONFIG_LOAD_FULFILLED': {
|
case 'CONFIG_LOAD_FULFILLED': {
|
||||||
// TODO set yaml schema
|
// TODO set yaml schema
|
||||||
autocases = action.payload.autocases;
|
|
||||||
return { ...state, ...yaml.load(action.payload.config) as object };
|
return { ...state, ...yaml.load(action.payload.config) as object };
|
||||||
}
|
}
|
||||||
case 'CONFIG_FORM_UPDATE': {
|
case 'CONFIG_FORM_UPDATE': {
|
||||||
@@ -30,7 +28,9 @@ export default function reducer(state = { type: 'default' } as ProblemConfigFile
|
|||||||
}
|
}
|
||||||
case 'CONFIG_AUTOCASES_UPDATE': {
|
case 'CONFIG_AUTOCASES_UPDATE': {
|
||||||
const next = { ...state };
|
const next = { ...state };
|
||||||
if (autocases.subtasks.length === 1) {
|
const autocases = action.value;
|
||||||
|
if (autocases.subtasks.length === 0) next.cases = [];
|
||||||
|
else if (autocases.subtasks.length === 1) {
|
||||||
next.score = autocases.subtasks[0].score;
|
next.score = autocases.subtasks[0].score;
|
||||||
next.cases = [];
|
next.cases = [];
|
||||||
autocases.subtasks[0].cases.map((i) => next.cases.push({
|
autocases.subtasks[0].cases.map((i) => next.cases.push({
|
||||||
@@ -50,21 +50,32 @@ export default function reducer(state = { type: 'default' } as ProblemConfigFile
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
case 'CONFIG_CASES_UPDATE': {
|
case 'CONFIG_CASES_UPDATE': {
|
||||||
const next = { ...state };
|
if (action.key === 'cases-add') return { ...state, cases: [...state.cases, action.value] };
|
||||||
if (action.key === 'cases-add') next.cases.push(action.value);
|
if (action.key === 'cases-edit') {
|
||||||
else if (action.key === 'cases-delete') {
|
const cases = cloneDeep(state.cases);
|
||||||
next.cases = next.cases.filter((k, v) => v !== action.value);
|
cases[action.casesId][action.casesKey] = action.value;
|
||||||
|
return { ...state, cases };
|
||||||
|
} if (action.key === 'cases-delete') {
|
||||||
|
return { ...state, cases: state.cases.filter((k, v) => v !== action.value) };
|
||||||
}
|
}
|
||||||
return next;
|
return state;
|
||||||
}
|
}
|
||||||
case 'CONFIG_SUBTASK_UPDATE': {
|
case 'CONFIG_SUBTASK_UPDATE': {
|
||||||
|
if (action.key.split('-')[0] === 'cases') {
|
||||||
|
const subtasks = cloneDeep(state.subtasks);
|
||||||
|
const subsubtasks = cloneDeep(state.subtasks[action.id]);
|
||||||
|
if (action.key === 'cases-add') subsubtasks.cases.push(action.value);
|
||||||
|
else if (action.key === 'cases-edit') subsubtasks.cases[action.casesId][action.casesKey] = action.value;
|
||||||
|
else if (action.key === 'cases-delete') {
|
||||||
|
subsubtasks.cases = subsubtasks[action.id].cases.filter((k, v) => v !== action.value);
|
||||||
|
}
|
||||||
|
subtasks[action.id] = subsubtasks;
|
||||||
|
return { ...state, subtasks };
|
||||||
|
}
|
||||||
const next = { ...state };
|
const next = { ...state };
|
||||||
if (action.key === 'add') next.subtasks.splice(action.id, 0, { id: 0 });
|
if (action.key === 'add') next.subtasks.splice(action.id, 0, { id: 0 });
|
||||||
else if (action.key === 'delete') delete next.subtasks[action.key];
|
else if (action.key === 'delete') delete next.subtasks[action.key];
|
||||||
else if (action.key === 'cases-add') next.subtasks[action.id].cases.push(action.value);
|
else next.subtasks[action.id][action.key] = action.value;
|
||||||
else if (action.key === 'cases-delete') {
|
|
||||||
next.subtasks[action.id].cases = next.subtasks[action.id].cases.filter((k, v) => v !== action.value);
|
|
||||||
} else next.subtasks[action.id][action.key] = action.value;
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ export default function reducer(state = [], action) {
|
|||||||
return state.concat(action.payload.testdata);
|
return state.concat(action.payload.testdata);
|
||||||
}
|
}
|
||||||
case 'CONFIG_DELETE_TESTDATA': {
|
case 'CONFIG_DELETE_TESTDATA': {
|
||||||
return state.filter((i) => i.name !== action.payload.name);
|
const next = state.filter((i) => i.name !== action.value[0]);
|
||||||
|
return next;
|
||||||
}
|
}
|
||||||
case 'CONFIG_ADD_TESTDATA': {
|
case 'CONFIG_ADD_TESTDATA': {
|
||||||
state.push(action.payload);
|
return state.concat([action.value]);
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.page--problem_config
|
.page--problem_config
|
||||||
.section--problem-sidebar-testdata
|
.section--problem-sidebar-testdata
|
||||||
.problem-sidebar-testdata__detail
|
.problem-sidebar-testdata__detail
|
||||||
height: 270px
|
max-height: 270px
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
|
||||||
&.collapsed .collapsed--hidden,
|
&.collapsed .collapsed--hidden,
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import loadReactRedux from 'vj/utils/loadReactRedux';
|
|||||||
import i18n from 'vj/utils/i18n';
|
import i18n from 'vj/utils/i18n';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
|
import Dialog, { ConfirmDialog } from 'vj/components/dialog/index';
|
||||||
|
import download from 'vj/components/zipDownloader';
|
||||||
|
import { size, readCasesFromFiles, readSubtasksFromFiles } from '@hydrooj/utils/lib/common';
|
||||||
|
import tpl from 'vj/utils/tpl';
|
||||||
|
|
||||||
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
||||||
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
||||||
@@ -21,7 +25,128 @@ async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any
|
|||||||
$section.removeClass('animating');
|
$section.removeClass('animating');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onBeforeUnload(e) {
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureFile(testdata) {
|
||||||
|
return (file: string) => testdata.filter((i) => i === file)[0];
|
||||||
|
}
|
||||||
|
|
||||||
const page = new NamedPage('problem_config', () => {
|
const page = new NamedPage('problem_config', () => {
|
||||||
|
let reduxStore;
|
||||||
|
|
||||||
|
async function handleClickUpload() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.multiple = true;
|
||||||
|
input.click();
|
||||||
|
await new Promise((resolve) => { input.onchange = resolve; });
|
||||||
|
const { files } = input;
|
||||||
|
const dialog = new Dialog({
|
||||||
|
$body: `
|
||||||
|
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
|
||||||
|
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||||
|
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
|
||||||
|
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||||
|
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
Notification.info(i18n('Uploading files...'));
|
||||||
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
|
dialog.open();
|
||||||
|
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
|
||||||
|
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
|
||||||
|
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
|
||||||
|
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
|
||||||
|
for (const i in files) {
|
||||||
|
if (Number.isNaN(+i)) continue;
|
||||||
|
const file = files[i];
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('filename', file.name);
|
||||||
|
data.append('file', file);
|
||||||
|
data.append('type', 'testdata');
|
||||||
|
data.append('operation', 'upload_file');
|
||||||
|
await request.postFile('./files', data, {
|
||||||
|
xhr() {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener('loadstart', () => {
|
||||||
|
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
|
||||||
|
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
|
||||||
|
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
|
||||||
|
$uploadProgress.width(0);
|
||||||
|
});
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||||
|
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
|
||||||
|
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
|
||||||
|
$uploadProgress.width(`${percentComplete}%`);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
reduxStore.dispatch({
|
||||||
|
type: 'CONFIG_ADD_TESTDATA',
|
||||||
|
value: {
|
||||||
|
_id: file.name,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$('.testdata-table tbody').append(
|
||||||
|
$(tpl`<tr data-filename="${file.name}" data-size="${file.size.toString()}">
|
||||||
|
<td class="col--name" title="${file.name}"><a href="./file/${file.name}?type=testdata">${file.name}</a></td>
|
||||||
|
<td class="col--size">${size(file.size)}</td>
|
||||||
|
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
|
||||||
|
</tr>`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||||
|
Notification.success(i18n('File uploaded successfully.'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||||
|
} finally {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClickRemove(ev: JQuery.ClickEvent<Document, undefined, any, any>) {
|
||||||
|
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
|
||||||
|
const action = await new ConfirmDialog({
|
||||||
|
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
|
||||||
|
}).open();
|
||||||
|
if (action !== 'yes') return;
|
||||||
|
try {
|
||||||
|
await request.post('./files', {
|
||||||
|
operation: 'delete_files',
|
||||||
|
files: file,
|
||||||
|
type: 'testdata',
|
||||||
|
});
|
||||||
|
Notification.success(i18n('File have been deleted.'));
|
||||||
|
reduxStore.dispatch({
|
||||||
|
type: 'CONFIG_DELETE_TESTDATA',
|
||||||
|
value: file,
|
||||||
|
});
|
||||||
|
$(ev.currentTarget).parent().parent().remove();
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClickDownloadAll() {
|
||||||
|
const files = reduxStore.getState().testdata.map((i) => i.name);
|
||||||
|
const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'testdata' });
|
||||||
|
const targets = [];
|
||||||
|
for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
|
||||||
|
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadConfig(config:object) {
|
async function uploadConfig(config:object) {
|
||||||
const configYaml = yaml.dump(config);
|
const configYaml = yaml.dump(config);
|
||||||
Notification.info(i18n('Saving file...'));
|
Notification.info(i18n('Saving file...'));
|
||||||
@@ -44,6 +169,8 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
React, render, Provider, store,
|
React, render, Provider, store,
|
||||||
} = await loadReactRedux(ProblemConfigReducer);
|
} = await loadReactRedux(ProblemConfigReducer);
|
||||||
|
|
||||||
|
reduxStore = store;
|
||||||
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'CONFIG_LOAD',
|
type: 'CONFIG_LOAD',
|
||||||
payload: request.get(),
|
payload: request.get(),
|
||||||
@@ -55,7 +182,20 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
<ProblemConfigEditor />
|
<ProblemConfigEditor />
|
||||||
</div>
|
</div>
|
||||||
<div className="medium-7 columns">
|
<div className="medium-7 columns">
|
||||||
<ProblemConfigForm />
|
<ProblemConfigForm
|
||||||
|
onAutoLoad={async () => {
|
||||||
|
const testdata = (reduxStore.getState().testdata || []).map((i) => i.name);
|
||||||
|
const checkFile = ensureFile(testdata);
|
||||||
|
let autocases = await readCasesFromFiles(testdata, checkFile, {});
|
||||||
|
if (!autocases.count) {
|
||||||
|
autocases = await readSubtasksFromFiles(testdata, checkFile, {}, { subtasks: [] });
|
||||||
|
}
|
||||||
|
reduxStore.dispatch({
|
||||||
|
type: 'CONFIG_AUTOCASES_UPDATE',
|
||||||
|
value: autocases,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="rounded primary button" onClick={() => uploadConfig(store.getState().config)}>{i18n('Submit')}</button>
|
<button className="rounded primary button" onClick={() => uploadConfig(store.getState().config)}>{i18n('Submit')}</button>
|
||||||
@@ -66,6 +206,9 @@ const page = new NamedPage('problem_config', () => {
|
|||||||
|
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
$(document).on('click', '[name="testdata__upload"]', () => handleClickUpload());
|
||||||
|
$(document).on('click', '[name="testdata__delete"]', (ev) => handleClickRemove(ev));
|
||||||
|
$(document).on('click', '[name="testdata__download__all"]', () => handleClickDownloadAll());
|
||||||
$(document).on('click', '[name="testdata__section__expand"]', (ev) => handleSection(ev, 'expand'));
|
$(document).on('click', '[name="testdata__section__expand"]', (ev) => handleSection(ev, 'expand'));
|
||||||
$(document).on('click', '[name="testdata__section__collapse"]', (ev) => handleSection(ev, 'collapse'));
|
$(document).on('click', '[name="testdata__section__collapse"]', (ev) => handleSection(ev, 'collapse'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { NamedPage } from 'vj/misc/Page';
|
|||||||
import request from 'vj/utils/request';
|
import request from 'vj/utils/request';
|
||||||
import tpl from 'vj/utils/tpl';
|
import tpl from 'vj/utils/tpl';
|
||||||
import i18n from 'vj/utils/i18n';
|
import i18n from 'vj/utils/i18n';
|
||||||
import { ConfirmDialog } from 'vj/components/dialog';
|
import Dialog, { ConfirmDialog } from 'vj/components/dialog';
|
||||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||||
import Editor from 'vj/components/editor/index';
|
import Editor from 'vj/components/editor/index';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||||
|
import download from 'vj/components/zipDownloader';
|
||||||
|
import { size } from '@hydrooj/utils/lib/common';
|
||||||
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
const dirtyCategories = [];
|
const dirtyCategories = [];
|
||||||
@@ -19,6 +21,10 @@ function setDomSelected($dom, selected) {
|
|||||||
else $dom.removeClass('selected');
|
else $dom.removeClass('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onBeforeUnload(e) {
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
async function updateSelection() {
|
async function updateSelection() {
|
||||||
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
||||||
let item = categories[category];
|
let item = categories[category];
|
||||||
@@ -186,6 +192,105 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
|||||||
buildCategoryFilter();
|
buildCategoryFilter();
|
||||||
parseCategorySelection();
|
parseCategorySelection();
|
||||||
|
|
||||||
|
async function handleClickUpload() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.multiple = true;
|
||||||
|
input.click();
|
||||||
|
await new Promise((resolve) => { input.onchange = resolve; });
|
||||||
|
const { files } = input;
|
||||||
|
const dialog = new Dialog({
|
||||||
|
$body: `
|
||||||
|
<div class="file-label" style="text-align: center; margin-bottom: 5px; color: gray; font-size: small;"></div>
|
||||||
|
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||||
|
<div class="file-progress bp3-progress-meter" style="width: 0"></div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-label" style="text-align: center; margin: 5px 0; color: gray; font-size: small;"></div>
|
||||||
|
<div class="bp3-progress-bar bp3-intent-primary bp3-no-stripes">
|
||||||
|
<div class="upload-progress bp3-progress-meter" style="width: 0"></div>
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
Notification.info(i18n('Uploading files...'));
|
||||||
|
window.addEventListener('beforeunload', onBeforeUnload);
|
||||||
|
dialog.open();
|
||||||
|
const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label');
|
||||||
|
const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress');
|
||||||
|
const $fileLabel = dialog.$dom.find('.dialog__body .file-label');
|
||||||
|
const $fileProgress = dialog.$dom.find('.dialog__body .file-progress');
|
||||||
|
for (const i in files) {
|
||||||
|
if (Number.isNaN(+i)) continue;
|
||||||
|
const file = files[i];
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('filename', file.name);
|
||||||
|
data.append('file', file);
|
||||||
|
data.append('type', 'additional_file');
|
||||||
|
data.append('operation', 'upload_file');
|
||||||
|
await request.postFile('./files', data, {
|
||||||
|
xhr() {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener('loadstart', () => {
|
||||||
|
$fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`);
|
||||||
|
$fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`);
|
||||||
|
$uploadLabel.text(i18n('Uploading... ({0}%)', 0));
|
||||||
|
$uploadProgress.width(0);
|
||||||
|
});
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||||
|
if (percentComplete === 100) $uploadLabel.text(i18n('Processing...'));
|
||||||
|
else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete));
|
||||||
|
$uploadProgress.width(`${percentComplete}%`);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$('.additionalfile-table tbody').append(
|
||||||
|
$(tpl`<tr data-filename="${file.name}" data-size="${file.size.toString()}">
|
||||||
|
<td class="col--name" title="${file.name}"><a href="./file/${file.name}?type=testdata">${file.name}</a></td>
|
||||||
|
<td class="col--size">${size(file.size)}</td>
|
||||||
|
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
|
||||||
|
</tr>`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
window.removeEventListener('beforeunload', onBeforeUnload);
|
||||||
|
Notification.success(i18n('File uploaded successfully.'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
Notification.error(i18n('File upload failed: {0}', e.toString()));
|
||||||
|
} finally {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClickRemove(ev) {
|
||||||
|
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
|
||||||
|
const action = await new ConfirmDialog({
|
||||||
|
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
|
||||||
|
}).open();
|
||||||
|
if (action !== 'yes') return;
|
||||||
|
try {
|
||||||
|
await request.post('./files', {
|
||||||
|
operation: 'delete_files',
|
||||||
|
files: file,
|
||||||
|
type: 'testdata',
|
||||||
|
});
|
||||||
|
Notification.success(i18n('File have been deleted.'));
|
||||||
|
$(ev.currentTarget).parent().parent().remove();
|
||||||
|
} catch (error) {
|
||||||
|
Notification.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClickDownloadAll() {
|
||||||
|
const files = $('.additionalfile-table tr').map(function () { return $(this).attr('data-filename'); }).get();
|
||||||
|
const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'additional_file' });
|
||||||
|
const targets = [];
|
||||||
|
for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] });
|
||||||
|
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
$('img').each(function () {
|
$('img').each(function () {
|
||||||
if (this.src.startsWith('file://')) {
|
if (this.src.startsWith('file://')) {
|
||||||
@@ -248,6 +353,9 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$(document).on('click', '[name="additional_file__upload"]', () => handleClickUpload());
|
||||||
|
$(document).on('click', '[name="additional_file__delete"]', (ev) => handleClickRemove(ev));
|
||||||
|
$(document).on('click', '[name="additional_file__download"]', () => handleClickDownloadAll());
|
||||||
$(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand'));
|
$(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand'));
|
||||||
$(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse'));
|
$(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse'));
|
||||||
$(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand'));
|
$(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand'));
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
{% import "components/problem.html" as problem with context %}
|
{% import "components/problem.html" as problem with context %}
|
||||||
<div class="section side section--problem-sidebar-additional_file {{ 'expanded' if page_name == 'problem_edit' else 'collapsed'}}">
|
<div class="section side section--problem-sidebar-additional_file {{ 'expanded' if page_name == 'problem_edit' else 'collapsed'}}">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title">{{ additional_file.length if additional_file.length else 0 }} {{_('Testdata')}}</h1>
|
<h1 class="section__title">{{ additional_file.length if additional_file.length else 0 }} {{_('Additional File')}}</h1>
|
||||||
</div>
|
</div>
|
||||||
<ol class="menu">
|
<ol class="menu">
|
||||||
<li class="menu__item flex-row">
|
<li class="menu__item flex-row">
|
||||||
<a class="menu__link half" href="javascript:;" name="create_file">
|
<a class="menu__link half" href="javascript:;" name="additional_file__upload">
|
||||||
<span class="icon icon-add"></span> {{ _('Create') }}
|
|
||||||
</a>
|
|
||||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
|
||||||
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="menu__link half" href="javascript:;" name="additional_file__download">
|
||||||
|
<span class="icon icon-download"></span> {{ _('Download All') }}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu__seperator"></li>
|
<li class="menu__seperator"></li>
|
||||||
<li class="menu__item">
|
<li class="menu__item">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
<div class="section__body no-padding problem-sidebar-additional_file__detail" data-fragment-id="problem-sidebar-additional_file">
|
<div class="section__body no-padding problem-sidebar-additional_file__detail" data-fragment-id="problem-sidebar-additional_file">
|
||||||
{% if additional_file.length %}
|
{% if additional_file.length %}
|
||||||
<table class="data-table">
|
<table class="data-table additionalfile-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col--name">
|
<col class="col--name">
|
||||||
<col class="col--size">
|
<col class="col--size">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
|
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="col--size">{{ size(file.size) }}</td>
|
<td class="col--size">{{ size(file.size) }}</td>
|
||||||
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
|
<td class="col--operation"><a href="javascript:;"name="additional_file__delete"><span class="icon icon-delete"></span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,29 +6,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<ol class="menu">
|
<ol class="menu">
|
||||||
<li class="menu__item flex-row">
|
<li class="menu__item flex-row">
|
||||||
<a class="menu__link half" href="javascript:;" name="create_file">
|
<a class="menu__link half" href="javascript:;" name="testdata__upload">
|
||||||
<span class="icon icon-add"></span> {{ _('Create') }}
|
|
||||||
</a>
|
|
||||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
|
||||||
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="menu__link half" href="javascript:;" name="testdata__download">
|
||||||
|
<span class="icon icon-download"></span> {{ _('Download All') }}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu__seperator"></li>
|
<li class="menu__seperator"></li>
|
||||||
<li class="menu__item flex-row">
|
<li class="menu__item flex-row">
|
||||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
<a class="menu__link expanded--hidden" href="javascript:;" name="testdata__section__expand">
|
||||||
<span class="icon icon-download"></span> {{ _('Download All') }}
|
|
||||||
</a>
|
|
||||||
<a class="menu__link half expanded--hidden" href="javascript:;" name="testdata__section__expand">
|
|
||||||
<span class="icon icon-expand_more"></span> {{ _('expand') }}
|
<span class="icon icon-expand_more"></span> {{ _('expand') }}
|
||||||
</a>
|
</a>
|
||||||
<a class="menu__link half collapsed--hidden" href="javascript:;" name="testdata__section__collapse">
|
<a class="menu__link collapsed--hidden" href="javascript:;" name="testdata__section__collapse">
|
||||||
<span class="icon icon-expand_less"></span> {{ _('collapse') }}
|
<span class="icon icon-expand_less"></span> {{ _('collapse') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="section__body no-padding problem-sidebar-testdata__detail" data-fragment-id="problem-sidebar-testdata">
|
<div class="section__body no-padding problem-sidebar-testdata__detail" data-fragment-id="problem-sidebar-testdata">
|
||||||
{% if testdata.length %}
|
{% if testdata.length %}
|
||||||
<table class="data-table">
|
<table class="data-table testdata-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col--name">
|
<col class="col--name">
|
||||||
<col class="col--size">
|
<col class="col--size">
|
||||||
@@ -46,7 +43,7 @@
|
|||||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||||
<td class="col--name" title="{{ file.name }}"><a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a></td>
|
<td class="col--name" title="{{ file.name }}"><a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a></td>
|
||||||
<td class="col--size">{{ size(file.size) }}</td>
|
<td class="col--size">{{ size(file.size) }}</td>
|
||||||
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
|
<td class="col--operation"><a href="javascript:;" name="testdata__delete"><span class="icon icon-delete"></span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,44 +1,5 @@
|
|||||||
import { findFileSync, parseMemoryMB, parseTimeMS } from './utils';
|
import { findFileSync, parseMemoryMB, parseTimeMS } from './utils';
|
||||||
|
|
||||||
interface Re0 {
|
|
||||||
reg: RegExp,
|
|
||||||
output: ((a: RegExpExecArray) => string)[],
|
|
||||||
id: (a: RegExpExecArray) => number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Re1 extends Re0 {
|
|
||||||
subtask: (a: RegExpExecArray) => number,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RE0: Re0[] = [
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)(\d+).(in|txt)$/,
|
|
||||||
output: [
|
|
||||||
(a) => `${a[1] + a[2]}.out`,
|
|
||||||
(a) => `${a[1] + a[2]}.ans`,
|
|
||||||
(a) => `${a[1] + a[2]}.out`.replace(/input/g, 'output'),
|
|
||||||
(a) => (a[1].includes('input') ? `${a[1] + a[2]}.txt`.replace(/input/g, 'output') : null),
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)\.in(\d+)$/,
|
|
||||||
output: [
|
|
||||||
(a) => `${a[1]}.ou${a[2]}`,
|
|
||||||
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
|
||||||
],
|
|
||||||
id: (a) => +a[2],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const RE1: Re1[] = [
|
|
||||||
{
|
|
||||||
reg: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
|
|
||||||
output: [(a) => `${a[1] + a[2]}${a[3]}${a[4]}.out`],
|
|
||||||
subtask: (a) => +a[2],
|
|
||||||
id: (a) => +a[4],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
export default async function readYamlCases(cfg: Record<string, any> = {}, checkFile = (s: string, errMsg: string) => s) {
|
||||||
const config: any = {
|
const config: any = {
|
||||||
@@ -137,111 +98,3 @@ export function convertIniConfig(ini: string) {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readCasesFromFiles(files: string[], checkFile, cfg) {
|
|
||||||
const cases = [];
|
|
||||||
for (const file of files) {
|
|
||||||
for (const REG of RE0) {
|
|
||||||
if (REG.reg.test(file)) {
|
|
||||||
const data = REG.reg.exec(file);
|
|
||||||
const c = { input: file, output: '', id: REG.id(data) };
|
|
||||||
for (const func of REG.output) {
|
|
||||||
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
||||||
else c.output = func(data);
|
|
||||||
if (c.output && (c.output === '/dev/null' || checkFile(c.output))) {
|
|
||||||
cases.push(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cases.sort((a, b) => (a.id - b.id));
|
|
||||||
const extra = cases.length - (100 % cases.length);
|
|
||||||
const config = {
|
|
||||||
count: 0,
|
|
||||||
subtasks: [{
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'sum',
|
|
||||||
cases: [],
|
|
||||||
score: Math.floor(100 / cases.length),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
for (let i = 0; i < extra; i++) {
|
|
||||||
config.count++;
|
|
||||||
config.subtasks[0].cases.push({
|
|
||||||
id: config.count,
|
|
||||||
input: checkFile(cases[i].input),
|
|
||||||
output: checkFile(cases[i].output),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (extra < cases.length) {
|
|
||||||
config.subtasks.push({
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'sum',
|
|
||||||
cases: [],
|
|
||||||
score: Math.floor(100 / cases.length) + 1,
|
|
||||||
});
|
|
||||||
for (let i = extra; i < cases.length; i++) {
|
|
||||||
config.count++;
|
|
||||||
config.subtasks[1].cases.push({
|
|
||||||
id: config.count,
|
|
||||||
input: checkFile(cases[i].input),
|
|
||||||
output: checkFile(cases[i].output),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
|
|
||||||
const subtask = {};
|
|
||||||
for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
|
|
||||||
const subtasks = [];
|
|
||||||
for (const file of files) {
|
|
||||||
for (const REG of RE1) {
|
|
||||||
if (REG.reg.test(file)) {
|
|
||||||
const data = REG.reg.exec(file);
|
|
||||||
const sid = REG.subtask(data);
|
|
||||||
const c = { input: file, output: '', id: REG.id(data) };
|
|
||||||
for (const func of REG.output) {
|
|
||||||
if (cfg.noOutputFile) c.output = '/dev/null';
|
|
||||||
else c.output = func(data);
|
|
||||||
if (c.output === '/dev/null' || checkFile(c.output)) {
|
|
||||||
if (!subtask[sid]) {
|
|
||||||
subtask[sid] = {
|
|
||||||
time: parseTimeMS(cfg.time || '1s'),
|
|
||||||
memory: parseMemoryMB(cfg.memory || '256m'),
|
|
||||||
type: 'min',
|
|
||||||
cases: [c],
|
|
||||||
};
|
|
||||||
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
|
|
||||||
else subtask[sid].cases.push(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const i in subtask) {
|
|
||||||
subtask[i].cases.sort((a, b) => (a.id - b.id));
|
|
||||||
subtasks.push(subtask[i]);
|
|
||||||
}
|
|
||||||
const base = Math.floor(100 / subtasks.length);
|
|
||||||
const extra = subtasks.length - (100 % subtasks.length);
|
|
||||||
const config = { count: 0, subtasks };
|
|
||||||
const keys = Object.keys(subtask);
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
|
||||||
if (i >= extra) subtask[keys[i]].score = base + 1;
|
|
||||||
else subtask[keys[i]].score = base;
|
|
||||||
for (const j of subtask[keys[i]].cases) {
|
|
||||||
config.count++;
|
|
||||||
j.input = checkFile(j.input);
|
|
||||||
j.output = checkFile(j.output);
|
|
||||||
j.id = config.count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import { inspect } from 'util';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface StringConstructor {
|
||||||
|
random: (digit?: number) => string;
|
||||||
|
}
|
||||||
|
interface String {
|
||||||
|
format: (...args: Array<any>) => string;
|
||||||
|
formatFromArray: (args: any[]) => string;
|
||||||
|
rawformat: (object: any) => string;
|
||||||
|
}
|
||||||
|
interface ArrayConstructor {
|
||||||
|
isDiff: (a: any[], b: any[]) => boolean;
|
||||||
|
}
|
||||||
|
interface Date {
|
||||||
|
format: (fmt?: string) => string;
|
||||||
|
}
|
||||||
|
interface Math {
|
||||||
|
sum: (...args: Array<number[] | number>) => number;
|
||||||
|
}
|
||||||
|
interface SetConstructor {
|
||||||
|
isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
|
||||||
|
intersection: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
|
||||||
|
union: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
||||||
|
|
||||||
|
String.random = function random(digit = 32, dict = defaultDict) {
|
||||||
|
let str = '';
|
||||||
|
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
String.prototype.format = function formatStr(...args) {
|
||||||
|
let result = this;
|
||||||
|
if (args.length) {
|
||||||
|
if (args.length === 1 && typeof args[0] === 'object') {
|
||||||
|
const t = args[0];
|
||||||
|
for (const key in t) {
|
||||||
|
if (!key.startsWith('_') && t[key] !== undefined) {
|
||||||
|
if (t._inspect && typeof t[key] === 'object') {
|
||||||
|
t[key] = inspect(t[key], { colors: process?.stderr?.isTTY });
|
||||||
|
}
|
||||||
|
const reg = new RegExp(`(\\{${key}\\})`, 'g');
|
||||||
|
result = result.replace(reg, t[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else return this.formatFromArray(args);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
String.prototype.formatFromArray = function formatStr(args) {
|
||||||
|
let result = this;
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] !== undefined) {
|
||||||
|
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
|
||||||
|
result = result.replace(reg, args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
String.prototype.rawformat = function rawFormat(object) {
|
||||||
|
const res = this.split('{@}');
|
||||||
|
return [res[0], object, res[1]].join();
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.isDiff = function isDiff(a, b) {
|
||||||
|
if (a.length !== b.length) return true;
|
||||||
|
a.sort();
|
||||||
|
b.sort();
|
||||||
|
for (const i in a) {
|
||||||
|
if (a[i] !== b[i]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
|
||||||
|
let m = this.getMonth() + 1;
|
||||||
|
if (m < 10) m = `0${m}`;
|
||||||
|
let d = this.getDate();
|
||||||
|
if (d < 10) d = `0${d}`;
|
||||||
|
let H = this.getHours();
|
||||||
|
if (H < 10) H = `0${H}`;
|
||||||
|
let M = this.getMinutes();
|
||||||
|
if (M < 10) M = `0${M}`;
|
||||||
|
let S = this.getSeconds();
|
||||||
|
if (S < 10) S = `0${S}`;
|
||||||
|
return fmt
|
||||||
|
.replace('%Y', this.getFullYear())
|
||||||
|
.replace('%m', m)
|
||||||
|
.replace('%d', d)
|
||||||
|
.replace('%H', H)
|
||||||
|
.replace('%M', M)
|
||||||
|
.replace('%S', S);
|
||||||
|
};
|
||||||
|
|
||||||
|
Math.sum = function sum(...args) {
|
||||||
|
let s = 0;
|
||||||
|
for (const i of args) {
|
||||||
|
if (i instanceof Array) {
|
||||||
|
for (const j of i) {
|
||||||
|
s += j;
|
||||||
|
}
|
||||||
|
} else s += i;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
Set.isSuperset = function isSuperset(set, subset) {
|
||||||
|
for (const elem of subset) {
|
||||||
|
if (!set.has(elem)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Set.union = function Union<T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) {
|
||||||
|
const union = new Set(setA);
|
||||||
|
for (const elem of setB) union.add(elem);
|
||||||
|
return union;
|
||||||
|
};
|
||||||
|
|
||||||
|
Set.intersection = function Intersection<T>(A: Set<T> | Array<T> = [], B: Set<T> | Array<T> = []) {
|
||||||
|
const intersection = new Set<T>();
|
||||||
|
if (A instanceof Array) A = new Set(A);
|
||||||
|
if (B instanceof Array) B = new Set(B);
|
||||||
|
for (const elem of B) if (A.has(elem)) intersection.add(elem);
|
||||||
|
return intersection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i;
|
||||||
|
const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
|
||||||
|
const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
|
||||||
|
const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
|
||||||
|
|
||||||
|
export function parseTimeMS(str: string | number) {
|
||||||
|
if (typeof str === 'number') return str;
|
||||||
|
const match = TIME_RE.exec(str);
|
||||||
|
if (!match) throw new Error(`${str} error parsing time`);
|
||||||
|
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMemoryMB(str: string | number) {
|
||||||
|
if (typeof str === 'number') return str;
|
||||||
|
const match = MEMORY_RE.exec(str);
|
||||||
|
if (!match) throw new Error(`${str} error parsing memory`);
|
||||||
|
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function size(s: number, base = 1) {
|
||||||
|
s *= base;
|
||||||
|
const unit = 1024;
|
||||||
|
const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
for (const unitName of unitNames) {
|
||||||
|
if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName);
|
||||||
|
s /= unit;
|
||||||
|
}
|
||||||
|
return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Re0 {
|
||||||
|
reg: RegExp,
|
||||||
|
output: ((a: RegExpExecArray) => string)[],
|
||||||
|
id: (a: RegExpExecArray) => number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Re1 extends Re0 {
|
||||||
|
subtask: (a: RegExpExecArray) => number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const RE0: Re0[] = [
|
||||||
|
{
|
||||||
|
reg: /^([^\d]*)(\d+).(in|txt)$/,
|
||||||
|
output: [
|
||||||
|
(a) => `${a[1] + a[2]}.out`,
|
||||||
|
(a) => `${a[1] + a[2]}.ans`,
|
||||||
|
(a) => `${a[1] + a[2]}.out`.replace(/input/g, 'output'),
|
||||||
|
(a) => (a[1].includes('input') ? `${a[1] + a[2]}.txt`.replace(/input/g, 'output') : null),
|
||||||
|
],
|
||||||
|
id: (a) => +a[2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reg: /^([^\d]*)\.in(\d+)$/,
|
||||||
|
output: [
|
||||||
|
(a) => `${a[1]}.ou${a[2]}`,
|
||||||
|
(a) => `${a[1]}.ou${a[2]}`.replace(/input/g, 'output'),
|
||||||
|
],
|
||||||
|
id: (a) => +a[2],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const RE1: Re1[] = [
|
||||||
|
{
|
||||||
|
reg: /^([^\d]*)([0-9]+)([-_])([0-9]+).in$/,
|
||||||
|
output: [(a) => `${a[1] + a[2]}${a[3]}${a[4]}.out`],
|
||||||
|
subtask: (a) => +a[2],
|
||||||
|
id: (a) => +a[4],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function readCasesFromFiles(files: string[], checkFile, cfg) {
|
||||||
|
const cases = [];
|
||||||
|
for (const file of files) {
|
||||||
|
for (const REG of RE0) {
|
||||||
|
if (REG.reg.test(file)) {
|
||||||
|
const data = REG.reg.exec(file);
|
||||||
|
const c = { input: file, output: '', id: REG.id(data) };
|
||||||
|
for (const func of REG.output) {
|
||||||
|
if (cfg.noOutputFile) c.output = '/dev/null';
|
||||||
|
else c.output = func(data);
|
||||||
|
if (c.output && (c.output === '/dev/null' || checkFile(c.output))) {
|
||||||
|
cases.push(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cases.sort((a, b) => (a.id - b.id));
|
||||||
|
const extra = cases.length - (100 % cases.length);
|
||||||
|
const config = {
|
||||||
|
count: 0,
|
||||||
|
subtasks: [{
|
||||||
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
|
type: 'sum',
|
||||||
|
cases: [],
|
||||||
|
score: Math.floor(100 / cases.length),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
for (let i = 0; i < extra; i++) {
|
||||||
|
config.count++;
|
||||||
|
config.subtasks[0].cases.push({
|
||||||
|
id: config.count,
|
||||||
|
input: checkFile(cases[i].input),
|
||||||
|
output: checkFile(cases[i].output),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (extra < cases.length) {
|
||||||
|
config.subtasks.push({
|
||||||
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
|
type: 'sum',
|
||||||
|
cases: [],
|
||||||
|
score: Math.floor(100 / cases.length) + 1,
|
||||||
|
});
|
||||||
|
for (let i = extra; i < cases.length; i++) {
|
||||||
|
config.count++;
|
||||||
|
config.subtasks[1].cases.push({
|
||||||
|
id: config.count,
|
||||||
|
input: checkFile(cases[i].input),
|
||||||
|
output: checkFile(cases[i].output),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readSubtasksFromFiles(files: string[], checkFile, cfg, rst) {
|
||||||
|
const subtask = {};
|
||||||
|
for (const s of rst.subtasks) if (s.id) subtask[s.id] = s;
|
||||||
|
const subtasks = [];
|
||||||
|
for (const file of files) {
|
||||||
|
for (const REG of RE1) {
|
||||||
|
if (REG.reg.test(file)) {
|
||||||
|
const data = REG.reg.exec(file);
|
||||||
|
const sid = REG.subtask(data);
|
||||||
|
const c = { input: file, output: '', id: REG.id(data) };
|
||||||
|
for (const func of REG.output) {
|
||||||
|
if (cfg.noOutputFile) c.output = '/dev/null';
|
||||||
|
else c.output = func(data);
|
||||||
|
if (c.output === '/dev/null' || checkFile(c.output)) {
|
||||||
|
if (!subtask[sid]) {
|
||||||
|
subtask[sid] = {
|
||||||
|
time: parseTimeMS(cfg.time || '1s'),
|
||||||
|
memory: parseMemoryMB(cfg.memory || '256m'),
|
||||||
|
type: 'min',
|
||||||
|
cases: [c],
|
||||||
|
};
|
||||||
|
} else if (!subtask[sid].cases) subtask[sid].cases = [c];
|
||||||
|
else subtask[sid].cases.push(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const i in subtask) {
|
||||||
|
subtask[i].cases.sort((a, b) => (a.id - b.id));
|
||||||
|
subtasks.push(subtask[i]);
|
||||||
|
}
|
||||||
|
const base = Math.floor(100 / subtasks.length);
|
||||||
|
const extra = subtasks.length - (100 % subtasks.length);
|
||||||
|
const config = { count: 0, subtasks };
|
||||||
|
const keys = Object.keys(subtask);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (i >= extra) subtask[keys[i]].score = base + 1;
|
||||||
|
else subtask[keys[i]].score = base;
|
||||||
|
for (const j of subtask[keys[i]].cases) {
|
||||||
|
config.count++;
|
||||||
|
j.input = checkFile(j.input);
|
||||||
|
j.output = checkFile(j.output);
|
||||||
|
j.id = config.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
+2
-162
@@ -1,143 +1,11 @@
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Duplex } from 'stream';
|
import { Duplex } from 'stream';
|
||||||
import { inspect } from 'util';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { Moment } from 'moment-timezone';
|
import type { Moment } from 'moment-timezone';
|
||||||
import { isMoment } from 'moment-timezone';
|
import { isMoment } from 'moment-timezone';
|
||||||
import { ObjectID } from 'mongodb';
|
import { ObjectID } from 'mongodb';
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface StringConstructor {
|
|
||||||
random: (digit?: number) => string;
|
|
||||||
}
|
|
||||||
interface String {
|
|
||||||
format: (...args: Array<any>) => string;
|
|
||||||
formatFromArray: (args: any[]) => string;
|
|
||||||
rawformat: (object: any) => string;
|
|
||||||
}
|
|
||||||
interface ArrayConstructor {
|
|
||||||
isDiff: (a: any[], b: any[]) => boolean;
|
|
||||||
}
|
|
||||||
interface Date {
|
|
||||||
format: (fmt?: string) => string;
|
|
||||||
}
|
|
||||||
interface Math {
|
|
||||||
sum: (...args: Array<number[] | number>) => number;
|
|
||||||
}
|
|
||||||
interface SetConstructor {
|
|
||||||
isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
|
|
||||||
intersection: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
|
|
||||||
union: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
|
||||||
|
|
||||||
String.random = function random(digit = 32, dict = defaultDict) {
|
|
||||||
let str = '';
|
|
||||||
for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
String.prototype.format = function formatStr(...args) {
|
|
||||||
let result = this;
|
|
||||||
if (args.length) {
|
|
||||||
if (args.length === 1 && typeof args[0] === 'object') {
|
|
||||||
const t = args[0];
|
|
||||||
for (const key in t) {
|
|
||||||
if (!key.startsWith('_') && t[key] !== undefined) {
|
|
||||||
if (t._inspect && typeof t[key] === 'object') {
|
|
||||||
t[key] = inspect(t[key], { colors: process?.stderr?.isTTY });
|
|
||||||
}
|
|
||||||
const reg = new RegExp(`(\\{${key}\\})`, 'g');
|
|
||||||
result = result.replace(reg, t[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else return this.formatFromArray(args);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
String.prototype.formatFromArray = function formatStr(args) {
|
|
||||||
let result = this;
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
if (args[i] !== undefined) {
|
|
||||||
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
|
|
||||||
result = result.replace(reg, args[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
String.prototype.rawformat = function rawFormat(object) {
|
|
||||||
const res = this.split('{@}');
|
|
||||||
return [res[0], object, res[1]].join();
|
|
||||||
};
|
|
||||||
|
|
||||||
Array.isDiff = function isDiff(a, b) {
|
|
||||||
if (a.length !== b.length) return true;
|
|
||||||
a.sort();
|
|
||||||
b.sort();
|
|
||||||
for (const i in a) {
|
|
||||||
if (a[i] !== b[i]) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
|
|
||||||
let m = this.getMonth() + 1;
|
|
||||||
if (m < 10) m = `0${m}`;
|
|
||||||
let d = this.getDate();
|
|
||||||
if (d < 10) d = `0${d}`;
|
|
||||||
let H = this.getHours();
|
|
||||||
if (H < 10) H = `0${H}`;
|
|
||||||
let M = this.getMinutes();
|
|
||||||
if (M < 10) M = `0${M}`;
|
|
||||||
let S = this.getSeconds();
|
|
||||||
if (S < 10) S = `0${S}`;
|
|
||||||
return fmt
|
|
||||||
.replace('%Y', this.getFullYear())
|
|
||||||
.replace('%m', m)
|
|
||||||
.replace('%d', d)
|
|
||||||
.replace('%H', H)
|
|
||||||
.replace('%M', M)
|
|
||||||
.replace('%S', S);
|
|
||||||
};
|
|
||||||
|
|
||||||
Math.sum = function sum(...args) {
|
|
||||||
let s = 0;
|
|
||||||
for (const i of args) {
|
|
||||||
if (i instanceof Array) {
|
|
||||||
for (const j of i) {
|
|
||||||
s += j;
|
|
||||||
}
|
|
||||||
} else s += i;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
Set.isSuperset = function isSuperset(set, subset) {
|
|
||||||
for (const elem of subset) {
|
|
||||||
if (!set.has(elem)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
Set.union = function Union<T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) {
|
|
||||||
const union = new Set(setA);
|
|
||||||
for (const elem of setB) union.add(elem);
|
|
||||||
return union;
|
|
||||||
};
|
|
||||||
|
|
||||||
Set.intersection = function Intersection<T>(A: Set<T> | Array<T> = [], B: Set<T> | Array<T> = []) {
|
|
||||||
const intersection = new Set<T>();
|
|
||||||
if (A instanceof Array) A = new Set(A);
|
|
||||||
if (B instanceof Array) B = new Set(B);
|
|
||||||
for (const elem of B) if (A.has(elem)) intersection.add(elem);
|
|
||||||
return intersection;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function folderSize(folderPath: string) {
|
export function folderSize(folderPath: string) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
let size = 0;
|
let size = 0;
|
||||||
@@ -161,25 +29,6 @@ export function folderSize(folderPath: string) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIME_RE = /^([0-9]+(?:\.[0-9]*)?)([mu]?)s?$/i;
|
|
||||||
const TIME_UNITS = { '': 1000, m: 1, u: 0.001 };
|
|
||||||
const MEMORY_RE = /^([0-9]+(?:\.[0-9]*)?)([kmg])b?$/i;
|
|
||||||
const MEMORY_UNITS = { k: 1 / 1024, m: 1, g: 1024 };
|
|
||||||
|
|
||||||
export function parseTimeMS(str: string | number) {
|
|
||||||
if (typeof str === 'number') return str;
|
|
||||||
const match = TIME_RE.exec(str);
|
|
||||||
if (!match) throw new Error(`${str} error parsing time`);
|
|
||||||
return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseMemoryMB(str: string | number) {
|
|
||||||
if (typeof str === 'number') return str;
|
|
||||||
const match = MEMORY_RE.exec(str);
|
|
||||||
if (!match) throw new Error(`${str} error parsing memory`);
|
|
||||||
return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isClass(obj: any, strict = false) {
|
export function isClass(obj: any, strict = false) {
|
||||||
if (typeof obj !== 'function') return false;
|
if (typeof obj !== 'function') return false;
|
||||||
const str = obj.toString();
|
const str = obj.toString();
|
||||||
@@ -290,17 +139,6 @@ export function errorMessage(err: Error | string) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function size(s: number, base = 1) {
|
|
||||||
s *= base;
|
|
||||||
const unit = 1024;
|
|
||||||
const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
||||||
for (const unitName of unitNames) {
|
|
||||||
if (s < unit) return '{0} {1}'.format(Math.round(s * 10) / 10, unitName);
|
|
||||||
s /= unit;
|
|
||||||
}
|
|
||||||
return `${Math.round(s * unit)} ${unitNames[unitNames.length - 1]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _digit2(number: number) {
|
function _digit2(number: number) {
|
||||||
return number < 10 ? `0${number}` : number.toString();
|
return number < 10 ? `0${number}` : number.toString();
|
||||||
}
|
}
|
||||||
@@ -449,3 +287,5 @@ export function Counter<T extends (string | number) = string>() {
|
|||||||
},
|
},
|
||||||
}) as Record<T, number>;
|
}) as Record<T, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from '@hydrooj/utils/lib/common';
|
||||||
Reference in new issue
Block a user