diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 880d8351..2784cff0 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -85,7 +85,7 @@ class RecordListHandler extends ContestDetailBaseHandler { let cursor = record.getMulti(all ? '' : domainId, q).sort('_id', -1); if (!full) cursor = cursor.project(buildProjection(record.PROJECTION_LIST)); const limit = full ? 10 : system.get('pagination.record'); - const rdocs = invalid + let rdocs = invalid ? [] as RecordDoc[] : await cursor.skip((page - 1) * limit).limit(limit).toArray(); const canViewProblem = tid || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM); @@ -97,6 +97,9 @@ class RecordListHandler extends ContestDetailBaseHandler { ? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewHiddenProblem, false, problem.PROJECTION_LIST) : Object.fromEntries(uniqBy(rdocs, 'pid').map((rdoc) => [rdoc.pid, { ...problem.default, pid: rdoc.pid }])), ]); + if (this.tdoc && !this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) { + rdocs = rdocs.map((i) => contest.applyProjection(tdoc, i, this.user)); + } this.response.body = { page, rdocs, @@ -157,6 +160,9 @@ class RecordDetailHandler extends ContestDetailBaseHandler { if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid); canViewDetail = canView; this.args.tid = this.tdoc.docId; + if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) { + this.rdoc = contest.applyProjection(this.tdoc, this.rdoc, this.user); + } } // eslint-disable-next-line prefer-const @@ -237,6 +243,7 @@ class RecordMainConnectionHandler extends ConnectionHandler { status: number; pretest = false; tdoc: Tdoc<30>; + applyProjection = false; @param('tid', Types.ObjectId, true) @param('pid', Types.ProblemId, true) @@ -253,6 +260,9 @@ class RecordMainConnectionHandler extends ConnectionHandler { if (!this.tdoc) throw new ContestNotFoundError(domainId, tid); if (pretest || contest.canShowScoreboard.call(this, this.tdoc, true)) this.tid = tid.toHexString(); else throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) { + this.applyProjection = true; + } } if (pretest) { this.pretest = true; @@ -312,6 +322,7 @@ class RecordMainConnectionHandler extends ConnectionHandler { } if (this.pretest) this.send({ rdoc: omit(rdoc, ['code', 'input']) }); else { + if (this.applyProjection) rdoc = contest.applyProjection(tdoc, rdoc, this.user); this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc, tdoc, all: this.all, @@ -323,20 +334,25 @@ class RecordMainConnectionHandler extends ConnectionHandler { class RecordDetailConnectionHandler extends ConnectionHandler { pdoc: ProblemDoc; + tdoc?: Tdoc<30>; rid: string = ''; disconnectTimeout: NodeJS.Timeout; throttleSend: any; + applyProjection = false; @param('rid', Types.ObjectId) async prepare(domainId: string, rid: ObjectId) { const rdoc = await record.get(domainId, rid); if (!rdoc) return; if (rdoc.contest && rdoc.input === undefined) { - const tdoc = await contest.get(domainId, rdoc.contest); - let canView = this.user.own(tdoc); - canView ||= contest.canShowRecord.call(this, tdoc); - canView ||= this.user._id === rdoc.uid && contest.canShowSelfRecord.call(this, tdoc); + this.tdoc = await contest.get(domainId, rdoc.contest); + let canView = this.user.own(this.tdoc); + canView ||= contest.canShowRecord.call(this, this.tdoc); + canView ||= this.user._id === rdoc.uid && contest.canShowSelfRecord.call(this, this.tdoc); if (!canView) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + if (!this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_EDIT_CONTEST)) { + this.applyProjection = true; + } } const [pdoc, self] = await Promise.all([ problem.get(rdoc.domainId, rdoc.pid), @@ -378,6 +394,7 @@ class RecordDetailConnectionHandler extends ConnectionHandler { clearTimeout(this.disconnectTimeout); this.disconnectTimeout = null; } + if (this.applyProjection && rdoc.input === undefined) rdoc = contest.applyProjection(this.tdoc, rdoc, this.user); // TODO: frontend doesn't support incremental update // if ($set) this.send({ $set, $push }); if (![STATUS.STATUS_WAITING, STATUS.STATUS_JUDGING, STATUS.STATUS_COMPILING, STATUS.STATUS_FETCHED].includes(rdoc.status)) { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 76083269..f6231309 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -6,7 +6,7 @@ import { ContestScoreboardHiddenError, ValidationError, } from '../error'; import { - BaseUserDict, ContestRule, ContestRules, ProblemDict, + BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc, ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc, } from '../interface'; import ranked from '../lib/rank'; @@ -32,6 +32,43 @@ interface AcmDetail extends AcmJournal { real: number; } +export function isNew(tdoc: Tdoc, days = 1) { + const readyAt = tdoc.beginAt.getTime(); + return Date.now() < readyAt - days * Time.day; +} + +export function isUpcoming(tdoc: Tdoc, days = 7) { + const now = Date.now(); + const readyAt = tdoc.beginAt.getTime(); + return (now > readyAt - days * Time.day && now < readyAt); +} + +export function isNotStarted(tdoc: Tdoc) { + return (new Date()) < tdoc.beginAt; +} + +export function isOngoing(tdoc: Tdoc, tsdoc?: any) { + const now = new Date(); + 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) { + if (tdoc.endAt <= new Date()) return true; + if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return true; + return false; +} + +export function isLocked(tdoc: Tdoc, time = new Date()) { + if (!tdoc.lockAt) return false; + return tdoc.lockAt < time && !tdoc.unlocked; +} + +export function isExtended(tdoc: Tdoc) { + const now = new Date().getTime(); + return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); +} + function buildContestRule(def: ContestRule): ContestRule; function buildContestRule(def: Partial>, baseRule: ContestRule): ContestRule; function buildContestRule(def: Partial>, baseRule: ContestRule = {} as any) { @@ -217,6 +254,16 @@ const acm = buildContestRule({ async ranked(tdoc, cursor) { return await ranked(cursor, (a, b) => a.accept === b.accept && a.time === b.time); }, + applyProjection(tdoc, rdoc) { + if (isDone(tdoc)) return rdoc; + delete rdoc.time; + delete rdoc.memory; + rdoc.testCases = []; + rdoc.judgeTexts = []; + delete rdoc.subtasks; + delete rdoc.score; + return rdoc; + }, }); const oi = buildContestRule({ @@ -367,6 +414,18 @@ const oi = buildContestRule({ async ranked(tdoc, cursor) { return await ranked(cursor, (a, b) => a.score === b.score); }, + applyProjection(tdoc, rdoc) { + if (isDone(tdoc)) return rdoc; + delete rdoc.status; + rdoc.compilerTexts = []; + rdoc.judgeTexts = []; + delete rdoc.memory; + delete rdoc.time; + delete rdoc.score; + rdoc.testCases = []; + delete rdoc.subtasks; + return rdoc; + }, }); const ioi = buildContestRule({ @@ -376,6 +435,9 @@ const ioi = buildContestRule({ showRecord: (tdoc, now) => now > tdoc.endAt && !isLocked(tdoc), showSelfRecord: () => true, showScoreboard: (tdoc, now) => now > tdoc.beginAt, + applyProjection(_, rdoc) { + return rdoc; + }, }, oi); const strictioi = buildContestRule({ @@ -501,6 +563,9 @@ const ledo = buildContestRule({ } return row; }, + applyProjection(_, rdoc) { + return rdoc; + }, }, oi); const homework = buildContestRule({ @@ -650,6 +715,9 @@ const homework = buildContestRule({ async ranked(tdoc, cursor) { return await ranked(cursor, (a, b) => a.score === b.score); }, + applyProjection(_, rdoc) { + return rdoc; + }, }); export const RULES: ContestRules = { @@ -780,44 +848,6 @@ export function getMultiStatus(domainId: string, query: any) { return document.getMultiStatus(domainId, document.TYPE_CONTEST, query); } -export function isNew(tdoc: Tdoc, days = 1) { - const now = new Date().getTime(); - const readyAt = tdoc.beginAt.getTime(); - return (now < readyAt - days * Time.day); -} - -export function isUpcoming(tdoc: Tdoc, days = 7) { - const now = Date.now(); - const readyAt = tdoc.beginAt.getTime(); - return (now > readyAt - days * Time.day && now < readyAt); -} - -export function isNotStarted(tdoc: Tdoc) { - return (new Date()) < tdoc.beginAt; -} - -export function isOngoing(tdoc: Tdoc, tsdoc?: any) { - const now = new Date(); - 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) { - if (tdoc.endAt <= new Date()) return true; - if (tsdoc && tdoc.duration && tsdoc.startAt <= new Date(Date.now() - Math.floor(tdoc.duration * Time.hour))) return true; - return false; -} - -export function isLocked(tdoc: Tdoc, time = new Date()) { - if (!tdoc.lockAt) return false; - return tdoc.lockAt < time && !tdoc.unlocked; -} - -export function isExtended(tdoc: Tdoc) { - const now = new Date().getTime(); - return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); -} - export function setStatus(domainId: string, tid: ObjectId, uid: number, $set: any) { return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); } @@ -942,6 +972,11 @@ export function getMultiClarification(domainId: string, tid: ObjectId, owner = 0 ).sort('_id', -1).toArray(); } +export function applyProjection(tdoc: Tdoc<30>, rdoc: RecordDoc, udoc: User) { + if (!RULES[tdoc.rule]) return rdoc; + return RULES[tdoc.rule].applyProjection(tdoc, rdoc, udoc); +} + export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( isNew(tdoc) ? 'New' @@ -990,5 +1025,6 @@ global.Hydro.model.contest = { isDone, isLocked, isExtended, + applyProjection, statusText, }; diff --git a/packages/ui-default/components/scratchpad/ScratchpadRecordsRowContainer.jsx b/packages/ui-default/components/scratchpad/ScratchpadRecordsRowContainer.jsx index 425f969d..f44aab1c 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadRecordsRowContainer.jsx +++ b/packages/ui-default/components/scratchpad/ScratchpadRecordsRowContainer.jsx @@ -9,7 +9,7 @@ import { emulateAnchorClick, i18n, mongoId, substitute, } from 'vj/utils'; -const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status]; +const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status] && data.testCases?.length; const getRecordDetail = (data) => { if (!shouldShowDetail(data)) { diff --git a/packages/ui-default/templates/record_detail_summary.html b/packages/ui-default/templates/record_detail_summary.html index 4944d454..699f7651 100644 --- a/packages/ui-default/templates/record_detail_summary.html +++ b/packages/ui-default/templates/record_detail_summary.html @@ -1,8 +1,14 @@
-
{{ _('Score') }}
-
{{ rdoc['score'] }}
-
{{ _('Total Time') }}
-
{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ rdoc.time|round|int }}ms
-
{{ _('Peak Memory') }}
-
{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ size(rdoc.memory, 1024) }}
+ {% if typeof(rdoc['score']) == 'number' %} +
{{ _('Score') }}
+
{{ rdoc['score'] }}
+ {% endif %} + {% if rdoc['time'] %} +
{{ _('Total Time') }}
+
{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ rdoc.time|round|int }}ms
+ {% endif %} + {% if rdoc['memory'] %} +
{{ _('Peak Memory') }}
+
{% if rdoc['status'] == STATUS.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ size(rdoc.memory, 1024) }}
+ {% endif %}