diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 3d8e12777..44519e080 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -7,7 +7,7 @@ import { Counter, sortFiles, streamToBuffer, Time, } from '@hydrooj/utils/lib/utils'; import { - BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, + BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ForbiddenError, InvalidTokenError, NotAssignedError, PermissionError, ValidationError, } from '../error'; @@ -592,6 +592,40 @@ export class ContestFileDownloadHandler extends ContestDetailBaseHandler { } } +export class ContestUserHandler extends ContestDetailBaseHandler { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray(); + tsdocs.forEach((i) => { + i.endAt = this.tdoc.duration ? moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate() : null; + }); + const udict = await user.getListForRender(domainId, [this.tdoc.owner, ...tsdocs.map((i) => i.uid)]); + this.response.body = { tdoc: this.tdoc, tsdocs, udict }; + this.response.pjax = 'partials/contest_user.html'; + this.response.template = 'contest_user.html'; + } + + @param('tid', Types.ObjectID) + @param('uids', Types.NumericArray) + @param('unrank', Types.Boolean) + async postAddUser(domainId: string, tid: ObjectID, uids: number[], unrank = false) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + await Promise.all(uids.map((uid) => contest.attend(domainId, tid, uid, { unrank }))); + this.back(); + } + + @param('tid', Types.ObjectID) + @param('uid', Types.PositiveInt) + async postRank(domainId: string, tid: ObjectID, uid: number) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + const tsdoc = await contest.getStatus(domainId, tid, uid); + if (!tsdoc) throw new ContestNotAttendedError(uid); + await contest.setStatus(domainId, tid, uid, { unrank: !tsdoc.unrank }); + this.back(); + } +} + export async function apply(ctx) { ctx.Route('contest_create', '/contest/create', ContestEditHandler); ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST); @@ -604,4 +638,5 @@ export async function apply(ctx) { ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_files', '/contest/:tid/file', ContestFilesHandler, 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); } diff --git a/packages/hydrooj/src/lib/rank.ts b/packages/hydrooj/src/lib/rank.ts index 841255d03..08666a2ff 100644 --- a/packages/hydrooj/src/lib/rank.ts +++ b/packages/hydrooj/src/lib/rank.ts @@ -9,12 +9,12 @@ const ranked: ranked = async (cursor, equ) => { const results = []; const docs = await cursor.toArray(); for (const doc of docs) { - count++; - if (!last || !equ(last, doc)) r = count; if (doc.unrank) { results.push([0, doc]); continue; } + count++; + if (!last || !equ(last, doc)) r = count; last = doc; results.push([r, doc]); } diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index d5c782540..a55e38116 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -59,6 +59,7 @@ Accepted Problems: 通过的题目 Accepted Ratio: 通过率 Accepted: 已通过 Access Control: 访问控制 +According to the contest rules, you cannot view your submission details at current.: 根据比赛规则,你现在不能查看你的评测记录详情。 Account Settings: 账户设置 Action: 动作 Active Sessions: 活动会话 @@ -92,6 +93,7 @@ At least 4 characters: 至少 4 个字符 Attend Contest: 参加比赛 Attend contests: 参加比赛 Attended: 已参加 +Attender Manage: 参赛者管理 author: 作者 Auto hide problems during the contest: 在比赛过程中自动隐藏题目 Auto Read Tasks: 自动识别任务 @@ -175,6 +177,7 @@ contest_detail_problem_submit: 递交比赛题目 contest_edit: 编辑比赛 contest_main: 比赛 contest_scoreboard: 比赛成绩表 +contest_user: 参赛者管理 Contest: 比赛 Continue: 继续 Contributed Problems: 贡献的题目 @@ -551,6 +554,7 @@ objective: 客观题 Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目~ Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论 Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_ +Oh, there is no submission!: 喔,还没有评测记录呢! Oh, there is no task in the queue!: 喔,队列中目前没有任务。 Oh, there is no task that matches the filter!: 喔,目前没有符合过滤条件的任务。 Ok: 确定 @@ -867,6 +871,7 @@ Unknown field {0}.: 未知字段 {0}。 Unknown: 未知 Unlimited: 无限制 Unordered List: 无序列表 +Unrank: 打星 Unstar Topic: 取消收藏 Unstar: 取消收藏 Unsupported configure this type of problem. Please refer to the documentation.: 暂不支持快捷配置此类题目。请参照文档操作。 diff --git a/packages/ui-default/pages/contest_user.page.ts b/packages/ui-default/pages/contest_user.page.ts new file mode 100644 index 000000000..e955a6a47 --- /dev/null +++ b/packages/ui-default/pages/contest_user.page.ts @@ -0,0 +1,97 @@ +import $ from 'jquery'; +import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; +import { ActionDialog } from 'vj/components/dialog'; +import Notification from 'vj/components/notification'; +import { NamedPage } from 'vj/misc/Page'; +import { + i18n, pjax, request, tpl, +} from 'vj/utils'; + +const page = new NamedPage('contest_user', () => { + const addUserDialogContent = $(tpl` +
{{ _('The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.') }}
{% endif %} - {% if tdoc.code %} --- {% endif %} diff --git a/packages/ui-default/templates/contest_user.html b/packages/ui-default/templates/contest_user.html new file mode 100644 index 000000000..5ecd0dff3 --- /dev/null +++ b/packages/ui-default/templates/contest_user.html @@ -0,0 +1,47 @@ +{% extends "layout/basic.html" %} +{% import "components/contest.html" as contest with context %} +{% import "components/record.html" as record with context %} +{% import "components/problem.html" as problem with context %} +{% block content %} +{{ set(UiContext, 'tdoc', tdoc) }} +{{ _('The contest is a invitational contest. However it allow users attend contest without the invitation code. Those users will not be ranked.') }}
-
| {{ _('User ID') }} | +{{ _('Username') }} | +{{ _('Begin Time') }} + | {{ _('End Time') }} | +{{ _('Rank') }} | +{{ _('Actions') }} | +
|---|