diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 85035ba7d..e4dd559b9 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -6,8 +6,8 @@ contest_detail_problem_submit: Submit Contest Problem contest_detail_problem: Problem Detail contest_detail: Contest Detail contest_edit: Contest Edit -contest_files: Contest Files contest_main: Contest +contest_manage: Contest Management contest_problemlist: Problem List contest_scoreboard: Scoreboard contest_user: Attendee Manage diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index cd7ef3459..76a20b1b4 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -151,6 +151,7 @@ Contact Us: 联系我们 Contact: 联系 content: 内容 Content: 内容 +Contest Management: 比赛管理 Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。 Contest Scoreboard: 比赛成绩表 contest_create: 创建比赛 @@ -158,8 +159,8 @@ contest_detail_problem_submit: 递交比赛题目 contest_detail_problem: 题目详情 contest_detail: 比赛详情 contest_edit: 编辑比赛 -contest_files: 比赛文件 contest_main: 比赛 +contest_manage: 比赛管理 contest_problemlist: 题目列表 contest_scoreboard: 比赛成绩表 contest_user: 参赛者管理 diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 44519e080..33b17c0be 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -8,7 +8,7 @@ import { } from '@hydrooj/utils/lib/utils'; import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, - ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ForbiddenError, + ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError, NotAssignedError, PermissionError, ValidationError, } from '../error'; import { Tdoc } from '../interface'; @@ -131,8 +131,7 @@ export class ContestDetailBaseHandler extends Handler { { name: 'contest_problemlist', args: { tid, prefix: 'contest_problemlist' }, - checker: () => contest.isDone(this.tdoc, this.tsdoc) - || (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend), + checker: () => true, }, { name: 'contest_scoreboard', @@ -175,7 +174,11 @@ export class ContestDetailHandler extends Handler { this.response.template = 'contest_detail.html'; const udict = await user.getList(domainId, [this.tdoc.owner]); this.response.body = { - tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict, + tdoc: this.tdoc, + tsdoc: pick(this.tsdoc, ['attend', 'startAt']), + udict, + files: sortFiles(this.tdoc.files || []), + urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }), }; if (this.request.json) return; this.response.body.tdoc.content = this.response.body.tdoc.content @@ -198,8 +201,7 @@ export class ContestDetailHandler extends Handler { { name: 'contest_problemlist', args: { tid }, - checker: () => contest.isDone(this.tdoc, this.tsdoc) - || (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend), + checker: () => true, }, { name: 'contest_scoreboard', @@ -227,7 +229,8 @@ export class ContestDetailHandler extends Handler { export class ContestProblemListHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) async get(domainId: string, tid: ObjectID) { - if (contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc))) throw new ForbiddenError(); + if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid); + if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid); const [pdict, udict] = await Promise.all([ problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), user.getList(domainId, [this.tdoc.owner, this.user._id]), @@ -255,27 +258,6 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { } } -export class ContestBroadcastHandler extends ContestDetailBaseHandler { - async prepare() { - if (!this.user.own(this.tdoc)) throw new PermissionError('Boardcast Message'); - } - - async get() { - this.response.template = 'contest_broadcast.html'; - } - - @param('tid', Types.ObjectID) - @param('content', Types.Content) - async post(domainId: string, tid: ObjectID, content: string) { - const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray(); - const uids: number[] = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid))); - await Promise.all( - uids.map((uid) => message.send(this.user._id, uid, content, message.FLAG_ALERT)), - ); - this.response.redirect = this.url('contest_detail', { tid }); - } -} - export class ContestScoreboardHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) @param('ext', Types.Range(['csv', 'html', 'ghost']), true) @@ -471,6 +453,12 @@ export class ContestEditHandler extends Handler { } } +export class ContestManagementBaseHandler extends ContestDetailBaseHandler { + async prepare() { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + } +} + export class ContestCodeHandler extends Handler { @param('tid', Types.ObjectID) @param('all', Types.Boolean) @@ -519,25 +507,29 @@ export class ContestCodeHandler extends Handler { } } -export class ContestFilesHandler extends ContestDetailBaseHandler { +export class ContestManagementHandler extends ContestManagementBaseHandler { @param('tid', Types.ObjectID) async get(domainId: string, tid: ObjectID) { - if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); this.response.body = { tdoc: this.tdoc, tsdoc: this.tsdoc, owner_udoc: await user.getById(domainId, this.tdoc.owner), + pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), files: sortFiles(this.tdoc.files || []), urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }), }; this.response.pjax = 'partials/files.html'; - this.response.template = 'contest_files.html'; + this.response.template = 'contest_manage.html'; } @param('tid', Types.ObjectID) - async post(domainId: string, tid: ObjectID) { - const tdoc = await contest.get(domainId, tid); - if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + @param('content', Types.Content) + async postBroadcast(domainId: string, tid: ObjectID, content: string) { + const tsdocs = await contest.getMultiStatus(domainId, { docId: tid }).toArray(); + const uids = Array.from(new Set(tsdocs.map((tsdoc) => tsdoc.uid))); + const flag = contest.isOngoing(this.tdoc) ? message.FLAG_ALERT : message.FLAG_UNREAD; + await Promise.all(uids.map((uid) => message.send(this.user._id, uid, content, flag))); + this.back(); } @param('tid', Types.ObjectID) @@ -592,10 +584,9 @@ export class ContestFileDownloadHandler extends ContestDetailBaseHandler { } } -export class ContestUserHandler extends ContestDetailBaseHandler { +export class ContestUserHandler extends ContestManagementBaseHandler { @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; @@ -610,7 +601,6 @@ export class ContestUserHandler extends ContestDetailBaseHandler { @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(); } @@ -618,25 +608,22 @@ export class ContestUserHandler extends ContestDetailBaseHandler { @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); ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_problemlist', '/contest/:tid/problems', ContestProblemListHandler, PERM.PERM_VIEW_CONTEST); - ctx.Route('contest_broadcast', '/contest/:tid/broadcast', ContestBroadcastHandler); ctx.Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD); ctx.Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST_SCOREBOARD); + ctx.Route('contest_manage', '/contest/:tid/management', ContestManagementHandler); 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/ui-default/pages/contest.page.styl b/packages/ui-default/pages/contest.page.styl index f7f1d80ae..04a53bb2c 100644 --- a/packages/ui-default/pages/contest.page.styl +++ b/packages/ui-default/pages/contest.page.styl @@ -159,3 +159,7 @@ $highlight-button-color = #F6DF45 .col--problem display: none + +.page--contest_user + .col--uid + width: rem(100px) diff --git a/packages/ui-default/pages/contest_user.page.ts b/packages/ui-default/pages/contest_user.page.ts index e955a6a47..764df81fe 100644 --- a/packages/ui-default/pages/contest_user.page.ts +++ b/packages/ui-default/pages/contest_user.page.ts @@ -65,7 +65,7 @@ const page = new NamedPage('contest_user', () => { }); if (res.url && res.url !== window.location.href) window.location.href = res.url; else { - Notification.success(i18n('User has been added.')); + Notification.success(i18n('User added.')); pjax.request({ push: false }); } } catch (error) { @@ -82,7 +82,7 @@ const page = new NamedPage('contest_user', () => { }); if (res.url && res.url !== window.location.href) window.location.href = res.url; else { - Notification.success(i18n('User Rank has been edited.')); + Notification.success(i18n('Ranking status updated.')); pjax.request({ push: false }); } } catch (error) { diff --git a/packages/ui-default/pages/files.page.js b/packages/ui-default/pages/files.page.js index 5eeed6270..78b80f93d 100644 --- a/packages/ui-default/pages/files.page.js +++ b/packages/ui-default/pages/files.page.js @@ -92,7 +92,7 @@ function handleDrop(ev) { handleClickUpload(files); } -const page = new NamedPage(['home_files', 'contest_files', 'training_files'], () => { +const page = new NamedPage(['home_files', 'contest_manage', 'training_files'], () => { $(document).on('click', '[name="upload_file"]', () => handleClickUpload()); $(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected()); $(document).on('dragover', '.files', (ev) => handleDragOver(ev)); diff --git a/packages/ui-default/pages/files.page.styl b/packages/ui-default/pages/files.page.styl index 9108c57c9..dda599e3b 100644 --- a/packages/ui-default/pages/files.page.styl +++ b/packages/ui-default/pages/files.page.styl @@ -1,4 +1,4 @@ -.page--home_files,.page--contest_files,.page--training_files +.page--home_files,.page--contest_manage,.page--training_files .col--checkbox width: 60px diff --git a/packages/ui-default/templates/contest_broadcast.html b/packages/ui-default/templates/contest_broadcast.html deleted file mode 100644 index 999f4429e..000000000 --- a/packages/ui-default/templates/contest_broadcast.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "layout/basic.html" %} -{% block content %} -
-
-
-
-
- {{ form.form_textarea({ - columns:null, - label:'Content', - name:'content', - value:'' - }) }} -
- - - -
-
-
-
-
-
- {% include 'components/md_hint.html' %} -
-
-{% endblock %} diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html index 46ed8036a..0c05ce457 100644 --- a/packages/ui-default/templates/contest_detail.html +++ b/packages/ui-default/templates/contest_detail.html @@ -37,6 +37,14 @@ {% endif %} + {% if tdoc.files.length %} +
+
+

{{ _('Files') }}

+
+ {% include "partials/files.html" %} +
+ {% endif %}
{% set owner_udoc = udict[tdoc.owner] %} diff --git a/packages/ui-default/templates/contest_files.html b/packages/ui-default/templates/contest_files.html deleted file mode 100644 index 4a0d233e9..000000000 --- a/packages/ui-default/templates/contest_files.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "layout/basic.html" %} -{% block content %} -{{ set(UiContext, 'tdoc', tdoc) }} -
-
-
-
-

{{ _('Files') }}

-
- -
-
- {{ noscript_note.render() }} - {% include "partials/files.html" %} -
- -
-
-
-
- {% include "partials/contest_sidebar.html" %} -
-
-{% endblock %} diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html new file mode 100644 index 000000000..a2b2185da --- /dev/null +++ b/packages/ui-default/templates/contest_manage.html @@ -0,0 +1,72 @@ +{% extends "layout/basic.html" %} +{% block content %} +
+
+
+
+ + + + + + + + + + + {%- for pid in tdoc.pids -%} + + + + {%- endfor -%} + +
{{ _('Problem') }}
+ + {{ String.fromCharCode(65+loop.index0) }}  {{ pdict[pid].title }} + +
+
+
+
+
+
+
+

{{ _('Files') }}

+
+ +
+
+ {{ noscript_note.render() }} + {% include "partials/files.html" %} +
+ +
+
+ {% if model.contest.isOngoing(tdoc) %} +
+
+

{{ _('Broadcast Message') }}

+
+
+
+ {{ form.form_textarea({ + columns:null, + label:'Content', + name:'content', + value:'' + }) }} +
+ +
+
+
+
+ {% endif %} +
+
+ {% include 'partials/contest_sidebar_management.html' %} +
+
+{% endblock %} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 742a97e04..9df5f6cfa 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -8,7 +8,7 @@
-

{{ _('Problem') }}

+

{{ _('Problems') }}

diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 07f18254e..fdb234951 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -58,7 +58,7 @@ {% endif %} - {% endif %} + {% endif %}{# not attend and not done #} {% if model.contest.canShowScoreboard.call(handler, tdoc, False) %} - - {% endif %} {% if handler.user.own(tdoc) or handler.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE) or handler.user.hasPerm(perm.PERM_READ_RECORD_CODE) %} diff --git a/packages/ui-default/templates/partials/contest_sidebar_management.html b/packages/ui-default/templates/partials/contest_sidebar_management.html new file mode 100644 index 000000000..0be6e7eed --- /dev/null +++ b/packages/ui-default/templates/partials/contest_sidebar_management.html @@ -0,0 +1,50 @@ +{% import "components/user.html" as user with context %} +{% import "components/contest.html" as contest with context %} +
+ +
+

{{ tdoc.title }}

+
+
+ +
+
+
{{ _('Status') }}
+
+ {{ _(model.contest.statusText(tdoc, tsdoc)) }} + {% if tsdoc.attend %} + ({{ _('Attended') }}) + {% endif %} +
+
{{ _('Rule') }}
{{ model.contest.RULES[tdoc.rule].TEXT }}
+
{{ _('Problem') }}
{{ tdoc.pids.length }}
+
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
+
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
+
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
+
{{ _('Host') }}
{{ user.render_inline(owner_udoc, badge=false) }}
+
{{ _('Partic.') }}
{{ tdoc['attend']|default(0) }}
+
+
+