core: contest: scoreboard-view: add advanced checker

This commit is contained in:
undefined committed 2026-07-19 19:44:23 +00:00
1 parent aba8d9c755
commit 06640c90fb
2 files changed
+24 -9

No files matched your search

+10 -6
View File
@@ -825,6 +825,7 @@ export interface ScoreboardView<T extends { [key: string]: keyof BuiltinInput |
cacheTime?: number; // in seconds cacheTime?: number; // in seconds
args: T; args: T;
display: (this: ContestScoreboardHandler, args: ParseArgs<T>) => Promise<void>; display: (this: ContestScoreboardHandler, args: ParseArgs<T>) => Promise<void>;
checker?: (this: ContestScoreboardHandler) => boolean;
} }
export class ContestScoreboardHandler extends ContestDetailBaseHandler { export class ContestScoreboardHandler extends ContestDetailBaseHandler {
@@ -884,16 +885,17 @@ class ScoreboardService extends Service {
addView<T extends { [key: string]: keyof BuiltinInput | AnyFunction | Type<any> }>( addView<T extends { [key: string]: keyof BuiltinInput | AnyFunction | Type<any> }>(
id: string, name: string, args: T, id: string, name: string, args: T,
{ display, supportedRules, cacheTime }: { { display, supportedRules, cacheTime, checker }: {
display: (this: ContestScoreboardHandler, args: ParseArgs<T>) => Promise<void>; display: (this: ContestScoreboardHandler, args: ParseArgs<T>) => Promise<void>;
supportedRules: string[]; supportedRules: string[];
cacheTime?: number; cacheTime?: number;
checker?: (this: ContestScoreboardHandler) => boolean;
}, },
) { ) {
if (this.views[id]) throw new Error(`View ${id} already exists`); if (this.views[id]) throw new Error(`View ${id} already exists`);
this.ctx.effect(() => { this.ctx.effect(() => {
this.views[id] = { this.views[id] = {
id, name, args, display, supportedRules, cacheTime, id, name, args, display, supportedRules, cacheTime, checker,
}; };
return () => { return () => {
delete this.views[id]; delete this.views[id];
@@ -901,9 +903,11 @@ class ScoreboardService extends Service {
}); });
} }
getAvailableViews(rule: string) { getAvailableViews(rule: string, handler: ContestScoreboardHandler) {
return Object.fromEntries(Object.values(this.views).filter((i) => i.supportedRules.includes(rule) || i.supportedRules.includes('*')) return Object.fromEntries(Object.values(this.views).filter((i) => (
.map((i) => [i.id, i.name])); (i.supportedRules.includes(rule) || i.supportedRules.includes('*'))
&& (!i.checker || i.checker.call(handler))
)).map((i) => [i.id, i.name]));
} }
getView(id: string) { getView(id: string) {
@@ -963,7 +967,7 @@ export async function apply(ctx: Context) {
const page_name = tdoc.rule === 'homework' const page_name = tdoc.rule === 'homework'
? 'homework_scoreboard' ? 'homework_scoreboard'
: 'contest_scoreboard'; : 'contest_scoreboard';
const availableViews = scoreboard.getAvailableViews(tdoc.rule); const availableViews = scoreboard.getAvailableViews(tdoc.rule, this);
this.response.body = { this.response.body = {
tdoc: this.tdoc, tsdoc: this.tsdocAsPublic(), rows, udict, pdict, page_name, groups, availableViews, tdoc: this.tdoc, tsdoc: this.tsdocAsPublic(), rows, udict, pdict, page_name, groups, availableViews,
}; };
+14 -3
View File
@@ -144,6 +144,7 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
...lockDuration && { ...lockDuration && {
scoreboard_freeze_duration: moment().startOf('day').seconds(lockDuration).format('HH:mm:ss.SSS'), scoreboard_freeze_duration: moment().startOf('day').seconds(lockDuration).format('HH:mm:ss.SSS'),
}, },
scoreboard_type: tdoc.rule === 'acm' ? 'pass-fail' : 'score',
penalty_time: 20, penalty_time: 20,
}), }),
...Object.keys(STATUS_SHORT_TEXTS).map((i) => getFeed('judgement-types', { ...Object.keys(STATUS_SHORT_TEXTS).map((i) => getFeed('judgement-types', {
@@ -265,7 +266,12 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError(); if (!ContestModel.isDone(tdoc)) throw new ContestNotEndedError();
this.binary(await generateCdpZip(tdoc), `contest-${tdoc._id}-cdp.zip`); this.binary(await generateCdpZip(tdoc), `contest-${tdoc._id}-cdp.zip`);
}, },
supportedRules: ['acm', 'oi'], checker() {
return ContestModel.isDone(this.tdoc)
&& !!this.tdoc.lockAt
&& (this.user.own(this.tdoc) || this.user.hasPerm(PERM.PERM_EDIT_CONTEST));
},
supportedRules: ['acm', 'oi', 'ioi'],
}); });
scoreboard.addView('resolver', 'Resolver', { tdoc: 'tdoc' }, { scoreboard.addView('resolver', 'Resolver', { tdoc: 'tdoc' }, {
@@ -279,10 +285,15 @@ export function apply(ctx: Context, config: ReturnType<typeof Config>) {
const source = new URL(`/d/${tdoc.domainId}/contest/${tdoc._id}/resolver-cdp/${tokenId}`, SystemModel.get('server.url')).toString(); const source = new URL(`/d/${tdoc.domainId}/contest/${tdoc._id}/resolver-cdp/${tokenId}`, SystemModel.get('server.url')).toString();
const target = new URL('https://resolver.hydrooj.com'); const target = new URL('https://resolver.hydrooj.com');
target.searchParams.set('source', source); target.searchParams.set('source', source);
target.searchParams.set('mode', tdoc.rule === 'oi' ? 'oi' : 'acm'); target.searchParams.set('mode', tdoc.rule === 'acm' ? 'acm' : 'oi');
this.response.redirect = target.toString(); this.response.redirect = target.toString();
}, },
supportedRules: ['acm', 'oi'], checker() {
return ContestModel.isDone(this.tdoc)
&& !!this.tdoc.lockAt
&& (this.user.own(this.tdoc) || this.user.hasPerm(PERM.PERM_EDIT_CONTEST));
},
supportedRules: ['acm', 'oi', 'ioi'],
}); });
}); });