From a43dc22599d0cba856f43451e2ee9b1d5dbae701 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 30 Nov 2022 17:44:17 +0800 Subject: [PATCH 01/24] core&ui: add contest_problemlist page --- packages/hydrooj/locales/en.yaml | 1 + packages/hydrooj/locales/zh.yaml | 6 +- packages/hydrooj/src/handler/contest.ts | 165 ++++++++++++------ packages/hydrooj/src/interface.ts | 2 + packages/hydrooj/src/model/contest.ts | 7 + ...ntest_main.page.styl => contest.page.styl} | 60 +++++++ .../ui-default/pages/contest_detail.page.styl | 13 -- .../ui-default/pages/contest_edit.page.styl | 6 - .../pages/contest_scoreboard.page.styl | 36 ---- .../ui-default/templates/contest_detail.html | 60 ------- .../ui-default/templates/contest_edit.html | 4 +- .../templates/contest_problemlist.html | 100 +++++++++++ 12 files changed, 291 insertions(+), 169 deletions(-) rename packages/ui-default/pages/{contest_main.page.styl => contest.page.styl} (72%) delete mode 100644 packages/ui-default/pages/contest_detail.page.styl delete mode 100644 packages/ui-default/pages/contest_edit.page.styl delete mode 100644 packages/ui-default/pages/contest_scoreboard.page.styl create mode 100644 packages/ui-default/templates/contest_problemlist.html diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index f8aaea602..9620ea657 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -8,6 +8,7 @@ contest_detail: Contest Detail contest_edit: Contest Edit contest_files: Contest Files contest_main: Contest +contest_problemlist: Proble List contest_scoreboard: Scoreboard discussion_create: Create Discussion discussion_detail: Discussion Detail diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 28f585018..84af58b3d 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -88,6 +88,7 @@ Attended: 已参加 author: 作者 Auto format code: 自动格式化代码 Avatar: 头像 +Back to contest list: 返回比赛列表 Be Copied: 被复制 Begin at: 开始于 Begin Date: 开始日期 @@ -159,6 +160,7 @@ contest_detail: 比赛详情 contest_edit: 编辑比赛 contest_files: 比赛文件 contest_main: 比赛 +contest_problemlist: 题目列表 contest_scoreboard: 比赛成绩表 Contest: 比赛 Contests per page: 每页展示的比赛数量 @@ -437,8 +439,8 @@ List View: 列表视图 Live: 正在进行 Live...: 正在进行… Location: 位置 -Lock Topic: 锁定主题 Lock discussions: 锁定讨论 +Lock Topic: 锁定主题 Login to Attend Contest: 登录后参加比赛 Login to Claim Homework: 登录后认领作业 Login to Create a Discussion: 登录后创建讨论 @@ -868,8 +870,8 @@ View or create discussion: 查看与发表讨论 View or submit solution: 查看与发表题解 View Owned Problems: 查看自己的题目 View Owned Training Plans: 查看自己的训练计划 -View problem solutions: 查看题解 View problem solutions after accept: 题目通过后查看题解 +View problem solutions: 查看题解 View Problem: 查看题目 View problems: 查看题目 View ranking: 查看排名 diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index aa0683d97..615e81cd6 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -6,8 +6,8 @@ import { ObjectID } from 'mongodb'; import { Counter, sortFiles, Time } from '@hydrooj/utils/lib/utils'; import { BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, - ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError, - NotAssignedError, PermissionError, ValidationError, + ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ForbiddenError, + InvalidTokenError, NotAssignedError, PermissionError, ValidationError, } from '../error'; import { Tdoc } from '../interface'; import paginate from '../lib/paginate'; @@ -92,6 +92,10 @@ export class ContestDetailBaseHandler extends Handler { @param('tid', Types.ObjectID, true) async __prepare(domainId: string, tid: ObjectID) { + if (this.session.tid && this.session.tid !== tid) { + // TODO: you are already in a contest + throw new ForbiddenError(); + } if (!tid) return; // ProblemDetailHandler also extends from ContestDetailBaseHandler [this.tdoc, this.tsdoc] = await Promise.all([ contest.get(domainId, tid), @@ -110,32 +114,33 @@ export class ContestDetailBaseHandler extends Handler { @param('tid', Types.ObjectID, true) async after(domainId: string, tid: ObjectID) { - if (!tid || this.tdoc.rule === 'homework' || !contest.isOngoing(this.tdoc, this.tsdoc)) return; + if (!tid || this.tdoc.rule === 'homework') return; if (this.request.json || !this.response.template) return; const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; this.response.body.overrideNav = [ - { name: 'homepage', args: { prefix: 'homepage' }, checker: () => true }, + { + name: 'contest_list', + args: {}, + displayName: 'Back to contest list', + checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), + }, { name: 'contest_detail', displayName: this.tdoc.title, args: { tid, prefix: 'contest_detail' }, checker: () => true, }, + { + name: 'contest_problemlist', + args: { tid, prefix: 'contest_problemlist' }, + checker: () => contest.isDone(this.tdoc, this.tsdoc) + || (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend), + }, { name: 'contest_scoreboard', args: { tid, prefix: 'contest_scoreboard' }, checker: () => contest.canShowScoreboard.call(this, this.tdoc, true), }, - { - name: 'record_main', - args: { - prefix: 'record', - query: contest.canShowRecord.call(this, this.tdoc, true) - ? { tid } - : { tid, uidOrName: this.user._id }, - }, - checker: () => contest.canShowSelfRecord.call(this, this.tdoc, true), - }, { displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, @@ -145,10 +150,30 @@ export class ContestDetailBaseHandler extends Handler { } } -export class ContestDetailHandler extends ContestDetailBaseHandler { +export class ContestDetailHandler extends Handler { + tdoc?: Tdoc<30>; + tsdoc?: any; + @param('tid', Types.ObjectID) async prepare(domainId: string, tid: ObjectID) { + [this.tdoc, this.tsdoc] = await Promise.all([ + contest.get(domainId, tid), + contest.getStatus(domainId, tid, this.user._id), + ]); if (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid); + if (this.session.tid && this.session.tid !== tid) { + // TODO: you are already in a contest + throw new ForbiddenError(); + } + if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) { + const groups = await user.listGroup(domainId, this.user._id); + if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) { + throw new NotAssignedError('contest', tid); + } + } + if (this.tdoc.duration && this.tsdoc?.startAt) { + this.tsdoc.endAt = moment(this.tsdoc.startAt).add(this.tdoc.duration, 'hours').toDate(); + } } @param('tid', Types.ObjectID) @@ -158,32 +183,41 @@ export class ContestDetailHandler extends ContestDetailBaseHandler { this.response.body = { tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict, }; - if (!this.request.json) { - this.response.body.tdoc.content = this.response.body.tdoc.content - .replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`) - .replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`); - } - if ( - (contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc))) - && !this.user.own(this.tdoc) - && !this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) - ) return; - const pdict = await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST); - let psdict = {}; - let rdict = {}; - if (this.tsdoc) { - if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { - await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); - this.tsdoc.startAt = new Date(); - } - psdict = this.tsdoc.detail || {}; - if (contest.canShowSelfRecord.call(this, this.tdoc)) { - rdict = await record.getList(domainId, Object.values(psdict).map((i: any) => i.rid)); - } else { - for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid }; - } - } - Object.assign(this.response.body, { pdict, psdict, rdict }); + if (this.request.json) return; + this.response.body.tdoc.content = this.response.body.tdoc.content + .replace(/\(file:\/\//g, `(./${this.tdoc.docId}/file/`) + .replace(/="file:\/\//g, `="./${this.tdoc.docId}/file/`); + const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; + this.response.body.overrideNav = [ + { + name: 'contest_list', + args: {}, + displayName: 'Back to contest list', + checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), + }, + { + name: 'contest_detail', + displayName: this.tdoc.title, + args: { tid, prefix: 'contest_detail' }, + checker: () => true, + }, + { + name: 'contest_problemlist', + args: { tid }, + checker: () => contest.isDone(this.tdoc, this.tsdoc) + || (contest.isOngoing(this.tdoc, this.tsdoc) && this.tsdoc?.attend), + }, + { + name: 'contest_scoreboard', + args: { tid, prefix: 'contest_scoreboard' }, + checker: () => contest.canShowScoreboard.call(this, this.tdoc, true), + }, + { + displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, + args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, + checker: () => 'pdoc' in this, + }, + ]; } @param('tid', Types.ObjectID) @@ -194,16 +228,35 @@ export class ContestDetailHandler extends ContestDetailBaseHandler { await contest.attend(domainId, tid, this.user._id); this.back(); } +} +export class ContestProblemListHandler extends ContestDetailBaseHandler { @param('tid', Types.ObjectID) - async postDelete(domainId: string, tid: ObjectID) { - if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); - await contest.del(domainId, tid); - await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' }); - await ScheduleModel.deleteMany({ - type: 'schedule', subType: 'contest', domainId, tid, - }); - this.response.redirect = this.url('contest_main'); + async get(domainId: string, tid: ObjectID) { + if (contest.isNotStarted(this.tdoc) || (!this.tsdoc?.attend && !contest.isDone(this.tdoc))) throw new ForbiddenError(); + 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]), + ]); + this.response.body = { + pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tsdoc: this.tsdoc, + }; + this.response.template = 'contest_problemlist.html'; + if (!this.tsdoc) return; + if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { + await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); + this.tsdoc.startAt = new Date(); + } + this.response.body.psdict = this.tsdoc.detail || {}; + const psdocs: any[] = Object.values(this.response.body.psdict); + if (contest.canShowSelfRecord.call(this, this.tdoc)) { + [this.response.body.rdict, this.response.body.rdocs] = await Promise.all([ + record.getList(domainId, psdocs.map((i: any) => i.rid)), + await record.getMulti(domainId, { tid, uid: this.user._id }).toArray(), + ]); + } else { + for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid }; + } } } @@ -358,7 +411,7 @@ export class ContestEditHandler extends Handler { @param('assign', Types.CommaSeperatedArray, true) @param('lock', Types.UnsignedInt, true) @param('contestDuration', Types.Float, true) - async post( + async postUpdate( domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number, title: string, content: string, rule: string, _pids: string, rated = false, _code = '', autoHide = false, assign: string[] = null, lock: number = null, @@ -408,6 +461,17 @@ export class ContestEditHandler extends Handler { this.response.body = { tid }; this.response.redirect = this.url('contest_detail', { tid }); } + + @param('tid', Types.ObjectID) + async postDelete(domainId: string, tid: ObjectID) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); + await contest.del(domainId, tid); + await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' }); + await ScheduleModel.deleteMany({ + type: 'schedule', subType: 'contest', domainId, tid, + }); + this.response.redirect = this.url('contest_main'); + } } export class ContestCodeHandler extends Handler { @@ -526,6 +590,7 @@ 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); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 862ac5530..ce5982d87 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -366,6 +366,8 @@ export interface Tdoc (a.rid.generationTime - b.rid.generationTime)); } diff --git a/packages/ui-default/pages/contest_main.page.styl b/packages/ui-default/pages/contest.page.styl similarity index 72% rename from packages/ui-default/pages/contest_main.page.styl rename to packages/ui-default/pages/contest.page.styl index 9476099dd..40b000d65 100644 --- a/packages/ui-default/pages/contest_main.page.styl +++ b/packages/ui-default/pages/contest.page.styl @@ -107,3 +107,63 @@ $highlight-button-color = #F6DF45 .detail-button width: auto + + +.page--contest_edit +.page--contest_create + label + &.checkbox + margin-top: 24px + + +.page--contest_problemlist + .col--status + width: rem(150px) + + .col--submit-at + width: rem(150px) + + .col--problem + border-left: 1px solid $table-border-color + + +mobile() + .col--submit-at + display: none + + +.page--contest_scoreboard + .main + .row + max-width: 100vw !important; + + .col--rank + width: rem(60px) + text-align: center + border-right: 1px solid $table-border-color + + .col--user + min-width: rem(150px) + + .col--solved, .col--time + width: rem(70px) + text-align: center + + .col--total_score + width: rem(100px) + text-align: center + + .col--problem + width: rem(100px) + border-left: 1px solid $table-border-color + text-align: center + + .scoreboard--acm + .col--problem + width: rem(70px) !important + + +mobile() + .col--user + width: auto + + .col--problem + display: none diff --git a/packages/ui-default/pages/contest_detail.page.styl b/packages/ui-default/pages/contest_detail.page.styl deleted file mode 100644 index 0724ef46f..000000000 --- a/packages/ui-default/pages/contest_detail.page.styl +++ /dev/null @@ -1,13 +0,0 @@ -.page--contest_detail - .col--status - width: rem(150px) - - .col--submit-at - width: rem(150px) - - .col--problem - border-left: 1px solid $table-border-color - - +mobile() - .col--submit-at - display: none diff --git a/packages/ui-default/pages/contest_edit.page.styl b/packages/ui-default/pages/contest_edit.page.styl deleted file mode 100644 index 136cc0a60..000000000 --- a/packages/ui-default/pages/contest_edit.page.styl +++ /dev/null @@ -1,6 +0,0 @@ -.page--contest_edit -.page--contest_create - label - &.checkbox - margin-top: 24px - \ No newline at end of file diff --git a/packages/ui-default/pages/contest_scoreboard.page.styl b/packages/ui-default/pages/contest_scoreboard.page.styl deleted file mode 100644 index a867dc06b..000000000 --- a/packages/ui-default/pages/contest_scoreboard.page.styl +++ /dev/null @@ -1,36 +0,0 @@ -.page--contest_scoreboard - .main - .row - max-width: 100vw !important; - - .col--rank - width: rem(60px) - text-align: center - border-right: 1px solid $table-border-color - - .col--user - min-width: rem(150px) - - .col--solved, .col--time - width: rem(70px) - text-align: center - - .col--total_score - width: rem(100px) - text-align: center - - .col--problem - width: rem(100px) - border-left: 1px solid $table-border-color - text-align: center - - .scoreboard--acm - .col--problem - width: rem(70px) !important - - +mobile() - .col--user - width: auto - - .col--problem - display: none diff --git a/packages/ui-default/templates/contest_detail.html b/packages/ui-default/templates/contest_detail.html index f504a1033..46ed8036a 100644 --- a/packages/ui-default/templates/contest_detail.html +++ b/packages/ui-default/templates/contest_detail.html @@ -37,66 +37,6 @@ {% endif %} -
-
-

{{ _('Problem') }}

-
- {% if pdict %} -
- - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - - - {% endif %} - - - - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - - - {% endif %} - - - - - {% set isAdmin = handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) %} - {% set ntdoc = model.contest.isDone(tdoc) or (tsdoc.attend and not model.contest.isNotStarted(tdoc)) %} - {%- for pid in tdoc.pids -%} - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - {% if psdict[pid] and psdict[pid].rid %} - {% set rdoc = rdict[psdict[pid].rid] %} - {% if model.contest.canShowSelfRecord.call(handler, tdoc) %} - {{ record.render_status_td(rdoc) }} - {% else %} - - {% endif %} - - {% else %} - - - {% endif %} - {% endif %} - - - {%- endfor -%} - -
{{ _('Status') }}{{ _('Last Submit At') }}{{ _('Problem') }}
{{ _('Submitted') }}{{ datetimeSpan(rdoc._id)|safe }}{{ _('No Submissions') }}- - {% if isAdmin and not ntdoc %} - {{ problem.render_problem_title(pdict[pid], show_invisible_flag=false, show_tags=false) }} - {% else %} - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }} - {% endif %} -
-
- {% elif tsdoc.attend %} - {{ nothing.render('This contest is not live.') }} - {% else %} - {{ nothing.render('Please attend contest to see the problems.') }} - {% endif %} -
{% set owner_udoc = udict[tdoc.owner] %} diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 0ed2cf529..80330e70f 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -108,7 +108,7 @@
{% if page_name == 'contest_edit' %} - {% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %} @@ -117,7 +117,7 @@ {% endif %} {% else %} - {% endif %} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html new file mode 100644 index 000000000..11688cdb0 --- /dev/null +++ b/packages/ui-default/templates/contest_problemlist.html @@ -0,0 +1,100 @@ +{% 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) }} +
+
+
+
+

{{ _('Problem') }}

+
+
+ + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + + + {% endif %} + + + + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + + + {% endif %} + + + + + {%- for pid in tdoc.pids -%} + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% if psdict[pid] and psdict[pid].rid %} + {% set rdoc = rdict[psdict[pid].rid] %} + {% if model.contest.canShowSelfRecord.call(handler, tdoc) %} + {{ record.render_status_td(rdoc) }} + {% else %} + + {% endif %} + + {% else %} + + + {% endif %} + {% endif %} + + + {%- endfor -%} + +
{{ _('Status') }}{{ _('Last Submit At') }}{{ _('Problem') }}
{{ _('Submitted') }}{{ datetimeSpan(rdoc._id)|safe }}{{ _('No Submissions') }}- + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }} +
+
+
+
+
+ {% if not rdocs.length %} + {{ nothing.render('Oh, there is no task in the queue!') }} + {% else %} + + + + + + + + + + + + + + + + + + + + + + + {%- for rdoc in rdocs -%} + {% set udoc = udict[rdoc['uid']] %} + {% set pdoc = pdict[rdoc['pid']] %} + {% include 'record_main_tr.html' %} + {%- endfor -%} + +
{{ _('Status') }}{{ _('Problem') }}{{ _('Submit By') }}{{ _('Time') }}{{ _('Memory') }}{{ _('Language') }}{{ _('Submit At') }}
+ {% endif %} +
+
+
+
+ {% set owner_udoc = udict[tdoc.owner] %} + {% include "partials/contest_sidebar.html" %} +
+
+{% endblock %} \ No newline at end of file From fee34811b43399b23872e29da89775ecc061382b Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 30 Nov 2022 17:52:57 +0800 Subject: [PATCH 02/24] ui: fix contest_delete --- packages/hydrooj/src/model/contest.ts | 1 + packages/ui-default/pages/contest_edit.page.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e8f57a485..e57bc7075 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -753,6 +753,7 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( global.Hydro.model.contest = { RULES, + AccessControl, add, getListStatus, getMultiStatus, diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 0efd92faf..d5862cea2 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -13,7 +13,7 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', $(document).on('click', '[name="operation"]', (ev) => { ev.preventDefault(); if (confirmed) { - return request.post('.', { operation: 'delete' }).then((res) => { + return request.post('', { operation: 'delete' }).then((res) => { window.location.href = res.url; }); } From f4127a806ae8f14133063b89fcb910b9735c77a5 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 12 Dec 2022 03:16:08 +0800 Subject: [PATCH 03/24] ui: update pid display --- packages/hydrooj/locales/en.yaml | 2 +- packages/hydrooj/src/handler/contest.ts | 3 ++- .../templates/contest_problemlist.html | 16 ++++++++++------ .../ui-default/templates/problem_detail.html | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 9620ea657..6c96ad873 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -8,7 +8,7 @@ contest_detail: Contest Detail contest_edit: Contest Edit contest_files: Contest Files contest_main: Contest -contest_problemlist: Proble List +contest_problemlist: Problem List contest_scoreboard: Scoreboard discussion_create: Create Discussion discussion_detail: Discussion Detail diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 615e81cd6..2811a1215 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -252,8 +252,9 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (contest.canShowSelfRecord.call(this, this.tdoc)) { [this.response.body.rdict, this.response.body.rdocs] = await Promise.all([ record.getList(domainId, psdocs.map((i: any) => i.rid)), - await record.getMulti(domainId, { tid, uid: this.user._id }).toArray(), + await record.getMulti(domainId, { tid, uid: this.user._id }).sort({ _id: -1 }).toArray(), ]); + this.response.body.canViewRecord = true; } else { for (const i of psdocs) this.response.body.rdict[i.rid] = { _id: i.rid }; } diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 11688cdb0..742a97e04 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -46,7 +46,9 @@ {% endif %} {% endif %} - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }} + + {{ String.fromCharCode(65+loop.index0) }}  {{ pdict[pid].title }} + {%- endfor -%} @@ -54,10 +56,12 @@
-
+
- {% if not rdocs.length %} - {{ nothing.render('Oh, there is no task in the queue!') }} + {% if not canViewRecord %} + {{ nothing.render('According to the contest rules, you cannot view your submission details at current.') }}' + {% elif not rdocs.length %} + {{ nothing.render('Oh, there is no submission!') }} {% else %} @@ -93,8 +97,8 @@
- {% set owner_udoc = udict[tdoc.owner] %} - {% include "partials/contest_sidebar.html" %} + {% set owner_udoc = udict[tdoc.owner] %} + {% include "partials/contest_sidebar.html" %}
{% endblock %} \ No newline at end of file diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 1563fa6e3..3237d79a7 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -44,7 +44,7 @@ {% endif %} - #{{ pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }} + #{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) if tdoc else pdoc.pid|default(pdoc.docId) }}. {{ pdoc.title }} {% if tdoc %}
From 3069271ecdacce5b53bd26507b398e0d89d29adc Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 12 Dec 2022 03:19:53 +0800 Subject: [PATCH 04/24] fix contest_edit warning --- packages/ui-default/pages/contest_edit.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 706735568..f00b65dc1 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -8,7 +8,7 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); if (pagename.endsWith('edit')) { let confirmed = false; - $(document).on('click', '[name="operation"]', (ev) => { + $(document).on('click', '[value="delete"]', (ev) => { ev.preventDefault(); if (confirmed) { return request.post('', { operation: 'delete' }).then((res) => { From 4f66c127da05568e85ba917cbb97599af8f6d03d Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 12 Dec 2022 13:00:13 +0800 Subject: [PATCH 05/24] ui: fix contest navigation bar --- packages/hydrooj/src/handler/contest.ts | 4 ++-- packages/ui-default/api.ts | 1 + packages/ui-default/templates/layout/html5.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 2811a1215..8a29dca07 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -119,7 +119,7 @@ export class ContestDetailBaseHandler extends Handler { const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; this.response.body.overrideNav = [ { - name: 'contest_list', + name: 'contest_main', args: {}, displayName: 'Back to contest list', checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), @@ -190,7 +190,7 @@ export class ContestDetailHandler extends Handler { const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; this.response.body.overrideNav = [ { - name: 'contest_list', + name: 'contest_main', args: {}, displayName: 'Back to contest list', checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), diff --git a/packages/ui-default/api.ts b/packages/ui-default/api.ts index 6411f9e00..347673750 100644 --- a/packages/ui-default/api.ts +++ b/packages/ui-default/api.ts @@ -2,6 +2,7 @@ export * from './utils'; export { default as Notification } from './components/notification'; export * from './components/dialog'; export * as bus from './bus'; +export { default as AnsiUp } from 'ansi_up'; export { default as loadMonaco } from './components/monaco/loader'; export * as redux from 'react-redux'; export * from './components/zipDownloader'; diff --git a/packages/ui-default/templates/layout/html5.html b/packages/ui-default/templates/layout/html5.html index 2391f01c5..769737d34 100644 --- a/packages/ui-default/templates/layout/html5.html +++ b/packages/ui-default/templates/layout/html5.html @@ -8,7 +8,7 @@ - + From 84d7094b22acb45be23f4618b52109ce0227a27c Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 12 Dec 2022 17:43:52 +0000 Subject: [PATCH 06/24] ui: new contest edit page and form_checkbox --- .../components/form/checkbox.page.styl | 16 +- packages/ui-default/pages/contest.page.styl | 8 - .../ui-default/pages/contest_edit.page.ts | 21 +++ .../ui-default/templates/components/form.html | 7 +- .../ui-default/templates/contest_edit.html | 151 +++++++++++++----- .../templates/partials/setting.html | 5 +- 6 files changed, 147 insertions(+), 61 deletions(-) diff --git a/packages/ui-default/components/form/checkbox.page.styl b/packages/ui-default/components/form/checkbox.page.styl index 889e85591..c7fa74117 100644 --- a/packages/ui-default/components/form/checkbox.page.styl +++ b/packages/ui-default/components/form/checkbox.page.styl @@ -3,16 +3,15 @@ $checkbox-size = 16px .checkbox - font-size: rem($font-size-secondary) - line-height: 1.4 + font-size: rem($font-size-small) !important + line-height: 1.4 !important border: 0 - display: inline-block + display: inline-block !important vertical-align: middle cursor: pointer user-select: none - color: lighten($text-1-color, 30%) - margin: $input-margin - padding: ((rem($form-control-height) - (@line-height * @font-size)) / 2) 0 + color: lighten($text-1-color, 30%) !important + margin: $input-margin !important &:hover color: $text-1-color @@ -20,13 +19,12 @@ $checkbox-size = 16px input appearance: none border: 1px solid #D0D0D0 - display: inline-block border-radius: 1px vertical-align: top width: rem($checkbox-size) height: rem($checkbox-size) - margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0 - margin-right: rem(5px) + margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0 !important + margin-right: rem(5px) !important position: relative outline: 0 transition: background .1s linear diff --git a/packages/ui-default/pages/contest.page.styl b/packages/ui-default/pages/contest.page.styl index 40b000d65..f7f1d80ae 100644 --- a/packages/ui-default/pages/contest.page.styl +++ b/packages/ui-default/pages/contest.page.styl @@ -108,14 +108,6 @@ $highlight-button-color = #F6DF45 .detail-button width: auto - -.page--contest_edit -.page--contest_create - label - &.checkbox - margin-top: 24px - - .page--contest_problemlist .col--status width: rem(150px) diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index f00b65dc1..5730be396 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -1,11 +1,32 @@ import $ from 'jquery'; +import moment from 'moment-timezone'; import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; +import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; import { ConfirmDialog } from 'vj/components/dialog'; import { NamedPage } from 'vj/misc/Page'; import { i18n, request, tpl } from 'vj/utils'; const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); + UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); + $('[name="rule"]').on('change', () => { + const rule = $('[name="rule"]').val(); + $('.contest-rule-settings').hide(); + $(`.contest-rule--${rule}`).show(); + }).trigger('change'); + $('[name="beginAtDate"], [name="beginAtTime"], [name="duration"]').on('change', () => { + const beginAtDate = $('[name="beginAtDate"]').val(); + const beginAtTime = $('[name="beginAtTime"]').val(); + const duration = $('[name="duration"]').val(); + console.log(beginAtDate, beginAtTime, duration); + const endAt = moment(`${beginAtDate} ${beginAtTime}`).add(+duration, 'hours').toDate(); + if (endAt) $('[name="endAt"]').val(moment(endAt).format('YYYY-MM-DD HH:mm')); + }); + $('[name="authorize"]').removeAttr('disabled').on('change', () => { + const authorized = $('[name="authorize"]').val(); + $('.authorize-settings').hide(); + $(`.authorize--${authorized}`).show(); + }).trigger('change'); if (pagename.endsWith('edit')) { let confirmed = false; $(document).on('click', '[value="delete"]', (ev) => { diff --git a/packages/ui-default/templates/components/form.html b/packages/ui-default/templates/components/form.html index 47d3fc122..b1558df22 100644 --- a/packages/ui-default/templates/components/form.html +++ b/packages/ui-default/templates/components/form.html @@ -199,13 +199,14 @@
{% endmacro %} {% macro form_checkbox(args) %} -{{ form_begin({columns:5, no_label:true}|assign(args)) }} +{{ form_begin({columns:5}|assign(args)) }} {{ checkbox(args) }} -{{ form_end({no_label:true}|assign(args)) }} +{{ form_end(args) }} {% endmacro %} diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 80330e70f..fa7537e51 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -3,8 +3,11 @@
-
-
+ +
+

{{ _('Basic Settings') }}

+
+
{{ form.form_select({ columns:3, @@ -15,7 +18,7 @@ row:false }) }} {{ form.form_text({ - columns:4, + columns:9, label:'Title', name:'title', placeholder:_('title'), @@ -23,22 +26,6 @@ autofocus:true, row:false }) }} - {{ form.form_text({ - columns:3, - label:'Invitation Code', - name:'code', - placeholder:_('(leave blank if none)'), - value:tdoc._code|default(''), - row:false - }) }} - {{ form.form_checkbox({ - columns:2, - label:'Rated', - name:'rated', - placeholder:_('Is rated'), - value:tdoc.rated|default(true), - row:false - }) }}
{{ form.form_text({ @@ -68,35 +55,22 @@ }) }} {{ form.form_text({ columns:4, - label:'Assign', - name:'assign', - placeholder:_('Group / UID'), - value:tdoc.assign|default(''), + label:'End Time', + name:'endAt', + placeholder:'YYYY-mm-dd HH:MM', + value:beginAt.add(duration, 'hours').format('YYYY-M-D HH:mm'), + disabled:true, row:false }) }}
-
{{ form.form_text({ - columns:9, + columns:12, label:'Problems', name:'pids', value:pids, placeholder:_("Seperated with ','"), - row:false + row:true }) }} - {{ form.form_checkbox({ - columns:3, - label:'Auto hide-and show', - name:'autoHide', - placeholder:'', - value:handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) and (tdoc.autoHide|default(true)), - disabled:not handler.user.hasPerm(perm.PERM_EDIT_PROBLEM), - row:false - }) }} - {% if tdoc.lockAt %} - - {% endif %} -
{{ form.form_textarea({ columns:null, label:'Content', @@ -104,6 +78,105 @@ value:tdoc['content']|default(''), markdown:true }) }} +
+
+

{{ _('Authorize Settings') }}

+
+
+
+ {{ form.form_select({ + columns:3, + label:'Authorize Control', + name:'authorize', + options:{ + 'public': _('Public'), + 'invite': _('Need Invitation Code'), + 'assign': _('Assign User/Group') + }, + value: ("public" if not tdoc._code else "invite") if not tdoc.assign else "assign", + disabled:true, + row:false + }) }} + + +
+ {{ form.form_text({ + columns:12, + label:'Maintainer', + name:'maintainer', + help_text:_('Allow to Maintain Contest'), + value:tdoc.maintainer|default('') + }) }} +
+
+

{{ _('Contest Settings') }}

+
+
+
+ {{ form.form_checkbox({ + columns:2, + label:'Rated', + name:'rated', + placeholder:_('Is rated'), + value:tdoc.rated|default(true), + row:false + }) }} + {{ form.form_checkbox({ + columns:3, + label:'Auto hide-and show', + name:'autoHide', + placeholder:_('Auto hide problems between contest'), + value:handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) and (tdoc.autoHide|default(true)), + disabled:not handler.user.hasPerm(perm.PERM_EDIT_PROBLEM), + row:false + }) }} + {{ form.form_checkbox({ + columns:3, + label:'Allow View Code', + name:'allowViewCode', + placeholder:_('Allow user view others code after contest'), + value:tdoc.allowViewCode|default(true), + row:false + }) }} + + +
diff --git a/packages/ui-default/templates/partials/setting.html b/packages/ui-default/templates/partials/setting.html index 6c76bc50a..ecb4f36f3 100644 --- a/packages/ui-default/templates/partials/setting.html +++ b/packages/ui-default/templates/partials/setting.html @@ -69,7 +69,7 @@ {% set _val = current[setting.key] %} {{ form.form_checkbox({ label:setting.name, - help_text:setting.desc, + placeholder:setting.desc, name:setting.key, value:setting.value if (_val === undefined or _val === null) else _val, disabled:setting.flag|bitand(2) @@ -78,7 +78,8 @@ label:setting.name, name:'booleanKeys.' + setting.key, value:true, - extra_class:'display-hidden' + extra_class:'display-hidden', + no_label:true })}} {% endif %} {% endif %} From d67b7f4f740a6d8d0ad19d38ac5cddb79bd6a322 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 12 Dec 2022 17:46:16 +0000 Subject: [PATCH 07/24] fix code --- packages/ui-default/pages/contest_edit.page.ts | 2 +- packages/ui-default/templates/components/form.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 5730be396..5e17c4936 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import moment from 'moment-timezone'; +import moment from 'moment'; import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; import { ConfirmDialog } from 'vj/components/dialog'; diff --git a/packages/ui-default/templates/components/form.html b/packages/ui-default/templates/components/form.html index b1558df22..04df8ee1d 100644 --- a/packages/ui-default/templates/components/form.html +++ b/packages/ui-default/templates/components/form.html @@ -199,8 +199,7 @@
{% endmacro %} From f2ec6997affd53dccfdfab13f2d154b6265ef80e Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 13 Dec 2022 13:39:53 +0800 Subject: [PATCH 08/24] ui: fix help-text display style --- .../autocomplete/autocomplete.page.styl | 27 ++++++++++++++----- .../autocomplete/components/AutoComplete.tsx | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/ui-default/components/autocomplete/autocomplete.page.styl b/packages/ui-default/components/autocomplete/autocomplete.page.styl index ff150f4fe..46233b99f 100644 --- a/packages/ui-default/components/autocomplete/autocomplete.page.styl +++ b/packages/ui-default/components/autocomplete/autocomplete.page.styl @@ -2,23 +2,36 @@ display: none !important .autocomplete-wrapper - width: 300px border: 1px solid #d9d9d9 - background-color: #fff padding: 1px display: flex flex-wrap: wrap overflow-y: auto + form-styles() + background-color: $input-background-color + outline: $input-outline + transition: outline-color .2s, border-color .2s + transition-timing-function: ease-out-cubic + + &:focus, &.focused + border-color: $input-focus-border-color + outline: $input-focus-outline + outline-offset: 0 + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) + + // Overwrite, for IE compatibility + &:read-only + outline: 0 + + &:disabled + opacity: 0.5 + &:hover border-color: #40a9ff - &.focused - border-color: #40a9ff - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) - input - background-color: #fff + background-color: $input-background-color color: rgba(0,0,0,.85) height: 30px box-sizing: border-box diff --git a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx index 88299462a..a3ce4ed7b 100644 --- a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx +++ b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx @@ -274,7 +274,7 @@ const AutoComplete = forwardRef(function Impl(props: AutoCompleteProps, re }; return ( -
+
Date: Tue, 13 Dec 2022 06:20:07 +0000 Subject: [PATCH 09/24] fix i18n text --- .../components/form/checkbox.page.styl | 2 +- packages/ui-default/locales/zh.yaml | 11 +++++++++ .../ui-default/templates/contest_edit.html | 24 ++++++++++--------- .../templates/partials/login_dialog.html | 2 +- packages/ui-default/templates/user_login.html | 4 ++-- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/ui-default/components/form/checkbox.page.styl b/packages/ui-default/components/form/checkbox.page.styl index c7fa74117..5c6b46146 100644 --- a/packages/ui-default/components/form/checkbox.page.styl +++ b/packages/ui-default/components/form/checkbox.page.styl @@ -69,7 +69,7 @@ $checkbox-size = 16px margin-right: rem(5px) label.checkbox.inverse - color: rgba($immersive-text-color, 0.8) + color: rgba($immersive-text-color, 0.8) !important input color: $immersive-text-color diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index f9081d180..74d4e2d45 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -74,6 +74,8 @@ All Languages: 所有语言 All Submissions: 所有递交 All Training Plans: 所有训练计划 All: 全部 +Allow to maintain this contest: 允许管理此比赛 +Allow user view others code after contest: 允许用户赛后查看其他人的代码 Always granted all privileges: 总是拥有所有权限 An error has occurred.: 发生了一个错误。 An example of dataset: 测试数据集的一个例子 @@ -83,6 +85,7 @@ API: API Argument {0} is invalid.: 非法的参数 {0} 。 Arguments: 参数 Assign: 分配给 +Assign User or Group: 分配给用户或小组 Assignment: 作业 At least 4 characters: 至少 4 个字符 Attend Contest: 参加比赛 @@ -90,6 +93,8 @@ Attend contests: 参加比赛 Attended: 已参加 author: 作者 Auto Read Tasks: 自动识别任务 +Auto hide problems during the contest: 在比赛过程中自动隐藏题目 +Basic Info: 基础信息 Be Copied: 被复制 Begin at: 开始于 Begin Date: 开始日期 @@ -167,6 +172,8 @@ contest_edit: 编辑比赛 contest_main: 比赛 contest_scoreboard: 比赛成绩表 Contest: 比赛 +Contest Maintainer: 比赛管理员 +Contest Settings: 比赛设置 Continue: 继续 Contributed Problems: 贡献的题目 Contributions: 贡献 @@ -452,6 +459,7 @@ LaTeX Expressions: LaTeX 表达式 Learn More: 了解更多 Leave blank to use numeric pid.: 留空来使用数字题号。 Light: 亮色 +Limit user in N hours finish contest: 限制用户在N小时内完成比赛 Limitations: 限制 Links: 链接 List View: 列表视图 @@ -460,6 +468,7 @@ Live...: 正在进行… Loading editor...: 正在加载编辑器... Loading file...: 正在加载文件内容... Location: 位置 +Lock scoreboard on last N minutes: 在赛事最后N分钟时锁定成绩表 Login to Attend Contest: 登录后参加比赛 Login to Claim Homework: 登录后认领作业 Login to Create a Discussion: 登录后创建讨论 @@ -508,6 +517,7 @@ Nav Logo Dark: 左上角小图标(暗) Nav Logo Dark@2x: 左上角图标(暗)(大) Nav Logo Light: 左上角图标(亮) Nav Logo Light@2x: 左上角图标(亮)(大) +Need Invitation Code: 需要邀请码 Network error: 网络错误 Never expire: 从不过期 New dataset: 新测试数据 @@ -583,6 +593,7 @@ perm_problem: 题库 perm_record: 记录 perm_training: 训练 Permission: 权限 +Permission Control: 权限控制 Permissions: 权限 Pick a problem randomly based on current filter: 根据当前过滤条件随机选择一道题 Pin: 置顶 diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index fa7537e51..9f490a715 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -5,7 +5,7 @@
-

{{ _('Basic Settings') }}

+

{{ _('Basic Info') }}

@@ -80,9 +80,16 @@ }) }}
-

{{ _('Authorize Settings') }}

+

{{ _('Permission Control') }}

+ {{ form.form_text({ + columns:12, + label:'Contest Maintainer', + name:'maintainer', + help_text:_('Allow to maintain this contest'), + value:tdoc.maintainer|default('') + }) }}
{{ form.form_select({ columns:3, @@ -91,7 +98,7 @@ options:{ 'public': _('Public'), 'invite': _('Need Invitation Code'), - 'assign': _('Assign User/Group') + 'assign': _('Assign User or Group') }, value: ("public" if not tdoc._code else "invite") if not tdoc.assign else "assign", disabled:true, @@ -118,13 +125,6 @@ }) }}
- {{ form.form_text({ - columns:12, - label:'Maintainer', - name:'maintainer', - help_text:_('Allow to Maintain Contest'), - value:tdoc.maintainer|default('') - }) }}

{{ _('Contest Settings') }}

@@ -143,7 +143,7 @@ columns:3, label:'Auto hide-and show', name:'autoHide', - placeholder:_('Auto hide problems between contest'), + placeholder:_('Auto hide problems during the contest'), value:handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) and (tdoc.autoHide|default(true)), disabled:not handler.user.hasPerm(perm.PERM_EDIT_PROBLEM), row:false @@ -161,6 +161,7 @@ columns:4, label:'Lock (minutes)', name:'lock', + placeholder:_('(leave blank if none)'), value:(((tdoc.endAt - tdoc.lockAt)/60000)|int)|default(''), help_text:_('Lock scoreboard on last N minutes'), row:false @@ -171,6 +172,7 @@ columns:4, label:'Flexable Duration', name:'contestDuration', + placeholder:_('(leave blank if none)'), value:tdoc.duration|default(''), help_text:_('Limit user in N hours finish contest'), row:false diff --git a/packages/ui-default/templates/partials/login_dialog.html b/packages/ui-default/templates/partials/login_dialog.html index eb73c585d..202cb4700 100644 --- a/packages/ui-default/templates/partials/login_dialog.html +++ b/packages/ui-default/templates/partials/login_dialog.html @@ -35,7 +35,7 @@
diff --git a/packages/ui-default/templates/user_login.html b/packages/ui-default/templates/user_login.html index 3e23962b7..1f8495a94 100644 --- a/packages/ui-default/templates/user_login.html +++ b/packages/ui-default/templates/user_login.html @@ -24,8 +24,8 @@
-
From c1c68caee6d0ea56df1bd9d11a08ab9126e02c3b Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 13 Dec 2022 06:29:12 +0000 Subject: [PATCH 10/24] disabled hide settings --- packages/ui-default/pages/contest_edit.page.ts | 5 ++++- packages/ui-default/templates/contest_edit.html | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 5e17c4936..df79b751e 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -11,20 +11,23 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); $('[name="rule"]').on('change', () => { const rule = $('[name="rule"]').val(); + $('.contest-rule-settings input').attr('disabled', 'disabled'); $('.contest-rule-settings').hide(); + $(`.contest-rule--${rule} input`).removeAttr('disabled'); $(`.contest-rule--${rule}`).show(); }).trigger('change'); $('[name="beginAtDate"], [name="beginAtTime"], [name="duration"]').on('change', () => { const beginAtDate = $('[name="beginAtDate"]').val(); const beginAtTime = $('[name="beginAtTime"]').val(); const duration = $('[name="duration"]').val(); - console.log(beginAtDate, beginAtTime, duration); const endAt = moment(`${beginAtDate} ${beginAtTime}`).add(+duration, 'hours').toDate(); if (endAt) $('[name="endAt"]').val(moment(endAt).format('YYYY-MM-DD HH:mm')); }); $('[name="authorize"]').removeAttr('disabled').on('change', () => { const authorized = $('[name="authorize"]').val(); + $('.authorize-settings input').attr('disabled', 'disabled'); $('.authorize-settings').hide(); + $(`.authorize--${authorized} input`).removeAttr('disabled'); $(`.authorize--${authorized}`).show(); }).trigger('change'); if (pagename.endsWith('edit')) { diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 9f490a715..fd8433570 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -111,6 +111,7 @@ name:'assign', placeholder:_('Group / UID'), value:tdoc.assign|default(''), + disabled:true, row:false }) }}
@@ -121,6 +122,7 @@ name:'code', placeholder:_('(leave blank if none)'), value:tdoc._code|default(''), + disabled:true, row:false }) }}
@@ -164,6 +166,7 @@ placeholder:_('(leave blank if none)'), value:(((tdoc.endAt - tdoc.lockAt)/60000)|int)|default(''), help_text:_('Lock scoreboard on last N minutes'), + disabled:true, row:false }) }}
@@ -175,6 +178,7 @@ placeholder:_('(leave blank if none)'), value:tdoc.duration|default(''), help_text:_('Limit user in N hours finish contest'), + disabled:true, row:false }) }}
From 514f5466a5310b18a4fa705a369f621586077a4b Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 13 Dec 2022 06:43:02 +0000 Subject: [PATCH 11/24] fix checkbox --- packages/ui-default/components/form/checkbox.page.styl | 1 + .../ui-default/templates/partials/discussion_edit_form.html | 6 ++---- packages/ui-default/templates/problem_edit.html | 2 +- packages/ui-default/templates/training_edit.html | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/ui-default/components/form/checkbox.page.styl b/packages/ui-default/components/form/checkbox.page.styl index 5c6b46146..80124bcdc 100644 --- a/packages/ui-default/components/form/checkbox.page.styl +++ b/packages/ui-default/components/form/checkbox.page.styl @@ -63,6 +63,7 @@ $checkbox-size = 16px margin-bottom: 0 line-height: 1.2 padding: ((rem($compact-control-height) - (@line-height * @font-size)) / 2) 0 + margin: 0 !important input margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0 diff --git a/packages/ui-default/templates/partials/discussion_edit_form.html b/packages/ui-default/templates/partials/discussion_edit_form.html index c3c81732c..741f361af 100644 --- a/packages/ui-default/templates/partials/discussion_edit_form.html +++ b/packages/ui-default/templates/partials/discussion_edit_form.html @@ -11,10 +11,9 @@
diff --git a/packages/ui-default/templates/training_edit.html b/packages/ui-default/templates/training_edit.html index cb91b2a5f..77ac742d8 100644 --- a/packages/ui-default/templates/training_edit.html +++ b/packages/ui-default/templates/training_edit.html @@ -6,7 +6,7 @@
-
+
From dc0e397d77cee58602e5517f8083d13e8d7aff6f Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 15 Dec 2022 18:27:30 +0000 Subject: [PATCH 12/24] add training files --- packages/hydrooj/src/handler/contest.ts | 2 +- packages/hydrooj/src/handler/training.ts | 93 ++++++++++++++++++- packages/hydrooj/src/interface.ts | 3 + packages/hydrooj/src/model/contest.ts | 2 +- packages/hydrooj/src/model/setting.ts | 2 + packages/ui-default/locales/zh.yaml | 1 + packages/ui-default/pages/files.page.js | 2 +- packages/ui-default/pages/files.page.styl | 2 +- .../ui-default/templates/contest_files.html | 3 - .../ui-default/templates/training_detail.html | 3 + .../ui-default/templates/training_files.html | 65 +++++++++++++ 11 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 packages/ui-default/templates/training_files.html diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 8a29dca07..73b8c1fd4 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -552,7 +552,7 @@ export class ContestFilesHandler extends ContestDetailBaseHandler { if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename'); await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id); const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`); - const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; + const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; if (!meta) throw new FileUploadError(); await contest.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] }); this.back(); diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 0fdb60ce9..7339348b7 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -1,15 +1,24 @@ import assert from 'assert'; +import { statSync } from 'fs-extra'; +import { pick } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; -import { ProblemNotFoundError, ValidationError } from '../error'; +import { sortFiles } from '@hydrooj/utils/lib/utils'; +import { + FileLimitExceededError, FileUploadError, ProblemNotFoundError, ValidationError, +} from '../error'; import { Tdoc, TrainingDoc } from '../interface'; import paginate from '../lib/paginate'; import { PERM, PRIV, STATUS } from '../model/builtin'; +import * as oplog from '../model/oplog'; import problem from '../model/problem'; +import storage from '../model/storage'; import * as system from '../model/system'; import * as training from '../model/training'; import user from '../model/user'; import * as bus from '../service/bus'; -import { Handler, param, Types } from '../service/server'; +import { + Handler, param, post, Types, +} from '../service/server'; async function _parseDagJson(domainId: string, _dag: string): Promise { const parsed = []; @@ -149,6 +158,9 @@ class TrainingDetailHandler extends Handler { this.response.body = { tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc, udict, selfPsdict, }; + this.response.body.tdoc.description = this.response.body.tdoc.description + .replace(/\(file:\/\//g, `(./${tdoc.docId}/file/`) + .replace(/="file:\/\//g, `="./${tdoc.docId}/file/`); this.response.pjax = 'partials/training_detail.html'; this.response.template = 'training_detail.html'; } @@ -219,9 +231,86 @@ class TrainingEditHandler extends Handler { } } +export class TrainingFilesHandler extends Handler { + tdoc: TrainingDoc; + + @param('tid', Types.ObjectID) + async prepare(domainId: string, tid: ObjectID) { + this.tdoc = await training.get(domainId, tid); + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING); + else this.checkPerm(PERM.PERM_EDIT_TRAINING_SELF); + } + + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { + if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_TRAINING); + this.response.body = { + tdoc: this.tdoc, + tsdoc: await training.getStatus(domainId, this.tdoc.docId, this.user._id), + udoc: await user.getById(domainId, this.tdoc.owner), + files: sortFiles(this.tdoc.files || []), + urlForFile: (filename: string) => this.url('training_file_download', { tid, filename }), + }; + this.response.pjax = 'partials/files.html'; + this.response.template = 'training_files.html'; + } + + @param('tid', Types.ObjectID) + @post('filename', Types.Name, true) + async postUploadFile(domainId: string, tid: ObjectID, filename: string) { + if ((this.tdoc.files?.length || 0) >= system.get('limit.training_files')) { + throw new FileLimitExceededError('count'); + } + const file = this.request.files?.file; + if (!file) throw new ValidationError('file'); + const f = statSync(file.filepath); + const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size; + if (size >= system.get('limit.training_files_size')) { + throw new FileLimitExceededError('size'); + } + if (!filename) filename = file.originalFilename || String.random(16); + if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename'); + await storage.put(`training/${domainId}/${tid}/${filename}`, file.filepath, this.user._id); + const meta = await storage.getMeta(`training/${domainId}/${tid}/${filename}`); + const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) }; + if (!meta) throw new FileUploadError(); + await training.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] }); + this.back(); + } + + @param('tid', Types.ObjectID) + @post('files', Types.Array) + async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) { + await Promise.all([ + storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id), + training.edit(domainId, tid, { files: this.tdoc.files.filter((i) => !files.includes(i.name)) }), + ]); + this.back(); + } +} +export class TrainingFileDownloadHandler extends Handler { + @param('tid', Types.ObjectID) + @param('filename', Types.Name) + @param('noDisposition', Types.Boolean) + async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) { + this.response.addHeader('Cache-Control', 'public'); + const target = `training/${domainId}/${tid}/${filename}`; + const file = await storage.getMeta(target); + await oplog.log(this, 'download.file.training', { + target, + size: file?.size || 0, + }); + this.response.redirect = await storage.signDownloadLink( + target, noDisposition ? undefined : filename, false, 'user', + ); + } +} + export async function apply(ctx) { ctx.Route('training_main', '/training', TrainingMainHandler, PERM.PERM_VIEW_TRAINING); ctx.Route('training_create', '/training/create', TrainingEditHandler); ctx.Route('training_detail', '/training/:tid', TrainingDetailHandler, PERM.PERM_VIEW_TRAINING); ctx.Route('training_edit', '/training/:tid/edit', TrainingEditHandler); + ctx.Route('training_files', '/training/:tid/file', TrainingFilesHandler, PERM.PERM_VIEW_TRAINING); + ctx.Route('training_file_download', '/training/:tid/file/:filename', TrainingFileDownloadHandler, PERM.PERM_VIEW_TRAINING); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index ce5982d87..b4ab16245 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -367,6 +367,8 @@ export interface Tdoc { export interface ContestStat extends Record { detail: Record>, + unrank?: boolean, } export interface ContestRule { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e57bc7075..32442f95c 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -540,7 +540,7 @@ export async function add( return res; } -export async function edit(domainId: string, tid: ObjectID, $set: any) { +export async function edit(domainId: string, tid: ObjectID, $set: Partial) { if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule'); const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(domainId, tid); diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 0a3a72341..054f47ecd 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -215,6 +215,8 @@ SystemSetting( Setting('setting_limits', 'limit.user_files_size', 128 * 1024 * 1024, 'number', 'limit.user_files_size', 'Max total file size for user'), Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest'), Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest'), + Setting('setting_limits', 'limit.training_files', 100, 'number', 'limit.training_files', 'Max files for training'), + Setting('setting_limits', 'limit.training_files_size', 128 * 1024 * 1024, 'number', 'limit.training_files_size', 'Max total file size for training'), Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'), Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'), Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'), diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 74d4e2d45..0c746a4ac 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -927,6 +927,7 @@ View problem solutions: 查看题解 View Problem: 查看题目 View problems: 查看题目 View this domain: 查看此域 +View Training: 查看训练计划 View training plans: 查看训练计划 View: 查看 Visible to registered users: 对注册用户可见 diff --git a/packages/ui-default/pages/files.page.js b/packages/ui-default/pages/files.page.js index dd16ab053..5eeed6270 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'], () => { +const page = new NamedPage(['home_files', 'contest_files', '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 435a0938d..9108c57c9 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--home_files,.page--contest_files,.page--training_files .col--checkbox width: 60px diff --git a/packages/ui-default/templates/contest_files.html b/packages/ui-default/templates/contest_files.html index 2e2e515bf..4a0d233e9 100644 --- a/packages/ui-default/templates/contest_files.html +++ b/packages/ui-default/templates/contest_files.html @@ -1,7 +1,4 @@ {% 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) }}
diff --git a/packages/ui-default/templates/training_detail.html b/packages/ui-default/templates/training_detail.html index 93738b524..c56c56ee5 100644 --- a/packages/ui-default/templates/training_detail.html +++ b/packages/ui-default/templates/training_detail.html @@ -75,6 +75,9 @@ + {% endif %}
+
+ {{ form.form_text({ + columns:9, + label:'UnRank User', + name:'unrankUser', + help_text:_('User who will not show rank in scoreboard'), + value:tdoc.unrankUser|default(''), + row:false + }) }} + +

{{ _('Contest Settings') }}

@@ -134,12 +154,12 @@
{{ form.form_checkbox({ - columns:2, - label:'Rated', - name:'rated', - placeholder:_('Is rated'), - value:tdoc.rated|default(true), - row:false + columns:2, + label:'Rated', + name:'rated', + placeholder:_('Is rated'), + value:tdoc.rated|default(true), + row:false }) }} {{ form.form_checkbox({ columns:3, @@ -164,7 +184,7 @@ label:'Lock (minutes)', name:'lock', placeholder:_('(leave blank if none)'), - value:(((tdoc.endAt - tdoc.lockAt)/60000)|int)|default(''), + value:(((tdoc.endAt - tdoc.lockAt)/60000)|int) if tdoc.lockAt else '', help_text:_('Lock scoreboard on last N minutes'), disabled:true, row:false diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index 463d0caa4..d35a755e4 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -22,12 +22,20 @@ {% if model.contest.isDone(tdoc) and tdoc.lockAt and handler.user.own(tdoc) and not tdoc.unlocked %}
- {% endif %} +
+ +
+

{{ tdoc.title }}

{% if model.contest.isLocked(tdoc) %}
@@ -71,6 +79,10 @@
{%- for column in row -%} + {%- for tsdoc in tsdocs -%} + + + + + + + + + {%- endfor -%} + \ No newline at end of file From b4f2efb139950e6cbc9006ade5bbcf53670cfa6b Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 22 Dec 2022 17:52:53 +0000 Subject: [PATCH 22/24] i18n: attender -> attendee --- packages/hydrooj/locales/en.yaml | 1 + packages/hydrooj/locales/zh.yaml | 1 + packages/ui-default/locales/zh.yaml | 3 +-- packages/ui-default/templates/contest_user.html | 2 +- packages/ui-default/templates/partials/contest_sidebar.html | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 6c96ad873..85035ba7d 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -10,6 +10,7 @@ contest_files: Contest Files contest_main: Contest contest_problemlist: Problem List contest_scoreboard: Scoreboard +contest_user: Attendee Manage discussion_create: Create Discussion discussion_detail: Discussion Detail discussion_edit: Discussion Edit diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index efab18766..cd7ef3459 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -162,6 +162,7 @@ contest_files: 比赛文件 contest_main: 比赛 contest_problemlist: 题目列表 contest_scoreboard: 比赛成绩表 +contest_user: 参赛者管理 Contest: 比赛 Contests per page: 每页展示的比赛数量 Continue: 继续 diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index a55e38116..89ed446ee 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -93,7 +93,7 @@ At least 4 characters: 至少 4 个字符 Attend Contest: 参加比赛 Attend contests: 参加比赛 Attended: 已参加 -Attender Manage: 参赛者管理 +Attendee Manage: 参赛者管理 author: 作者 Auto hide problems during the contest: 在比赛过程中自动隐藏题目 Auto Read Tasks: 自动识别任务 @@ -177,7 +177,6 @@ contest_detail_problem_submit: 递交比赛题目 contest_edit: 编辑比赛 contest_main: 比赛 contest_scoreboard: 比赛成绩表 -contest_user: 参赛者管理 Contest: 比赛 Continue: 继续 Contributed Problems: 贡献的题目 diff --git a/packages/ui-default/templates/contest_user.html b/packages/ui-default/templates/contest_user.html index 5ecd0dff3..5641730dd 100644 --- a/packages/ui-default/templates/contest_user.html +++ b/packages/ui-default/templates/contest_user.html @@ -8,7 +8,7 @@
-

{{ _('Attender Manage') }}

+

{{ _('Attendee Manage') }}

diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 882ae41f5..07f18254e 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -76,7 +76,7 @@ {{ _('Files') }} {% endif %} {% if handler.user.own(tdoc) or handler.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE) or handler.user.hasPerm(perm.PERM_READ_RECORD_CODE) %} From 6cf6a57914b1c0d392ccf85fb7cc91f7120bf772 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 23 Dec 2022 14:59:50 +0800 Subject: [PATCH 23/24] core: rejudge: skip ignored submissions --- packages/hydrooj/src/handler/problem.ts | 1 + packages/hydrooj/src/handler/record.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index d204a31fd..f1463034c 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -442,6 +442,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { const rdocs = await record.getMulti(domainId, { pid, contest: { $ne: new ObjectID('0'.repeat(24)) }, + status: { $ne: STATUS.STATUS_CANCELED }, 'files.hack': { $exists: false }, }).project({ _id: 1, contest: 1 }).toArray(); if (!this.pdoc.config || typeof this.pdoc.config === 'string') throw new ProblemConfigError(); diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index effd3568b..1616ee716 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -216,7 +216,10 @@ class RecordDetailHandler extends ContestDetailBaseHandler { record.update(domainId, rid, $set), TaskModel.deleteMany({ rid: this.rdoc._id }), ]); - if (latest) await postJudge(latest); + if (latest) { + this.ctx.broadcast('record/change', latest); + await postJudge(latest); + } this.back(); } } From 4487f16f9ba92b7f59140deff6a3f7e86e98725e Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 24 Dec 2022 19:36:10 +0800 Subject: [PATCH 24/24] bump version --- README.md | 20 ++++++++++++++++++++ packages/fps-importer/package.json | 2 +- packages/hydrojudge/package.json | 2 +- packages/hydrooj/package.json | 2 +- packages/import-qduoj/package.json | 2 +- packages/ui-default/package.json | 2 +- packages/vjudge/package.json | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8bb70f49e..afce8536f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,26 @@ Telegram [@webpack_exports_undefined](https://t.me/webpack_exports_undefined)

更新日志(点击展开)

+### Hydro 4.5.0 / UI 4.44.0 +- fps: 修复题目中含空文件导致导入失败的问题 +- core: 封禁用户时支持附加理由 +- vjudge: codeforces: 跳过无法访问的 1769 和 1772 比赛 +- ui: 收藏题目操作不再触发页面刷新 +- core: 重测时检查题目配置文件有效性 +- core: 退出时自动清理临时文件 +- core: 禁止使用 . 作为文件名 +- import-qduoj: 跳过不合法的题目 +- core: 修复提交答案题的比赛代码导出 +- judge: 添加 stdioLimit 项 +- ui: 修复 message.FLAG_ALERT 显示 +- core: training 可上传文件 +- ui: 优化比赛导航栏 +- ui: 比赛成绩表支持关注队伍 +- core: 允许克隆比赛/作业 +- ui: 比赛编辑页面添加功能入口 +- core: 支持打星参赛 +- core: 整题重测时跳过已取消成绩的提交 + ### Hydro 4.4.5 / UI 4.43.0 - core: 修复比赛基于 ID 搜索题目的功能 - judge: 修复 testlib 错误信息显示异常的问题 diff --git a/packages/fps-importer/package.json b/packages/fps-importer/package.json index fa6c4a651..9afeaea60 100644 --- a/packages/fps-importer/package.json +++ b/packages/fps-importer/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/fps-importer", - "version": "1.5.2", + "version": "1.5.3", "description": "Import FPS problems", "main": "index.ts", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index 5ea0a7f9d..dde0b1e6c 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -1,7 +1,7 @@ { "name": "@hydrooj/hydrojudge", "bin": "bin/hydrojudge.js", - "version": "3.1.12", + "version": "3.1.13", "main": "package.json", "author": "undefined ", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 886aaeefb..8e46dd15c 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "4.4.5", + "version": "4.5.0", "bin": "bin/hydrooj.js", "main": "src/plugin-api", "module": "src/plugin-api", diff --git a/packages/import-qduoj/package.json b/packages/import-qduoj/package.json index 185204317..c801431d8 100644 --- a/packages/import-qduoj/package.json +++ b/packages/import-qduoj/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/import-qduoj", - "version": "1.5.0", + "version": "1.5.1", "description": "Import QDUOJ problem export", "main": "index.ts", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 0877a5c25..20eb7b977 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.43.0", + "version": "4.44.0", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index 122e6e921..efd19a951 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/vjudge", - "version": "1.7.1", + "version": "1.7.2", "description": "Submit problems to remote oj", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git",
{%- if column.type == 'user' -%} {%- set canView = canViewAll or handler.user._id == column.raw -%} + {{ user.render_inline(udict[column.raw], badge=false) }} {%- elif column.type == 'record' and column.raw -%} From 9470dd9cfadb251f5bc8a1dddeccc108df12c418 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 19 Dec 2022 14:13:09 +0800 Subject: [PATCH 14/24] ui: allow contest and homework clone --- packages/ui-default/templates/contest_edit.html | 3 +++ packages/ui-default/templates/homework_edit.html | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index bea9cf753..276232ef3 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -210,6 +210,9 @@ + {% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %} + {% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_HOMEWORK) %}
- {%- if column.type == 'user' -%} + {%- if column.type == 'rank' -%} + {{ column.value|string|nl2br|safe }} + {%- elif column.type == 'user' -%} {%- set canView = canViewAll or handler.user._id == column.raw -%} - From 8024122e8f27b1c93410944d4c3a66cb36258db9 Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 22 Dec 2022 23:38:32 +0800 Subject: [PATCH 17/24] core: contest: simplify --- packages/hydrooj/src/handler/contest.ts | 20 ++++------- packages/hydrooj/src/handler/training.ts | 4 +-- packages/hydrooj/src/interface.ts | 2 -- packages/hydrooj/src/lib/rank.ts | 2 +- packages/hydrooj/src/model/contest.ts | 16 +++------ packages/hydrooj/src/model/document.ts | 5 ++- packages/hydrooj/src/model/setting.ts | 6 ++-- .../pages/contest_scoreboard.page.ts | 36 ++++++------------- .../templates/contest_scoreboard.html | 3 +- 9 files changed, 32 insertions(+), 62 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index aaff7eb74..7e7f1b613 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -92,10 +92,6 @@ export class ContestDetailBaseHandler extends Handler { @param('tid', Types.ObjectID, true) async __prepare(domainId: string, tid: ObjectID) { - if (this.session.tid && this.session.tid !== tid) { - // TODO: you are already in a contest - throw new ForbiddenError(); - } if (!tid) return; // ProblemDetailHandler also extends from ContestDetailBaseHandler [this.tdoc, this.tsdoc] = await Promise.all([ contest.get(domainId, tid), @@ -122,7 +118,7 @@ export class ContestDetailBaseHandler extends Handler { name: 'contest_main', args: {}, displayName: 'Back to contest list', - checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), + checker: () => true, }, { name: 'contest_detail', @@ -161,10 +157,6 @@ export class ContestDetailHandler extends Handler { contest.getStatus(domainId, tid, this.user._id), ]); if (contest.RULES[this.tdoc.rule].hidden) throw new ContestNotFoundError(domainId, tid); - if (this.session.tid && this.session.tid !== tid) { - // TODO: you are already in a contest - throw new ForbiddenError(); - } if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) { const groups = await user.listGroup(domainId, this.user._id); if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) { @@ -193,7 +185,7 @@ export class ContestDetailHandler extends Handler { name: 'contest_main', args: {}, displayName: 'Back to contest list', - checker: () => !(this.tdoc.access & contest.AccessControl.LOCK_OTHER), + checker: () => true, }, { name: 'contest_detail', @@ -225,9 +217,11 @@ export class ContestDetailHandler extends Handler { async postAttend(domainId: string, tid: ObjectID, code = '') { if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid); if (this.tdoc._code && code !== this.tdoc._code && !this.tdoc.uninvite) throw new InvalidTokenError('Contest Invitation', code); - await contest.attend(domainId, tid, this.user._id); - if (this.tdoc.unrankUser.includes(this.user._id) - || (this.tdoc._code && code !== this.tdoc._code && this.tdoc.uninvite)) contest.setStatus(domainId, tid, this.user._id, { unrank: true }); + await contest.attend( + domainId, tid, this.user._id, + (this.tdoc._code && code !== this.tdoc._code && this.tdoc.uninvite) + ? { unrank: true } : {}, + ); this.back(); } } diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 7339348b7..1a1a2f61e 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -258,14 +258,14 @@ export class TrainingFilesHandler extends Handler { @param('tid', Types.ObjectID) @post('filename', Types.Name, true) async postUploadFile(domainId: string, tid: ObjectID, filename: string) { - if ((this.tdoc.files?.length || 0) >= system.get('limit.training_files')) { + if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) { throw new FileLimitExceededError('count'); } const file = this.request.files?.file; if (!file) throw new ValidationError('file'); const f = statSync(file.filepath); const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size; - if (size >= system.get('limit.training_files_size')) { + if (size >= system.get('limit.contest_files_size')) { throw new FileLimitExceededError('size'); } if (!filename) filename = file.originalFilename || String.random(16); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index d6e12bdad..e9d9c6ca5 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -366,9 +366,7 @@ export interface Tdoc(cursor: Cursor, equ: ((a: T, b: T) => boolean)) => Promise<[number, T][]>; +type ranked = >(cursor: Cursor, equ: ((a: T, b: T) => boolean)) => Promise<[number, T][]>; const ranked: ranked = async (cursor, equ) => { let last = null; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e2a347d1d..b4f5b75c7 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -127,7 +127,7 @@ const acm = buildContestRule({ async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { const tsddict = tsdoc.detail || {}; const row: ScoreboardRow = [ - { type: 'rank', value: rank ? rank.toString() : '*', raw: rank }, + { type: 'rank', value: rank.toString() }, { type: 'user', value: udoc.uname, raw: tsdoc.uid }, ]; if (isExport) { @@ -263,7 +263,7 @@ const oi = buildContestRule({ async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { const tsddict = tsdoc.detail || {}; const row: ScoreboardNode[] = [ - { type: 'rank', value: rank ? rank.toString() : '*', raw: rank }, + { type: 'rank', value: rank.toString() }, { type: 'user', value: udoc.uname, raw: tsdoc.uid }, ]; if (isExport) { @@ -510,13 +510,6 @@ export const RULES: ContestRules = { acm, oi, homework, ioi, }; -export enum AccessControl { - FREE, - LOCK_OTHER = 2, - UNIQUE_LOGIN = 4, - CLIENT_REQUIRED = 8, -} - function _getStatusJournal(tsdoc) { return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); } @@ -600,9 +593,9 @@ export async function getListStatus(domainId: string, uid: number, tids: ObjectI return r; } -export async function attend(domainId: string, tid: ObjectID, uid: number) { +export async function attend(domainId: string, tid: ObjectID, uid: number, payload: any = {}) { try { - await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1); + await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1, payload); } catch (e) { throw new ContestAlreadyAttendedError(tid, uid); } @@ -753,7 +746,6 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( global.Hydro.model.contest = { RULES, - AccessControl, add, getListStatus, getMultiStatus, diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 2a764a033..3388e2c8a 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -350,12 +350,15 @@ export async function setStatusIfNotCondition( export async function cappedIncStatus( domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number, key: NumberKeys, value: number, minValue = -1, maxValue = 1, + setPayload: Partial = {}, ): Promise { assert(value !== 0); const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue }; + const operation = { $inc: { [key]: value } } as UpdateQuery; + if (Object.keys(setPayload).length) operation.$set = setPayload; const res = await collStatus.findOneAndUpdate( { domainId, docType, docId, uid, [key]: { $not } }, - { $inc: { [key]: value } }, + operation, { upsert: true, returnDocument: 'after' }, ); return res.value; diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 054f47ecd..ea60bcef9 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -213,10 +213,8 @@ SystemSetting( Setting('setting_limits', 'limit.problem_files_max_size', 256 * 1024 * 1024, 'number', 'limit.problem_files_max_size', 'Max files size per problem'), Setting('setting_limits', 'limit.user_files', 100, 'number', 'limit.user_files', 'Max files for user'), Setting('setting_limits', 'limit.user_files_size', 128 * 1024 * 1024, 'number', 'limit.user_files_size', 'Max total file size for user'), - Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest'), - Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest'), - Setting('setting_limits', 'limit.training_files', 100, 'number', 'limit.training_files', 'Max files for training'), - Setting('setting_limits', 'limit.training_files_size', 128 * 1024 * 1024, 'number', 'limit.training_files_size', 'Max total file size for training'), + Setting('setting_limits', 'limit.contest_files', 100, 'number', 'limit.contest_files', 'Max files for contest or training'), + Setting('setting_limits', 'limit.contest_files_size', 128 * 1024 * 1024, 'number', 'limit.contest_files_size', 'Max total file size for contest or training'), Setting('setting_limits', 'limit.submission', 60, 'number', 'limit.submission', 'Max submission count per minute'), Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'), Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'), diff --git a/packages/ui-default/pages/contest_scoreboard.page.ts b/packages/ui-default/pages/contest_scoreboard.page.ts index d88193a39..5c84228c3 100644 --- a/packages/ui-default/pages/contest_scoreboard.page.ts +++ b/packages/ui-default/pages/contest_scoreboard.page.ts @@ -3,35 +3,23 @@ import { NamedPage } from 'vj/misc/Page'; const page = new NamedPage('contest_scoreboard', () => { const { tdoc } = UiContext; - let starData = JSON.parse(localStorage.getItem('Hydro.starData')) || {}; - Object.keys(starData).forEach((key) => { - if (starData[key].expire < Date.now()) delete starData[key]; - }); - if (!starData[tdoc.docId]) { - starData[tdoc.docId] = { - expire: tdoc.endAt + 1000 * 60 * 60 * 24 * 7, - starUids: [], - }; - } else { - starData[tdoc.docId].starUids.forEach((uid) => { - console.log(uid); - $(`.star.user--${uid}`).addClass('activated'); - }); - } - console.log(starData[tdoc.docId].starUids); - localStorage.setItem('Hydro.starData', JSON.stringify(starData)); + const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`; + const read = () => JSON.parse(localStorage.getItem(key) || '[]'); + const write = (data) => localStorage.setItem(key, JSON.stringify(data)); + + read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated')); $('.star').on('click', (e) => { - starData = JSON.parse(localStorage.getItem('Hydro.starData')); + const star = read(); const $target = $(e.currentTarget); const uid = $target.data('uid'); - if (starData[tdoc.docId].starUids.includes(uid)) { + if (star.includes(uid)) { $target.removeClass('activated'); - starData[tdoc.docId].starUids.splice(starData[tdoc.docId].starUids.indexOf(uid), 1); + star.splice(star.indexOf(uid), 1); } else { $target.addClass('activated'); - starData[tdoc.docId].starUids.push(uid); + star.push(uid); } - localStorage.setItem('Hydro.starData', JSON.stringify(starData)); + write(star); }); $('.select.filter').on('change', (e) => { @@ -40,9 +28,7 @@ const page = new NamedPage('contest_scoreboard', () => { $('.data-table tbody tr').show(); } else if (val === 'star') { $('.data-table tbody tr').hide(); - starData[tdoc.docId].starUids.forEach((uid) => { - $(`.star.user--${uid}`).closest('tr').show(); - }); + read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show()); } else { $('.data-table tbody tr').hide(); $('.rank--unrank').closest('tr').show(); diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index 6d2a03ee3..214767403 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -35,7 +35,6 @@ -

{{ tdoc.title }}

{% if model.contest.isLocked(tdoc) %}
@@ -78,7 +77,7 @@ {%- for column in row -%}
{%- if column.type == 'rank' -%} - {{ column.value|string|nl2br|safe }} + {{ '*' if column.value == '0' else column.value|string|nl2br|safe }} {%- elif column.type == 'user' -%} {%- set canView = canViewAll or handler.user._id == column.raw -%} + + + {{ 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 %} +
{{ 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') }} +