diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 569bd0009..2b49e7abb 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -14,7 +14,7 @@ import { ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError, MethodNotAllowedError, NotAssignedError, NotFoundError, PermissionError, ValidationError, } from '../error'; -import { FileInfo, ScoreboardConfig, Tdoc } from '../interface'; +import { ContestStatusDoc, FileInfo, ScoreboardConfig, Tdoc } from '../interface'; import { PERM, PRIV, STATUS } from '../model/builtin'; import * as contest from '../model/contest'; import * as discussion from '../model/discussion'; @@ -77,7 +77,7 @@ export class ContestListHandler extends Handler { export class ContestDetailBaseHandler extends Handler { tdoc?: Tdoc; - tsdoc?: any; + tsdoc?: ContestStatusDoc; @param('tid', Types.ObjectId, true) async __prepare(domainId: string, tid: ObjectId) { diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index dc1428215..f83e2792c 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -127,7 +127,7 @@ class HomeworkDetailHandler extends Handler { const valid = (tsdoc.journal || []).filter((p) => this.tdoc.pids.includes(p.pid)); for (const pdetail of valid) { psdict[pdetail.pid] = pdetail; - rdict[pdetail.rid] = { _id: pdetail.rid }; + rdict[pdetail.rid.toHexString()] = { _id: pdetail.rid }; } if (contest.canShowSelfRecord.call(this, this.tdoc) && valid.length) { rdict = await record.getList(domainId, valid.map((pdetail) => pdetail.rid)); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 9aea232c6..a2bd37421 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -368,7 +368,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { if (this.tdoc && this.tsdoc) { const fields = ['attend', 'startAt', 'endAt']; if (contest.canShowSelfRecord.call(this, this.tdoc, true)) fields.push('detail'); - this.tsdoc = pick(this.tsdoc, fields); + this.tsdoc = pick(this.tsdoc, fields) as typeof this.tsdoc; this.response.body.tsdoc = this.tsdoc; } this.response.template = 'problem_detail.html'; diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 0f0a5a037..115bad631 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -195,7 +195,7 @@ export class RecordDetailHandler extends ContestDetailBaseHandler { this.tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id); canViewCode ||= this.user.own(this.tdoc); if (this.tdoc.allowViewCode && contest.isDone(this.tdoc)) { - canViewCode ||= this.tsdoc?.attend; + canViewCode ||= !!this.tsdoc?.attend; } if (!this.tsdoc?.attend && pdoc && !problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN); } else if (pdoc && !problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN); diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 630d52214..9696266c9 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -80,7 +80,7 @@ class TrainingMainHandler extends Handler { $or: [{ docId: { $in: Array.from(tids) } }, { enroll: 1 }], }).toArray(); for (const tsdoc of tsdocs) { - tsdict[tsdoc.docId] = tsdoc; + tsdict[tsdoc.docId.toHexString()] = tsdoc; enrolledTids.add(tsdoc.docId); } for (const tid of tids) enrolledTids.delete(tid); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index f2bf5bbbf..a8034123a 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -5,7 +5,7 @@ import type fs from 'fs'; import type { Dictionary, NumericDictionary } from 'lodash'; import type { Binary, FindCursor, ObjectId } from 'mongodb'; import type { - FileInfo, RecordJudgeInfo, RecordPayload, + FileInfo, RecordJudgeInfo, RecordPayload, SubtaskResult, } from '@hydrooj/common/types'; import type { Context } from './context'; import type { PrintTaskStatus } from './model/contest'; @@ -412,11 +412,70 @@ export interface OplogDoc extends Record { type: string; } +export interface ContestJournalEntry extends Record { + rid: ObjectId; + pid: number; + score: number; + status: number; + time?: number; + lang?: string; + subtasks?: Record; +} + +export interface ContestDetailEntry extends Record { + rid?: ObjectId; + pid?: number; + score?: number; + status?: number; + /** ACM: penalty + elapsed time (in seconds). */ + time?: number; + /** ACM: elapsed time since contest begin (in seconds). */ + real?: number; + /** ACM: accumulated penalty (in seconds). */ + penalty?: number; + /** ACM: number of non-accepted submissions on this problem. */ + naccept?: number; + /** Number of submissions hidden after the scoreboard is locked. */ + npending?: number; + /** Ledo: number of effective attempts. */ + ntry?: number; + /** Ledo / homework: score after rule-specific bonus or penalty. */ + penaltyScore?: number; + subtasks?: Record; +} + export interface ContestStat extends Record { - detail: Record>; + detail: Record; + display?: Record; + score?: number; + originalScore?: number; + accept?: number; + time?: number; + penaltyScore?: number; unrank?: boolean; } +export interface ContestStatusDoc extends StatusDocBase, ContestStat { + docId: ObjectId; + docType: document['TYPE_CONTEST']; + attend?: number; + subscribe?: number; + journal?: ContestJournalEntry[]; + startAt?: Date; + endAt?: Date; // 灵活时间模式的结束时间,或者是提前结束比赛的时间 + rev?: number; +} + +export interface TrainingStatusDoc extends StatusDocBase, Record { + docId: ObjectId; + docType: document['TYPE_TRAINING']; + enroll?: number; + attend?: number; + doneNids?: number[]; + donePids?: number[]; + done?: boolean; +} + export interface ScoreboardConfig { isExport: boolean; showDisplayName: boolean; @@ -436,21 +495,21 @@ export interface ContestRule { showScoreboard: (tdoc: Tdoc, now: Date) => boolean; showSelfRecord: (tdoc: Tdoc, now: Date) => boolean; showRecord: (tdoc: Tdoc, now: Date) => boolean; - stat: (this: ContestRule, tdoc: Tdoc, journal: any[]) => ContestStat & T; + stat: (this: ContestRule, tdoc: Tdoc, journal: ContestJournalEntry[]) => ContestStat & T; scoreboardHeader: ( this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, tdoc: Tdoc, pdict: ProblemDict, ) => Promise; scoreboardRow: ( this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, - tdoc: Tdoc, pdict: ProblemDict, udoc: BaseUser, rank: number, tsdoc: ContestStat & T, + tdoc: Tdoc, pdict: ProblemDict, udoc: BaseUser, rank: number, tsdoc: ContestStatusDoc & T, meta?: any, ) => Promise; scoreboard: ( this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, - tdoc: Tdoc, pdict: ProblemDict, cursor: FindCursor, + tdoc: Tdoc, pdict: ProblemDict, cursor: FindCursor, ) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>; - ranked: (tdoc: Tdoc, cursor: FindCursor) => Promise<[number, ContestStat & T][]>; + ranked: (tdoc: Tdoc, cursor: FindCursor) => Promise<[number, ContestStatusDoc & T][]>; applyProjection: (tdoc: Tdoc, rdoc: RecordDoc, user: User) => RecordDoc; } diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index c0765c94c..0c5622858 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -9,8 +9,9 @@ import { ContestScoreboardHiddenError, ValidationError, } from '../error'; import { - BaseUserDict, ContestPrintDoc, ContestRule, ContestRules, ProblemDict, RecordDoc, - ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc, + BaseUserDict, ContestJournalEntry, ContestPrintDoc, ContestRule, ContestRules, + ContestStatusDoc, ProblemDict, RecordDoc, ScoreboardConfig, ScoreboardNode, + ScoreboardRow, SubtaskResult, Tdoc, } from '../interface'; import avatar from '../lib/avatar'; import bus from '../service/bus'; @@ -60,14 +61,14 @@ export function isNotStarted(tdoc: Tdoc) { return (new Date()) < tdoc.beginAt; } -export function isOngoing(tdoc: Tdoc, tsdoc?: any) { +export function isOngoing(tdoc: Tdoc, tsdoc?: ContestStatusDoc): boolean { const now = new Date(); if (tsdoc?.endAt && tsdoc.endAt <= now) return false; if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return false; return (tdoc.beginAt <= now && now < tdoc.endAt); } -export function isDone(tdoc: Tdoc, tsdoc?: any) { +export function isDone(tdoc: Tdoc, tsdoc?: ContestStatusDoc): boolean { if (tdoc.endAt <= new Date()) return true; if (tsdoc?.endAt && tsdoc.endAt <= new Date()) return true; if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return true; @@ -362,7 +363,7 @@ const oi = buildContestRule({ row.push({ type: 'string', value: udoc.displayName || '' }); row.push({ type: 'string', value: udoc.studentId || '' }); } - row.push({ type: 'total_score', value: tsdoc.score || 0 }); + row.push({ type: 'total_score', value: (tsdoc.score || 0).toString() }); const accepted = {}; for (const s of tsdoc.journal || []) { if (!pdict[s.pid]) continue; @@ -510,7 +511,7 @@ const strictioi = buildContestRule({ row.push({ type: 'string', value: udoc.displayName || '' }); row.push({ type: 'string', value: udoc.studentId || '' }); } - row.push({ type: 'total_score', value: tsdoc.score || 0 }); + row.push({ type: 'total_score', value: (tsdoc.score || 0).toString() }); const accepted = {}; for (const s of tsdoc.journal || []) { if (!pdict[s.pid]) continue; @@ -603,7 +604,7 @@ const ledo = buildContestRule({ } row.push({ type: 'total_score', - value: tsdoc.score || 0, + value: (tsdoc.score || 0).toString(), hover: tsdoc.score !== tsdoc.originalScore ? _('Original score: {0}').format(tsdoc.originalScore) : '', }); const accepted = {}; @@ -742,9 +743,9 @@ const homework = buildContestRule({ row.push({ type: 'string', value: udoc.displayName || '' }); row.push({ type: 'string', value: udoc.studentId || '' }); } - row.push({ type: 'string', value: tsdoc.penaltyScore || 0 }); + row.push({ type: 'string', value: (tsdoc.penaltyScore || 0).toString() }); if (config.isExport) { - row.push({ type: 'string', value: tsdoc.score || 0 }); + row.push({ type: 'string', value: (tsdoc.score || 0).toString() }); } row.push({ type: 'time', value: formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time }); const accepted = {}; @@ -758,9 +759,9 @@ const homework = buildContestRule({ } for (const pid of tdoc.pids) { const rid = tsddict[pid]?.rid; - const colScore = tsddict[pid]?.penaltyScore ?? ''; - const colOriginalScore = tsddict[pid]?.score ?? ''; - const colTime = tsddict[pid]?.time || ''; + const colScore = (tsddict[pid]?.penaltyScore ?? '').toString(); + const colOriginalScore = (tsddict[pid]?.score ?? '').toString(); + const colTime = (tsddict[pid]?.time || '').toString(); const colTimeStr = colTime ? formatSeconds(colTime, false) : ''; if (config.isExport) { row.push( @@ -805,8 +806,8 @@ export const RULES: ContestRules = { const collBalloon = db.collection('contest.balloon'); -function _getStatusJournal(tsdoc) { - return tsdoc.journal.sort((a, b) => (a.rid.getTimestamp() - b.rid.getTimestamp())); +function _getStatusJournal(tsdoc: ContestStatusDoc): ContestJournalEntry[] { + return tsdoc.journal!.sort((a, b) => (a.rid.getTimestamp().getTime() - b.rid.getTimestamp().getTime())); } export async function add( @@ -1071,7 +1072,7 @@ export function applyProjection(tdoc: Tdoc, rdoc: RecordDoc, udoc: User) { return RULES[tdoc.rule].applyProjection(tdoc, rdoc, udoc); } -export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( +export const statusText = (tdoc: Tdoc, tsdoc?: ContestStatusDoc): string => ( isNew(tdoc) ? 'New' : isUpcoming(tdoc) diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 0749a99d8..c0d886675 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -4,9 +4,9 @@ import { } from 'mongodb'; import { Context } from '../context'; import { - Content, ContestClarificationDoc, ContestPrintDoc, DiscussionDoc, - DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, - Tdoc, TrainingDoc, + Content, ContestClarificationDoc, ContestPrintDoc, ContestStatusDoc, + DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, + Tdoc, TrainingDoc, TrainingStatusDoc, } from '../interface'; import bus from '../service/bus'; import db from '../service/db'; @@ -45,6 +45,8 @@ export interface DocType { export interface DocStatusType { [TYPE_PROBLEM]: ProblemStatusDoc; + [TYPE_CONTEST]: ContestStatusDoc; + [TYPE_TRAINING]: TrainingStatusDoc; // FIXME: this need to be typed [key: number]: any; } diff --git a/packages/hydrooj/src/model/training.ts b/packages/hydrooj/src/model/training.ts index 4f940f0e0..2c6986310 100644 --- a/packages/hydrooj/src/model/training.ts +++ b/packages/hydrooj/src/model/training.ts @@ -1,14 +1,14 @@ import { flatten } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; import { TrainingAlreadyEnrollError, TrainingNotFoundError } from '../error'; -import { TrainingDoc, TrainingNode } from '../interface'; +import { TrainingDoc, TrainingNode, TrainingStatusDoc } from '../interface'; import * as document from './document'; export function getStatus(domainId: string, tid: ObjectId, uid: number) { return document.getStatus(domainId, document.TYPE_TRAINING, tid, uid); } -export function getMultiStatus(domainId: string, query: Filter) { +export function getMultiStatus(domainId: string, query: Filter) { return document.getMultiStatus(domainId, document.TYPE_TRAINING, query); } @@ -16,8 +16,8 @@ export async function getListStatus(domainId: string, uid: number, tids: ObjectI const tsdocs = await getMultiStatus( domainId, { uid, docId: { $in: Array.from(new Set(tids)) } }, ).toArray(); - const r = {}; - for (const tsdoc of tsdocs) r[tsdoc.docId] = tsdoc; + const r: Record = {}; + for (const tsdoc of tsdocs) r[tsdoc.docId.toHexString()] = tsdoc; return r; } @@ -30,7 +30,7 @@ export async function enroll(domainId: string, tid: ObjectId, uid: number) { return await document.inc(domainId, document.TYPE_TRAINING, tid, 'attend', 1); } -export function setStatus(domainId: string, tid: ObjectId, uid: number, $set: any) { +export function setStatus(domainId: string, tid: ObjectId, uid: number, $set: Partial) { return document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, $set); } diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index cebbddbf6..5869daa8c 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -102,10 +102,10 @@ export const coreScripts: MigrationScript[] = [ doc.domainId, document.TYPE_CONTEST, { docId: doc.docId }, ).toArray(); for (const ctdoc of ctdocs) { - if (!ctdoc.journal?.filter((i) => isStringPid(i.pid)).length) continue; + if (!ctdoc.journal?.filter((i) => isStringPid(i.pid as any)).length) continue; const journal = []; for (const i of ctdoc.journal) { - const pdoc = await getProblem(doc.domainId, i.pid); + const pdoc = await getProblem(doc.domainId, i.pid as any); if (pdoc) i.pid = pdoc.docId; journal.push(i); } diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index 2ad9965b2..32c0a6f49 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -1,7 +1,7 @@ import path from 'path'; import { LRUCache } from 'lru-cache'; import { - avatar, ContestModel, Context, fs, getAlphabeticId, Logger, ObjectId, + avatar, ContestJournalEntry, ContestModel, Context, fs, getAlphabeticId, Logger, ObjectId, PERM, RecordDoc, Schema, STATUS, superagent, Tdoc, Types, UserModel, } from 'hydrooj'; @@ -43,15 +43,17 @@ const statusPrivate = { [STATUS.STATUS_CANCELED]: 'CANCELED', }; -function submissionBase(tdoc: Tdoc, rdoc: RecordDoc, uid?: number) { +function submissionBase(tdoc: Tdoc, rdoc: RecordDoc | ContestJournalEntry, uid?: number) { // NOTE: rdoc can be either record, or a tsdoc detail entry - const submit = new ObjectId(rdoc._id || (rdoc as any).rid).getTimestamp().getTime(); + const id: ObjectId = '_id' in rdoc ? rdoc._id : rdoc.rid; + const submit = new ObjectId(id).getTimestamp().getTime(); + const teamId = (uid || (rdoc as RecordDoc).uid).toString(); return { problem_id: tdoc.pids.indexOf(rdoc.pid), - team_id: `${uid || rdoc.uid}`, + team_id: teamId, timestamp: Math.floor(submit - tdoc.beginAt.getTime()), - language: rdoc.lang || '', - submission_id: rdoc._id, + language: ('lang' in rdoc && rdoc.lang) || '', + submission_id: id.toHexString(), }; } @@ -76,7 +78,7 @@ async function loadContestState(tdoc: Tdoc, realtime: boolean) { }); return { submissions: tsdocs.flatMap((i) => (i.journal || []).map((j) => { - const submit = new ObjectId(j.rid as string).getTimestamp().getTime(); + const submit = new ObjectId(j.rid).getTimestamp().getTime(); const curStatus = (ended ? status : statusPrivate)[j.status] || 'SYSTEM_ERROR'; return { ...submissionBase(tdoc, j, i.uid),