mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
onsite-toolkit: integrate with external resolver
This commit is contained in:
8 files changed
+197
-693
No files matched your search
@@ -17,12 +17,12 @@
|
|||||||
"@aws-sdk/s3-presigned-post": "3.726.1",
|
"@aws-sdk/s3-presigned-post": "3.726.1",
|
||||||
"@aws-sdk/s3-request-presigner": "3.726.1",
|
"@aws-sdk/s3-request-presigner": "3.726.1",
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@cordisjs/plugin-logger": "^1.0.1",
|
"@cordisjs/plugin-logger": "^1.0.2",
|
||||||
"@cordisjs/plugin-timer": "1.1.1",
|
"@cordisjs/plugin-timer": "1.1.1",
|
||||||
"@hydrooj/common": "workspace:^",
|
"@hydrooj/common": "workspace:^",
|
||||||
"@hydrooj/framework": "workspace:^",
|
"@hydrooj/framework": "workspace:^",
|
||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"@mongodb-js/saslprep": "^1.4.6",
|
"@mongodb-js/saslprep": "^1.4.8",
|
||||||
"@simplewebauthn/server": "13.2.2",
|
"@simplewebauthn/server": "13.2.2",
|
||||||
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
||||||
"@zip.js/zip.js": "^2.8.26",
|
"@zip.js/zip.js": "^2.8.26",
|
||||||
@@ -37,15 +37,15 @@
|
|||||||
"koa-proxies": "^0.12.4",
|
"koa-proxies": "^0.12.4",
|
||||||
"koa-static-cache": "^5.1.4",
|
"koa-static-cache": "^5.1.4",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"lru-cache": "^11.2.7",
|
"lru-cache": "^11.3.5",
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"moment-timezone": "^0.6.1",
|
"moment-timezone": "^0.6.1",
|
||||||
"mongodb": "^7.1.1",
|
"mongodb": "^7.1.1",
|
||||||
"mongodb-uri": "^0.9.7",
|
"mongodb-uri": "^0.9.7",
|
||||||
"nanoid": "^5.1.7",
|
"nanoid": "^5.1.9",
|
||||||
"nodemailer": "^8.0.4",
|
"nodemailer": "^8.0.5",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"p-queue": "^9.1.1",
|
"p-queue": "^9.1.2",
|
||||||
"sanitize-filename": "^1.6.4",
|
"sanitize-filename": "^1.6.4",
|
||||||
"schemastery": "^3.18.0",
|
"schemastery": "^3.18.0",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/mongodb-uri": "^0.9.4",
|
"@types/mongodb-uri": "^0.9.4",
|
||||||
"@types/nodemailer": "^7.0.11",
|
"@types/nodemailer": "^8.0.0",
|
||||||
"@types/notp": "^2.0.5",
|
"@types/notp": "^2.0.5",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/superagent": "^8.1.9",
|
"@types/superagent": "^8.1.9",
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ app.plugin(Loader);
|
|||||||
async function preload() {
|
async function preload() {
|
||||||
global.app = await new Promise((resolve) => {
|
global.app = await new Promise((resolve) => {
|
||||||
app.inject(['timer', 'i18n', 'logger', '$api'], (c) => {
|
app.inject(['timer', 'i18n', 'logger', '$api'], (c) => {
|
||||||
resolve(c);
|
c.inject({ domain: { required: false } }, resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
for (const a of [path.resolve(__dirname, '..'), ...getAddons()]) {
|
for (const a of [path.resolve(__dirname, '..'), ...getAddons()]) {
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
import {
|
|
||||||
addPage, NamedPage, React, ReactDOM,
|
|
||||||
} from '@hydrooj/ui-default';
|
|
||||||
import { animated, easings, useSprings } from '@react-spring/web';
|
|
||||||
import useKey from 'react-use/lib/useKey';
|
|
||||||
import { ResolverInput } from '../interface';
|
|
||||||
|
|
||||||
async function scrollTo(offset) {
|
|
||||||
const fixedOffset = offset.toFixed();
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
const onScroll = function () {
|
|
||||||
if (window.pageYOffset.toFixed() === fixedOffset) {
|
|
||||||
window.removeEventListener('scroll', onScroll);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', onScroll);
|
|
||||||
onScroll();
|
|
||||||
window.scrollTo({
|
|
||||||
top: offset,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export interface DisplaySettings {
|
|
||||||
showAvatar: boolean;
|
|
||||||
showSchool: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props extends DisplaySettings {
|
|
||||||
data: ResolverInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProblemStatus {
|
|
||||||
old: number;
|
|
||||||
frozen: number;
|
|
||||||
pass: boolean;
|
|
||||||
time: number;
|
|
||||||
index: number;
|
|
||||||
id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function status(problem: ProblemStatus) {
|
|
||||||
if (!problem) return 'untouched';
|
|
||||||
if (problem.pass) return 'ac';
|
|
||||||
if (!problem.old && !problem.frozen) return 'untouched';
|
|
||||||
if (problem.frozen) return 'frozen';
|
|
||||||
return 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
function submissions(problem: ProblemStatus) {
|
|
||||||
const st = status(problem);
|
|
||||||
if (st === 'ac') { return `${problem.old} / ${problem.time}`; }
|
|
||||||
if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; }
|
|
||||||
if (st === 'failed') { return problem.old; }
|
|
||||||
return String.fromCharCode('A'.charCodeAt(0) + problem.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function start(data: ResolverInput, options: DisplaySettings) {
|
|
||||||
$('title').text(`${data.name} - Resolver`);
|
|
||||||
$('.header .title').text(`${data.name}`);
|
|
||||||
const teams = data.teams.map((v) => ({
|
|
||||||
id: v.id,
|
|
||||||
rank: 0,
|
|
||||||
score: 0,
|
|
||||||
penalty: 0,
|
|
||||||
ranked: !v.exclude,
|
|
||||||
total: 0,
|
|
||||||
problems: data.problems.map((problem, idx) => ({
|
|
||||||
old: 0,
|
|
||||||
frozen: 0,
|
|
||||||
pass: false,
|
|
||||||
id: problem.id,
|
|
||||||
index: idx,
|
|
||||||
time: 0,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
const allSubmissions = data.submissions.filter((i) => !['CE', 'SE', 'FE', 'IGN'].includes(i.verdict)).sort((a, b) => a.time - b.time);
|
|
||||||
const allAc = allSubmissions.filter((i) => i.verdict === 'AC');
|
|
||||||
for (const submission of allSubmissions) {
|
|
||||||
const team = teams.find((v) => v.id === submission.team);
|
|
||||||
if (!team) continue;
|
|
||||||
const isFrozen = submission.time > data.frozen;
|
|
||||||
const problem = team.problems.find((i) => i.id === submission.problem);
|
|
||||||
if (!problem || problem.pass) continue;
|
|
||||||
team.total++;
|
|
||||||
if (isFrozen) problem.frozen += 1;
|
|
||||||
else {
|
|
||||||
if (submission.verdict === 'AC') {
|
|
||||||
problem.pass = true;
|
|
||||||
problem.time = Math.floor(submission.time / 60);
|
|
||||||
team.score += 1;
|
|
||||||
team.penalty += Math.floor(submission.time / 60) + problem.old * 20;
|
|
||||||
}
|
|
||||||
problem.old += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function MainList(props: Props) {
|
|
||||||
const [selectedTeam, setTeam] = React.useState('');
|
|
||||||
const [selectedProblem, setP] = React.useState<string | null>(null);
|
|
||||||
const [executeIdx, setExecuteIdx] = React.useState(0);
|
|
||||||
const [, setRenderC] = React.useState(0);
|
|
||||||
|
|
||||||
function processRank(source = teams) {
|
|
||||||
const clone = [...source];
|
|
||||||
clone.sort((a, b) => b.score - a.score || a.penalty - b.penalty || b.total - a.total);
|
|
||||||
let rank = 1;
|
|
||||||
for (const team of clone) {
|
|
||||||
if (team.ranked) {
|
|
||||||
team.rank = rank;
|
|
||||||
rank++;
|
|
||||||
} else {
|
|
||||||
team.rank = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clone.map((i) => source.indexOf(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
const order = React.useRef(processRank());
|
|
||||||
|
|
||||||
const [springs, api] = useSprings(teams.length, (index) => ({
|
|
||||||
y: order.current.indexOf(index) * 80 - index * 80,
|
|
||||||
scale: 1,
|
|
||||||
zIndex: 0,
|
|
||||||
shadow: 1,
|
|
||||||
immediate: (key: string) => key === 'y' || key === 'zIndex',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const operations = {
|
|
||||||
async highlightTeam(teamId: string, scrollIdx: number) {
|
|
||||||
setP(null);
|
|
||||||
setTeam(teamId);
|
|
||||||
await scrollTo(scrollIdx * 80 - window.innerHeight + 241 + 40);
|
|
||||||
},
|
|
||||||
async highlightProblem(problemId: string) {
|
|
||||||
setP(problemId);
|
|
||||||
},
|
|
||||||
async revealProblem(teamId: string, problemId: string) {
|
|
||||||
const team = teams.find((i) => i.id === teamId);
|
|
||||||
const problem = team?.problems.find((i) => i.id === problemId);
|
|
||||||
if (!team || !problem) return;
|
|
||||||
if (allAc.find((s) => s.team === teamId && s.problem === problemId)) {
|
|
||||||
const sub = allSubmissions.filter((s) => s.team === teamId && s.problem === problemId);
|
|
||||||
let penalty = 0;
|
|
||||||
for (const s of sub) {
|
|
||||||
problem.old++;
|
|
||||||
if (s.verdict !== 'AC') {
|
|
||||||
penalty += 20;
|
|
||||||
} else {
|
|
||||||
problem.time = Math.floor(s.time / 60);
|
|
||||||
penalty += Math.floor(s.time / 60);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
team.penalty += penalty;
|
|
||||||
team.score += 1;
|
|
||||||
problem.pass = true;
|
|
||||||
problem.frozen = 0;
|
|
||||||
} else {
|
|
||||||
problem.old += problem.frozen;
|
|
||||||
problem.frozen = 0;
|
|
||||||
}
|
|
||||||
setP(null);
|
|
||||||
},
|
|
||||||
async updateRank() {
|
|
||||||
order.current = processRank();
|
|
||||||
api.start((index) => ({
|
|
||||||
y: order.current.indexOf(index) * 80 - index * 80,
|
|
||||||
scale: 1,
|
|
||||||
zIndex: 0,
|
|
||||||
shadow: 1,
|
|
||||||
config: {
|
|
||||||
easing: easings.steps(5),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculated = React.useMemo(() => {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
const clone = JSON.parse(JSON.stringify(teams));
|
|
||||||
const ops: { name: string, args: any[] }[] = [];
|
|
||||||
function queueOperations(name: string, ...args: any[]) {
|
|
||||||
ops.push({ name, args });
|
|
||||||
}
|
|
||||||
let orders = processRank(clone);
|
|
||||||
for (let i = clone.length - 1; i >= 0; i--) {
|
|
||||||
const team = clone[orders[i]];
|
|
||||||
queueOperations('highlightTeam', team.id, i);
|
|
||||||
for (const pinfo of data.problems) {
|
|
||||||
const problem: ProblemStatus = team.problems.find((idx) => idx.id === pinfo.id);
|
|
||||||
if (!problem || !problem.frozen || problem.pass) continue;
|
|
||||||
queueOperations('highlightProblem', pinfo.id);
|
|
||||||
queueOperations('revealProblem', team.id, pinfo.id);
|
|
||||||
// scroll to selected line
|
|
||||||
if (allAc.find((s) => s.team === team.id && s.problem === problem.id)) {
|
|
||||||
const sub = allSubmissions.filter((s) => s.team === team.id && s.problem === problem.id);
|
|
||||||
let penalty = 0;
|
|
||||||
for (const s of sub) {
|
|
||||||
problem.old++;
|
|
||||||
if (s.verdict !== 'AC') {
|
|
||||||
penalty += 20;
|
|
||||||
} else {
|
|
||||||
problem.time = Math.floor(s.time / 60);
|
|
||||||
penalty += Math.floor(s.time / 60);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
team.penalty += penalty;
|
|
||||||
team.score += 1;
|
|
||||||
problem.pass = true;
|
|
||||||
problem.frozen = 0;
|
|
||||||
queueOperations('updateRank');
|
|
||||||
const oldOrder = JSON.stringify(orders);
|
|
||||||
orders = processRank(clone);
|
|
||||||
if (oldOrder !== JSON.stringify(orders)) {
|
|
||||||
i++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
problem.old += problem.frozen;
|
|
||||||
problem.frozen = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(ops.length, 'operations');
|
|
||||||
return ops;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useKey((key) => ['n', 'ArrowRight'].includes(key.key), async () => {
|
|
||||||
const op = calculated[executeIdx];
|
|
||||||
if (!op) return;
|
|
||||||
setExecuteIdx(executeIdx + 1);
|
|
||||||
await operations[op.name](...op.args);
|
|
||||||
setRenderC((i) => i + 1);
|
|
||||||
}, {}, [executeIdx, calculated]);
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
{springs.map(({
|
|
||||||
zIndex, y,
|
|
||||||
}, i) => {
|
|
||||||
const team = teams[i];
|
|
||||||
const teamInfo = data.teams.find((idx) => idx.id === team.id);
|
|
||||||
if (!teams[i]) return <animated.div key={i}>Team {i} not found</animated.div>;
|
|
||||||
if (!teamInfo) return <animated.div key={i}>Team info for id {team.id} not found</animated.div>;
|
|
||||||
return <animated.div
|
|
||||||
key={i}
|
|
||||||
className="rank-list-item clearfix"
|
|
||||||
style={{
|
|
||||||
zIndex,
|
|
||||||
// boxShadow: shadow.to((s) => `rgba(0, 0, 0, 0.15) 0px ${s}px ${2 * s}px 0px`),
|
|
||||||
y,
|
|
||||||
...(selectedTeam === team.id ? {
|
|
||||||
backgroundColor: '#406b82',
|
|
||||||
} : {
|
|
||||||
background: 'transparent',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="rank">{team.rank === -1 ? '⭐' : team.rank}</div>
|
|
||||||
{props.showAvatar && <img className="avatar" src={`${teamInfo?.avatar}`} />}
|
|
||||||
<div className="content">
|
|
||||||
<div className="name">
|
|
||||||
{props.showSchool ? `${teamInfo.institution} - ` : ''}{teamInfo.name}
|
|
||||||
</div>
|
|
||||||
<div className="problems">
|
|
||||||
{data.problems.map((v) => {
|
|
||||||
const uncover = team.id === selectedTeam && selectedProblem === v.id;
|
|
||||||
const problemStatus = team.problems.find((idx) => idx.id === v.id);
|
|
||||||
if (!problemStatus) return <span key={v.id} className="item">ERR</span>;
|
|
||||||
return <span key={v.id} className={`${status(problemStatus)} ${uncover ? 'uncover' : ''} item`}>
|
|
||||||
{submissions(problemStatus)}
|
|
||||||
</span>;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="solved">{team.score}</div>
|
|
||||||
<div className="penalty">{team.penalty}</div>
|
|
||||||
</animated.div>;
|
|
||||||
})}
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
ReactDOM.createRoot(document.getElementById('rank-list')!).render(<MainList {...options} data={data} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
addPage(new NamedPage(['resolver'], () => {
|
|
||||||
start(UiContext.payload, {
|
|
||||||
showAvatar: true,
|
|
||||||
showSchool: true,
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
+188
-231
@@ -3,11 +3,10 @@ import { LRUCache } from 'lru-cache';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
_, avatar, BadRequestError, ContestModel, ContestNotEndedError, Context, db, findFileSync,
|
_, avatar, BadRequestError, ContestModel, ContestNotEndedError, Context, db, findFileSync,
|
||||||
ForbiddenError, fs, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel,
|
ForbiddenError, fs, Handler, InvalidTokenError, ObjectId, param, parseTimeMS, PERM, ProblemConfig, ProblemModel,
|
||||||
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
|
randomstring, Schema, SettingModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS,
|
||||||
SystemModel, Time, Types, UserModel, Zip,
|
SystemModel, TokenModel, Types, UserModel, Zip,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import { ResolverInput } from './interface';
|
|
||||||
|
|
||||||
interface IpLoginInfo {
|
interface IpLoginInfo {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -89,245 +88,203 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
|
|||||||
if (['domain', 'account'].includes(that.args.category)) throw new ForbiddenError('Not available');
|
if (['domain', 'account'].includes(that.args.category)) throw new ForbiddenError('Not available');
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.inject(['scoreboard'], ({ scoreboard }) => {
|
async function generateCdpZip(tdoc) {
|
||||||
scoreboard.addView('resolver-tiny', 'Resolver(Tiny)', { tdoc: 'tdoc' }, {
|
const serverUrl = SystemModel.get('server.url');
|
||||||
async display({ tdoc }) {
|
try {
|
||||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
new URL(serverUrl);
|
||||||
const teams = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray();
|
} catch (e) {
|
||||||
const udict = await UserModel.getList(tdoc.domainId, teams.map((i) => i.uid));
|
throw new BadRequestError('Server URL not set');
|
||||||
const teamIds: Record<number, number> = {};
|
}
|
||||||
for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i;
|
let token = 0;
|
||||||
const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second);
|
const getFeed = (type: string, data: any) => ({
|
||||||
const pid = (i: number) => String.fromCharCode(65 + i);
|
type, id: data.id, data, token: `t${token++}`,
|
||||||
const unknownSchool = this.translate('Unknown School');
|
|
||||||
const submissions = teams.flatMap((i) => {
|
|
||||||
if (!i.journal) return [];
|
|
||||||
return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid }));
|
|
||||||
});
|
|
||||||
this.response.body = {
|
|
||||||
payload: {
|
|
||||||
name: tdoc.title,
|
|
||||||
duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000),
|
|
||||||
frozen: Math.floor((new Date(tdoc.lockAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000),
|
|
||||||
problems: tdoc.pids.map((i, n) => ({ name: pid(n), id: i.toString() })),
|
|
||||||
teams: teams.map((t) => ({
|
|
||||||
id: t.uid.toString(),
|
|
||||||
name: udict[t.uid].displayName || udict[t.uid].uname,
|
|
||||||
avatar: avatar(udict[t.uid].avatar),
|
|
||||||
institution: udict[t.uid].school || unknownSchool,
|
|
||||||
exclude: t.unrank,
|
|
||||||
})),
|
|
||||||
submissions: submissions.map((i) => ({
|
|
||||||
team: udict[i.uid]?.seat || i.uid.toString(),
|
|
||||||
problem: i.pid.toString(),
|
|
||||||
verdict: STATUS_SHORT_TEXTS[i.status],
|
|
||||||
time: time(i.rid),
|
|
||||||
})),
|
|
||||||
} as ResolverInput,
|
|
||||||
};
|
|
||||||
this.response.template = 'resolver.html';
|
|
||||||
},
|
|
||||||
supportedRules: ['acm'],
|
|
||||||
});
|
});
|
||||||
|
const [pdict, tsdocs] = await Promise.all([
|
||||||
|
ProblemModel.getList(tdoc.domainId, tdoc.pids, true, false, ProblemModel.PROJECTION_LIST.concat('config'), true),
|
||||||
|
ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(),
|
||||||
|
]);
|
||||||
|
const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid));
|
||||||
|
const teams = tsdocs.map((i) => {
|
||||||
|
const udoc = udict[i.uid];
|
||||||
|
return {
|
||||||
|
team_id: udoc.seat || `team-${udoc._id}`,
|
||||||
|
name: udoc.uname,
|
||||||
|
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
|
||||||
|
organization: udoc.school || udoc.uname,
|
||||||
|
avatar: avatar(udoc.avatar),
|
||||||
|
group: [
|
||||||
|
...(udoc.group.filter((g) => g !== `${udoc._id}`)),
|
||||||
|
i.unrank ? 'observers' : 'participants',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const relatedGroups = Array.from(new Set(teams.flatMap((i) => i.group)));
|
||||||
|
const groupId = {};
|
||||||
|
let gid = 1;
|
||||||
|
for (const i of relatedGroups) groupId[i] = `group-${gid++}`;
|
||||||
|
const organizations = Array.from(new Set(teams.flatMap((i) => i.organization)));
|
||||||
|
const orgId = {};
|
||||||
|
let oid = 1;
|
||||||
|
for (const i of organizations) orgId[i] = `org-${oid++}`;
|
||||||
|
const duration = moment(tdoc.endAt).diff(tdoc.beginAt, 'seconds');
|
||||||
|
const lockDuration = tdoc.lockAt
|
||||||
|
? moment(tdoc.endAt).diff(tdoc.lockAt, 'seconds')
|
||||||
|
: tdoc.rule === 'oi' ? duration : null;
|
||||||
|
const eventfeed: Record<string, any>[] = [
|
||||||
|
getFeed('contest', {
|
||||||
|
id: tdoc._id.toHexString(),
|
||||||
|
name: tdoc.title,
|
||||||
|
formal_name: tdoc.title,
|
||||||
|
start_time: moment(tdoc.beginAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
duration: moment().startOf('day').seconds(duration).format('HH:mm:ss.SSS'),
|
||||||
|
...lockDuration && {
|
||||||
|
scoreboard_freeze_duration: moment().startOf('day').seconds(lockDuration).format('HH:mm:ss.SSS'),
|
||||||
|
},
|
||||||
|
penalty_time: 20,
|
||||||
|
}),
|
||||||
|
...Object.keys(STATUS_SHORT_TEXTS).map((i) => getFeed('judgement-types', {
|
||||||
|
id: STATUS_SHORT_TEXTS[i],
|
||||||
|
name: STATUS_TEXTS[i],
|
||||||
|
penalty: ![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_SYSTEM_ERROR].includes(+i),
|
||||||
|
solved: +i === STATUS.STATUS_ACCEPTED,
|
||||||
|
})),
|
||||||
|
getFeed('languages', { id: 'c', name: 'C', entry_point_required: false, extensions: ['c'] }),
|
||||||
|
getFeed('languages', { id: 'cpp', name: 'C++', entry_point_required: false, extensions: ['cpp', 'cc', 'cxx', 'c++'] }),
|
||||||
|
getFeed('languages', { id: 'java', name: 'Java', entry_point_required: true, extensions: ['java'] }),
|
||||||
|
getFeed('languages', { id: 'python3', name: 'Python 3', entry_point_required: false, entry_point_name: 'Main file', extensions: ['py', 'py3'] }),
|
||||||
|
getFeed('languages', { id: 'kotlin', name: 'Kotlin', entry_point_required: true, extensions: ['kt'] }),
|
||||||
|
getFeed('languages', { id: 'rust', name: 'Rust', entry_point_required: true, extensions: ['rs'] }),
|
||||||
|
getFeed('languages', { id: 'go', name: 'Go', entry_point_required: true, extensions: ['go'] }),
|
||||||
|
getFeed('groups', { id: 'participants', name: '正式队伍' }),
|
||||||
|
getFeed('groups', { id: 'observers', name: '打星队伍' }),
|
||||||
|
...relatedGroups.map((i) => getFeed('groups', { id: groupId[i], name: i })),
|
||||||
|
...organizations.map((i) => getFeed('organizations', {
|
||||||
|
id: orgId[i], name: i, formal_name: i,
|
||||||
|
logo: [{ href: `contest/${tdoc._id}/organizations/${orgId[i]}/logo`, filename: 'logo.png', mime: 'image/png', width: 64, height: 64 }],
|
||||||
|
})),
|
||||||
|
...teams.map((i) => getFeed('teams', {
|
||||||
|
id: i.team_id, label: i.team_id, name: i.name,
|
||||||
|
display_name: i.displayName || i.name,
|
||||||
|
group_ids: i.group.map((j) => groupId[j]),
|
||||||
|
organization_id: orgId[i.organization],
|
||||||
|
photo: [{ href: `contest/${tdoc._id}/teams/${i.team_id}/photo`, filename: 'photo.png', mime: 'image/png', width: 1920, height: 1080 }],
|
||||||
|
logo: [{ href: new URL(i.avatar, serverUrl).toString(), filename: 'logo.webp', mime: 'image/webp', width: 128, height: 128 }],
|
||||||
|
})),
|
||||||
|
...tdoc.pids.map((i, idx) => getFeed('problems', {
|
||||||
|
id: `${i}`, label: String.fromCharCode(65 + idx), name: pdict[i].title, ordinal: idx,
|
||||||
|
color: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].name : tdoc.balloon?.[idx]) || 'white',
|
||||||
|
rgb: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].color : null) || '#ffffff',
|
||||||
|
time_limit: Math.floor(parseTimeMS((pdict[i].config as ProblemConfig).timeMax) / 100) / 10,
|
||||||
|
test_data_count: 20,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
let cntJudge = 0;
|
||||||
|
const submissions = tsdocs.flatMap((i) => {
|
||||||
|
if (!i.journal) return [];
|
||||||
|
const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid));
|
||||||
|
const result: any[] = [];
|
||||||
|
for (const s of journal) {
|
||||||
|
const submitTime = moment(s.rid.getTimestamp());
|
||||||
|
const submitDelta = submitTime.diff(tdoc.beginAt, 'seconds');
|
||||||
|
const submitAt = moment().startOf('day').seconds(submitDelta).format('HH:mm:ss.SSS');
|
||||||
|
const judgeTime = submitTime.add(1, 'seconds');
|
||||||
|
const judgeDelta = judgeTime.diff(tdoc.beginAt, 'seconds');
|
||||||
|
const judgeAt = moment().startOf('day').seconds(judgeDelta).format('HH:mm:ss.SSS');
|
||||||
|
result.push(getFeed('submissions', {
|
||||||
|
id: s.rid, team_id: udict[i.uid]?.seat || `team-${i.uid}`,
|
||||||
|
problem_id: `${s.pid}`, language_id: s.lang?.split('.')[0] || 'cpp',
|
||||||
|
files: [], contest_time: submitAt,
|
||||||
|
time: submitTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
}));
|
||||||
|
result.push(getFeed('judgements', {
|
||||||
|
id: `${cntJudge}`, submission_id: s.rid,
|
||||||
|
judgement_type_id: STATUS_SHORT_TEXTS[s.status], max_run_time: 0.1,
|
||||||
|
start_contest_time: submitAt, end_contest_time: judgeAt,
|
||||||
|
start_time: submitTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
end_time: judgeTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
...s.score != null && { score: s.score },
|
||||||
|
}));
|
||||||
|
cntJudge++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const endState = [
|
||||||
|
getFeed('state', {
|
||||||
|
started: moment(tdoc.beginAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
ended: moment(tdoc.endAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
...tdoc.lockAt && { frozen: moment(tdoc.lockAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00') },
|
||||||
|
finalized: moment().format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const zip = new Zip.ZipWriter(new Zip.BlobWriter());
|
||||||
|
await Promise.all([
|
||||||
|
zip.add('event-feed.ndjson', new Zip.TextReader(eventfeed.concat(submissions).concat(endState).map((i) => JSON.stringify(i)).join('\n'))),
|
||||||
|
zip.add('contest/logo.png', new Zip.BlobReader(new Blob([fs.readFileSync(findFileSync('@hydrooj/onsite-toolkit/public/logo.png'))]))),
|
||||||
|
zip.add('teams/', null, { directory: true }),
|
||||||
|
zip.add('organizations/', null, { directory: true }),
|
||||||
|
]);
|
||||||
|
await Promise.all(teams.map(async (i) => {
|
||||||
|
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
|
||||||
|
await zip.add(`teams/${i.team_id}/photo.url`, new Zip.TextReader(`URL=${i.avatar}`));
|
||||||
|
}));
|
||||||
|
await Promise.all(organizations.map(async (i) => {
|
||||||
|
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
|
||||||
|
if (!avatarSrc) return;
|
||||||
|
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
|
||||||
|
await zip.add(`organizations/${orgId[i]}/photo.url`, new Zip.TextReader(`URL=${avatar(avatarSrc)}`));
|
||||||
|
}));
|
||||||
|
return zip.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContestResolverCdpHandler extends Handler {
|
||||||
|
allowCors = true;
|
||||||
|
@param('tid', Types.ObjectId)
|
||||||
|
@param('token', Types.String)
|
||||||
|
async get(domainId: string, tid: ObjectId, token: string) {
|
||||||
|
const tokenDoc = await TokenModel.get(token, TokenModel.TYPE_EXPORT);
|
||||||
|
if (!tokenDoc || tokenDoc.domainId !== domainId || tokenDoc.contestId?.toHexString() !== tid.toHexString()) {
|
||||||
|
throw new InvalidTokenError(token);
|
||||||
|
}
|
||||||
|
const tdoc = await ContestModel.get(domainId, tid);
|
||||||
|
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
||||||
|
this.response.addHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
this.binary(await generateCdpZip(tdoc), `contest-${tid}-cdp.zip`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Route('contest_resolver_cdp', '/contest/:tid/resolver-cdp/:token', ContestResolverCdpHandler);
|
||||||
|
|
||||||
|
ctx.inject(['scoreboard'], ({ scoreboard }) => {
|
||||||
scoreboard.addView('cdp', 'CDP', { tdoc: 'tdoc' }, {
|
scoreboard.addView('cdp', 'CDP', { tdoc: 'tdoc' }, {
|
||||||
async display({ tdoc }) {
|
async display({ tdoc }) {
|
||||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||||
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
||||||
|
this.binary(await generateCdpZip(tdoc), `contest-${tdoc._id}-cdp.zip`);
|
||||||
|
},
|
||||||
|
supportedRules: ['acm', 'oi'],
|
||||||
|
});
|
||||||
|
|
||||||
|
scoreboard.addView('resolver', 'Resolver', { tdoc: 'tdoc' }, {
|
||||||
|
async display({ tdoc }) {
|
||||||
|
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||||
|
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
|
||||||
|
const serverUrl = SystemModel.get('server.url');
|
||||||
try {
|
try {
|
||||||
new URL(SystemModel.get('server.url')); // eslint-disable-line no-new
|
new URL(serverUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new BadRequestError('Server URL not set');
|
throw new BadRequestError('Server URL not set');
|
||||||
}
|
}
|
||||||
let token = 0;
|
const [tokenId] = await TokenModel.add(
|
||||||
const getFeed = (type: string, data: any) => ({
|
TokenModel.TYPE_EXPORT, 600,
|
||||||
type, id: data.id, data, token: `t${token++}`,
|
{ domainId: tdoc.domainId, contestId: tdoc._id },
|
||||||
});
|
);
|
||||||
const [pdict, tsdocs] = await Promise.all([
|
const source = new URL(`/d/${tdoc.domainId}/contest/${tdoc._id}/resolver-cdp/${tokenId}`, serverUrl).toString();
|
||||||
ProblemModel.getList(tdoc.domainId, tdoc.pids, true, false, ProblemModel.PROJECTION_LIST.concat('config'), true),
|
const target = new URL('https://resolver.hydrooj.com');
|
||||||
ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(),
|
target.searchParams.set('source', source);
|
||||||
]);
|
target.searchParams.set('mode', tdoc.rule === 'oi' ? 'oi' : 'acm');
|
||||||
const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid));
|
this.response.redirect = target.toString();
|
||||||
const teams = tsdocs.map((i) => {
|
|
||||||
const udoc = udict[i.uid];
|
|
||||||
return {
|
|
||||||
team_id: udoc.seat || `team-${udoc._id}`,
|
|
||||||
name: udoc.uname,
|
|
||||||
displayName: (i.unrank ? '⭐' : '') + (udoc.displayName || udoc.uname),
|
|
||||||
organization: udoc.school || udoc.uname,
|
|
||||||
avatar: avatar(udoc.avatar),
|
|
||||||
group: [
|
|
||||||
...(udoc.group.filter((g) => g !== `${udoc._id}`)),
|
|
||||||
i.unrank ? 'observers' : 'participants',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const relatedGroups = Array.from(new Set(teams.flatMap((i) => i.group)));
|
|
||||||
const groupId = {};
|
|
||||||
let gid = 1;
|
|
||||||
for (const i of relatedGroups) groupId[i] = `group-${gid++}`;
|
|
||||||
const organizations = Array.from(new Set(teams.flatMap((i) => i.organization)));
|
|
||||||
const orgId = {};
|
|
||||||
let oid = 1;
|
|
||||||
for (const i of organizations) orgId[i] = `org-${oid++}`;
|
|
||||||
const duration = moment(tdoc.endAt).diff(tdoc.beginAt, 'seconds');
|
|
||||||
const lockDuration = tdoc.lockAt ? moment(tdoc.endAt).diff(tdoc.lockAt, 'seconds') : null;
|
|
||||||
const eventfeed: Record<string, any>[] = [
|
|
||||||
getFeed('contest', {
|
|
||||||
id: tdoc._id.toHexString(),
|
|
||||||
name: tdoc.title,
|
|
||||||
formal_name: tdoc.title,
|
|
||||||
start_time: moment(tdoc.beginAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
// 1:00:00.000
|
|
||||||
duration: moment().startOf('day').seconds(duration).format('HH:mm:ss.SSS'),
|
|
||||||
...lockDuration && {
|
|
||||||
scoreboard_freeze_duration: moment().startOf('day').seconds(lockDuration).format('HH:mm:ss.SSS'),
|
|
||||||
},
|
|
||||||
penalty_time: 20,
|
|
||||||
}),
|
|
||||||
...Object.keys(STATUS_SHORT_TEXTS).map((i) => getFeed('judgement-types', {
|
|
||||||
id: STATUS_SHORT_TEXTS[i],
|
|
||||||
name: STATUS_TEXTS[i],
|
|
||||||
penalty: ![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_SYSTEM_ERROR].includes(+i),
|
|
||||||
solved: +i === STATUS.STATUS_ACCEPTED,
|
|
||||||
})),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'c', name: 'C', entry_point_required: false, extensions: ['c'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'cpp', name: 'C++', entry_point_required: false, extensions: ['cpp', 'cc', 'cxx', 'c++'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'java', name: 'Java', entry_point_required: true, extensions: ['java'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'python3', name: 'Python 3', entry_point_required: false, entry_point_name: 'Main file', extensions: ['py', 'py3'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'kotlin', name: 'Kotlin', entry_point_required: true, extensions: ['kt'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'rust', name: 'Rust', entry_point_required: true, extensions: ['rs'],
|
|
||||||
}),
|
|
||||||
getFeed('languages', {
|
|
||||||
id: 'go', name: 'Go', entry_point_required: true, extensions: ['go'],
|
|
||||||
}),
|
|
||||||
getFeed('groups', { id: 'participants', name: '正式队伍' }),
|
|
||||||
getFeed('groups', { id: 'observers', name: '打星队伍' }),
|
|
||||||
...relatedGroups.map((i) => getFeed('groups', {
|
|
||||||
id: groupId[i],
|
|
||||||
name: i,
|
|
||||||
})),
|
|
||||||
...organizations.map((i) => getFeed('organizations', {
|
|
||||||
id: orgId[i],
|
|
||||||
name: i,
|
|
||||||
formal_name: i,
|
|
||||||
logo: [{
|
|
||||||
href: `contest/${tdoc._id}/organizations/${orgId[i]}/logo`,
|
|
||||||
filename: 'logo.png',
|
|
||||||
mime: 'image/png',
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
}],
|
|
||||||
})),
|
|
||||||
...teams.map((i) => getFeed('teams', {
|
|
||||||
id: i.team_id,
|
|
||||||
label: i.team_id,
|
|
||||||
name: i.name,
|
|
||||||
display_name: i.displayName || i.name,
|
|
||||||
group_ids: i.group.map((j) => groupId[j]),
|
|
||||||
organization_id: orgId[i.organization],
|
|
||||||
photo: [{
|
|
||||||
href: `contest/${tdoc._id}/teams/${i.team_id}/photo`,
|
|
||||||
filename: 'photo.png',
|
|
||||||
mime: 'image/png',
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
}],
|
|
||||||
logo: [{
|
|
||||||
href: new URL(i.avatar, SystemModel.get('server.url')).toString(),
|
|
||||||
filename: 'logo.webp',
|
|
||||||
mime: 'image/webp',
|
|
||||||
width: 128,
|
|
||||||
height: 128,
|
|
||||||
}],
|
|
||||||
})),
|
|
||||||
...tdoc.pids.map((i, idx) => getFeed('problems', {
|
|
||||||
id: `${i}`,
|
|
||||||
label: String.fromCharCode(65 + idx),
|
|
||||||
name: pdict[i].title,
|
|
||||||
ordinal: idx,
|
|
||||||
color: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].name : tdoc.balloon?.[idx]) || 'white',
|
|
||||||
rgb: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].color : null) || '#ffffff',
|
|
||||||
time_limit: Math.floor(parseTimeMS((pdict[i].config as ProblemConfig).timeMax) / 100) / 10,
|
|
||||||
test_data_count: 20,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
let cntJudge = 0;
|
|
||||||
const submissions = tsdocs.flatMap((i) => {
|
|
||||||
if (!i.journal) return [];
|
|
||||||
const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid));
|
|
||||||
const result: any[] = [];
|
|
||||||
for (const s of journal) {
|
|
||||||
const submitTime = moment(s.rid.getTimestamp());
|
|
||||||
const submitDelta = submitTime.diff(tdoc.beginAt, 'seconds');
|
|
||||||
const submitAt = moment().startOf('day').seconds(submitDelta).format('HH:mm:ss.SSS');
|
|
||||||
const judgeTime = submitTime.add(1, 'seconds');
|
|
||||||
const judgeDelta = judgeTime.diff(tdoc.beginAt, 'seconds');
|
|
||||||
const judgeAt = moment().startOf('day').seconds(judgeDelta).format('HH:mm:ss.SSS');
|
|
||||||
result.push(getFeed('submissions', {
|
|
||||||
id: s.rid,
|
|
||||||
team_id: udict[i.uid]?.seat || `team-${i.uid}`,
|
|
||||||
problem_id: `${s.pid}`,
|
|
||||||
language_id: s.lang?.split('.')[0] || 'cpp',
|
|
||||||
files: [],
|
|
||||||
contest_time: submitAt,
|
|
||||||
time: submitTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
}));
|
|
||||||
result.push(getFeed('judgements', {
|
|
||||||
id: `${cntJudge}`,
|
|
||||||
submission_id: s.rid,
|
|
||||||
judgement_type_id: STATUS_SHORT_TEXTS[s.status],
|
|
||||||
max_run_time: 0.1,
|
|
||||||
start_contest_time: submitAt,
|
|
||||||
end_contest_time: judgeAt,
|
|
||||||
start_time: submitTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
end_time: judgeTime.format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
}));
|
|
||||||
cntJudge++;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
const endState = [
|
|
||||||
getFeed('state', {
|
|
||||||
started: moment(tdoc.beginAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
ended: moment(tdoc.endAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
...tdoc.lockAt && {
|
|
||||||
frozen: moment(tdoc.lockAt).format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
},
|
|
||||||
finalized: moment().format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
const zip = new Zip.ZipWriter(new Zip.BlobWriter());
|
|
||||||
await Promise.all([
|
|
||||||
zip.add('event-feed.ndjson', new Zip.TextReader(eventfeed.concat(submissions).concat(endState).map((i) => JSON.stringify(i)).join('\n'))),
|
|
||||||
zip.add('contest/logo.png', new Zip.BlobReader(new Blob([fs.readFileSync(findFileSync('@hydrooj/onsite-toolkit/public/logo.png'))]))),
|
|
||||||
zip.add('teams/', null, { directory: true }),
|
|
||||||
zip.add('organizations/', null, { directory: true }),
|
|
||||||
]);
|
|
||||||
await Promise.all(teams.map(async (i) => {
|
|
||||||
await zip.add(`teams/${i.team_id}/`, null, { directory: true });
|
|
||||||
await zip.add(`teams/${i.team_id}/photo.url`, new Zip.TextReader(`URL=${i.avatar}`));
|
|
||||||
}));
|
|
||||||
await Promise.all(organizations.map(async (i) => {
|
|
||||||
const avatarSrc = teams.find((j) => j.organization === i)?.avatar;
|
|
||||||
if (!avatarSrc) return;
|
|
||||||
await zip.add(`organizations/${orgId[i]}/`, null, { directory: true });
|
|
||||||
await zip.add(`organizations/${orgId[i]}/photo.url`, new Zip.TextReader(`URL=${avatar(avatarSrc)}`));
|
|
||||||
}));
|
|
||||||
this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`);
|
|
||||||
},
|
},
|
||||||
supportedRules: ['acm'],
|
supportedRules: ['acm', 'oi'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
type Verdict = 'RJ' | 'AC' | 'NA';
|
|
||||||
export interface ProblemInfo {
|
|
||||||
color?: string; // hex color
|
|
||||||
id: string;
|
|
||||||
name: string; // A, B, C, ...
|
|
||||||
}
|
|
||||||
export interface TeamInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatar?: string;
|
|
||||||
institution?: string;
|
|
||||||
exclude?: boolean; // false by default
|
|
||||||
}
|
|
||||||
export interface InstitiutionInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
export interface SubmissionInfo {
|
|
||||||
team: string;
|
|
||||||
problem: string;
|
|
||||||
verdict: Verdict;
|
|
||||||
time: number; // in seconds
|
|
||||||
}
|
|
||||||
export interface ResolverInput {
|
|
||||||
name: string;
|
|
||||||
duration: number; // in seconds
|
|
||||||
frozen: number; // in seconds
|
|
||||||
problems: ProblemInfo[];
|
|
||||||
submissions: SubmissionInfo[];
|
|
||||||
teams: TeamInfo[];
|
|
||||||
institutions: Record<string, InstitiutionInfo>;
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
"version": "0.1.0-beta.6",
|
"version": "0.1.0-beta.6",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro",
|
"repository": "https://github.com/hydro-dev/Hydro",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/web": "^10.0.3",
|
"lru-cache": "^11.3.5"
|
||||||
"lru-cache": "^11.2.7",
|
|
||||||
"react-use": "^17.6.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
body {
|
|
||||||
color: #eee;
|
|
||||||
background-color: #121212;
|
|
||||||
}
|
|
||||||
.rank-list {
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
180deg,
|
|
||||||
#3e3e3e 0px,
|
|
||||||
#3e3e3e 80px,
|
|
||||||
#121212 80px,
|
|
||||||
#121212 160px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.rank-list-item {
|
|
||||||
padding: 10px 0;
|
|
||||||
height: 80px;
|
|
||||||
position: relative;
|
|
||||||
background: transparent;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
.header .rank, .header .solved, .header .penalty {
|
|
||||||
width: 90px;
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.header .content {
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.header .content .title {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.header .content .copyright {
|
|
||||||
color: #8a919b;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.rank-list-item .rank, .rank-list-item .solved, .rank-list-item .penalty {
|
|
||||||
padding: 10px 0px;
|
|
||||||
width: 90px;
|
|
||||||
font-size: 40px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.rank-list-item .avatar {
|
|
||||||
height: 60px;
|
|
||||||
width: 60px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
.rank-list-item .content {
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 60px
|
|
||||||
}
|
|
||||||
.rank-list-item .content .name {
|
|
||||||
font-size: 30px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.rank-list-item .content .problems {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.rank-list-item .problems .item {
|
|
||||||
padding: 2px;
|
|
||||||
width: 95px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.ac {
|
|
||||||
background-color: #009c00;
|
|
||||||
}
|
|
||||||
.failed, .WA {
|
|
||||||
background-color: #dd514c;
|
|
||||||
}
|
|
||||||
.frozen {
|
|
||||||
background-color: #3c6ef6;
|
|
||||||
}
|
|
||||||
.untouched {
|
|
||||||
background-color: #282828;
|
|
||||||
color: #8a919b;
|
|
||||||
}
|
|
||||||
.uncover {
|
|
||||||
animation: flashing 1000ms infinite;
|
|
||||||
-webkit-animation: flashing 1000ms infinite; /*Safari and Chrome*/
|
|
||||||
}
|
|
||||||
@keyframes flashing {
|
|
||||||
from { background-color: #3c6ef6 }
|
|
||||||
50% { background-color: #7699ea }
|
|
||||||
to { background-color: #3c6ef6 }
|
|
||||||
}
|
|
||||||
@-webkit-keyframes flashing {/*Safari and Chrome*/
|
|
||||||
from { background-color: #3c6ef6 }
|
|
||||||
50% { background-color: #7699ea }
|
|
||||||
to { background-color: #3c6ef6 }
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{% extends "layout/html5.html" %}
|
|
||||||
{% block body %}
|
|
||||||
{{ set(UiContext, 'payload', payload) }}
|
|
||||||
<header class="header">
|
|
||||||
<span class="rank">#Rank</span>
|
|
||||||
<span class="content">
|
|
||||||
<span class="title">Contest</span>
|
|
||||||
<span class="copyright">@Hydro/TinyResolver</span>
|
|
||||||
</span>
|
|
||||||
<span class="solved">Solved</span>
|
|
||||||
<span class="penalty">Penalty</span>
|
|
||||||
</header>
|
|
||||||
<div class="rank-list" id="rank-list"></div>
|
|
||||||
<link rel="stylesheet" href="/resolver.css?_={{ Date.now() }}" />
|
|
||||||
{% endblock %}
|
|
||||||
Reference in new issue
Block a user