mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: contest: acm: lock scoreboard
This commit is contained in:
1 parent
cb02b8dafe
commit
e67ff511bb
17 files changed
+224
-196
No files matched your search
@@ -47,13 +47,18 @@ registerResolver(
|
||||
'Get a contest by ID',
|
||||
);
|
||||
|
||||
TaskModel.Worker.addHandler('contest.problemHide', async (doc) => {
|
||||
TaskModel.Worker.addHandler('contest', async (doc) => {
|
||||
const tdoc = await contest.get(doc.domainId, doc.tid);
|
||||
if (!tdoc) return;
|
||||
for (const pid of tdoc.pids) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await problem.edit(doc.domainId, pid, { hidden: false });
|
||||
const tasks = [];
|
||||
for (const op of doc.operation) {
|
||||
if (op === 'unhide') {
|
||||
for (const pid of tdoc.pids) {
|
||||
tasks.push(problem.edit(doc.domainId, pid, { hidden: false }));
|
||||
}
|
||||
} else if (op === 'unlock') tasks.push(contest.recalcStatus(doc.domainId, doc.tid));
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
});
|
||||
|
||||
export class ContestListHandler extends Handler {
|
||||
@@ -129,7 +134,7 @@ export class ContestDetailHandler extends Handler {
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
|
||||
await contest.del(domainId, tid);
|
||||
await TaskModel.deleteMany({
|
||||
type: 'schedule', subType: 'contest.problemHide', domainId, tid,
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
});
|
||||
this.response.redirect = this.url('contest_main');
|
||||
}
|
||||
@@ -167,8 +172,13 @@ export class ContestBroadcastHandler extends Handler {
|
||||
export class ContestScoreboardHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const [tdoc, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page);
|
||||
@param('ignoreLock', Types.Boolean, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1, ignoreLock = false) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (ignoreLock && !this.user.own(tdoc)) {
|
||||
this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
@@ -185,13 +195,18 @@ export class ContestScoreboardHandler extends Handler {
|
||||
export class ContestScoreboardDownloadHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('ext', Types.Range(['csv', 'html']))
|
||||
async get(domainId: string, tid: ObjectID, ext: string) {
|
||||
@param('ignoreLock', Types.Boolean, true)
|
||||
async get(domainId: string, tid: ObjectID, ext: string, ignoreLock = false) {
|
||||
await this.limitRate('scoreboard_download', 120, 3);
|
||||
const getContent = {
|
||||
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
|
||||
html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }),
|
||||
};
|
||||
const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0);
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (ignoreLock && !this.user.own(tdoc)) {
|
||||
this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
const [, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0, ignoreLock);
|
||||
this.binary(await getContent[ext](rows, tdoc), `${tdoc.title}.${ext}`);
|
||||
}
|
||||
}
|
||||
@@ -240,10 +255,11 @@ export class ContestEditHandler extends Handler {
|
||||
@param('code', Types.String, true)
|
||||
@param('autoHide', Types.String, true)
|
||||
@param('assign', Types.CommaSeperatedArray, true)
|
||||
@param('lock', Types.UnsignedInt, true)
|
||||
async post(
|
||||
domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number,
|
||||
title: string, content: string, rule: string, _pids: string, rated = false,
|
||||
_code = '', autoHide = false, assign: string[] = null,
|
||||
_code = '', autoHide = false, assign: string[] = null, lock: number = null,
|
||||
) {
|
||||
if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i);
|
||||
@@ -252,20 +268,22 @@ export class ContestEditHandler extends Handler {
|
||||
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
|
||||
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
|
||||
const beginAt = beginAtMoment.toDate();
|
||||
const lockAt = lock ? moment(endAt).add(-lock, 'minutes').toDate() : null;
|
||||
await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, this.user.group, true);
|
||||
if (tid) {
|
||||
await contest.edit(domainId, tid, {
|
||||
title, content, rule, beginAt, endAt, pids, rated,
|
||||
});
|
||||
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|
||||
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
|
||||
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule
|
||||
|| lockAt !== this.tdoc.lockAt) {
|
||||
await contest.recalcStatus(domainId, this.tdoc.docId);
|
||||
}
|
||||
} else {
|
||||
tid = await contest.add(domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated);
|
||||
}
|
||||
const task = {
|
||||
type: 'schedule', subType: 'contest.problemHide', domainId, tid,
|
||||
type: 'schedule', subType: 'contest', domainId, tid,
|
||||
};
|
||||
await TaskModel.deleteMany(task);
|
||||
if (Date.now() <= endAt.getTime() && autoHide) {
|
||||
@@ -273,10 +291,20 @@ export class ContestEditHandler extends Handler {
|
||||
await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
|
||||
await TaskModel.add({
|
||||
...task,
|
||||
operation: ['unhide'],
|
||||
executeAfter: endAt,
|
||||
});
|
||||
}
|
||||
await contest.edit(domainId, tid, { assign, _code, autoHide });
|
||||
if (lock && lockAt <= endAt) {
|
||||
await TaskModel.add({
|
||||
...task,
|
||||
operation: ['unlock'],
|
||||
executeAfter: endAt,
|
||||
});
|
||||
}
|
||||
await contest.edit(domainId, tid, {
|
||||
assign, _code, autoHide, lockAt,
|
||||
});
|
||||
this.response.body = { tid };
|
||||
this.response.redirect = this.url('contest_detail', { tid });
|
||||
}
|
||||
|
||||
@@ -348,6 +348,9 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
|
||||
_code?: string;
|
||||
assign?: string[];
|
||||
|
||||
// For contest
|
||||
lockAt?: Date;
|
||||
|
||||
// For homework
|
||||
penaltySince?: Date;
|
||||
penaltyRules?: PenaltyRules;
|
||||
@@ -478,19 +481,21 @@ export interface ContestStat extends Record<string, any> {
|
||||
detail: any,
|
||||
}
|
||||
|
||||
export interface ContestRule {
|
||||
export interface ContestRule<T = any> {
|
||||
_originalRule?: Partial<ContestRule<T>>;
|
||||
TEXT: string;
|
||||
check: (args: any) => any;
|
||||
statusSort: any;
|
||||
submitAfterAccept: boolean;
|
||||
showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
showRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
stat: (tdoc: Tdoc<30>, journal: any[]) => ContestStat;
|
||||
stat: (this: ContestRule<T>, tdoc: Tdoc<30>, journal: any[], ignoreLock?: boolean) => ContestStat & T;
|
||||
scoreboard: (
|
||||
isExport: boolean, _: (s: string) => string,
|
||||
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<any>, page: number,
|
||||
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
|
||||
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<ContestStat & T>, page: number,
|
||||
) => Promise<[board: ScoreboardRow[], udict: Udict, nPages: number]>;
|
||||
ranked: (tdoc: Tdoc<30>, cursor: Cursor<any>) => Promise<any[]>;
|
||||
ranked: (tdoc: Tdoc<30>, cursor: Cursor<ContestStat & T>) => Promise<[Array<[number, ContestStat & T]>, number]>;
|
||||
}
|
||||
|
||||
export type ContestRules = Dictionary<ContestRule>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FilterQuery, ObjectID } from 'mongodb';
|
||||
import { Counter } from '@hydrooj/utils/lib/utils';
|
||||
import {
|
||||
ContestAlreadyAttendedError, ContestNotAttendedError, ContestNotFoundError,
|
||||
ContestScoreboardHiddenError, ValidationError,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
import * as misc from '../lib/misc';
|
||||
import ranked from '../lib/rank';
|
||||
import * as bus from '../service/bus';
|
||||
import type { Handler } from '../service/server';
|
||||
import { PERM, STATUS } from './builtin';
|
||||
import * as document from './document';
|
||||
import problem from './problem';
|
||||
@@ -29,41 +31,51 @@ interface AcmDetail extends AcmJournal {
|
||||
real: number;
|
||||
}
|
||||
|
||||
const acm: ContestRule = {
|
||||
function buildContestRule<T>(def: ContestRule<T>): ContestRule<T> {
|
||||
def._originalRule = { scoreboard: def.scoreboard, stat: def.stat };
|
||||
def.scoreboard = (def._originalRule?.scoreboard || def.scoreboard).bind(def);
|
||||
def.stat = (def._originalRule?.stat || def.stat).bind(def);
|
||||
return def;
|
||||
}
|
||||
|
||||
function filterEffective<T extends AcmJournal>(tdoc: Tdoc, journal: T[], ignoreLock = false): T[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
if (isLocked(tdoc) && !ignoreLock) journal = journal.filter((i) => i.rid.generationTime * 1000 < tdoc.lockAt.getTime());
|
||||
return journal.filter((i) => tdoc.pids.includes(i.pid));
|
||||
}
|
||||
|
||||
const acm = buildContestRule({
|
||||
TEXT: 'ACM/ICPC',
|
||||
check: () => { },
|
||||
statusSort: { accept: -1, time: 1 },
|
||||
submitAfterAccept: false,
|
||||
showScoreboard: () => true,
|
||||
showSelfRecord: () => true,
|
||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||
stat: (tdoc, journal: AcmJournal[]) => {
|
||||
const naccept: Record<number, number> = {};
|
||||
stat(tdoc, journal: AcmJournal[], ignoreLock = false) {
|
||||
const naccept = Counter<number>();
|
||||
const effective: Record<number, AcmJournal> = {};
|
||||
const detail: AcmDetail[] = [];
|
||||
let accept = 0;
|
||||
let time = 0;
|
||||
for (const j of journal) {
|
||||
if (tdoc.pids.includes(j.pid)
|
||||
&& effective[j.pid]?.status !== STATUS.STATUS_ACCEPTED) {
|
||||
effective[j.pid] = j;
|
||||
if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR].includes(j.status)) {
|
||||
naccept[j.pid] = (naccept[j.pid] || 0) + 1;
|
||||
}
|
||||
for (const j of filterEffective(tdoc, journal, ignoreLock)) {
|
||||
if (!this.submitAfterAccept && effective[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue;
|
||||
effective[j.pid] = j;
|
||||
if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR].includes(j.status)) {
|
||||
naccept[j.pid]++;
|
||||
}
|
||||
}
|
||||
for (const key in effective) {
|
||||
const j = effective[key];
|
||||
for (const pid in effective) {
|
||||
const j = effective[pid];
|
||||
const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000);
|
||||
const penalty = 20 * 60 * (naccept[j.pid] || 0);
|
||||
const penalty = 20 * 60 * naccept[j.pid];
|
||||
detail.push({
|
||||
...j, naccept: naccept[j.pid] || 0, time: real + penalty, real, penalty,
|
||||
...j, naccept: naccept[j.pid], time: real + penalty, real, penalty,
|
||||
});
|
||||
}
|
||||
for (const d of detail) {
|
||||
if (d.status === STATUS.STATUS_ACCEPTED) {
|
||||
accept += 1;
|
||||
time += d.time;
|
||||
}
|
||||
for (const d of detail.filter((i) => i.status === STATUS.STATUS_ACCEPTED)) {
|
||||
accept++;
|
||||
time += d.time;
|
||||
}
|
||||
return { accept, time, detail };
|
||||
},
|
||||
@@ -80,7 +92,6 @@ const acm: ContestRule = {
|
||||
columns.push(
|
||||
{ type: 'total_time', value: _('Penalty') },
|
||||
{ type: 'total_time', value: _('Total Time (Seconds)') },
|
||||
{ type: 'total_time_str', value: _('Total Time') },
|
||||
);
|
||||
}
|
||||
for (let i = 1; i <= tdoc.pids.length; i++) {
|
||||
@@ -108,41 +119,43 @@ const acm: ContestRule = {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find first accept
|
||||
const first = {};
|
||||
for (const pid of tdoc.pids) first[pid] = new ObjectID().generationTime;
|
||||
for (const [, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
for (const item of tsdoc.journal || []) tsddict[item.pid] = item;
|
||||
for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
|
||||
for (const pid of tdoc.pids) {
|
||||
if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid].rid.generationTime < first[pid]) {
|
||||
first[pid] = tsddict[pid].rid.generationTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: ScoreboardRow[] = [columns];
|
||||
for (const [rank, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
|
||||
const tsddict: Record<number, AcmDetail> = {};
|
||||
for (const item of tsdoc.detail) tsddict[item.pid] = item;
|
||||
const row: ScoreboardRow = [
|
||||
{ type: 'string', value: rank.toString() },
|
||||
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
|
||||
{ type: 'string', value: tsdoc.accept || 0 },
|
||||
];
|
||||
if (isExport) {
|
||||
const penalty = Math.sum(tdoc.pids.map((i) => (tsddict[i]?.naccept || 0))) * 20 * 60;
|
||||
const penalty = Math.sum(tdoc.pids.map((i) => tsddict[i]?.naccept || 0)) * 20 * 60;
|
||||
row.push(
|
||||
{ type: 'string', value: penalty.toString() },
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
);
|
||||
}
|
||||
for (const pid of tdoc.pids) {
|
||||
const doc = tsddict[pid] || {};
|
||||
const doc = tsddict[pid] || {} as Partial<AcmDetail>;
|
||||
const accept = doc.status === STATUS.STATUS_ACCEPTED;
|
||||
const rid = accept ? doc.rid : null;
|
||||
const colAccepted = `${(accept && isExport) ? `${_('Accepted')} ` : ''}${doc.naccept ? ` (-${doc.naccept})` : ''}`;
|
||||
const colTime = accept ? doc.time : '-';
|
||||
const colTimeStr = accept ? misc.formatSeconds(colTime) : '-';
|
||||
const colTime = accept ? doc.time.toString() : '-';
|
||||
const colTimeStr = accept ? misc.formatSeconds(doc.time) : '-';
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: colAccepted },
|
||||
@@ -161,7 +174,6 @@ const acm: ContestRule = {
|
||||
});
|
||||
}
|
||||
}
|
||||
row.raw = tsdoc;
|
||||
rows.push(row);
|
||||
}
|
||||
return [rows, udict, nPages];
|
||||
@@ -169,17 +181,18 @@ const acm: ContestRule = {
|
||||
async ranked(tdoc, cursor) {
|
||||
return await ranked.all(cursor, (a, b) => a.score === b.score);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const oi: ContestRule = {
|
||||
const oi = buildContestRule({
|
||||
TEXT: 'OI',
|
||||
check: () => { },
|
||||
submitAfterAccept: true,
|
||||
statusSort: { score: -1 },
|
||||
stat: (tdoc, journal) => {
|
||||
stat(tdoc, journal) {
|
||||
const detail = {};
|
||||
let score = 0;
|
||||
for (const j of journal) {
|
||||
if (tdoc.pids.includes(j.pid)) detail[j.pid] = j;
|
||||
for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) {
|
||||
if (detail[j.pid]?.status === STATUS.STATUS_ACCEPTED && !this.submitAfterAccept) detail[j.pid] = j;
|
||||
}
|
||||
for (const i in detail) score += detail[i].score;
|
||||
return { score, detail };
|
||||
@@ -278,19 +291,21 @@ const oi: ContestRule = {
|
||||
async ranked(tdoc, cursor) {
|
||||
return await ranked.all(cursor, (a, b) => a.score === b.score);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ioi: ContestRule = {
|
||||
const ioi = buildContestRule({
|
||||
...oi,
|
||||
TEXT: 'IOI',
|
||||
submitAfterAccept: false,
|
||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||
showSelfRecord: () => true,
|
||||
showScoreboard: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
const homework: ContestRule = {
|
||||
const homework = buildContestRule({
|
||||
TEXT: 'Assignment',
|
||||
check: () => { },
|
||||
submitAfterAccept: false,
|
||||
statusSort: { penaltyScore: -1, time: 1 },
|
||||
stat: (tdoc, journal) => {
|
||||
const effective = {};
|
||||
@@ -434,7 +449,7 @@ const homework: ContestRule = {
|
||||
async ranked(tdoc, cursor) {
|
||||
return await ranked.all(cursor, (a, b) => a.score === b.score);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const RULES: ContestRules = {
|
||||
acm, oi, homework, ioi,
|
||||
@@ -449,12 +464,12 @@ export async function add(
|
||||
rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [],
|
||||
rated = false, data: Partial<Tdoc> = {},
|
||||
) {
|
||||
if (!this.RULES[rule]) throw new ValidationError('rule');
|
||||
if (!RULES[rule]) throw new ValidationError('rule');
|
||||
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
|
||||
Object.assign(data, {
|
||||
content, owner, title, rule, beginAt, endAt, pids, attend: 0,
|
||||
});
|
||||
this.RULES[rule].check(data);
|
||||
RULES[rule].check(data);
|
||||
await bus.serial('contest/before-add', data);
|
||||
const res = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
|
||||
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
|
||||
@@ -464,10 +479,10 @@ export async function add(
|
||||
}
|
||||
|
||||
export async function edit(domainId: string, tid: ObjectID, $set: any) {
|
||||
if ($set.rule && !this.RULES[$set.rule]) throw new ValidationError('rule');
|
||||
if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule');
|
||||
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(domainId, tid);
|
||||
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
|
||||
RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
|
||||
return await document.set(domainId, document.TYPE_CONTEST, tid, $set);
|
||||
}
|
||||
|
||||
@@ -489,8 +504,14 @@ export async function getRelated(domainId: string, pid: number) {
|
||||
return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid }).toArray();
|
||||
}
|
||||
|
||||
export function getStatus(domainId: string, tid: ObjectID, uid: number) {
|
||||
return document.getStatus(domainId, document.TYPE_CONTEST, tid, uid);
|
||||
export async function getStatus(domainId: string, tid: ObjectID, uid: number) {
|
||||
const [tdoc, status] = await Promise.all([
|
||||
get(domainId, tid),
|
||||
document.getStatus(domainId, document.TYPE_CONTEST, tid, uid),
|
||||
]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
if (isLocked(tdoc)) Object.assign(status, RULES[tdoc.rule].stat(tdoc, status.journal || [], true));
|
||||
return status;
|
||||
}
|
||||
|
||||
export async function updateStatus(
|
||||
@@ -556,6 +577,12 @@ export function isDone(tdoc: Tdoc) {
|
||||
return tdoc.endAt <= new Date();
|
||||
}
|
||||
|
||||
export function isLocked(tdoc: Tdoc) {
|
||||
if (!tdoc.lockAt) return false;
|
||||
const now = new Date();
|
||||
return (tdoc.lockAt < now && now < tdoc.endAt);
|
||||
}
|
||||
|
||||
export function isExtended(tdoc: Tdoc) {
|
||||
const now = new Date().getTime();
|
||||
return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime();
|
||||
@@ -627,11 +654,12 @@ export function canShowScoreboard(tdoc: Tdoc<30>, allowPermOverride = true) {
|
||||
}
|
||||
|
||||
export async function getScoreboard(
|
||||
domainId: string, tid: ObjectID,
|
||||
isExport = false, page: number,
|
||||
this: Handler, domainId: string, tid: ObjectID,
|
||||
isExport = false, page: number, ignoreLock = false,
|
||||
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, ProblemDict, number]> {
|
||||
const tdoc = await get(domainId, tid);
|
||||
if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
|
||||
if (ignoreLock) delete tdoc.lockAt;
|
||||
const tsdocsCursor = getMultiStatus(domainId, { docId: tid }).sort(RULES[tdoc.rule].statusSort);
|
||||
const pdict = await problem.getList(domainId, tdoc.pids, true);
|
||||
const [rows, udict, nPages] = await RULES[tdoc.rule].scoreboard(
|
||||
@@ -684,6 +712,7 @@ global.Hydro.model.contest = {
|
||||
isNotStarted,
|
||||
isOngoing,
|
||||
isDone,
|
||||
isLocked,
|
||||
isExtended,
|
||||
statusText,
|
||||
getStatusText,
|
||||
|
||||
@@ -286,6 +286,7 @@ export class ProblemModel {
|
||||
group: string[] = [], doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC,
|
||||
indexByDocIdOnly = false,
|
||||
): Promise<ProblemDict> {
|
||||
if (!pids?.length) return [];
|
||||
const r: Record<number, ProblemDoc> = {};
|
||||
const l: Record<string, ProblemDoc> = {};
|
||||
const q: any = { docId: { $in: pids } };
|
||||
|
||||
@@ -70,7 +70,10 @@ class WorkerService implements BaseService {
|
||||
try {
|
||||
logger.debug('Worker task: %o', doc);
|
||||
const start = Date.now();
|
||||
await this.handlers[doc.subType](doc);
|
||||
await Promise.race([
|
||||
this.handlers[doc.subType](doc),
|
||||
new Promise((resolve) => setTimeout(resolve, 300000)),
|
||||
]);
|
||||
const spent = Date.now() - start;
|
||||
if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc);
|
||||
} catch (e) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import problem from './model/problem';
|
||||
import RecordModel from './model/record';
|
||||
import StorageModel from './model/storage';
|
||||
import * as system from './model/system';
|
||||
import TaskModel from './model/task';
|
||||
import user from './model/user';
|
||||
import {
|
||||
iterateAllDomain, iterateAllProblem, iterateAllPsdoc, iterateAllUser,
|
||||
@@ -673,6 +674,14 @@ const scripts: UpgradeScript[] = [
|
||||
);
|
||||
return true;
|
||||
},
|
||||
async function _58_59() {
|
||||
const _FRESH_INSTALL_IGNORE = 1;
|
||||
const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray();
|
||||
for (const task of tasks) {
|
||||
await TaskModel.add({ ...task, subType: 'contest', operation: ['unhide'] });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
];
|
||||
|
||||
export default scripts;
|
||||
Reference in new issue
Block a user