mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: add contest balloon list (#578)
Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
1 parent
8e8c263477
commit
867cd069c5
15 files changed
+341
-8
No files matched your search
+1
-1
@@ -65,7 +65,7 @@
|
||||
"simple-git": "^3.20.0",
|
||||
"style-loader": "^3.3.3",
|
||||
"stylus": "^0.61.0",
|
||||
"stylus-loader": "7.1.3",
|
||||
"stylus-loader": "7.1.2",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-loader": "^9.5.0",
|
||||
"typescript": "^5.2.2",
|
||||
|
||||
@@ -151,6 +151,7 @@ Content: 内容
|
||||
Contest Management: 比赛管理
|
||||
Contest scoreboard is not visible.: 当前比赛成绩表隐藏,暂不可显示。
|
||||
Contest Scoreboard: 比赛成绩表
|
||||
contest_balloon: 气球状态
|
||||
contest_create: 创建比赛
|
||||
contest_detail_problem_submit: 递交比赛题目
|
||||
contest_detail_problem: 题目详情
|
||||
|
||||
@@ -4,8 +4,9 @@ import { pick } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import {
|
||||
Counter, sortFiles, streamToBuffer, Time,
|
||||
Counter, sortFiles, streamToBuffer, Time, yaml,
|
||||
} from '@hydrooj/utils/lib/utils';
|
||||
import { Context } from '../context';
|
||||
import {
|
||||
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
|
||||
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
|
||||
@@ -663,7 +664,55 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
|
||||
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_main', '/contest', ContestListHandler, 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_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_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;
|
||||
unlocked?: boolean;
|
||||
autoHide?: boolean;
|
||||
balloon?: Record<number, string>;
|
||||
|
||||
/**
|
||||
* In hours
|
||||
@@ -651,6 +652,18 @@ export interface DiscussionHistoryDoc {
|
||||
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' {
|
||||
interface Collections {
|
||||
'blacklist': BlacklistDoc;
|
||||
@@ -678,6 +691,7 @@ declare module './service/db' {
|
||||
'event': EventDoc;
|
||||
'opcount': OpCountDoc;
|
||||
'schedule': Schedule;
|
||||
'contest.balloon': ContestBalloonDoc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../interface';
|
||||
import ranked from '../lib/rank';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
import type { Handler } from '../service/server';
|
||||
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
|
||||
import * as document from './document';
|
||||
@@ -655,6 +656,8 @@ export const RULES: ContestRules = {
|
||||
acm, oi, homework, ioi, ledo, strictioi,
|
||||
};
|
||||
|
||||
const collBalloon = db.collection('contest.balloon');
|
||||
|
||||
function _getStatusJournal(tsdoc) {
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
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> = {},
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -778,10 +808,9 @@ export function isDone(tdoc: Tdoc, tsdoc?: any) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isLocked(tdoc: Tdoc) {
|
||||
export function isLocked(tdoc: Tdoc, time = new Date()) {
|
||||
if (!tdoc.lockAt) return false;
|
||||
const now = new Date();
|
||||
return tdoc.lockAt < now && !tdoc.unlocked;
|
||||
return tdoc.lockAt < time && !tdoc.unlocked;
|
||||
}
|
||||
|
||||
export function isExtended(tdoc: Tdoc) {
|
||||
@@ -910,6 +939,10 @@ global.Hydro.model.contest = {
|
||||
getAndListStatus,
|
||||
recalcStatus,
|
||||
unlockScoreboard,
|
||||
getBalloon,
|
||||
addBalloon,
|
||||
getMultiBalloon,
|
||||
updateBalloon,
|
||||
canShowRecord,
|
||||
canShowSelfRecord,
|
||||
canShowScoreboard,
|
||||
|
||||
@@ -6,7 +6,7 @@ import pm2 from '@hydrooj/utils/lib/locate-pm2';
|
||||
import type { ProblemSolutionHandler } from '../handler/problem';
|
||||
import type { UserRegisterHandler } from '../handler/user';
|
||||
import type {
|
||||
BaseUserDict, DiscussionDoc, DomainDoc, FileInfo,
|
||||
BaseUserDict, ContestBalloonDoc, DiscussionDoc, DomainDoc, FileInfo,
|
||||
MessageDoc, ProblemDict, ProblemDoc, RecordDoc,
|
||||
ScoreboardRow, Tdoc, TrainingDoc, User,
|
||||
} from '../interface';
|
||||
@@ -92,6 +92,7 @@ export interface EventMap extends LifecycleEvents, HandlerEvents {
|
||||
'contest/before-add': (payload: Partial<Tdoc<30>>) => VoidReturn
|
||||
'contest/add': (payload: Partial<Tdoc<30>>, id: ObjectId) => 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;
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
name(module) {
|
||||
const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0];
|
||||
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('@', '')}`;
|
||||
},
|
||||
|
||||
@@ -109,6 +109,8 @@ Auto configure: 自动配置
|
||||
Auto detect: 自动检测
|
||||
Auto hide problems during the contest: 在比赛过程中自动隐藏题目
|
||||
Auto Read Tasks: 自动识别任务
|
||||
Awards: 奖项
|
||||
Balloon Status: 气球状态
|
||||
Basic Info: 基础信息
|
||||
Basic: 基础
|
||||
Be Copied: 被复制
|
||||
@@ -159,6 +161,7 @@ Code language: 代码语言
|
||||
Code: 代码
|
||||
codeFontFamily: 代码字体
|
||||
collapse: 收缩
|
||||
Color: 颜色
|
||||
Comment: 评论
|
||||
Comments: 评论
|
||||
CommonMark Syntax: CommonMark 语法
|
||||
@@ -374,6 +377,8 @@ File: 文件
|
||||
Filename: 文件名
|
||||
Files: 文件
|
||||
Filter: 过滤
|
||||
First of Contest: 全场一血
|
||||
First of Problem: 题目一血
|
||||
fontFamily: 字体
|
||||
Footer: 页脚
|
||||
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 role 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.: 请等待比赛主办方解除封榜。
|
||||
Preference Settings: 偏好设置
|
||||
preferredPrefix_hint: 此选项用于重排题号。例如,若题目包中所给的题号分别是 P1001, P1002, P1003,而此选项填写了 T,则导入后三道题的题号分别为 T1001, T1002 和 T1003。
|
||||
@@ -776,6 +782,7 @@ Selected files have been deleted.: 所选文件已被删除。
|
||||
Selected problems have been deleted.: 所选题目已被删除。
|
||||
Selected roles have been deleted.: 所选角色已删除。
|
||||
Selected users have been removed from the domain.: 所选用户已从此域中移除。
|
||||
Send By: 递送者
|
||||
Send Code after acceptance: 通过题目后发送源代码
|
||||
Send Message: 发送站内信息
|
||||
Send Password Reset Email: 发送密码重置邮件
|
||||
@@ -783,6 +790,7 @@ Send Verification Email: 发送验证邮件
|
||||
Send: 发送
|
||||
Seperated with ',': 用 ',' 分隔
|
||||
Service Status: 服务状态
|
||||
Set Color: 设置颜色
|
||||
Set Privilege: 设置权限
|
||||
Set Role: 设置角色
|
||||
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",
|
||||
"queue-microtask": "^1.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -195,3 +195,16 @@ $highlight-button-color = #F6DF45
|
||||
.page--contest_user
|
||||
.col--uid
|
||||
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> ({{ 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}) }}">
|
||||
<span class="icon icon-flag"></span> {{ _('All Submissions') }}
|
||||
</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>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user