mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-18 01:36:08 +08:00
ui: enhanced contest management page (#476)
This commit is contained in:
1 parent
d4cc5e9c5b
commit
ee1cfd8fe5
14 files changed
+173
-109
No files matched your search
@@ -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
|
||||
|
||||
@@ -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: 参赛者管理
|
||||
|
||||
@@ -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<number>(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);
|
||||
}
|
||||
@@ -159,3 +159,7 @@ $highlight-button-color = #F6DF45
|
||||
|
||||
.col--problem
|
||||
display: none
|
||||
|
||||
.page--contest_user
|
||||
.col--uid
|
||||
width: rem(100px)
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__body">
|
||||
<form method="post">
|
||||
{{ form.form_textarea({
|
||||
columns:null,
|
||||
label:'Content',
|
||||
name:'content',
|
||||
value:''
|
||||
}) }}
|
||||
<div class="row"><div class="columns">
|
||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||
<button type="submit" class="rounded primary button">
|
||||
{{ _('Send') }}
|
||||
</button>
|
||||
<button type="button" class="rounded button" onclick="window.history.go(-1)">
|
||||
{{ _('Cancel') }}
|
||||
</button>
|
||||
</div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include 'components/md_hint.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -37,6 +37,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if tdoc.files.length %}
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Files') }}</h1>
|
||||
</div>
|
||||
{% include "partials/files.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% set owner_udoc = udict[tdoc.owner] %}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
{{ set(UiContext, 'tdoc', tdoc) }}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Files') }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="upload_file">{{ _('Upload File') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
{% include "partials/files.html" %}
|
||||
<div class="section__body">
|
||||
<button class="rounded button" name="remove_selected">{{ _('Remove Selected') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include "partials/contest_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-5 columns">
|
||||
<div class="section">
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--problem">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--problem">{{ _('Problem') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for pid in tdoc.pids -%}
|
||||
<tr>
|
||||
<td class="col--problem col--problem-name">
|
||||
<a href="{{ url('problem_detail', pid=pid) }}">
|
||||
<b>{{ String.fromCharCode(65+loop.index0) }}</b> {{ pdict[pid].title }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-4 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Files') }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="upload_file">{{ _('Upload File') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
{% include "partials/files.html" %}
|
||||
<div class="section__body">
|
||||
<button class="rounded button" name="remove_selected">{{ _('Remove Selected') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if model.contest.isOngoing(tdoc) %}
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Broadcast Message') }}</h1>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<form method="post">
|
||||
{{ form.form_textarea({
|
||||
columns:null,
|
||||
label:'Content',
|
||||
name:'content',
|
||||
value:''
|
||||
}) }}
|
||||
<div class="row"><div class="columns">
|
||||
<button type="submit" class="rounded primary button">
|
||||
{{ _('Send') }}
|
||||
</button>
|
||||
</div></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include 'partials/contest_sidebar_management.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Problem') }}</h1>
|
||||
<h1 class="section__title">{{ _('Problems') }}</h1>
|
||||
</div>
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}{# not attend and not done #}
|
||||
{% if model.contest.canShowScoreboard.call(handler, tdoc, False) %}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_scoreboard', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-statistics"></span> {{ _('Scoreboard') }}
|
||||
@@ -72,11 +72,8 @@
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit Contest') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_files', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-file"></span> {{ _('Files') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_user', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-user"></span> {{ _('Attendee Manage') }}
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_manage', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-settings"></span> {{ _('Contest Management') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if handler.user.own(tdoc) or handler.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE) or handler.user.hasPerm(perm.PERM_READ_RECORD_CODE) %}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{% import "components/user.html" as user with context %}
|
||||
{% import "components/contest.html" as contest with context %}
|
||||
<div class="section side">
|
||||
<a class="contest-sidebar__bg" href="{{ url('contest_detail', tid=tdoc.docId) }}">
|
||||
<div class="section__body">
|
||||
<h1>{{ tdoc.title }}</h1>
|
||||
</div>
|
||||
</a>
|
||||
<div>
|
||||
<ol class="menu">
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_detail', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-award"></span> {{ _('View Contest') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_manage', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-settings"></span> {{ _('Contest Management') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_edit', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit Contest') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_user', tid=tdoc.docId) }}">
|
||||
<span class="icon icon-user"></span> {{ _('Attendee Manage') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('contest_code', tid=tdoc.docId) }}" data-no-instant>
|
||||
<span class="icon icon-download"></span> {{ _('Export All Code') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link" href="{{ url('record_main', query={tid:tdoc.docId}) }}">
|
||||
<span class="icon icon-flag"></span> {{ _('All Submissions') }}
|
||||
</a></li>
|
||||
<li class="menu__seperator"></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
<dl class="large horizontal">
|
||||
<dt>{{ _('Status') }}</dt>
|
||||
<dd>
|
||||
{{ _(model.contest.statusText(tdoc, tsdoc)) }}
|
||||
{% if tsdoc.attend %}
|
||||
({{ _('Attended') }})
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{{ _('Rule') }}</dt><dd>{{ model.contest.RULES[tdoc.rule].TEXT }}</dd>
|
||||
<dt>{{ _('Problem') }}</dt><dd>{{ tdoc.pids.length }}</dd>
|
||||
<dt>{{ _('Start at') }}</dt><dd>{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}</dd>
|
||||
<dt>{{ _('End at') }}</dt><dd>{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}</dd>
|
||||
<dt>{{ _('Duration') }}</dt><dd>{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}</dd>
|
||||
<dt>{{ _('Host') }}</dt><dd>{{ user.render_inline(owner_udoc, badge=false) }}</dd>
|
||||
<dt>{{ _('Partic.') }}</dt><dd>{{ tdoc['attend']|default(0) }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user