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` +
+
+

${i18n('Add User')}

+
+
+ +
+
+ +
+
+ `); + addUserDialogContent.appendTo(document.body); + const userSelect = UserSelectAutoComplete.getOrConstruct>( + addUserDialogContent.find('[name="add_user_users"]'), + { multi: true, height: 'auto' }, + ); + + const addUserDialog = new ActionDialog({ + $body: addUserDialogContent, + onDispatch(action) { + if (action === 'ok' && !userSelect.value()) { + userSelect.focus(); + return false; + } + return true; + }, + }); + addUserDialog.clear = function () { + userSelect.clear(); + addUserDialog.$dom.find('[name="unrank"]').prop('checked', false); + return this; + }; + + async function handleClickAddUser() { + const action = await addUserDialog.clear().open(); + if (action !== 'ok') return; + const unrank = addUserDialog.$dom.find('[name="unrank"]').prop('checked'); + const uids = userSelect.value(); + try { + const res = await request.post('', { + operation: 'add_user', + uids: uids.join(','), + unrank, + }); + if (res.url && res.url !== window.location.href) window.location.href = res.url; + else { + Notification.success(i18n('User has been added.')); + pjax.request({ push: false }); + } + } catch (error) { + Notification.error([error.message, ...error.params].join(' ')); + } + } + + async function handleEditRank(ev) { + const uid = $(ev.target).data('uid'); + try { + const res = await request.post('', { + operation: 'rank', + uid, + }); + if (res.url && res.url !== window.location.href) window.location.href = res.url; + else { + Notification.success(i18n('User Rank has been edited.')); + pjax.request({ push: false }); + } + } catch (error) { + Notification.error([error.message, ...error.params].join(' ')); + } + } + + $('[name="add_user"]').on('click', () => handleClickAddUser()); + $(document).on('click', '[name="edit_rank"]', handleEditRank); +}); + +export default page; diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html index 7dbabc8ef..46ed8036a 100644 --- a/packages/ui-default/templates/contest_detail.html +++ b/packages/ui-default/templates/contest_detail.html @@ -35,11 +35,6 @@

{{ _('The contest is a flexible time contest. You need to complete the contest within a specified time after you attended.') }}

{% endif %} - {% if tdoc.code %} -
-

{{ _('The contest is a invitational contest. However it allow users attend contest without the invitation code. Those users will not be ranked.') }}

-
- {% 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) }} +
+
+
+
+

{{ _('Attender Manage') }}

+
+ +
+
+ {{ noscript_note.render() }} +
+ + + + + + + + + + + + + + + + + + + {% include "partials/contest_user.html" %} +
{{ _('User ID') }}{{ _('Username') }}{{ _('Begin Time') }} + {{ _('End Time') }}{{ _('Rank') }}{{ _('Actions') }}
+
+
+
+
+ {% set owner_udoc = udict[tdoc.owner] %} + {% include "partials/contest_sidebar.html" %} +
+
+{% endblock %} diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 578085577..882ae41f5 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -75,6 +75,9 @@ + {% endif %} {% if handler.user.own(tdoc) or handler.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE) or handler.user.hasPerm(perm.PERM_READ_RECORD_CODE) %} {% if model.contest.canShowRecord.call(handler, tdoc, true) %} diff --git a/packages/ui-default/templates/partials/contest_user.html b/packages/ui-default/templates/partials/contest_user.html new file mode 100644 index 000000000..3aa4e631e --- /dev/null +++ b/packages/ui-default/templates/partials/contest_user.html @@ -0,0 +1,16 @@ +{% import "components/user.html" as user with context %} +{% import "components/contest.html" as contest with context %} + + {%- for tsdoc in tsdocs -%} + + {{ tsdoc.uid }} + {{ user.render_inline(udict[tsdoc.uid], badge=false) }} + {{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }} + {{ contest.render_time(tsdoc.endAt or tdoc.endAt) }} + {{ _('UnRank') if tsdoc.unrank else _('Rank') }} + + {{ _('Rank') if tsdoc.unrank else _('UnRank') }} + + + {%- endfor -%} + \ No newline at end of file