core&ui: add contest balloon list (#578)

Co-authored-by: undefined <i@undefined.moe>
pull/642/head^2
panda 11 months ago committed by GitHub
parent 8e8c263477
commit 867cd069c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -65,7 +65,7 @@
"simple-git": "^3.20.0", "simple-git": "^3.20.0",
"style-loader": "^3.3.3", "style-loader": "^3.3.3",
"stylus": "^0.61.0", "stylus": "^0.61.0",
"stylus-loader": "7.1.3", "stylus-loader": "7.1.2",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",

@ -151,6 +151,7 @@ Content: 内容
Contest Management: 比赛管理 Contest Management: 比赛管理
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。 Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
Contest Scoreboard: 比赛成绩表 Contest Scoreboard: 比赛成绩表
contest_balloon: 气球状态
contest_create: 创建比赛 contest_create: 创建比赛
contest_detail_problem_submit: 递交比赛题目 contest_detail_problem_submit: 递交比赛题目
contest_detail_problem: 题目详情 contest_detail_problem: 题目详情

@ -4,8 +4,9 @@ import { pick } from 'lodash';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { import {
Counter, sortFiles, streamToBuffer, Time, Counter, sortFiles, streamToBuffer, Time, yaml,
} from '@hydrooj/utils/lib/utils'; } from '@hydrooj/utils/lib/utils';
import { Context } from '../context';
import { import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
@ -663,7 +664,55 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
this.back(); this.back();
} }
} }
export async function apply(ctx) {
export class ContestBalloonHandler extends ContestManagementBaseHandler {
@param('tid', Types.ObjectId)
@param('todo', Types.Boolean)
async get(domainId: string, tid: ObjectId, todo = false) {
const bdocs = await contest.getMultiBalloon(domainId, tid, {
...todo ? { sent: { $exists: false } } : {},
...(!this.tdoc.lockAt || this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD))
? {} : { _id: { $lt: this.tdoc.lockAt } },
}).sort({ _id: -1 }).project({ uid: 1 }).toArray();
const uids = bdocs.map((i) => i.uid).concat(bdocs.filter((i) => i.sent).map((i) => i.sent));
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),
bdocs,
udict: await user.getListForRender(domainId, uids),
};
this.response.pjax = 'partials/contest_balloon.html';
this.response.template = 'contest_balloon.html';
}
@param('tid', Types.ObjectId)
@param('color', Types.Content)
async postSetColor(domainId: string, tid: ObjectId, color: string) {
const config = yaml.load(color);
if (typeof config !== 'object') throw new ValidationError('color');
const balloon = {};
for (const pid of this.tdoc.pids) {
if (!config[pid]) throw new ValidationError('color');
balloon[pid] = config[pid.toString()];
}
await contest.edit(domainId, tid, { balloon });
this.back();
}
@param('tid', Types.ObjectId)
@param('balloon', Types.ObjectId)
async postDone(domainId: string, tid: ObjectId, bid: ObjectId) {
const balloon = await contest.getBalloon(domainId, tid, bid);
if (!balloon) throw new ValidationError('balloon');
if (balloon.sent) throw new ValidationError('balloon', null, 'Balloon already sent');
await contest.updateBalloon(domainId, tid, bid, { sent: this.user._id, sentAt: new Date() });
this.back();
}
}
export async function apply(ctx: Context) {
ctx.Route('contest_create', '/contest/create', ContestEditHandler); ctx.Route('contest_create', '/contest/create', ContestEditHandler);
ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_detail', '/contest/:tid', ContestDetailHandler, PERM.PERM_VIEW_CONTEST);
@ -675,4 +724,5 @@ export async function apply(ctx) {
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST); ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, 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); ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
} }

@ -393,6 +393,7 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
lockAt?: Date; lockAt?: Date;
unlocked?: boolean; unlocked?: boolean;
autoHide?: boolean; autoHide?: boolean;
balloon?: Record<number, string>;
/** /**
* In hours * In hours
@ -651,6 +652,18 @@ export interface DiscussionHistoryDoc {
ip: string; ip: string;
} }
export interface ContestBalloonDoc {
_id: ObjectId;
domainId: string;
tid: ObjectId;
pid: number;
uid: number;
first?: boolean;
/** Sent by */
sent?: number;
sentAt?: Date;
}
declare module './service/db' { declare module './service/db' {
interface Collections { interface Collections {
'blacklist': BlacklistDoc; 'blacklist': BlacklistDoc;
@ -678,6 +691,7 @@ declare module './service/db' {
'event': EventDoc; 'event': EventDoc;
'opcount': OpCountDoc; 'opcount': OpCountDoc;
'schedule': Schedule; 'schedule': Schedule;
'contest.balloon': ContestBalloonDoc;
} }
} }

@ -11,6 +11,7 @@ import {
} from '../interface'; } from '../interface';
import ranked from '../lib/rank'; import ranked from '../lib/rank';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import db from '../service/db';
import type { Handler } from '../service/server'; import type { Handler } from '../service/server';
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin'; import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
import * as document from './document'; import * as document from './document';
@ -655,6 +656,8 @@ export const RULES: ContestRules = {
acm, oi, homework, ioi, ledo, strictioi, acm, oi, homework, ioi, ledo, strictioi,
}; };
const collBalloon = db.collection('contest.balloon');
function _getStatusJournal(tsdoc) { function _getStatusJournal(tsdoc) {
return tsdoc.journal.sort((a, b) => (a.rid.getTimestamp() - b.rid.getTimestamp())); return tsdoc.journal.sort((a, b) => (a.rid.getTimestamp() - b.rid.getTimestamp()));
} }
@ -705,6 +708,32 @@ export async function getRelated(domainId: string, pid: number, rule?: string) {
return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid, rule: rule || { $in: rules } }).toArray(); return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid, rule: rule || { $in: rules } }).toArray();
} }
export async function addBalloon(domainId: string, tid: ObjectId, uid: number, rid: ObjectId, pid: number) {
const balloon = await collBalloon.findOne({
domainId, tid, pid, uid,
});
if (balloon) return null;
const bdcount = await collBalloon.countDocuments({ domainId, tid, pid });
const newBdoc = {
_id: rid, domainId, tid, pid, uid, ...(!bdcount ? { first: true } : {}),
};
await collBalloon.insertOne(newBdoc);
bus.broadcast('contest/balloon', domainId, tid, newBdoc);
return rid;
}
export async function getBalloon(domainId: string, tid: ObjectId, _id: ObjectId) {
return await collBalloon.findOne({ domainId, tid, _id });
}
export function getMultiBalloon(domainId: string, tid: ObjectId, query: any = {}) {
return collBalloon.find({ domainId, tid, ...query });
}
export async function updateBalloon(domainId: string, tid: ObjectId, _id: ObjectId, $set: any) {
return await collBalloon.findOneAndUpdate({ domainId, tid, _id }, { $set });
}
export async function getStatus(domainId: string, tid: ObjectId, uid: number) { export async function getStatus(domainId: string, tid: ObjectId, uid: number) {
return await document.getStatus(domainId, document.TYPE_CONTEST, tid, uid); return await document.getStatus(domainId, document.TYPE_CONTEST, tid, uid);
} }
@ -726,6 +755,7 @@ export async function updateStatus(
status = STATUS.STATUS_WRONG_ANSWER, score = 0, subtasks: Record<number, SubtaskResult> = {}, status = STATUS.STATUS_WRONG_ANSWER, score = 0, subtasks: Record<number, SubtaskResult> = {},
) { ) {
const tdoc = await get(domainId, tid); const tdoc = await get(domainId, tid);
if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid);
return await _updateStatus(tdoc, uid, rid, pid, status, score, subtasks); return await _updateStatus(tdoc, uid, rid, pid, status, score, subtasks);
} }
@ -778,10 +808,9 @@ export function isDone(tdoc: Tdoc, tsdoc?: any) {
return false; return false;
} }
export function isLocked(tdoc: Tdoc) { export function isLocked(tdoc: Tdoc, time = new Date()) {
if (!tdoc.lockAt) return false; if (!tdoc.lockAt) return false;
const now = new Date(); return tdoc.lockAt < time && !tdoc.unlocked;
return tdoc.lockAt < now && !tdoc.unlocked;
} }
export function isExtended(tdoc: Tdoc) { export function isExtended(tdoc: Tdoc) {
@ -910,6 +939,10 @@ global.Hydro.model.contest = {
getAndListStatus, getAndListStatus,
recalcStatus, recalcStatus,
unlockScoreboard, unlockScoreboard,
getBalloon,
addBalloon,
getMultiBalloon,
updateBalloon,
canShowRecord, canShowRecord,
canShowSelfRecord, canShowSelfRecord,
canShowScoreboard, canShowScoreboard,

@ -6,7 +6,7 @@ import pm2 from '@hydrooj/utils/lib/locate-pm2';
import type { ProblemSolutionHandler } from '../handler/problem'; import type { ProblemSolutionHandler } from '../handler/problem';
import type { UserRegisterHandler } from '../handler/user'; import type { UserRegisterHandler } from '../handler/user';
import type { import type {
BaseUserDict, DiscussionDoc, DomainDoc, FileInfo, BaseUserDict, ContestBalloonDoc, DiscussionDoc, DomainDoc, FileInfo,
MessageDoc, ProblemDict, ProblemDoc, RecordDoc, MessageDoc, ProblemDict, ProblemDoc, RecordDoc,
ScoreboardRow, Tdoc, TrainingDoc, User, ScoreboardRow, Tdoc, TrainingDoc, User,
} from '../interface'; } from '../interface';
@ -92,6 +92,7 @@ export interface EventMap extends LifecycleEvents, HandlerEvents {
'contest/before-add': (payload: Partial<Tdoc<30>>) => VoidReturn 'contest/before-add': (payload: Partial<Tdoc<30>>) => VoidReturn
'contest/add': (payload: Partial<Tdoc<30>>, id: ObjectId) => VoidReturn 'contest/add': (payload: Partial<Tdoc<30>>, id: ObjectId) => VoidReturn
'contest/scoreboard': (tdoc: Tdoc<30>, rows: ScoreboardRow[], udict: BaseUserDict, pdict: ProblemDict) => VoidReturn 'contest/scoreboard': (tdoc: Tdoc<30>, rows: ScoreboardRow[], udict: BaseUserDict, pdict: ProblemDict) => VoidReturn
'contest/balloon': (domainId: string, tid: ObjectId, bdoc: ContestBalloonDoc) => VoidReturn
'oplog/log': (type: string, handler: Handler, args: any, data: any) => VoidReturn; 'oplog/log': (type: string, handler: Handler, args: any, data: any) => VoidReturn;

@ -201,7 +201,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
name(module) { name(module) {
const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0]; const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0];
if (packageName === 'monaco-editor-nls') { if (packageName === 'monaco-editor-nls') {
return `i.monaco.${module.userRequest.split('/').pop().split('.')[0]}`; return `i.monaco.${module.userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`;
} }
return `n.${packageName.replace('@', '')}`; return `n.${packageName.replace('@', '')}`;
}, },

@ -109,6 +109,8 @@ Auto configure: 自动配置
Auto detect: 自动检测 Auto detect: 自动检测
Auto hide problems during the contest: 在比赛过程中自动隐藏题目 Auto hide problems during the contest: 在比赛过程中自动隐藏题目
Auto Read Tasks: 自动识别任务 Auto Read Tasks: 自动识别任务
Awards: 奖项
Balloon Status: 气球状态
Basic Info: 基础信息 Basic Info: 基础信息
Basic: 基础 Basic: 基础
Be Copied: 被复制 Be Copied: 被复制
@ -159,6 +161,7 @@ Code language: 代码语言
Code: 代码 Code: 代码
codeFontFamily: 代码字体 codeFontFamily: 代码字体
collapse: 收缩 collapse: 收缩
Color: 颜色
Comment: 评论 Comment: 评论
Comments: 评论 Comments: 评论
CommonMark Syntax: CommonMark 语法 CommonMark Syntax: CommonMark 语法
@ -374,6 +377,8 @@ File: 文件
Filename: 文件名 Filename: 文件名
Files: 文件 Files: 文件
Filter: 过滤 Filter: 过滤
First of Contest: 全场一血
First of Problem: 题目一血
fontFamily: 字体 fontFamily: 字体
Footer: 页脚 Footer: 页脚
Footnote: 脚注 Footnote: 脚注
@ -636,6 +641,7 @@ Please follow the instructions on your device to complete the verification.: 请
Please select at least one file to perform this operation.: 请选择至少一个文件来执行此操作。 Please select at least one file to perform this operation.: 请选择至少一个文件来执行此操作。
Please select at least one role to perform this operation.: 请选择至少一个角色来进行操作。 Please select at least one role to perform this operation.: 请选择至少一个角色来进行操作。
Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。 Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。
Please set the balloon color for each problem first.: 请先为每道题设置气球颜色。
Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。 Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。
Preference Settings: 偏好设置 Preference Settings: 偏好设置
preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003而此选项填写了 T则导入后三道题的题号分别为 T1001, T1002 和 T1003。 preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003而此选项填写了 T则导入后三道题的题号分别为 T1001, T1002 和 T1003。
@ -776,6 +782,7 @@ Selected files have been deleted.: 所选文件已被删除。
Selected problems have been deleted.: 所选题目已被删除。 Selected problems have been deleted.: 所选题目已被删除。
Selected roles have been deleted.: 所选角色已删除。 Selected roles have been deleted.: 所选角色已删除。
Selected users have been removed from the domain.: 所选用户已从此域中移除。 Selected users have been removed from the domain.: 所选用户已从此域中移除。
Send By: 递送者
Send Code after acceptance: 通过题目后发送源代码 Send Code after acceptance: 通过题目后发送源代码
Send Message: 发送站内信息 Send Message: 发送站内信息
Send Password Reset Email: 发送密码重置邮件 Send Password Reset Email: 发送密码重置邮件
@ -783,6 +790,7 @@ Send Verification Email: 发送验证邮件
Send: 发送 Send: 发送
Seperated with ',': 用 ',' 分隔 Seperated with ',': 用 ',' 分隔
Service Status: 服务状态 Service Status: 服务状态
Set Color: 设置颜色
Set Privilege: 设置权限 Set Privilege: 设置权限
Set Role: 设置角色 Set Role: 设置角色
Set Roles for Selected User: 设置所选用户的角色 Set Roles for Selected User: 设置所选用户的角色

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M763.672 600.137c63.965-90.528 90.996-198.496 76.172-304.063C813.402 108.27 639.719-22.539 451.914 3.906c-187.5 26.348-318.633 200.352-292.285 387.852 14.785 105.254 70.547 201.484 156.973 270.976 48.007 38.438 102.168 65.43 153.554 77.5 8.067 1.938 13.215 9.848 11.719 18.008l-9.512 51.524a31.817 31.817 0 0 0 2.89 20.644 31.247 31.247 0 0 0 27.735 16.817 29.546 29.546 0 0 0 4.356-.313l34.785-4.883a3.905 3.905 0 0 1 4.082 2.227C578.867 915.176 635 968.965 706.66 997.305a31.251 31.251 0 0 0 41.016-18.848c5.526-15.977-3.204-33.34-18.907-39.61A231.571 231.571 0 0 1 612.95 837.52a3.907 3.907 0 0 1 2.831-5.86l14.453-1.953a32.32 32.32 0 0 0 19.688-10.215 31.25 31.25 0 0 0 4.668-34.765l-23.555-47.325c-3.719-7.468-.914-16.539 6.367-20.605 46.075-25.723 90.723-66.406 126.27-116.66Zm-302.54 32.773c-3.933 0-7.827-.742-11.484-2.187-80.586-31.875-149.023-102.93-178.613-185.43a31.242 31.242 0 0 1 5.367-31.004 31.25 31.25 0 0 1 53.461 9.91c28.32 78.985 92.325 128.457 142.774 148.438 13.957 5.515 22.023 20.176 19.21 34.918-2.812 14.742-15.706 25.402-30.714 25.394Zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -84,6 +84,7 @@
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"queue-microtask": "^1.2.3", "queue-microtask": "^1.2.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

@ -195,3 +195,16 @@ $highlight-button-color = #F6DF45
.page--contest_user .page--contest_user
.col--uid .col--uid
width: rem(100px) width: rem(100px)
.page--contest_balloon
.col--bid
width: rem(100px)
.col--status
width: rem(100px)
.col--problem
width: rem(150px)
.col--awards
width: rem(130px)

@ -0,0 +1,123 @@
import $ from 'jquery';
import yaml from 'js-yaml';
import React from 'react';
import { HexColorInput, HexColorPicker } from 'react-colorful';
import { createRoot } from 'react-dom/client';
import { ActionDialog } from 'vj/components/dialog';
import Notification from 'vj/components/notification';
import { NamedPage } from 'vj/misc/Page';
import {
i18n, pjax, request, tpl,
} from 'vj/utils';
function Balloon({ tdoc, val }) {
const [color, setColor] = React.useState('');
const [now, setNow] = React.useState('');
return (
<div className="row">
<div className="medium-12 columns">
<table className="data-table">
<thead>
<tr>
<th>{i18n('Problem')}</th>
<th>{i18n('Color')}</th>
<th>{i18n('Name')}</th>
<th><span className="icon icon-wrench"></span></th>
</tr>
</thead>
<tbody>
{tdoc.pids.map((pid) => {
const { color: c, name } = val[+pid];
return (
<tr key={pid}>
<td>
{now === pid
? (<b>{String.fromCharCode(65 + tdoc.pids.indexOf(+pid))}</b>)
: (<span>{String.fromCharCode(65 + tdoc.pids.indexOf(+pid))}</span>)}
</td>
<td>
<HexColorInput
className='textbox'
color={c}
onFocus={() => { setNow(pid); setColor(c); }}
onChange={(e) => { val[+pid].color = e; setColor(e); }}
/>
</td>
<td>
<input
type="text"
className="textbox"
defaultValue={name}
onFocus={() => { setNow(pid); setColor(c); }}
onChange={(e) => { val[+pid].name = e.target.value; }}
/>
</td>
{tdoc.pids.indexOf(+pid) === 0 && <td rowSpan={0}>
{now && <HexColorPicker color={color} onChange={(e) => { val[+now].color = e; setColor(e); }} style={{ padding: '1rem' }} />}
</td>}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
async function handleSetColor(tdoc) {
let val = tdoc.balloon;
if (!val) {
val = {};
for (const pid of tdoc.pids) val[+pid] = { color: '#ffffff', name: '' };
}
Notification.info(i18n('Loading...'));
const promise = new ActionDialog({
$body: tpl`
<div className="row"><div className="columns">
<h1>${i18n('Set Color')}</h1>
</div></div>
<div id="balloon"></div>`,
}).open();
createRoot($('#balloon').get(0)).render(
<Balloon tdoc={tdoc} val={val} />,
);
const action = await promise;
if (action !== 'ok') return;
Notification.info(i18n('Updating...'));
try {
await request.post('', { operation: 'set_color', color: yaml.dump(val) });
} catch (e) {
Notification.error(`${e.message} ${e.params?.[0]}`);
}
Notification.info(i18n('Successfully updated.'));
pjax.request({ url: '', push: false });
}
const page = new NamedPage('contest_balloon', () => {
const { tdoc } = UiContext;
const beginAt = new Date(tdoc.beginAt).getTime();
const endAt = new Date(tdoc.endAt).getTime();
function update() {
const now = Date.now();
if (beginAt <= now && now <= endAt) pjax.request({ url: '', push: false });
}
$('[name="set_color"]').on('click', () => handleSetColor(tdoc));
setInterval(update, 60000);
$(document).on('click', '[value="done"]', async (ev) => {
ev.preventDefault();
const balloon = $(ev.currentTarget).data('balloon');
try {
await request.post('', { balloon, operation: 'done' });
} catch (e) {
Notification.error(`${e.message} ${e.params?.[0]}`);
}
Notification.info(i18n('Successfully updated.'));
pjax.request({ url: '', push: false });
});
});
export default page;

@ -0,0 +1,48 @@
{% import "components/nothing.html" as nothing with context %}
{% 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">{{ _('Balloon Status') }}</h1>
<div class="section__tools">
<button class="primary rounded button" name="set_color">{{ _('Set Color') }}</button>
</div>
</div>
{{ noscript_note.render() }}
<div class="section__body no-padding">
{% if not tdoc.balloon|length %}
{{ nothing.render('Please set the balloon color for each problem first.') }}
{% else %}
<table class="data-table record_main__table">
<colgroup>
<col class="col--status">
<col class="col--bid">
<col class="col--problem">
<col class="col--submit-by">
<col class="col--send-by">
<col class="col--awards">
</colgroup>
<thead>
<tr>
<th class="col--status record-status--border">{{ _('Status') }}</th>
<th class="col--bid">{{ _('#') }}</th>
<th class="col--problem">{{ _('Problem') }}</th>
<th class="col--submit-by">{{ _('Submit By') }}</th>
<th class="col--send-by">{{ _('Send By') }}</th>
<th class="col--awards">{{ _('Awards') }}</th>
</tr>
</thead>
{% include "partials/contest_balloon.html" %}
</table>
{% endif %}
</div>
</div>
</div>
<div class="medium-3 columns">
{% include 'partials/contest_sidebar_management.html' %}
</div>
</div>
{% endblock %}

@ -0,0 +1,37 @@
{% import "components/user.html" as user with context %}
{% import "components/contest.html" as contest with context %}
<tbody data-fragment-id="constest_balloon-tbody">
{%- for bdoc in bdocs -%}
<tr data-bid="{{ bdoc.bid }}">
{% if bdoc.sent %}
<td class="col--status record-status--border pass">
<span class="icon record-status--icon pass"></span>
<span class="record-status--text pass">{{ _('Sent') }}</span>
</td>
{% else %}
<td class="col--status record-status--border pending">
<span class="icon record-status--icon pending"></span>
<span class="record-status--text pending">{{ _('Waiting') }}</span>
</td>
{% endif %}
<td class="col--bid">{{ bdoc._id.toHexString()|truncate(8,true,'') }}</td>
<td class="col--problem col--problem-name" style="color:{{ tdoc.balloon[bdoc.pid].color }}">
{% if not bdoc.sent %}
<form class="form--inline" method="post">
<input type="hidden" name="balloon" value="{{ bdoc._id.toHexString() }}">
<button type="submit" name="operation" value="done" class="link text-maroon lighter" data-balloon="{{ bdoc._id.toHexString() }}">
<span class="icon icon-send"></span>
{{ _('Send') }}
</button>
</form>
{% endif %}
<b>{{ String.fromCharCode(65+tdoc.pids.indexOf(bdoc.pid)) }}</b>&nbsp;&nbsp;({{ tdoc.balloon[bdoc.pid].name }})
</td>
<td class="col--submit-by" data-tooltip="{{ bdoc._id.getTimestamp().format() }}">{{ user.render_inline(udict[bdoc.uid], badge=false) }}</td>
<td class="col--send-by"{% if bdoc.sent %} data-tooltip="{{ bdoc.sentAt.format() }}"{% endif %}>
{% if bdoc.sent %}{{ user.render_inline(udict[bdoc.sent], avatar=false, badge=false) }}{% else %}-{% endif %}
</td>
<td class="col--awards">{% if bdoc.first %}{{ _('First of Problem') }}{% endif %}</td>
</tr>
{%- endfor -%}
</tbody>

@ -26,6 +26,9 @@
<li class="menu__item"><a class="menu__link" href="{{ url('record_main', query={tid:tdoc.docId}) }}"> <li class="menu__item"><a class="menu__link" href="{{ url('record_main', query={tid:tdoc.docId}) }}">
<span class="icon icon-flag"></span> {{ _('All Submissions') }} <span class="icon icon-flag"></span> {{ _('All Submissions') }}
</a></li> </a></li>
<li class="menu__item"><a class="menu__link" href="{{ url('contest_balloon', tid=tdoc.docId) }}">
<span class="icon icon-balloon"></span> {{ _('Balloon Status') }}
</a></li>
<li class="menu__seperator"></li> <li class="menu__seperator"></li>
</ol> </ol>
</div> </div>

Loading…
Cancel
Save