From f131076d008dd8b3526a5f4c7d177a66b7dbe0b9 Mon Sep 17 00:00:00 2001 From: Lotuses-robot Date: Tue, 2 May 2023 00:46:34 +0800 Subject: [PATCH 1/5] core&ui: Add Codeforces contest mode --- packages/hydrooj/src/error.ts | 1 + packages/hydrooj/src/handler/contest.ts | 17 ++- packages/hydrooj/src/handler/problem.ts | 10 +- packages/hydrooj/src/handler/record.ts | 5 +- packages/hydrooj/src/model/contest.ts | 102 +++++++++++++++++- .../components/contest/contest.page.ts | 10 ++ .../templates/contest_problemlist.html | 13 +++ .../ui-default/templates/record_detail.html | 12 ++- 8 files changed, 160 insertions(+), 10 deletions(-) diff --git a/packages/hydrooj/src/error.ts b/packages/hydrooj/src/error.ts index 96945e63b..aa63a519a 100644 --- a/packages/hydrooj/src/error.ts +++ b/packages/hydrooj/src/error.ts @@ -109,6 +109,7 @@ export const ProblemAlreadyExistError = Err('ProblemAlreadyExistError', Forbidde export const ProblemAlreadyUsedByContestError = Err('ProblemAlreadyUsedByContestError', ForbiddenError, 'Problem {0} is already used by contest {1}.'); export const ProblemNotAllowPretestError = Err('ProblemNotAllowPretestError', ForbiddenError, 'This {0} is not allow run pretest.'); export const ProblemNotAllowLanguageError = Err('ProblemNotAllowSubmitError', ForbiddenError, 'This language is not allow to submit.'); +export const ProblemLockError = Err('ProblemLockError', ForbiddenError, 'Lock Error: {0}'); export const HackRejudgeFailedError = Err('HackRejudgeFailedError', BadRequestError, 'Cannot rejudge a hack record.'); export const CannotDeleteSystemDomainError = Err('CannotDeleteSystemDomainError', BadRequestError, 'You are not allowed to delete system domain.'); diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 6d633b95b..032333523 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -9,7 +9,7 @@ import { import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, - InvalidTokenError, NotAssignedError, PermissionError, ValidationError, + InvalidTokenError, NotAssignedError, PermissionError, ValidationError, ProblemLockError, } from '../error'; import { ScoreboardConfig, Tdoc } from '../interface'; import paginate from '../lib/paginate'; @@ -640,6 +640,20 @@ export class ContestUserHandler extends ContestManagementBaseHandler { this.back(); } } + +export class ContestProblemLockHandler extends Handler { + @param('tid', Types.ObjectId) + @param('pid', Types.UnsignedInt) + async get(domainId: string, tid: ObjectId, pid: number) { + const lockList = await contest.getLockedList(domainId, tid); + if (!lockList) throw new ProblemLockError('This contest is not lockable.'); + if (lockList[pid].includes(this.user._id)) throw new ProblemLockError('This problem has Locked before.'); + lockList[pid].push(this.user._id); + await contest.updateLockedList(domainId, tid, lockList); + this.back(); + } +} + export async function apply(ctx) { ctx.Route('contest_create', '/contest/create', ContestEditHandler); ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST); @@ -652,4 +666,5 @@ export async function apply(ctx) { ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST); + ctx.Route('contest_lock_problem', '/contest/:tid/lock', ContestProblemLockHandler, PERM.PERM_VIEW_CONTEST); } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index b47f00c83..d55b6ea31 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -11,7 +11,7 @@ import { FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError, PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError, ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError, - RecordNotFoundError, SolutionNotFoundError, ValidationError, + RecordNotFoundError, SolutionNotFoundError, ValidationError, ProblemLockError, } from '../error'; import { ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User, @@ -503,6 +503,12 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { @param('input', Types.String, true) @param('tid', Types.ObjectId, true) async post(domainId: string, lang: string, code: string, pretest = false, input = '', tid?: ObjectId) { + if (tid) { + const tdoc = await contest.get(domainId, tid); + if (tdoc.rule === 'cf' && tdoc.lockedList[this.pdoc.docId].includes(this.user._id)) { + throw new ProblemLockError("You have locked this problem."); + } + } const config = this.pdoc.config; if (typeof config === 'string' || config === null) throw new ProblemConfigError(); if (['submit_answer', 'objective'].includes(config.type)) { @@ -570,7 +576,7 @@ export class ProblemHackHandler extends ProblemDetailHandler { if (!this.rdoc || this.rdoc.pid !== this.pdoc.docId || this.rdoc.contest?.toString() !== tid?.toString()) throw new RecordNotFoundError(domainId, rid); if (tid) { - if (this.tdoc.rule !== 'codeforces') throw new HackFailedError('This contest is not hackable.'); + if (this.tdoc.rule !== 'cf') throw new HackFailedError('This contest is not hackable.'); if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId); } if (this.rdoc.uid === this.user._id) throw new HackFailedError('You cannot hack your own submission'); diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 9eae08f92..9daf8c924 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -58,7 +58,8 @@ class RecordListHandler extends ContestDetailBaseHandler { this.tdoc = tdoc; if (!tdoc) throw new ContestNotFoundError(domainId, pid); if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) { + if ((!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) + && (!pid || tdoc.rule === "cf" && !tdoc.lockedList[parseInt(pid.toString())].includes(this.user._id))) { throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) { @@ -154,6 +155,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler { let canView = this.user.own(this.tdoc); canView ||= contest.canShowRecord.call(this, this.tdoc); canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id; + canView ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id); if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid); canViewDetail = canView; this.args.tid = this.tdoc.docId; @@ -170,6 +172,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler { canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED; + canViewCode ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id); if (this.tdoc) { const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id); if (this.tdoc.allowViewCode && contest.isDone(this.tdoc)) { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 0d60214e2..60f562cf0 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -502,6 +502,87 @@ const ledo = buildContestRule({ }, }, oi); +const cf = buildContestRule({ + TEXT: 'Codeforces', + check: () => { }, + submitAfterAccept: false, + showScoreboard: (tdoc, now) => now > tdoc.beginAt, + showSelfRecord: () => true, + showRecord: (tdoc, now) => now > tdoc.endAt, + stat(tdoc, journal) { + const ntry = Counter(); + const hackSucc = Counter(); + const hackFail = Counter(); + const detail = {}; + for (const j of journal) { + if ([STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status)) continue; + // if (this.submitAfterAccept) continue; + if (j.status === STATUS.STATUS_HACK_SUCCESSFUL) hackSucc[j.pid]++; + if (j.status === STATUS.STATUS_HACK_UNSUCCESSFUL) hackFail[j.pid]++; + if (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++; + const timePenaltyScore = Math.round(Math.max(j.score * 100 - (j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000 / 60 * j.score * 100 / 250, j.score * 100 * 0.3)); + const penaltyScore = Math.max(timePenaltyScore - 50 * (ntry[j.pid]), 0); + if (!detail[j.pid] || detail[j.pid].penaltyScore < penaltyScore) { + detail[j.pid] = { + ...j, + penaltyScore, + timePenaltyScore, + ntry: ntry[j.pid], + hackFail: hackFail[j.pid], + hackSucc: hackSucc[j.pid], + }; + } + } + let score = 0; + let originalScore = 0; + for (const pid of tdoc.pids) { + if (!detail[pid]) continue; + detail[pid].score -= 50 * detail[pid].hackFail; + detail[pid].score += 100 * detail[pid].hackSucc; + score += detail[pid].penaltyScore; + originalScore += detail[pid].score; + } + return { + score, originalScore, detail, + }; + }, + async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { + const tsddict = tsdoc.detail || {}; + const row: ScoreboardRow = [ + { type: 'rank', value: rank.toString() }, + { type: 'user', value: udoc.uname, raw: tsdoc.uid }, + ]; + if (config.isExport) { + row.push({ type: 'email', value: udoc.mail }); + row.push({ type: 'string', value: udoc.school || '' }); + row.push({ type: 'string', value: udoc.displayName || '' }); + row.push({ type: 'string', value: udoc.studentId || '' }); + } + row.push({ + type: 'total_score', + value: tsdoc.score || 0, + hover: tsdoc.score !== tsdoc.originalScore ? _('Original score: {0}').format(tsdoc.originalScore) : '', + }); + for (const s of tsdoc.journal || []) { + if (!pdict[s.pid]) continue; + pdict[s.pid].nSubmit++; + if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++; + } + for (const pid of tdoc.pids) { + row.push({ + type: 'record', + value: tsddict[pid]?.penaltyScore || '', + hover: tsddict[pid]?.penaltyScore ? `${tsddict[pid].timePenaltyScore}, -${tsddict[pid].ntry}, +${tsddict[pid].hackSucc} , -${tsddict[pid].hackFail}` : '', + raw: tsddict[pid]?.rid, + style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid] + ? 'background-color: rgb(217, 240, 199);' + : undefined, + }); + } + return row; + }, +}, oi); + const homework = buildContestRule({ TEXT: 'Assignment', hidden: true, @@ -652,7 +733,7 @@ const homework = buildContestRule({ }); export const RULES: ContestRules = { - acm, oi, homework, ioi, ledo, strictioi, + acm, oi, homework, ioi, ledo, strictioi, cf, }; function _getStatusJournal(tsdoc) { @@ -667,12 +748,12 @@ export async function add( 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, + content, owner, title, rule, beginAt, endAt, pids, attend: 0, lockedList: {}, }); RULES[rule].check(data); await bus.parallel('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, + ...data, title, rule, beginAt, endAt, pids, attend: 0, rated, lockedList: pids.reduce((acc, curr) => ({ ...acc, [curr]: [] }), {}), }); await bus.parallel('contest/add', data, res); return res; @@ -890,6 +971,19 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( ? 'Live...' : 'Done'); +export async function getLockedList(domainId: string, tid: ObjectId) { + const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); + if (tdoc.rule !== 'cf') return false; + return tdoc.lockedList; +} + +export async function updateLockedList(domainId: string, tid: ObjectId, $lockList: any) { + const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); + tdoc.lockedList = $lockList; + edit(domainId, tid, tdoc); +} + + global.Hydro.model.contest = { RULES, add, @@ -922,4 +1016,6 @@ global.Hydro.model.contest = { isLocked, isExtended, statusText, + getLockedList, + updateLockedList, }; diff --git a/packages/ui-default/components/contest/contest.page.ts b/packages/ui-default/components/contest/contest.page.ts index fe98096a3..20ff5c1d8 100644 --- a/packages/ui-default/components/contest/contest.page.ts +++ b/packages/ui-default/components/contest/contest.page.ts @@ -18,6 +18,16 @@ const contestPage = new AutoloadPage('contestPage', () => { Notification.error(e.message || e); }); }); + $('.nav__logo').on('click', (ev) => { + console.log('fk'); + const id = $(this).attr('id'); + request.get(`./lock?pid=${id}`).then(() => { + Notification.success(i18n('Successfully locked')); + delay(1000).then(() => window.location.reload()); + }).catch((e) => { + Notification.error(e.message || e); + }) + }); }); export default contestPage; diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 84374a0c3..476eba245 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -49,6 +49,19 @@

{{ _('Problems') }}

{{ String.fromCharCode(65+loop.index0) }}  {{ pdict[pid].title }} + {% if tdoc.rule === "cf" %} + Hack + {% if not tdoc.lockedList[pid].includes(handler.user._id) %} +
+ + +
+ {% else %} + Locked + {% endif %} + {% endif %} {%- endfor -%} diff --git a/packages/ui-default/templates/record_detail.html b/packages/ui-default/templates/record_detail.html index 2c5c24693..d6fee3c7e 100644 --- a/packages/ui-default/templates/record_detail.html +++ b/packages/ui-default/templates/record_detail.html @@ -52,9 +52,15 @@

{{ _('Information') }}

From e3ef85e3ed5f4ab2cf392d7f8250529eff844ce0 Mon Sep 17 00:00:00 2001 From: Lotuses-robot Date: Tue, 2 May 2023 08:12:22 +0800 Subject: [PATCH 2/5] core&ui: Fix & change requested --- packages/hydrooj/src/model/contest.ts | 4 ++-- packages/ui-default/components/contest/contest.page.ts | 2 +- packages/ui-default/templates/contest_problemlist.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 60f562cf0..e097aa597 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -748,7 +748,7 @@ export async function add( 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, lockedList: {}, + content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); @@ -973,7 +973,7 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( export async function getLockedList(domainId: string, tid: ObjectId) { const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); - if (tdoc.rule !== 'cf') return false; + if (tdoc.rule !== 'cf') return null; return tdoc.lockedList; } diff --git a/packages/ui-default/components/contest/contest.page.ts b/packages/ui-default/components/contest/contest.page.ts index 20ff5c1d8..7b8739dc7 100644 --- a/packages/ui-default/components/contest/contest.page.ts +++ b/packages/ui-default/components/contest/contest.page.ts @@ -18,7 +18,7 @@ const contestPage = new AutoloadPage('contestPage', () => { Notification.error(e.message || e); }); }); - $('.nav__logo').on('click', (ev) => { + $('.btn--lock').on('click', (ev) => { console.log('fk'); const id = $(this).attr('id'); request.get(`./lock?pid=${id}`).then(() => { diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 476eba245..272bf0d28 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -54,7 +54,7 @@

{{ _('Problems') }}

{% if not tdoc.lockedList[pid].includes(handler.user._id) %}
-
From b5f7ba87b189cd54c7d6f7fce712f591cce12f0a Mon Sep 17 00:00:00 2001 From: Lotuses-robot Date: Thu, 4 May 2023 20:31:55 +0800 Subject: [PATCH 3/5] core&ui: Fix a bug about counting hacks --- packages/hydrooj/src/handler/contest.ts | 2 +- packages/hydrooj/src/model/contest.ts | 11 +++++++---- .../ui-default/components/contest/contest.page.ts | 10 ---------- .../ui-default/templates/contest_problemlist.html | 10 +++++----- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 032333523..66434a5e1 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -644,7 +644,7 @@ export class ContestUserHandler extends ContestManagementBaseHandler { export class ContestProblemLockHandler extends Handler { @param('tid', Types.ObjectId) @param('pid', Types.UnsignedInt) - async get(domainId: string, tid: ObjectId, pid: number) { + async get(domainId: string, tid: ObjectId, pid: number) { // Maybe use method get was more convenient const lockList = await contest.getLockedList(domainId, tid); if (!lockList) throw new ProblemLockError('This contest is not lockable.'); if (lockList[pid].includes(this.user._id)) throw new ProblemLockError('This problem has Locked before.'); diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e097aa597..20b867713 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -517,9 +517,12 @@ const cf = buildContestRule({ for (const j of journal) { if ([STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status)) continue; // if (this.submitAfterAccept) continue; - if (j.status === STATUS.STATUS_HACK_SUCCESSFUL) hackSucc[j.pid]++; - if (j.status === STATUS.STATUS_HACK_UNSUCCESSFUL) hackFail[j.pid]++; if (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++; + if ([STATUS.STATUS_HACK_SUCCESSFUL, STATUS.STATUS_HACK_UNSUCCESSFUL].includes(j.status)) { + if (j.status == STATUS.STATUS_HACK_SUCCESSFUL) detail[j.pid].hackSucc++; + else detail[j.pid].hackFail++; + continue; + } const timePenaltyScore = Math.round(Math.max(j.score * 100 - (j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000 / 60 * j.score * 100 / 250, j.score * 100 * 0.3)); const penaltyScore = Math.max(timePenaltyScore - 50 * (ntry[j.pid]), 0); if (!detail[j.pid] || detail[j.pid].penaltyScore < penaltyScore) { @@ -537,8 +540,8 @@ const cf = buildContestRule({ let originalScore = 0; for (const pid of tdoc.pids) { if (!detail[pid]) continue; - detail[pid].score -= 50 * detail[pid].hackFail; - detail[pid].score += 100 * detail[pid].hackSucc; + detail[pid].penaltyScore -= 50 * detail[pid].hackFail; + detail[pid].penaltyScore += 100 * detail[pid].hackSucc; score += detail[pid].penaltyScore; originalScore += detail[pid].score; } diff --git a/packages/ui-default/components/contest/contest.page.ts b/packages/ui-default/components/contest/contest.page.ts index 7b8739dc7..fe98096a3 100644 --- a/packages/ui-default/components/contest/contest.page.ts +++ b/packages/ui-default/components/contest/contest.page.ts @@ -18,16 +18,6 @@ const contestPage = new AutoloadPage('contestPage', () => { Notification.error(e.message || e); }); }); - $('.btn--lock').on('click', (ev) => { - console.log('fk'); - const id = $(this).attr('id'); - request.get(`./lock?pid=${id}`).then(() => { - Notification.success(i18n('Successfully locked')); - delay(1000).then(() => window.location.reload()); - }).catch((e) => { - Notification.error(e.message || e); - }) - }); }); export default contestPage; diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 272bf0d28..3589f2abb 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -51,16 +51,16 @@

{{ _('Problems') }}

{% if tdoc.rule === "cf" %} Hack - {% if not tdoc.lockedList[pid].includes(handler.user._id) %}
- {% else %} - Locked - {% endif %} {% endif %} From e5b4a8a5b68e8e783032e36f422bbbc0b4abceb1 Mon Sep 17 00:00:00 2001 From: Lotuses-robot Date: Sat, 6 May 2023 19:53:32 +0800 Subject: [PATCH 4/5] core&ui: Button disable feature & fix rule check --- packages/hydrooj/src/handler/contest.ts | 5 ++-- packages/hydrooj/src/handler/problem.ts | 10 ++++---- packages/hydrooj/src/handler/record.ts | 8 +++---- packages/hydrooj/src/interface.ts | 5 +++- packages/hydrooj/src/model/contest.ts | 23 ++++++++++++------- .../scratchpad/ScratchpadToolbarContainer.jsx | 3 ++- .../templates/contest_problemlist.html | 2 +- .../ui-default/templates/problem_submit.html | 9 +++++++- 8 files changed, 42 insertions(+), 23 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 66434a5e1..88109587c 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -9,7 +9,8 @@ import { import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, - InvalidTokenError, NotAssignedError, PermissionError, ValidationError, ProblemLockError, + InvalidTokenError, NotAssignedError, PermissionError, ProblemLockError, + ValidationError, } from '../error'; import { ScoreboardConfig, Tdoc } from '../interface'; import paginate from '../lib/paginate'; @@ -644,7 +645,7 @@ export class ContestUserHandler extends ContestManagementBaseHandler { export class ContestProblemLockHandler extends Handler { @param('tid', Types.ObjectId) @param('pid', Types.UnsignedInt) - async get(domainId: string, tid: ObjectId, pid: number) { // Maybe use method get was more convenient + async post(domainId: string, tid: ObjectId, pid: number) { const lockList = await contest.getLockedList(domainId, tid); if (!lockList) throw new ProblemLockError('This contest is not lockable.'); if (lockList[pid].includes(this.user._id)) throw new ProblemLockError('This problem has Locked before.'); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index d55b6ea31..8a0dd23ac 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -10,8 +10,9 @@ import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError, PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError, - ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError, - RecordNotFoundError, SolutionNotFoundError, ValidationError, ProblemLockError, + ProblemIsReferencedError, ProblemLockError, + ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError, + RecordNotFoundError, SolutionNotFoundError, ValidationError, } from '../error'; import { ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User, @@ -479,7 +480,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { if (typeof this.pdoc.config === 'string') throw new ProblemConfigError(); } - async get() { + async get(domainId: string, tid?: ObjectId) { this.response.template = 'problem_submit.html'; const langRange = (typeof this.pdoc.config === 'object' && this.pdoc.config.langs) ? Object.fromEntries(this.pdoc.config.langs.map((i) => [i, setting.langs[i]?.display || i])) @@ -488,6 +489,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { langRange, pdoc: this.pdoc, udoc: this.udoc, + tdoc: this.tdoc, title: this.pdoc.title, page_name: this.tdoc ? this.tdoc.rule === 'homework' @@ -506,7 +508,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { if (tid) { const tdoc = await contest.get(domainId, tid); if (tdoc.rule === 'cf' && tdoc.lockedList[this.pdoc.docId].includes(this.user._id)) { - throw new ProblemLockError("You have locked this problem."); + throw new ProblemLockError('You have locked this problem.'); } } const config = this.pdoc.config; diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 9daf8c924..8130ae2a2 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -1,3 +1,4 @@ +import { pid } from 'process'; import { omit, pick, throttle, uniqBy, } from 'lodash'; @@ -58,8 +59,7 @@ class RecordListHandler extends ContestDetailBaseHandler { this.tdoc = tdoc; if (!tdoc) throw new ContestNotFoundError(domainId, pid); if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - if ((!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) - && (!pid || tdoc.rule === "cf" && !tdoc.lockedList[parseInt(pid.toString())].includes(this.user._id))) { + if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true, pid ? await problem.get(domainId, pid) : null)) { throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) { @@ -153,9 +153,8 @@ class RecordDetailHandler extends ContestDetailBaseHandler { } else if (rdoc.contest) { this.tdoc = await contest.get(domainId, rdoc.contest); let canView = this.user.own(this.tdoc); - canView ||= contest.canShowRecord.call(this, this.tdoc); + canView ||= contest.canShowRecord.call(this, this.tdoc, true, await problem.get(domainId, rdoc.pid)); canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id; - canView ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id); if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid); canViewDetail = canView; this.args.tid = this.tdoc.docId; @@ -172,7 +171,6 @@ class RecordDetailHandler extends ContestDetailBaseHandler { canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE); canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED; - canViewCode ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id); if (this.tdoc) { const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id); if (this.tdoc.allowViewCode && contest.isDone(this.tdoc)) { diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 9d0f77c68..2bd243c1b 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -407,6 +407,9 @@ export interface Tdoc { submitAfterAccept: boolean; showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean; showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean; - showRecord: (tdoc: Tdoc<30>, now: Date) => boolean; + showRecord: (tdoc: Tdoc<30>, now: Date, user?: User, pdoc?: ProblemDoc) => boolean; stat: (this: ContestRule, tdoc: Tdoc<30>, journal: any[]) => ContestStat & T; scoreboardHeader: ( this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 20b867713..d955a865c 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -14,7 +14,7 @@ import * as bus from '../service/bus'; import type { Handler } from '../service/server'; import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin'; import * as document from './document'; -import problem from './problem'; +import problem, { ProblemDoc } from './problem'; import user, { User } from './user'; interface AcmJournal { @@ -508,7 +508,11 @@ const cf = buildContestRule({ submitAfterAccept: false, showScoreboard: (tdoc, now) => now > tdoc.beginAt, showSelfRecord: () => true, - showRecord: (tdoc, now) => now > tdoc.endAt, + showRecord: (tdoc, now, user, pdoc) => { + if (now > tdoc.endAt) return true; + if (pdoc && tdoc.lockedList[pdoc.docId].includes(user._id)) return true; + return false; + }, stat(tdoc, journal) { const ntry = Counter(); const hackSucc = Counter(); @@ -519,11 +523,13 @@ const cf = buildContestRule({ // if (this.submitAfterAccept) continue; if (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++; if ([STATUS.STATUS_HACK_SUCCESSFUL, STATUS.STATUS_HACK_UNSUCCESSFUL].includes(j.status)) { - if (j.status == STATUS.STATUS_HACK_SUCCESSFUL) detail[j.pid].hackSucc++; + if (j.status === STATUS.STATUS_HACK_SUCCESSFUL) detail[j.pid].hackSucc++; else detail[j.pid].hackFail++; continue; } - const timePenaltyScore = Math.round(Math.max(j.score * 100 - (j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000 / 60 * j.score * 100 / 250, j.score * 100 * 0.3)); + const timePenaltyScore = Math.round(Math.max(j.score * 100 + - ((j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) * (j.score * 100)) / (250 * 60000), + j.score * 100 * 0.3)); const penaltyScore = Math.max(timePenaltyScore - 50 * (ntry[j.pid]), 0); if (!detail[j.pid] || detail[j.pid].penaltyScore < penaltyScore) { detail[j.pid] = { @@ -575,7 +581,9 @@ const cf = buildContestRule({ row.push({ type: 'record', value: tsddict[pid]?.penaltyScore || '', - hover: tsddict[pid]?.penaltyScore ? `${tsddict[pid].timePenaltyScore}, -${tsddict[pid].ntry}, +${tsddict[pid].hackSucc} , -${tsddict[pid].hackFail}` : '', + hover: tsddict[pid]?.penaltyScore + ? `${tsddict[pid].timePenaltyScore}, -${tsddict[pid].ntry}, +${tsddict[pid].hackSucc} , -${tsddict[pid].hackFail}` + : '', raw: tsddict[pid]?.rid, style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid] ? 'background-color: rgb(217, 240, 199);' @@ -932,8 +940,8 @@ export function canViewHiddenScoreboard(this: { user: User }, tdoc: Tdoc<30>) { return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } -export function canShowRecord(this: { user: User }, tdoc: Tdoc<30>, allowPermOverride = true) { - if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true; +export function canShowRecord(this: { user: User }, tdoc: Tdoc<30>, allowPermOverride = true, pdoc?: ProblemDoc) { + if (RULES[tdoc.rule].showRecord(tdoc, new Date(), this.user, pdoc)) return true; if (allowPermOverride && canViewHiddenScoreboard.call(this, tdoc)) return true; return false; } @@ -986,7 +994,6 @@ export async function updateLockedList(domainId: string, tid: ObjectId, $lockLis edit(domainId, tid, tdoc); } - global.Hydro.model.contest = { RULES, add, diff --git a/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx b/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx index 0efbc32a9..2c3671274 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx +++ b/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx @@ -123,7 +123,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool )} this.props.postSubmit(this.props)} data-global-hotkey="f10" diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 3589f2abb..79148e80f 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -51,7 +51,7 @@

{{ _('Problems') }}

{% if tdoc.rule === "cf" %} Hack -
+