From f29971a30d6eb3b5d2d9ec7baea190ec5c892eb6 Mon Sep 17 00:00:00 2001 From: Baoshuo Date: Mon, 3 Aug 2026 12:42:33 +0800 Subject: [PATCH] core: admin can remove contestant before contest start (#1196) --- packages/hydrooj/locales/ko.yaml | 1 + packages/hydrooj/locales/zh.yaml | 1 + packages/hydrooj/locales/zh_TW.yaml | 1 + packages/hydrooj/src/error.ts | 1 + packages/hydrooj/src/handler/contest.ts | 14 ++++++++++++-- packages/hydrooj/src/model/contest.ts | 7 +++++++ packages/ui-default/locales/ko.yaml | 4 ++++ packages/ui-default/locales/zh.yaml | 4 ++++ packages/ui-default/locales/zh_TW.yaml | 4 ++++ packages/ui-default/pages/contest_user.page.ts | 13 ++++++++++++- .../templates/partials/contest_user.html | 3 +++ 11 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/hydrooj/locales/ko.yaml b/packages/hydrooj/locales/ko.yaml index ce6cb36ec..ead825801 100644 --- a/packages/hydrooj/locales/ko.yaml +++ b/packages/hydrooj/locales/ko.yaml @@ -685,6 +685,7 @@ The user is too lazy to leave something here...: 설명이 없습니다... (´ There are no contests...: 대회 없음 There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯ This contest is not live.: 대회가 실시간이 아닙니다. +This contest has already started.: 대회가 이미 시작되었습니다. This email was sent by {0} automatically, and please do not reply directly.: '{0}으로 이메일을 보냈습니다.' This homework is not open and you cannot view problems.: 과제를 열지 않았기에 문제를 볼 수 없습니다. This homework is not open.: 과제가 아직 닫혀 있습니다. diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 97a7b329c..530fc2415 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -811,6 +811,7 @@ theme: 主题 There are no contests...: 没有比赛… There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯ This contest is not live.: 比赛没有开始。 +This contest has already started.: 比赛已开始。 This email was sent by {0} automatically, and please do not reply directly.: 这封邮件由 {0} 自动发送,请勿直接回复。 This homework is not open and you cannot view problems.: 该作业还未到开放时间,您无法查看作业题目。 This homework is not open.: 该作业还未开放递交。 diff --git a/packages/hydrooj/locales/zh_TW.yaml b/packages/hydrooj/locales/zh_TW.yaml index 72380e380..d648ac8ba 100644 --- a/packages/hydrooj/locales/zh_TW.yaml +++ b/packages/hydrooj/locales/zh_TW.yaml @@ -808,6 +808,7 @@ theme: 主題 There are no contests...: 沒有比賽… There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯ This contest is not live.: 比賽沒有開始。 +This contest has already started.: 比賽已開始。 This email was sent by {0} automatically, and please do not reply directly.: 這封郵件由 {0} 自動傳送,請勿直接回復。 This homework is not open and you cannot view problems.: 功課尚未開放,不能檢視題目。 This homework is not open.: 功課尚未開放遞交。 diff --git a/packages/hydrooj/src/error.ts b/packages/hydrooj/src/error.ts index 94d42010e..45b8de758 100644 --- a/packages/hydrooj/src/error.ts +++ b/packages/hydrooj/src/error.ts @@ -35,6 +35,7 @@ export const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenE export const RequireProError = Err('RequireProError', ForbiddenError, 'RequireProError'); export const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest."); export const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.'); +export const ContestAlreadyStartedError = Err('ContestAlreadyStartedError', ForbiddenError, 'This contest has already started.'); export const ContestNotEndedError = Err('ContestNotEndedError', ForbiddenError, 'This contest is not ended.'); export const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.'); export const TrainingAlreadyEnrollError = Err('TrainingAlreadyEnrollError', ForbiddenError, "You've already enrolled this training."); diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 57082f314..d574e762a 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -10,8 +10,8 @@ import { } from '@hydrooj/utils/lib/utils'; import { Context, Service } from '../context'; import { - BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, - ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, + BadRequestError, ContestAlreadyStartedError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, + ContestNotLiveError, ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError, MethodNotAllowedError, NotAssignedError, NotFoundError, PermissionError, ValidationError, } from '../error'; import { ContestStatusDoc, FileInfo, ScoreboardConfig, Tdoc } from '../interface'; @@ -761,6 +761,16 @@ export class ContestUserHandler extends ContestManagementBaseHandler { await contest.setStatus(domainId, tid, uid, null, { endAt: '' }); this.back(); } + + @param('tid', Types.ObjectId) + @param('uid', Types.Int) + async postRemoveUser(domainId: string, tid: ObjectId, uid: number) { + if (!contest.isNotStarted(this.tdoc)) throw new ContestAlreadyStartedError(); + const tsdoc = await contest.getStatus(domainId, tid, uid); + if (!tsdoc?.attend) throw new ContestNotAttendedError(uid); + await contest.cancelAttend(domainId, tid, uid); + this.back(); + } } export class ContestBalloonHandler extends ContestManagementBaseHandler { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 0c5622858..ba1f8faee 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -939,6 +939,12 @@ export async function attend(domainId: string, tid: ObjectId, uid: number, paylo return {}; } +export async function cancelAttend(domainId: string, tid: ObjectId, uid: number) { + await document.deleteMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid, uid }); + await document.inc(domainId, document.TYPE_CONTEST, tid, 'attend', -1); + return {}; +} + export function getMultiStatus(domainId: string, query: any) { return document.getMultiStatus(domainId, document.TYPE_CONTEST, query); } @@ -1148,6 +1154,7 @@ global.Hydro.model.contest = { getListStatus, getMultiStatus, attend, + cancelAttend, edit, del, get, diff --git a/packages/ui-default/locales/ko.yaml b/packages/ui-default/locales/ko.yaml index d2c6006e2..e359b6687 100644 --- a/packages/ui-default/locales/ko.yaml +++ b/packages/ui-default/locales/ko.yaml @@ -59,6 +59,9 @@ Active Sessions: 활성 세션 Add module: 앱 모듈 Add new data: 데이터 추가 Add User: 사용자 추가 +Cancel Registration: 참가 취소 +Registration cancelled.: 참가가 취소되었습니다. +Are you sure to cancel the registration of this user?: 이 사용자의 참가를 취소하시겠습니까? Add: 추가 All {0} Contests: 전체 {0} 대회 All Contests: 전체 대회 @@ -690,6 +693,7 @@ Their account will not be deleted and they will be with the guest role until the There are no contests...: 대회 없음 There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯ This contest is not live.: 대회가 실시간이 아닙니다. +This contest has already started.: 대회가 이미 시작되었습니다. This email was sent by {0} automatically, and please do not reply directly.: '{0}으로 이메일을 보냈습니다.' This homework is not open and you cannot view problems.: 과제를 열지 않았기에 문제를 볼 수 없습니다. This homework is not open.: 과제가 아직 닫혀 있습니다. diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 153729051..833e4bd68 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -87,6 +87,9 @@ Add prefix/suffix: 添加前缀/后缀 Add suffix: 添加后缀 Add testcase: 添加测试点 Add User: 添加用户 +Cancel Registration: 取消报名 +Registration cancelled.: 报名已取消。 +Are you sure to cancel the registration of this user?: 确定要取消该用户的报名吗? Add: 添加 Additional Files: 附加文件 additional_file: 附加文件 @@ -966,6 +969,7 @@ There are no contests...: 没有比赛… There are no files currently.: 目前还没有文件。 There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯ This contest is not live.: 比赛没有开始。 +This contest has already started.: 比赛已开始。 This email was sent by {0} automatically, and please do not reply directly.: 这封邮件由 {0} 自动发送,请勿直接回复。 This homework is not open and you cannot view problems.: 该作业还未到开放时间,您无法查看作业题目。 This homework is not open.: 该作业还未开放递交。 diff --git a/packages/ui-default/locales/zh_TW.yaml b/packages/ui-default/locales/zh_TW.yaml index 76124346b..6ec057c91 100644 --- a/packages/ui-default/locales/zh_TW.yaml +++ b/packages/ui-default/locales/zh_TW.yaml @@ -87,6 +87,9 @@ Add prefix/suffix: 新增字首/字尾 Add suffix: 新增字尾 Add testcase: 新增測試點 Add User: 新增使用者 +Cancel Registration: 取消報名 +Registration cancelled.: 報名已取消。 +Are you sure to cancel the registration of this user?: 確定要取消該使用者的報名嗎? Add: 新增 Additional Files: 附加檔案 additional_file: 附加檔案 @@ -962,6 +965,7 @@ There are no contests...: 沒有比賽… There are no files currently.: 目前還沒有檔案。 There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯ This contest is not live.: 比賽沒有開始。 +This contest has already started.: 比賽已開始。 This email was sent by {0} automatically, and please do not reply directly.: 這封郵件由 {0} 自動傳送,請勿直接回復。 This homework is not open and you cannot view problems.: 功課尚未開放,不能檢視題目。 This homework is not open.: 功課尚未開放遞交。 diff --git a/packages/ui-default/pages/contest_user.page.ts b/packages/ui-default/pages/contest_user.page.ts index 88ee578dc..88be949f5 100644 --- a/packages/ui-default/pages/contest_user.page.ts +++ b/packages/ui-default/pages/contest_user.page.ts @@ -1,6 +1,6 @@ import $ from 'jquery'; import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; -import { ActionDialog } from 'vj/components/dialog'; +import { ActionDialog, confirm } from 'vj/components/dialog'; import Notification from 'vj/components/notification'; import { NamedPage } from 'vj/misc/Page'; import { @@ -93,9 +93,20 @@ const page = new NamedPage('contest_user', () => { }, i18n('Contest resumed.')); } + async function handleCancelRegistration(ev) { + const uid = $(ev.target).data('uid'); + const yes = await confirm(i18n('Are you sure to cancel the registration of this user?')); + if (!yes) return; + await handlePostRequest({ + operation: 'remove_user', + uid, + }, i18n('Registration cancelled.')); + } + $('[name="add_user"]').on('click', () => handleClickAddUser()); $(document).on('click', '[name="edit_rank"]', handleEditRank); $(document).on('click', '[name="resume_contest"]', handleResume); + $(document).on('click', '[name="cancel_registration"]', handleCancelRegistration); }); export default page; diff --git a/packages/ui-default/templates/partials/contest_user.html b/packages/ui-default/templates/partials/contest_user.html index 5d518193f..52f012609 100644 --- a/packages/ui-default/templates/partials/contest_user.html +++ b/packages/ui-default/templates/partials/contest_user.html @@ -13,6 +13,9 @@ {% if tsdoc.endAt and tsdoc.endAt < tdoc.endAt and tdoc.endAt.getTime() > Date.now() and (not tdoc.duration or not tsdoc.startAt or tsdoc.startAt.getTime() + tdoc.duration * 3600000 > Date.now()) %} | {{ _('Resume') }} {% endif %} + {% if tdoc.beginAt.getTime() > Date.now() %} + | {{ _('Cancel Registration') }} + {% endif %} {%- endfor -%}